前回は、コレクションインタフェースが IEnumerable<T>、ICollection<T>、IList<T> という小さな単位に分かれていることを確認しました。今回は「コレクションインタフェースの必要性」の2つ目として挙げていた「クライアントができる操作を制限させる」という点を解説します。
図1 コレクションインタフェースの必要性の2つ目「クライアントができる操作を制限させる」
インタフェースが小さく分かれているということは、そのうちのどれを使うかによって、クライアント(コレクションを使う側のコード)ができる操作を制限できるということです。
図2 IEnumerable<T>・ICollection<T>・IList<T>のどれを使うかで操作を制限できる
テスト用のボタンを追加する
フォームにボタンを1個追加し、Textプロパティを「クライアントができる操作を制限させる」にします。ボタンをダブルクリックして、Clickイベントハンドラー(button20_Click)を生成します。
図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> という継承関係
この継承関係では、右側(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>を常に使うことの問題点をコメントで整理する
マスターデータを持つProductsクラスを作成する
具体例として、何かのマスターデータを List で保持しているクラスを仮定して試してみます。プロジェクトに新しいクラス Products を追加し、public static なクラスにします。
このクラスに List<string> 型のフィールド _values を持たせます。本来であれば ProductEntity のような、Product の1レコード分のデータを保持する型を要素にするところですが、今回はシンプルに string にします。staticクラスなので、フィールドも static にします。
図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コンストラクターで「ボール」「スパイク」を追加する
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メソッドを追加する
使う側ではAddやClearができてしまう
使う側(button20_Click)に戻り、Products クラスの GetData を呼び出して変数 a で受け取ります。そして「a.」と入力すると、IntelliSense に Add、Clear、Remove、Insert などが表示されます。
図9 IList<string>で受け取ると、Add・Clear・Removeなどが候補に出てくる
つまり、クライアント側でマスターデータに対して Add ができてしまいます。実際に Add を書いてみます。
リスト7 クライアント側でAddする
var a = Products.GetData();
a.Add("AAA");
図10 受け取ったリストに対してAddを記述する
実行して元データへの影響を確認する
a.Add(“AAA”) の行にブレークポイントを置いて実行し、追加したボタンを押します。Add の行を実行したあとで a をウォッチウィンドウで確認すると、Count が 3 になっています。もともとの「ボール」「スパイク」に「AAA」が加わった状態です。
図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」が含まれている
これは、GetData が _values の参照をそのまま返しているためです。a も a2 も、Products クラス内の static な List と同じインスタンスを指しています。実際に Products 側の _values をウォッチで確認すると、こちらにも「AAA」が入っています。図13はボタンをもう一度押したあとの状態で、static なリストはアプリケーションの実行中ずっと残り続けるため、「AAA」が2つに増えて Count が 4 になっています。
図13 Products側のstaticな _values にも影響が出ている
このように、内部のリストを IList<T> でそのまま返すと、クライアントに Add されたり Clear されたりして、元ネタに影響が出てしまいます。
コピーを返したとしても選択肢が多すぎる
では、リストのコピーを返せば元ネタには影響しないので問題ないかというと、それでもまだ課題が残ります。IList<T> で返している以上、クライアント側の IntelliSense には Add、Insert、Clear といったメソッドがたくさん表示されます。使う側からすると「これは使ってもよいのだろうか」と迷うメソッドが大量に出てくることになります。
図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#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新機能」を観る













