ここまで、コレクションインタフェースの切り分けを見てきました。読むだけなら IEnumerable<T>、追加・削除をしたいなら ICollection<T>、インデックス指定も必要なら IList<T> という使い分けです。
図1 これまでに見てきた3つのコレクションインタフェースの使い分け
3つのインタフェースだけでは困る場面
ところが、実際にデータを戻す場面では、この3つだけでは少し困ることがあります。たとえば、戻したデータを編集はしてほしくないけれど、IEnumerable<T> だけでは使いづらい、というケースです。
Count を取りたいのに IEnumerable<T> には Count プロパティがありません。かといって ICollection<T> を返すと、Add や Remove まで許可してしまいます。同じように、インデックスを指定して値を取りたいので IList<T> を返したいけれど、それでは Add されたり、値を変更されたりしてしまいます。
(参考)ICollection<T> や IList<T> を返すと編集まで許可してしまう
ICollection<string> c = Products.GetData();
var count = c.Count; // Countは取れるが…
c.Add("AAA"); // 追加もできてしまう
IList<string> list = Products.GetData();
var first = list[0]; // インデックス指定はできるが…
list[0] = "XXX"; // 値の変更もできてしまう
IReadOnlyCollection<T> と IReadOnlyList<T>
そのような場合のために、実はこの3つ以外にも ReadOnly 系のインタフェースが用意されています。IReadOnly と入力すると、インテリセンスに IReadOnlyCollection<> や IReadOnlyList<> が表示されます。
図2 インテリセンスに表示されるReadOnly系のインタフェース
今回取り上げるのは、IReadOnlyCollection<T> と IReadOnlyList<T> の2つです。コメントに追記しておきます。
ReadOnly系のインタフェースをコメントに追記する
//読むだけ → IEnumerable<T> //追加・削除したい → ICollection<T> //インデックス指定も必要 → IList<T> //IReadOnlyCollection<T> //IReadOnlyList<T>
図3 IReadOnlyCollection<T>とIReadOnlyList<T>を追記
IReadOnlyCollection<T> は、ICollection<T> で使える Count などは使えるけれど、編集はさせない、というインタフェースです。つまり ICollection<T> の ReadOnly 版(編集できない版)です。
IReadOnlyList<T> は、IList<T> のようにインデックス指定はしたいけれど、編集はさせたくない、という場合に使います。こちらは IList<T> の ReadOnly 版です。
なお、IEnumerable<T> はもともと繰り返ししかできず、編集する機能を持っていません。そのため、IEnumerable<T> には ReadOnly 版はありません。
IReadOnlyCollection<T> を返す
まずは Count を使いたい場合です。Products クラスに、IReadOnlyCollection<string> を返す GetDataCount メソッドを追加します。既存の GetDataB メソッドをコピーして、戻り値の型とメソッド名を変更します。
図4 戻り値をIReadOnlyCollection<string>にしたGetDataCountメソッド
Products.cs:GetDataCountメソッドを追加する
public static IEnumerable<string> GetDataB()
{
return _values;
}
public static IReadOnlyCollection<string> GetDataCount()
{
return _values;
}
中身は今までと同じく _values をそのまま返しているだけです。List<T> は IReadOnlyCollection<T> も実装しているため、戻り値の型を変えるだけでコンパイルが通ります。
呼び出し側のフォームに戻り、Products.GetDataCount() の結果を変数 c で受け取ります。c.cou と入力すると、インテリセンスに Count プロパティが出てきます。
図5 IReadOnlyCollection<T>ではCountプロパティが使える
一方で、c.ad と入力しても Add は候補に出てきません。表示されるのは LINQ の拡張メソッドである ElementAtOrDefault だけです。
図6 c.adと入力してもAddは候補に出てこない
Count は取れるけれど、Add などはない、ということが確認できました。
Form1.cs:IReadOnlyCollection<T>の確認
//IReadOnlyCollection<T>
var c = Products.GetDataCount();
if (c.Count == 0) { } //Countは取れる
//Addとかはない
図7 Countは取れるがAddはない
IReadOnlyList<T> を返す
続いて IReadOnlyList<T> です。Products クラスに、IReadOnlyList<string> を返す GetDataList メソッドを追加します。
Products.cs:GetDataListメソッドを追加する
public static IReadOnlyCollection<string> GetDataCount()
{
return _values;
}
public static IReadOnlyList<string> GetDataList()
{
return _values;
}
図8 戻り値をIReadOnlyList<string>にしたGetDataListメソッド
フォーム側で Products.GetDataList() の結果を変数 d で受け取ります。d[ と入力すると、IReadOnlyList<string> のインデクサーの説明が表示され、インデックスを使った操作ができることがわかります。
図9 IReadOnlyList<T>ではインデックス指定ができる
Form1.cs:インデックスを指定して値を取得する
//IReadOnlyList<T> var d = Products.GetDataList(); var ssss = d[0]; //インデックス指定ができる
ブレークポイントを置いて実行してみると、変数 ssss に 0 番目の値である「ボール」が入っていることが確認できます。
図10 0番目の値「ボール」が取得できている
このように 0 番目の情報を取る、といったことはできます。しかし、d に対して Add しようとしても、Add メソッドはありません。さらに、d[0] = “XXX”; のように値を変えようとすると、コンパイルエラーになります。
図11 値を代入しようとするとCS0200のエラーになる
エラーメッセージは「CS0200: プロパティまたはインデクサー ‘IReadOnlyList<string>.this[int]’ は読み取り専用であるため、割り当てることはできません」です。読み取り専用なのでできない、ということをコンパイラーが教えてくれます。
Form1.cs:IReadOnlyList<T>では値を変えることはできない
//IReadOnlyList<T>
var d = Products.GetDataList();
var ssss = d[0]; //インデックス指定ができる
//d.Add("AAA"); //Addはない(コンパイルエラー)
//d[0] = "XXX"; //値を変えることはできない(CS0200)
まとめ
ここまでの内容をまとめると、次の5行になります。
コレクションインタフェースの使い分け
//まとめ //読むだけ → IEnumerable<T> //読むだけ+Count → IReadOnlyCollection<T> //読むだけ+インデックス指定 → IReadOnlyList<T> //追加・削除したい → ICollection<T> //インデックス指定も必要 → IList<T>
図12 コレクションインタフェースの使い分けのまとめ
読むだけであれば IEnumerable<T> で十分です。読むだけだけれど Count は欲しいという場合は IReadOnlyCollection<T>、読むだけだけれどインデックス指定もしたいという場合は IReadOnlyList<T> を使います。追加・削除をしたければ ICollection<T>、インデックス指定もしたいし編集もしたいという場合は IList<T> です。
この中で実際によく使うのは、IEnumerable<T> と IReadOnlyList<T> です。ICollection<T> や IReadOnlyCollection<T> といったコレクション系は、あまり使いません。
ICollection<T> が使われる例:HashSet<T>
ICollection<T> の代表的なものとしては HashSet<T> があります。HashSet<T> は、値からハッシュ値を算出して格納しておく場所を決めることで、高速な検索ができる構造になっています。
図13 ICollection<T>の代表例であるHashSet<T>
この場合、値は順番に並んでいるわけではないので、インデックスでアクセスするという概念がありません。そのため、インデックスアクセスを提供しない ICollection<T> が使われています。
(参考)HashSet<T>はICollection<T>として扱えるが、インデックス指定はできない
ICollection<string> set = new HashSet<string>();
set.Add("ボール");
set.Add("グローブ");
var exists = set.Contains("ボール"); // 高速に検索できる
var count = set.Count;
//var s = set[0]; // インデクサーがないためコンパイルエラー
//IList<string> list = new HashSet<string>(); // IList<T>は実装していない
とはいえ、基本的にはインデックスの指定は使うことが多いはずです。そのため、くるくる回すだけの IEnumerable<T> にするのか、List 系にするのか。List 系であれば、読み取り専用の IReadOnlyList<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新機能」を観る












