前半では、ProductsクラスのGetDataメソッドがIList<string>を返す例を見ました。この場合、受け取った側でAddを呼ぶと元のデータにまで影響が出てしまいますし、IntelliSenseにもたくさんのメンバーが表示されて、クライアントに選択肢を与えすぎてしまう、という話でした。
後半となるこの章では、戻り値を別のコレクションインタフェースに変えると、クライアントができる操作がどう変わるのかを確認します。
前回の振り返り
まず、前回までのコードを確認しておきます。クライアント側(Form1のbutton20_Click)は次のようになっていました。
図1 前回のクライアントコード。IList<string>を受け取ってAddしている
リスト1 前回のクライアントコード(Form1.cs)
private void button20_Click(object sender, EventArgs e)
{
//階層構造になっている
//IList<T> : ICollection<T> : IEnumerable<T>
//すべてIList<T> を使えばいいのでは?
//→それだと、クライアントに権限を与えすぎたり、
//選択肢を与えすぎる場合がある
var a = Products.GetData();
a.Add("AAA");
var a2 = Products.GetData();
}
データを提供する側のProductsクラスは、内部に持っているList<string>をIList<string>としてそのまま返しています。
図2 GetDataメソッドはIList<string>を返している
リスト2 IList<string>を返すGetData(Products.cs、抜粋)
private static List<string> _values = new List<string>();
static Products()
{
_values.Add("ボール");
_values.Add("スパイク");
}
public static IList<string> GetData()
{
return _values;
}
IList<T>は3つのコレクションインタフェースの中でいちばん多くの機能を持っています。そのため、a.Add("AAA")のように要素を追加でき、その結果a2として取り直したデータにも"AAA"が入った状態になってしまうのでした。
IEnumerable<string>を返すメソッドを追加する
それでは、別のコレクションインタフェースを返してみます。GetDataメソッドをコピーして、戻り値の型をIEnumerable<string>に、メソッド名をGetDataBに変更します。
図3 戻り値をIEnumerable<string>にしたGetDataBメソッドを追加する
リスト3 IEnumerable<string>を返すGetDataB(Products.cs、抜粋)
public static IList<string> GetData()
{
return _values;
}
public static IEnumerable<string> GetDataB()
{
return _values;
}
変えたのは戻り値の型とメソッド名だけです。return _values;の部分はGetDataとまったく同じで、返しているインスタンスはどちらも同じList<string>です。List<T>はIEnumerable<T>を実装しているので、そのまま返すことができます。
クライアント側で呼び出してみる
クライアント側に戻って、ProductsのGetDataBを呼び出し、変数bで受け取ります。
リスト4 GetDataBを呼び出す(Form1.cs)
var a = Products.GetData();
a.Add("AAA");
var a2 = Products.GetData();
var b = Products.GetDataB();
ここでb.と入力して、IntelliSenseに何が表示されるかを見てみます。
図4 b.と入力したときのIntelliSense。GetEnumeratorとobjectのメソッドしかない
Addなどは出てきません。内容としてはGetEnumeratorと、あとはEquals、GetHashCode、GetType、ToStringというobjectのメソッドがあるだけになっています。
試しにb.adまで入力してみても、候補にAddは現れません。
図5 b.adと入力してもAddは候補に出てこない
つまり、やれることがないわけです。「Addしたいな」「Clearしたいな」と思っても、呼ぶことができません。
リスト5 IEnumerable<string>では変更系のメソッドを呼べない
var b = Products.GetDataB();
//b.Add("AAA"); // コンパイルエラー:IEnumerable<string>にAddの定義はない
//b.Clear(); // コンパイルエラー:IEnumerable<string>にClearの定義はない
//b[0] = "BBB"; // コンパイルエラー:インデクサも使えない
そういう意味で、クライアントにやれることを限定しているわけです。
foreachで回すだけにしてほしい場合
IEnumerable<T>が持っているのはGetEnumeratorだけですが、これはforeachで要素を順番に取り出すための仕組みです。ですから、「戻ったデータをforeachでくるくると回すだけにしてほしい」という場合は、IEnumerable<T>を返せば十分です。
リスト6 IEnumerable<string>で受け取ったデータをforeachで回す(参考)
var b = Products.GetDataB();
foreach (var value in b)
{
Console.WriteLine(value);
}
受け取った側は読み取ることしかできないので、GetDataのときのように、クライアントがAddして元のデータが変わってしまう、という心配がなくなります。
ICollection<T>の機能だけを与えたい場合
同じ考え方で、ICollection<T>の機能だけを与えたいと思ったら、戻り値をICollection<T>にすればよいわけです。
図6 戻り値の型をICollection<string>に書き換える
図7 ICollection<string>を返すようにしたGetDataB
リスト7 ICollection<string>を返す場合(Products.cs、抜粋)
public static ICollection<string> GetDataB()
{
return _values;
}
ICollection<T>にすると、Add、Remove、Clear、Contains、Countといった「件数の把握と追加・削除」の機能は使えるようになりますが、b[0]のようなインデックスによるアクセスはできません。3つのインタフェースで、クライアントに許される操作を整理すると次のようになります。
リスト8 戻り値の型によってクライアントができる操作が変わる(参考)
var list = new List<string> { "ボール", "スパイク" };
// IEnumerable<string> … foreachで読み取るだけ
IEnumerable<string> e = list;
foreach (var value in e) { }
//e.Add("AAA"); // コンパイルエラー
// ICollection<string> … 件数の取得と追加・削除まで
ICollection<string> c = list;
c.Add("AAA"); // OK
var count = c.Count; // OK
//var x = c[0]; // コンパイルエラー
// IList<string> … インデックスによるアクセスまで
IList<string> l = list;
l.Add("BBB"); // OK
var y = l[0]; // OK
l.Insert(0, "CCC"); // OK
まとめ
図8 この章で追加したクライアントコード
戻り値の型をどのコレクションインタフェースにするかによって、クライアントができることを限定できます。見せる情報を限定したり、権限を与えすぎないようにしたりするために、コレクションインタフェースを使って制御することができる、というのがこの章のポイントです。
前章で見たように、3つのインタフェースはIList<T> : ICollection<T> : IEnumerable<T>という階層構造になっています。「すべてIList<T>を使えばいいのでは?」と思うかもしれませんが、それではクライアントに権限を与えすぎたり、選択肢を与えすぎたりする場合があります。クライアントに何をさせたいのかを考え、必要な範囲のインタフェースを選ぶようにしてください。
なお、これ以外にも、リードオンリー系のコレクションインタフェースというものがあります。読み取り専用のコレクションを返したい場合は、後の章で扱うリードオンリー系のコレクションインタフェースの解説で確認してください。
■非公開コース「C#14新機能」プレゼント:
非公開コース「C#14新機能」(80分)をご覧になりたい方は
こちらからURLとパスワードを発行していますので、ご覧になってみてください。
非公開コース「C#14新機能」を観る
A01_はじめに
A02_プロジェクトの作成
B01_配列とは
B02_配列の生成とアクセス
B03_生成と同時に値を設定する
B04_型推論による生成
B05_メソッドの引数などにする場合の注意点
B06_Length
B07_IndexOfでの検索
B08_FindIndexでの検索
B09_Find
B10_Exists
B11_FindAllとFindLast
B12_誤ったコピー
B13_Array.Copy
B14_範囲指定のコピー
B15_Resize
C01_ArrayList
C02_List
C03_List 動的な要素の変更
C04_Listのコンストラクタ
C05_Listのコンストラクタ_Capacity
C06_ListTからArrayクラスのメソッドが呼ばれている
D01_コレクションインタフェースとは
D02_異なるコレクションクラスに互換性を持たせる
D03_インタフェースの階層構造
D04_クライアントができる操作を制限させる
D05_クライアントができる操作を制限させる_後半
D06_Enumerableの拡張メソッドに関して
D07_ReadOnly系のコレクションインタフェース
D08_AsReadOnly
D09_ToListでコピーする
D10_ListTはprivateで使う
■非公開コース「C#14新機能」プレゼント:
非公開コース「C#14新機能」(80分)をご覧になりたい方は
こちらからURLとパスワードを発行していますので、ご覧になってみてください。
非公開コース「C#14新機能」を観る







