この記事は、あるクラスの中で管理しているリストや配列をクラス外に公開して参照してもらう時に安全に公開する方法や考え方や実装の紹介です。
あるクラス内で管理しているリストをクラス外に公開する場合に具体的にどういった危険性があるのか、どうすれば安全なのかを考えていきます。
はじめに
例えば、以下の例のようにあるクラス内でリストを持っていてそれを外部に公開するケースです。
// リストを持っているクラス public class ListContainer { List<Item> _list = new List<Item>(); // リストを読み取り専用で外部に公開してるつもり public IReadOnlyCollection<Item> Items => _list; public ListContainer() { _list.Add(new Item() { No = 0 }); _list.Add(new Item() { No = 1 }); _list.Add(new Item() { No = 2 }); _list.Add(new Item() { No = 3 }); } } // リストの要素 public class Item { public int No { get; set; } // 誰でも変更できる }
IReadOnlyCollection で変更できない旨を外部に表明してはいますが、要素の内容の変更までは保護されない & 以下のようなコードを書くと割と簡単に変更できてしまいます。
ListContainer container = new (); foreach (var item in container.Items) { item.No = 1; // (1)mutableな参照型の要素だとIReadOnlyCollectionでも変更できる } if (container.Items is List<Item> list) { list.Add(new() { No = 4 }); // (2)元の型にアクセスしやすいとキャストして自由に追加削除できる } // (3)クラス外で勝手に変更されてクラス内のリストも変わってしまう foreach (var item in container.Items) { Console.WriteLine(item.No); // > 1 // 中身が書き換わっている // > 1 // > 1 // > 1 // > 4 // 追加されている } // ★想定しない状態になってしまう
こうなると自分のクラス内で管理してるリストがクラスが外で意図せず変更されてクラス内の処理で問題が発生する可能性があります。
リストを外部に渡す時の要件
複数人で作業していた場合は自分の作成したコンポーネントは割と想定と違う使われ方をされることが多いです。なるべく他人の操作で自分のコンポーネントの中身が壊れるようなことは避けたい所です。その場合コレクションを外部公開する時の要件は概ね以下になります。
- (1) 配列やリストの各要素の内容は変更されない
- (2) 配列やリスト自体は変更されない(追加されたり削除されたり)
- (3) もしクラス外で変更されてもその変更がクラス内に伝わらない
解決方法
先述のそれぞれの項目の解決方法は以下の通りです。
- (1) 配列やリスト内の各要素の内容は変更できない
- 可能であれば要素クラスを immutable にする。無理な場合はオブジェクトのコピーを返す
- (2) 配列やリスト自体が変更されない(追加されたり削除されたり)
- IReadOnlyCollection を正しく使用する
- (3) もしクラス外で変更されてもその変更はクラス内に伝わらない
- (1)と(2)が守られていれば自動的に達成可能
要素を変更できないようにする
immutable 化するとか言いますが、一度作成したら後で内容を変更できないようします。
色々実装方法があるのでいくつか紹介します。
// 元の定義 public class Item { public int No { get; set; } // 変更可能 } // ↓↓↓↓ // 読み取り専用プロパティ public class Item { public int No { get; private set; } public Item(int no) => No = no; } // もしくはreadonlyフィールド public class Item { public readonly int No; public Item(int no) => No = no; }
上記の方法の他に、外部にはインターフェースだけ公開、クラス内では内部してクラスを用いて安全を確保する方法もあります。
public class ListContainer { List<Item> _list = new(); // ★インターフェースの要素を返すようにする public IEnumerable<IItem> Items => _list.ConvertAll(item => item as IItem); public ListContainer() { // ... } // ★内部で使用するクラス private class Item : IItem { public int No { get; set; } } } // 外部に公開するインターフェース public interface IItem { int No { get; } }
immurable なクラスの場合 struct と大差ないのですが、struct だとオブジェクトのメンバーとして持つケースでコンポーネント内の通常の操作が面倒になるため使い方を考慮しつつ適切な宣言を行います。インターフェースを返す方法は最初のコードからは形が大きく違うので実装が少し大きくなります。これも状況と規模によってどうするか決めましょう。
配列やリストを変更されないようにする
外部に公開するリストは IReadOnlyCollection で公開しますが公開する方法を押さえておけば大丈夫です。
以下のように IReadOnlyCollection のインスタンスをそれぞれの方法で作成して実装します。配列の場合サイズを変更したら割り当て直します。
public class Sample { // 外部に公開する読み取り専用リスト public readonly IReadOnlyCollection<Item> List; List<Item> _list = new List<Item>(); // 配列も同じように公開できる public IReadOnlyCollection<Item> ListArray { get; private set; } Item[] _itemsArray = new Item[10]; public Sample() { // リストをコンストラクタに渡すと作成できる List = new ReadOnlyCollection<Item>(_list); // 配列はAsReadOnlyでReadOnlyCollectionを作成できる ListArray = Array.AsReadOnly(_itemsArray); // 配列はリサイズしたら割り当て直す Array.Resize(ref _itemsArray, 20); ListArray = Array.AsReadOnly(_itemsArray); } }
これで追加や削除できない、内容も変更できないようになりました。
以上です。