再びCの空の列挙について#

この投稿には、 IEnumerableが空であることを確認する方法に関する長年の問題(およびアドバイス記事 )に言及しているHabréの新しい記事に触発されました。 ただし、元の記事では、著者は、フォームのチェックが次のようになっていると仮定して、チェックの発行方法に重点を置いています。



public static bool IsNullOrEmpty<T>(this IEnumerable<T> items) { return items == null || !items.Any(); }
      
      





十分ですが、私の意見では、そのようなアプローチは常に適用できるとは限りません。



事実、 IEnumerableは基本的にIIteratorのファクトリーですが、呼び出しコードの観点から見ると、イテレーターを作成するコストはまったく予測できません。 C#では、リストと配列、およびIEnumerableの両方で、同じforeach演算子と同じLINQメソッドを使用して、 IIteratorの作成を隠し、多くの開発者にとって配列と区別できない配列を作成できるため、状況は複雑です。およびIEnumerable 。 ただし、一見無害なアイテムを呼び出す場合、その差は非常に大きくなる可能性があります。 任意の()コードでは、接続の作成やデータベースのクエリ、REST Apiの呼び出し、または多くの重い計算など、リソースを大量に消費する操作が発生します。



 private IEnumerable<int> GetItemsDb() { using (var connection = new SqlConnection("connection string")) { connection.Open(); using (var command = new SqlCommand("SELECT Id FROM Table")) { using (SqlDataReader reader = command.ExecuteReader()) { while (reader.Read()) { yield return reader.GetInt32(0); } } } } }
      
      





 private IEnumerable<int> GetItemsLinq() { return Enumerable .Range(0, 100) .Reverse() .Select( i => { Thread.Sleep(100); return i; }) .Where(i => i < 10); }
      
      





しかし、そのような反復子が要素を返すかどうかを調べる必要がある場合はどうすればよいでしょうか? たとえば、 GetItemsLinq()によって返されるすべての要素を合計するコードを記述する必要がありますが、要素がない場合、コードはnullを返す必要があります



 int? result = GetSum(GetItemsLinq()); ... private static int? GetSum(IEnumerable<int> items) { ... }
      
      





私は、多くの人が次のようにGetSumメソッドを実装すると思います。



 private static int? GetSum(IEnumerable<int> items) { return items != null && items.Any() ? (int?)items.Sum() : null; }
      
      





そして... GetSum(GetItemsLinq())が予想される10ではなく19.1秒以内に実行される状況に直面します。 実際、 items.Any()については、出力に何かがあることを理解するために91個の元の要素を反復処理する必要があり、各要素に100ミリ秒を費やしています。 GetSumメソッドを少し最適化してみましょう。



 private static int? GetSumm(IEnumerable<int> items) { var list = items as IReadOnlyCollection<int> ?? items?.ToList(); return list != null && list.Count > 0 ? (int?)list.Sum() : null; }
      
      





これで、コードは予想される10秒で実行されますが、メモリ内に中間バッファーを割り当てるコストがかかります。 そして、もし10個以上の要素があるとしたら? 一般に、引き続き最適化できます:



 private static int? GetSum(IEnumerable<int> items) { if (items == null) return null; using (var enumerator = items.GetEnumerator()) { if (enumerator.MoveNext()) { int result = enumerator.Current; while (enumerator.MoveNext()) { result += enumerator.Current; } return result; } return null; } }
      
      





これでコードは10秒で実行され、不要なメモリを消費しませんが、... IEnumerableの要素をチェックする必要があるたびにそのようなコードを書きたくありません。 このコードを再利用する方法はありますか? 私の提案は、関数を引数として取る拡張メソッドを作成し、そのメソッドに既知の空でないIEnumerableを渡すことです。 この関数は、このIEnumerableの処理結果を返すことになっています。 「明らかに空でないIEnumerable」は、既に開いているイテレータのラッパーであり、最初の要素の要求に応じて、既に受信した最初の要素を返し、列挙を続行します。



 public static class EnumerableHelper { public static TRes ProcessIfNotEmpty<T, TRes>( this IEnumerable<T> source, Func<IEnumerable<T>, TRes> handler, Func<TRes> defaultValue) { switch (source) { case null: return defaultValue(); case IReadOnlyCollection<T> collection: return collection.Count > 0 ? handler(collection) : defaultValue(); default: using (var enumerator = new DisposeGuardWrapper<T>(source.GetEnumerator())) { if (enumerator.MoveNext()) { return handler(Continue(enumerator.Current, enumerator)); } } return defaultValue(); } } private static IEnumerable<T> Continue<T>(T first, IEnumerator<T> startedEnumerator) { yield return first; while (startedEnumerator.MoveNext()) { yield return startedEnumerator.Current; } } private class DisposeGuardWrapper<T> : IEnumerator<T> { ... } }
      
      





完全なソースコードはこちら



このヘルパーメソッドを使用して、次のように問題を解決できます。



 int? result = GetItemsLinq().ProcessIfNotEmpty(items=> items.Sum(), () => (int?)null);
      
      





残念ながら、このアプローチには重大な欠点が1つあります。要素の存在を確認するために作成されたイテレータが適切に閉じられないため、 ProcessIfNotEmptyの外部のアイテムを操作できません。 そのような場合を防ぐために、DisposeGuardWrapperクラスが作成され 、次のコードは例外をスローします。



 int? result = GetItemsLinq().ProcessIfNotEmpty(items=> items, () => null).Sum();
      
      





もちろん不快ですが、私の意見では、毎回余分なリソースを浪費したり、テンプレートコードを記述したりするよりはましです。 誰かが最良の選択肢を提供するかもしれません。



All Articles