コレクション【C#】

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

前回は、コレクションインタフェースが IEnumerable<T>、ICollection<T>、IList<T> という小さな単位に分かれていることを確認しました。今回は「コレクションインタフェースの必要性」の2つ目として挙げていた「クライアントができる操作を制限させる」という点を解説します。

図1 コレクションインタフェースの必要性の2つ目「クライアントができる操作を制限させる」

図1 コレクションインタフェースの必要性の2つ目「クライアントができる操作を制限させる」

インタフェースが小さく分かれているということは、そのうちのどれを使うかによって、クライアント(コレクションを使う側のコード)ができる操作を制限できるということです。

図2 IEnumerable<T>・ICollection<T>・IList<T>のどれを使うかで操作を制限できる

図2 IEnumerable<T>・ICollection<T>・IList<T>のどれを使うかで操作を制限できる

テスト用のボタンを追加する

フォームにボタンを1個追加し、Textプロパティを「クライアントができる操作を制限させる」にします。ボタンをダブルクリックして、Clickイベントハンドラー(button20_Click)を生成します。

図3 フォームにボタンを追加する

図3 フォームにボタンを追加する

やり方としては、メソッドの引数や戻り値をコレクションインタフェースにすることで、使う側に制限をかけます。まずは、この方針をコメントとして書いておきます。

リスト1 button20_Clickに方針をコメントで記述する

private void button20_Click(object sender, EventArgs e)
{
    //2.クライアントができる操作を制限させる
    //引数や戻り値を
    //コレクションインタフェースにすることで、
    //使う側に制限をかける
}

コレクションインタフェースは階層構造になっている

前回も説明したとおり、コレクションインタフェースは階層関係になっています。IList<T> は ICollection<T> と IEnumerable<T> を継承しており、ICollection<T> は IEnumerable<T> を継承しています。

リスト2 階層構造のコメント

//階層構造になっている
//IList<T> : ICollection<T> : IEnumerable<T>

図4 IList<T> : ICollection<T> : IEnumerable<T> という継承関係

図4 IList<T> : ICollection<T> : IEnumerable<T> という継承関係

この継承関係では、右側(IEnumerable<T>側)に行くほど機能が少なくなります。IEnumerable<T> は繰り返しだけ、ICollection<T> は項目の追加や削除、IList<T> になるとインデックス指定での操作ができる、というように機能が増えていきます。

リスト3 (参考)それぞれのインタフェースでできる操作のイメージ

IEnumerable<string> e = new List<string> { "A", "B" };
foreach (var s in e) { }   // 繰り返しのみ

ICollection<string> c = new List<string> { "A", "B" };
c.Add("C");                // 追加・削除、Count
var count = c.Count;

IList<string> l = new List<string> { "A", "B" };
l[0] = "Z";                // インデックス指定での操作
l.Insert(1, "Y");

すべてIList<T>を使えばよいのでは?

ここで、「IList<T> には基本的にフル機能があるのだから、常に IList<T> を使えばよいのではないか」という考え方も出てきます。しかし、それだとクライアントに権限を与えすぎたり、選択肢を与えすぎたりする場合がある、という問題点があります。

リスト4 問題点のコメント

//すべてIList<T> を使えばいいのでは?
//→それだと、クライアントに権限を与えすぎたり、
//選択肢を与えすぎる場合がある

図5 IList<T>を常に使うことの問題点をコメントで整理する

図5 IList<T>を常に使うことの問題点をコメントで整理する

マスターデータを持つProductsクラスを作成する

具体例として、何かのマスターデータを List で保持しているクラスを仮定して試してみます。プロジェクトに新しいクラス Products を追加し、public static なクラスにします。

このクラスに List<string> 型のフィールド _values を持たせます。本来であれば ProductEntity のような、Product の1レコード分のデータを保持する型を要素にするところですが、今回はシンプルに string にします。staticクラスなので、フィールドも static にします。

図6 Productsクラスにprivate staticなList<string>を持たせる

図6 Productsクラスにprivate staticなList<string>を持たせる

続いて static なコンストラクターを作成し、そこで _values にデータを Add しておきます。内容は何でもかまいません。

リスト5 Productsクラス(staticコンストラクターまで)

namespace WinFormsApp1
{
    public static class Products
    {
        private static List<string> _values = new List<string>();

        static Products()
        {
            _values.Add("ボール");
            _values.Add("スパイク");
        }
    }
}

図7 staticコンストラクターで「ボール」「スパイク」を追加する

図7 staticコンストラクターで「ボール」「スパイク」を追加する

GetDataメソッドでIList<T>として返す

この _values を、GetData メソッドでクライアント側に返すことにします。ここでは戻り値の型を IList<string> にして、_values をそのまま return したらどうなるかを見ていきます。

リスト6 Productsクラス(完成形)

public static class Products
{
    private static List<string> _values = new List<string>();

    static Products()
    {
        _values.Add("ボール");
        _values.Add("スパイク");
    }

    public static IList<string> GetData()
    {
        return _values;
    }
}

図8 IList<string>を返すGetDataメソッドを追加する

図8 IList<string>を返すGetDataメソッドを追加する

使う側ではAddやClearができてしまう

使う側(button20_Click)に戻り、Products クラスの GetData を呼び出して変数 a で受け取ります。そして「a.」と入力すると、IntelliSense に Add、Clear、Remove、Insert などが表示されます。

図9 IList<string>で受け取ると、Add・Clear・Removeなどが候補に出てくる

図9 IList<string>で受け取ると、Add・Clear・Removeなどが候補に出てくる

つまり、クライアント側でマスターデータに対して Add ができてしまいます。実際に Add を書いてみます。

リスト7 クライアント側でAddする

var a = Products.GetData();
a.Add("AAA");

図10 受け取ったリストに対してAddを記述する

図10 受け取ったリストに対してAddを記述する

実行して元データへの影響を確認する

a.Add(“AAA”) の行にブレークポイントを置いて実行し、追加したボタンを押します。Add の行を実行したあとで a をウォッチウィンドウで確認すると、Count が 3 になっています。もともとの「ボール」「スパイク」に「AAA」が加わった状態です。

図11 Addを実行すると a のCountが3になる

図11 Addを実行すると a のCountが3になる

ここまでは a に追加しただけに見えますが、本当に問題なのは元ネタへの影響です。確かめるために、もう一度 Products の GetData を呼び出して、別の変数 a2 で受け取ってみます。

リスト8 もう一度GetDataして確かめる

var a = Products.GetData();
a.Add("AAA");
var a2 = Products.GetData();

a2 を見ても Count は 3 で、中身は「ボール」「スパイク」「AAA」になっています。新しく取得し直したはずのデータに、クライアントが追加した「AAA」が含まれているわけです。

図12 取得し直した a2 にも「AAA」が含まれている

図12 取得し直した a2 にも「AAA」が含まれている

これは、GetData が _values の参照をそのまま返しているためです。a も a2 も、Products クラス内の static な List と同じインスタンスを指しています。実際に Products 側の _values をウォッチで確認すると、こちらにも「AAA」が入っています。図13はボタンをもう一度押したあとの状態で、static なリストはアプリケーションの実行中ずっと残り続けるため、「AAA」が2つに増えて Count が 4 になっています。

図13 Products側のstaticな _values にも影響が出ている

図13 Products側のstaticな _values にも影響が出ている

このように、内部のリストを IList<T> でそのまま返すと、クライアントに Add されたり Clear されたりして、元ネタに影響が出てしまいます。

コピーを返したとしても選択肢が多すぎる

では、リストのコピーを返せば元ネタには影響しないので問題ないかというと、それでもまだ課題が残ります。IList<T> で返している以上、クライアント側の IntelliSense には Add、Insert、Clear といったメソッドがたくさん表示されます。使う側からすると「これは使ってもよいのだろうか」と迷うメソッドが大量に出てくることになります。

図14 IList<T>で返す限り、変更系のメソッドが候補に並び続ける

図14 IList<T>で返す限り、変更系のメソッドが候補に並び続ける

リスト9 (参考)コピーを返しても、クライアントはAddを呼べてしまう

public static IList<string> GetData()
{
    // コピーを返せば元の _values は守られるが……
    return new List<string>(_values);
}

// 使う側
var a = Products.GetData();
a.Add("AAA");   // コンパイルは通る。しかし元データには反映されず、
                // 呼び出した側の意図どおりに動かない

そのため、戻り値はできるだけ機能を限定して返したほうがよい、ということになります。

まとめ

今回は、内部で保持している List<T> を IList<T> としてそのまま返すと何が起きるかを確認しました。ポイントは次の2点です。

1つ目は、クライアント側で Add や Clear を呼ばれると、元のデータ(今回は Products クラスの static な _values)に影響が出てしまうことです。2つ目は、仮にコピーを返して元データを守ったとしても、使ってよいのかどうか判断に迷うメソッドが IntelliSense に多数表示され、クライアントに選択肢を与えすぎてしまうことです。

次回は、機能を狭めた別のインタフェースで返すパターンを見ていきます。

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新機能」を観る