C#:読み取り専用コレクションとLSP

多くの場合、開発者は.NETの読み取り専用コレクションがBarbara Liskovの代替原則に違反していると主張しています。 そうですか? いいえ、IListインターフェイスにはIsReadOnlyフラグが含まれているため、そうではありません。 例外はArrayクラスです。.NET2.0以降、LSPの原則に違反しています。 しかし、順番に理解しましょう。





.NETの読み取り専用コレクションの履歴



この図は、読み取り専用コレクションがバージョンごとに.NETでどのように進化したかを示しています。







ご覧のとおり、IListインターフェイスには、IsReadOnlyとIsFixedSizeの2つのプロパティが含まれています。 元のアイデアは、これら2つの概念を分解することでした。 コレクションは読み取り専用コレクションである可能性があります。つまり、コレクションはまったく変更できません。 一方、コレクションは固定サイズ、つまり その中の既存の要素を変更することはできましたが、新しい要素を追加したり、既存の要素を削除することはできませんでした。 つまり、IsReadOnlyフラグがtrueのコレクションは常にIsFixedSizeでしたが、IsFixedSizeコレクションは常にIsReadOnlyではありませんでした。



したがって、読み取り専用コレクションを作成する場合は、両方のプロパティ(IsReadOnlyとIsFixedSize)を実装して、trueを返す必要があります。 .NET 1.0の期間中にBCLで構築された読み取り専用コレクションはありませんでしたが、アーキテクトは将来の実装の基盤を築きました。 最初のアイデアは、開発者がそのようなコレクションを次のように多態的に使用できるということでした。



public void AddAndUpdate(IList list) { if (list.IsReadOnly) { // No action return; } if (list.IsFixedSize) { // Update only list[0] = 1; return; } // Both add and update list[0] = 1; list.Add(1); }
      
      







もちろん、これはコレクションを操作するのに最も便利な方法ではありませんが、それでもインターフェイスの背後にあるクラスを認識せずに例外を回避できます。 したがって、この設計はLSPに違反しません。 もちろん、IListインターフェイス(私を含む)で作業している間、誰もこのようなチェックを行っていないため、読み取り専用コレクションがLSPに違反しているという主張をたくさん聞くことができます。



.NET 2.0



.NET 2.0でジェネリックが追加された後、BCLチームはインターフェイス階層の新しいバージョンを構築できました。 彼らは、コレクションインターフェイスをより理解しやすくするためにいくつかの作業を行いました。 一部のメンバーをIList <T>からICollection <T>に移動することに加えて、IsFixedSizeフラグを削除することにしました。



これは、配列がこのフラグを必要とする唯一のクラスであったために行われました。 Arrayクラスは、新しい要素の追加または既存の要素の削除を禁止した唯一のクラスでしたが、既存の要素の変更は許可していました。 BCLチームは、IsFixedSizeフラグを追加すると複雑さが増し、ほとんど価値がないと判断しました。 興味深いことに、彼らは.NET 2.0の配列のIsReadOnlyフラグの実装を変更し、物事の状態を反映しないようにしました。



 public void Test() { int[] array = { 1 }; bool isReadOnly1 = ((IList)array).IsReadOnly; // isReadOnly1 is false bool isReadOnly2 = ((ICollection<int>)array).IsReadOnly; // isReadOnly2 is true }
      
      







IsReadOnlyフラグは配列に対してtrueを返しますが、コレクションは変更できます。 これは、LSP原則違反が発生する場所です。 IList <int>を受け入れるメソッドがある場合、次のようなコードを書くことはできません。



 public void AddAndUpdate(IList<int> list) { if (list.IsReadOnly) { // No action return; } // Both add and update list[0] = 1; list.Add(1); }
      
      







クラスにReadOnlyCollection <int>のオブジェクトをメソッドに渡すと、(予定どおり)何も起こりません。 コレクションは読み取り専用のコレクションです。 一方、List <int>クラスのオブジェクト(意図したとおり)が変更されます。新しい要素が追加され、既存の要素が変更されます。 しかし、配列を渡すと、何も起こりません。 配列はICollection <T> .IsReadOnlyプロパティに対してtrueを返します。 また、インターフェースの背後にあるタイプをチェックすることを除いて、既存の要素を更新する機能があるかどうかを知ることはできません。



 public void AddAndUpdate(IList<int> list) { if (list is int[]) { // Update only list[0] = 1; return; } if (list.IsReadOnly) { // No action return; } // Both add and update list[0] = 1; list.Add(1); }
      
      







したがって、配列はLSPに違反します。 ジェネリックインターフェイスを使用する場合にのみ、この原則に違反することに注意してください。



これはマイクロソフト側の間違いでしたか? 妥協でした。 バランスのとれた決定でした。このようなアーキテクチャは単純ですが、ある特定の場所でLSPに違反しています。



.NET 4.5



インターフェイスの階層はよりシンプルになりましたが、それでも大きな欠点があります。コレクションを変更できるかどうかを確認するために毎回IsReadOnlyフラグをチェックする必要があります。 これは開発者が慣れている方法ではありません。 そして一般的に、これらの目的でこのフラグを使用した人はいませんでした。 このプロパティは、自動データバインディングのシナリオでのみ使用されました。IsReadOnlyがtrueを返した場合はデータバインディングは一方向で、それ以外の場合は双方向でした。



残りのシナリオでは、誰もが単にIEnumerable <T>インターフェイスまたはReadOnlyCollection <T>クラスを使用しました。 この問題を解決するために、2つの新しいインターフェイスが.NET 4.5に追加されました:IReadOnlyCollection <T>およびIReadOnlyList <T>。



これらのインターフェイスは既存のエコシステムに追加されたため、アーキテクトは後方互換性を破ることができませんでした。 これが、ReadOnlyCollection <T>クラスがIReadOnlyList <T>だけでなく、IList、IList <T>、およびIReadOnlyList <T>インターフェイスを実装する理由です。 このような変更は、古いバージョンの.NETでコンパイルされた既存のアセンブリの作業でエラーを引き起こす可能性があります。 それらが機能するためには、開発者は新しいバージョンでそれらを再コンパイルする必要があります。



すべてを一から書き直す



後方互換性の要件により現在の状況を変更することはできないという事実にもかかわらず、蓄積された知識を考慮して、コレクションの階層が今日どのように形成されるかを考えることは依然として興味深いです。



私はそれがこのように見えると思う:







これが行われた内容です。

1)以降、非汎用インターフェイスが削除されました 全体像に価値を加えることはありません。

2)IFixedList <T>インターフェイスが追加されたため、IList <T>インターフェイスを実装するためにArrayクラスは必要なくなりました。

3)ReadOnlyCollection <T>クラスの名前はReadOnlyList <T>に変更されました。 これは彼にとってより適切な名前です。 また、IReadOnlyList <T>インターフェイスからのみ継承するようになりました。

4)IsReadOnlyおよびIsFixedSizeフラグが削除されました。 これらはデータバインディングシナリオに追加できますが、コレクションを使用した多態的な作業には不要であることを示すために削除しました。



LSPに関する質問



BCLには興味深いコード例があります。



 public static int Count<T>(this IEnumerable<T> source) { ICollection<T> collection1 = source as ICollection<T>; if (collection1 != null) return collection1.Count; ICollection collection2 = source as ICollection; if (collection2 != null) return collection2.Count; int count = 0; using (IEnumerator<T> enumerator = source.GetEnumerator()) { while (enumerator.MoveNext()) checked { ++count; } } return count; }
      
      







これは、EnumerableクラスからのLINQ-to-objectsのCount拡張メソッドの実装です。 ここで着信するオブジェクトは、要素の数をカウントするために、ICollectionおよびICollection <T>インターフェイスとの互換性についてテストされています。 この方法はLSPの原則に違反しますか?



いいえ、壊れません。 メソッドがオブジェクトを実際のクラスに属しているかどうかをチェックするという事実にもかかわらず、これらのクラスはすべてCountプロパティの同じ実装を持っています。 つまり、ICollection.CountプロパティとICollection <T> .Countプロパティには、whileループ内の要素の数をカウントする式と同じ事後条件があります。



元の記事へのリンク: C#読み取り専用コレクションとLSP



All Articles