コレクション【C#】

C#文法:コレクション:D07_ReadOnly系のコレクションインタフェース

ここまで、コレクションインタフェースの切り分けを見てきました。読むだけなら IEnumerable<T>、追加・削除をしたいなら ICollection<T>、インデックス指定も必要なら IList<T> という使い分けです。

図1 これまでに見てきた3つのコレクションインタフェースの使い分け

図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系のインタフェース

図2 インテリセンスに表示されるReadOnly系のインタフェース

今回取り上げるのは、IReadOnlyCollection<T> と IReadOnlyList<T> の2つです。コメントに追記しておきます。

ReadOnly系のインタフェースをコメントに追記する

//読むだけ → IEnumerable<T>
//追加・削除したい → ICollection<T>
//インデックス指定も必要 → IList<T>
//IReadOnlyCollection<T>
//IReadOnlyList<T>

図3 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メソッド

図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プロパティが使える

図5 IReadOnlyCollection<T>ではCountプロパティが使える

一方で、c.ad と入力しても Add は候補に出てきません。表示されるのは LINQ の拡張メソッドである ElementAtOrDefault だけです。

図6 c.adと入力してもAddは候補に出てこない

図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はない

図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メソッド

図8 戻り値をIReadOnlyList<string>にしたGetDataListメソッド

フォーム側で Products.GetDataList() の結果を変数 d で受け取ります。d[ と入力すると、IReadOnlyList<string> のインデクサーの説明が表示され、インデックスを使った操作ができることがわかります。

図9 IReadOnlyList<T>ではインデックス指定ができる

図9 IReadOnlyList<T>ではインデックス指定ができる

Form1.cs:インデックスを指定して値を取得する

//IReadOnlyList<T>
var d = Products.GetDataList();
var ssss = d[0]; //インデックス指定ができる

ブレークポイントを置いて実行してみると、変数 ssss に 0 番目の値である「ボール」が入っていることが確認できます。

図10 0番目の値「ボール」が取得できている

図10 0番目の値「ボール」が取得できている

このように 0 番目の情報を取る、といったことはできます。しかし、d に対して Add しようとしても、Add メソッドはありません。さらに、d[0] = “XXX”; のように値を変えようとすると、コンパイルエラーになります。

図11 値を代入しようとするとCS0200のエラーになる

図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 コレクションインタフェースの使い分けのまとめ

図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>

図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#文法:コレクション

■非公開コース「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新機能」を観る