LSPおよびIEnumerableインターフェイス
子孫クラスがIEnumerable LSP原則に違反しているかどうかの質問に答えるために、この原則にどのように違反するかを見てみましょう。
次の条件のいずれかが満たされている場合、LSPに違反していると主張できます。
- クラスのサブクラス(または、この場合はインターフェイス)は、親の不変式を保持しません
- サブクラスは親の事後条件を緩和します
- サブクラスは親の前提条件を強化します
IEnumerableインターフェイスの問題は、その事前条件と事後条件が明示的に定義されておらず、しばしば誤って解釈されることです。 IEnumerableおよびIEnumeratorインターフェイスの正式な契約は、 実際にはこれを助けませ ん 。 さらに、IEnumerableインターフェイスのさまざまな実装は、しばしば互いに矛盾します。
IEnumerableインターフェイスの実装
実装に飛び込む前に、インターフェース自体を見てみましょう。 IEnumerable <T>、IEnumerator <T>、およびIEnumeratorインターフェイスのコードは次のとおりです。 IEnumerableインターフェイスは、IEnumerable <T>と実質的に同じです。
public interface IEnumerable<out T> : IEnumerable { IEnumerator<T> GetEnumerator(); } public interface IEnumerator<out T> : IDisposable, IEnumerator { T Current { get; } } public interface IEnumerator { object Current { get; } bool MoveNext(); void Reset(); }
彼らは非常に簡単です。 ただし、異なるBCLクラスはそれらを異なる方法で実装します。 おそらく最も明らかな例は、List <T>クラスでの実装でしょう。
public class List<T> { public struct Enumerator : IEnumerator<T> { private List<T> list; private int index; private T current; public T Current { get { return this.current; } } object IEnumerator.Current { get { if (this.index == 0 || this.index == this.list._size + 1) throw new InvalidOperationException(); return (object)this.Current; } } } }
タイプTのCurrentプロパティはMoveNext()の呼び出しを必要としませんが、タイプオブジェクトのCurrentプロパティは以下を必要とします。
public void Test() { List<int>.Enumerator enumerator = new List<int>().GetEnumerator(); int current = enumerator.Current; // 0 object current2 = ((IEnumerator)enumerator).Current; // exception }
Reset()メソッドの実装方法も異なります。 List <T> .Enumerator.Reset()はEnumeratorをリストの最上部に忠実に配置しますが、イテレータはそれらをまったく実装しないため、次のコードは機能しません。
public void Test() { Test2().Reset(); // NotSupportedException } private IEnumerator<int> Test2() { yield return 1; }
IEnumerableを操作するときに確認できる唯一のことは、IEnumerable <T> .GetEnumerator()メソッドがゼロ以外(null以外)の列挙子オブジェクトを返すことです。 IEnumerableを実装するクラスは、空のセットとして使用できます。
private IEnumerable<int> Test2() { yield break; }
したがって、要素の無限のシーケンス:
private IEnumerable<int> Test2() { Random random = new Random(); while (true) { yield return random.Next(); } }
そして、これは作られた例ではありません。 BlockingCollectionクラスは、他のスレッドがコレクションにアイテムを追加するまで、呼び出しスレッドがMoveNext()メソッドでブロックされるようにIEnumeratorを実装します。
public void Test() { BlockingCollection<int> collection = new BlockingCollection<int>(); IEnumerator<int> enumerator = collection.GetConsumingEnumerable().GetEnumerator(); bool moveNext = enumerator.MoveNext(); // The calling thread is blocked }
つまり、 IEnumerableインターフェイスは、要素の基になるセットについての保証を提供しません;このセットが有限であることさえ保証しません 。 彼が私たちに言うのは、このセットが何らかの形で繰り返されるということだけです。
IEnumerableおよびLSP
それで、LSPはIEnumerableクラスに違反しますか? 次の例を考えてみましょう。
public void Process(IEnumerable<Order> orders) { foreach (Order order in orders) { // Do something } }
変数ordersの基礎となるタイプがList <Orders>である場合、すべてが順序どおりです。リスト項目は簡単に反復できます。 しかし、実際には、MoveNext()を呼び出すたびにordersが新しいオブジェクトを作成する無限ジェネレーターであるとしたらどうでしょうか?
internal class OrderCollection : IEnumerable<Order> { public IEnumerator<Order> GetEnumerator() { while (true) { yield return new Order(); } } }
明らかに、Processメソッドは意図したとおりに機能しません。 しかし、OrderCollectionクラスがLSPに違反しているためでしょうか? いや OrderCollectionはIEnumerableインターフェイスコントラクトに細心の注意を払っています。要求されるたびに新しいオブジェクトを提供します。
問題は、ProcessメソッドがIEnumerableを実装するオブジェクトが、このインターフェイスが約束するよりも大きいことを期待していることです。 orders変数の基底クラスが最終コレクションであるという保証はありません。 前に述べたように、ordersはBlockingCollectionクラスのインスタンスである場合があります。これにより、すべての要素を反復処理する無駄な試みが行われます。
問題を回避するには、入力パラメーターのタイプをICollection <T>に変更するだけです。 IEnumerableとは異なり、ICollectionはCountプロパティを提供し、基になるコレクションが有限であることを保証します。
IEnumerableおよび読み取り専用のコレクション
ICollectionの使用には欠点があります。 ICollectionを使用すると、要素を変更できます。これは、コレクションを読み取り専用コレクションとして使用する場合に望ましくないことがよくあります。 .Net 4.5より前は、この目的のためにIEnumerableインターフェイスがよく使用されていました。
これは良い解決策のように思えますが、インターフェイスファインダーに多くの制限を課しています。
public int GetTheTenthElement(IEnumerable<int> collection) { return collection.Skip(9).Take(1).SingleOrDefault(); }
これは最も一般的なアプローチの1つです。LINQを使用してIEnumerable制約をバイパスします。 このようなコードは非常に単純であるという事実にもかかわらず、1つの明らかな欠点があります。コレクションを10回繰り返しますが、インデックスを逆にするだけで同じ結果を得ることができます。
解決策は明らかです-IReadOnlyListを使用します。
public int GetTheTenthElement(IReadOnlyList<int> collection) { if (collection.Count < 10) return 0; return collection[9]; }
コレクションが計算可能であると予想される場所でIEnumerableインターフェイスを使用し続ける理由はありません(ほとんどの場合、これを期待します)。 .Net 4.5に追加されたIReadOnlyCollection <T>およびIReadOnlyList <T>インターフェイスは、この作業をはるかに簡単にします。
IEnumerableおよびLSPの実装
LSPに違反するIEnumerable実装はどうですか? IEnumerable <T>の基本型がDbQuery <T>である例を見てみましょう。 次のように取得できます。
private IEnumerable<Order> FindByName(string name) { using (MyContext db = new MyContext()) { return db.Orders.Where(x => x.Name == name); } }
このコードには明らかな問題があります。データベースへのアクセスは、クライアントコードが結果セットの反復を開始するまで遅延します。 なぜなら この時点で、データベースへの接続が閉じられ、控訴は例外になります:
public void Process(IEnumerable<Order> orders) { foreach (Order order in orders) // Exception: DB connection is closed { } }
このような実装は、LSPに違反しています。 IEnumerableインターフェイス自体には、データベースへのオープン接続を必要とする前提条件はありません。 このインターフェイスに従って、そのような接続が存在するかどうかに関係なく、IEnumerableを反復処理できるはずです。 ご覧のとおり、DbQueryクラスはIEnumerableの前提条件を強化し、LSPに違反しました。
一般的に、これは必ずしもデザインが悪いことを示すものではありません。 レイジーコンピューティングは、データベースを操作する際のかなり一般的なアプローチです。 データベースへの1回の呼び出しで複数のクエリを実行できるため、システム全体のパフォーマンスが向上します。 ここでの価格は、LSPの原則に違反しています。
元の記事へのリンク: .NETおよびLSPのIEnumerableインターフェイス