コレクション【C#】

C#文法:コレクション:D05_クライアントができる操作を制限させる_後半

前半では、ProductsクラスのGetDataメソッドがIList<string>を返す例を見ました。この場合、受け取った側でAddを呼ぶと元のデータにまで影響が出てしまいますし、IntelliSenseにもたくさんのメンバーが表示されて、クライアントに選択肢を与えすぎてしまう、という話でした。

後半となるこの章では、戻り値を別のコレクションインタフェースに変えると、クライアントができる操作がどう変わるのかを確認します。

前回の振り返り

まず、前回までのコードを確認しておきます。クライアント側(Form1のbutton20_Click)は次のようになっていました。

図1 前回のクライアントコード。IList<string>を受け取ってAddしている

図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 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メソッドを追加する

リスト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のメソッドしかない

図4 b.と入力したときのIntelliSense。GetEnumeratorとobjectのメソッドしかない

Addなどは出てきません。内容としてはGetEnumeratorと、あとはEquals、GetHashCode、GetType、ToStringというobjectのメソッドがあるだけになっています。

試しにb.adまで入力してみても、候補にAddは現れません。

図5 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>に書き換える

図6 戻り値の型をICollection<string>に書き換える

図7 ICollection<string>を返すようにしたGetDataB

図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 この章で追加したクライアントコード

図8 この章で追加したクライアントコード

戻り値の型をどのコレクションインタフェースにするかによって、クライアントができることを限定できます。見せる情報を限定したり、権限を与えすぎないようにしたりするために、コレクションインタフェースを使って制御することができる、というのがこの章のポイントです。

前章で見たように、3つのインタフェースはIList<T> : ICollection<T> : IEnumerable<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新機能」を観る