IEnumerableの使用に関する問題

この記事では、IEnumerableインターフェイスを使用する際の問題について説明します。 このインターフェイスの使用が本当に必要なときにどのような問題を引き起こす可能性があるのか​​、そしてそれをどのように置き換えるのかを見ていきます。



そして、いくつかのコード例から、またはむしろ実際のプロジェクトで遭遇したいくつかのバグから記事を始めたかったのです。



問題の例





これが最初の例です-実際のプロジェクトからのコード、名前だけが変更されます。



private IEnumerable<Account> GetAccountsByOrder(IEnumerable<Account> accounts, IEnumerable<OrderItem> orderItems) { var orderItemsWithQuotaOwners = _restsProvider.GetQuotaOwner(orderItems); return accounts.Where( q => orderItemsWithSourceQuotaOwners.Any(s => s.QuotaOwner == q.QuotaOwner && ... )); }
      
      







これは一見複雑ではないように見えますが、かなりのトラブルを引き起こしました。 GetQuotaOwnerメソッドがすべてです。 その中で、LINQ to SQLクエリが実行され、LINQ to entitiesのプロジェクションが構築され、IEnumerableが返されます。 その結果、quotedAccountsの各行に対して、GetQuotaOwnerメソッドの内部の新しい実行を取得します。 興味深いことに、この場合、再削者は危険について私たちに警告しませんでした。



これは2番目の例です。 ただし、ここでは、実際のプロジェクトのコードではなく、コードのアイデアと問題が実際のプロジェクトから取られました。



 class Foo { public string Value; } class Bar { public string Value; public int ACount; } static void Main() { Foo[] foo = new[] { new Foo { Value = "Abba" }, new Foo { Value = "Deep Purple" }, new Foo { Value = "Metallica" } }; var bar = foo.Select(x => new Bar { Value = x.Value, ACount = x.Value.Count(c => c == 'a' || c == 'A') }); Censure(bar); foreach (var one in bar) { Console.WriteLine(one.Value); } } private static void Censure(IEnumerable<Bar> bar) { foreach (var one in bar) { if (one.ACount > 1) { one.Value = "<censored>"; } } }
      
      







ここで、いくつかのデータを取得し、それらの投影を構築して、それを打ち切ります。 そして、驚いたことに、検閲されていないデータが画面に表示されることがわかります...



この問題の理由は非常に単純です。コレクションを2回繰り返します。つまり、Barクラスのインスタンスの2つの独立したコレクションを取得します。



これら2つのコードを修正するのは難しくないことは明らかで、ToArrayを追加するだけです。 質問は異なります-基本的に間違ったことと、IEnumerableを正しく操作する方法。



IEnumerableを抽象化するもの



まず、IEnumerableをそのように考えてください。 技術的な詳細に入ることなく、このインターフェイスは要素のシーケンスを抽象化します。 さらに、このシーケンスについてはまったく知られていません。有限であるか無限であるかにかかわらず、その操作のコストはいくらですか。



以下に簡単な例を示します-var lines = File.ReadLines( "data.txt");



ラインで今何ができますか? プログラムのパフォーマンスを停止したくない場合は、このコレクションを2回繰り返すことはできません。 無邪気なコードを意味します



  var lines = File.ReadLines("data.txt"); string lastLine = lines.ElementAt(lines.Count());
      
      







私たちにとってタブーであるべきです。



悪化する可能性があります:



 class RandomStrings : IEnumerable<int> { Random _rnd = new Random(); public IEnumerator<int> GetEnumerator() { while (true) yield return _rnd.Next(); } }
      
      







これで、1つの罪のない1つのCount()でもアプリケーションがハングします。



このことから、1つの簡単な結論が得られます。IEnumerableが内部にあると仮定せずに作業することは非常に困難です。



もちろん、読み取りファイルの例では、最後の行(または、ある種の行のストリーム処理など)を効率的に取得できますが、そのためには、IEnumerableをコレクションとして考えるのをやめて、より複雑なコードの記述を開始する必要があります。



しかし、実際のプログラムでは、ほとんどの場合、プログラマーはIEnumerableをコレクションと考えています。 たとえば、そのようなパターンさえ現れました-IEnumerableの保護コピー。 つまり IEnumerableが到着したときにメソッドの先頭でToArray()を呼び出します。



つまり、すぐに言う-メモリに簡単に収まる有限シーケンスがやってきたのですが、コレクションを意味するときにIEnumerableを使用するのはなぜですか?



しかし、ここで、うるさい読者は尋ねるかもしれません-コレクションで正しく動作することはどういう意味ですか? コレクションは異なります-リンクリストもコレクションであり、上記のスタイルでリンクリストの最後の行を取得することは、一般に非常に非効率的です(ただし、IEnumerableの場合と同じように、繰り返し処理を行うのは恐ろしくありません)コレクションは膨大な量の作業に関連付けることができます)。



したがって、概念を明確にし、 ベクトル (.NETリストで、今後このコレクションをシートと呼びます)または配列について話す価値があります。



その後、実際にコントラクトの下でプログラミングできます-IListがメソッドの入力に渡される場合、任意の要素へのアクセスと要素数の取得がO(1)であることがわかっているシートと同様に動作し、彼との効果的な仕事。



戻り値に関する同様の状況-IEnumerableを返すと、ユーザーはシートではなくシーケンスで機能するはるかに複雑なコードを書くように強制されます。



LINQ



IEnumerableの優位性を伴う.NETの状況は、LINQの導入によりエスカレートしました。 アプリケーションがこのインターフェースを数回見ることができる前に、今ではLINQ要求がIEnumerableを生成します。



疑問が生じます-そのようなIEnumerableをどうするか? あなたは極端に行くことができます-すぐに配列またはリストに変換します。 このアプローチには生命に対する権利があります。 繰り返しの問題がないことを保証します。 一方、多くの余分な配列を生成することができます。それらは、ガベージコレクターによって収集する必要があります。



妥協することができます:メソッド内でIEnumerableを操作し、配列またはシートのみを提供します。 このアプローチの欠点は、パフォーマンスに悪影響を与える可能性がある場合、IEnumerable型の変数(実際のソースのvar ...)により注意を払わなければならないことです。 概念的には、このアプローチも受け入れられます。1つのメソッド内で、IEnumerableのこの特定のインスタンスの性質を十分に知っている可能性があり、真空中で球状のIEnumerableとして処理しようとはしません。



コレクションタイプの選択



既に述べたように、ICollectionは、メソッド間でコレクションを転送するための最も成功したタイプではありません。 ICollectionの任意の実装での作業は、IEnumerableを使用するよりも少し簡単です。



IListを選択できますが、このインターフェイスにはIEnumerableと比較して1つの大きなマイナス点があります-コレクションを編集できますが、95%の場合、コレクション自体は読み取り専用オブジェクトです。



IListの代わりに、古き良き配列を使用できます。 確かに、要素を割り当てることができます。 しかし、私の意見では、コレクションに対する最も一般的な操作は要素の削除と追加です。 ビジネスアプリケーションのインデックスでアイテムを割り当てるのは珍しいことです。 したがって、配列を読み取り専用コレクションとして使用できます。



別の可能性は、ReadOnlyCollectionを使用することです。 このクラスの名前が間違っているとすぐに言いたいです。 唯一のコンストラクタには、次の署名パブリックReadOnlyCollection(IListリスト)があります。 つまり、ReadOnlyListと呼ぶ方がより適切です。 一見、このクラスをどこでも使用することはあまり便利ではありませんが、拡張機能を作成する場合

 public ReadOnlyCollection<T> ToReadOnly(this IEnumerable<T> data)
      
      



これは有効なオプションです。



さて、フレームワーク4.5はすでにこの問題を解決しています。IReadOnlyCollectionおよびIReadOnlyListインターフェイスを導入しています。 さらに、ListはIReadOnlyListを実装します。 書ける



 IReadOnlyList<Foo> Do(IReadOnlyList<Bar> bar) { return bar.Where(x => IsGood(x)).ToList(); }
      
      







結論



シグネチャでのIEnumerableメソッドの広範な使用は、契約プログラミングの原則に違反し、エラーにつながります。



メソッド間での受け渡しには、IEnumerableを実際に使用する必要がある場合にのみIEnumerableを使用できます。通常のコレクションの使用は効果的でも不可能でもありません。



メソッド間で読み取り専用コレクションを転送するには、配列、ReadOnlyCollectionクラス、およびIReadOnlyListインターフェイスを使用できます。



PS

同じトピックについて、Habrに関する別の記事があります。



All Articles