web-dev-qa-db-ja.com

IEnumerableの複数の列挙動の可能性がある場合の警告の処理

私のコードではIEnumerable<>を数回使う必要があるので、「IEnumerableが複数回列挙される可能性があります」というResharperエラーを受け取ります。

サンプルコード

public List<object> Foo(IEnumerable<object> objects)
{
    if (objects == null || !objects.Any())
        throw new ArgumentException();

    var firstObject = objects.First();
    var list = DoSomeThing(firstObject);        
    var secondList = DoSomeThingElse(objects);
    list.AddRange(secondList);

    return list;
}
  • objectsパラメーターをListに変更してから、可能性のある多重列挙を回避することはできますが、処理できる最高のオブジェクトを取得できません。
  • 私ができるもう一つのことは、メソッドの先頭でIEnumerableListに変換することです。

 public List<object> Foo(IEnumerable<object> objects)
 {
    var objectList = objects.ToList();
    // ...
 }

しかしこれはまさにぎこちないです。

このシナリオで何をしますか?

329
gdoron

パラメータとしてIEnumerableを使用することの問題は、呼び出し側に「これを列挙したい」と伝えることです。何回列挙したいのかはわかりません。

オブジェクトパラメータをListに変更して、可能性のある多重列挙を回避することはできますが、その場合は取得できません処理できる最高のオブジェクト

最も高いオブジェクトを取得するという目標は高貴ですが、あまりにも多くの仮定の余地があります。このメソッドにLINQ to SQLクエリを渡してほしいのですが、2回列挙するだけです(毎回異なる結果になる可能性があります)。

ここで欠けている意味論は、おそらくメソッドの詳細を読むのに時間がかからない呼び出し側があなたが一度だけ繰り返すと仮定するかもしれないということです - それで彼らはあなたに高価なオブジェクトを渡します。メソッドシグネチャはどちらの方法も示していません。

メソッドのシグネチャをIList/ICollectionに変更することで、少なくとも呼び出し元には何が期待されているのかを明確にし、コストのかかるミスを回避できます。

そうでなければ、メソッドを見ているほとんどの開発者はあなたが一度だけ繰り返すと仮定するかもしれません。 IEnumerableを取ることが非常に重要な場合は、メソッドの先頭で.ToList()を実行することを検討してください。

それは残念だ。NETはAdd/Removeなどのメソッドなしで、IEnumerable + Count + Indexerであるインターフェースを持っていない、それが私がこの問題を解決すると疑うものである。

442
Paul Stovell

あなたのデータが常に再現可能になるのであれば、おそらくそれについて心配しないでください。ただし、展開することもできます。着信データが大きくなる可能性がある場合(ディスクやネットワークからの読み取りなど)、これは特に便利です。

if(objects == null) throw new ArgumentException();
using(var iter = objects.GetEnumerator()) {
    if(!iter.MoveNext()) throw new ArgumentException();

    var firstObject = iter.Current;
    var list = DoSomeThing(firstObject);  

    while(iter.MoveNext()) {
        list.Add(DoSomeThingElse(iter.Current));
    }
    return list;
}

注意DoSomethingElseの意味を少し変更しましたが、これは主に展開された使用法を示すためです。たとえば、イテレータをラップし直すことができます。イテレータブロックにすることもできます。これはいいかもしれません。それでlistはありません - そして返されるリストに追加するのではなく、アイテムを取得したときにyield returnします。

29
Marc Gravell

目的がMarc Gravellによる答えよりも複数の列挙を防ぐことである場合は、同じ意味を維持しながら、冗長なAnyFirstの呼び出しを単純に削除して、次のようにすることができます。

public List<object> Foo(IEnumerable<object> objects)
{
    if (objects == null)
        throw new ArgumentNullException("objects");

    var first = objects.FirstOrDefault();

    if (first == null)
        throw new ArgumentException(
            "Empty enumerable not supported.", 
            "objects");

    var list = DoSomeThing(first);  

    var secondList = DoSomeThingElse(objects);

    list.AddRange(secondList);

    return list;
}

これは、あなたがIEnumerableをジェネリックではないか、少なくとも参照型になるように制約されていると仮定していることに注意してください。

4
João Angelo

メソッドのシグネチャでIReadOnlyCollection<T>の代わりにIReadOnlyList<T>またはIEnumerable<T>を使用すると、反復前にカウントをチェックする必要があること、またはその他の理由で複数回反復する必要があることを明示的に示すことができます。

ただし、コードをインターフェイスを使用するようにリファクタリングしようとすると、動的プロキシに対してよりテストしやすくなり、使いやすくなるなど、大きな欠点があります。重要な点は、IList<T>IReadOnlyList<T>から継承されないことです。他のコレクションおよびそれぞれの読み取り専用インターフェースについても同様です。 (要するに、これは.NET 4.5が以前のバージョンとABIの互換性を保ちたがっていたためです。 しかし、.NETコアでそれを変更する機会さえも取られませんでした。

つまり、プログラムのある部分からIList<T>を取得し、それをIReadOnlyList<T>を予期する別の部分に渡したい場合は、できません。 ただし、IList<T>IEnumerable<T>として渡すこともできます

結局、IEnumerable<T>は、すべてのコレクションインターフェイスを含むすべての.NETコレクションでサポートされている唯一の読み取り専用インターフェイスです。アーキテクチャーの選択肢から締め出されたことに気付いたときには、他の方法であなたを噛んでください。だから私はそれがあなただけの読み取り専用のコレクションが欲しいことを表現するために関数のシグネチャで使用するのが適切な型だと思います。

(基礎となる型が両方のインタフェースをサポートしていれば、単純キャストするIReadOnlyList<T> ToReadOnly<T>(this IList<T> list)拡張メソッドをいつでも書くことができますが、リファクタリングするときはどこにでも手動で追加する必要があります。IEnumerable<T>は常に互換性があります。)

いつものようにこれは絶対的なものではありません、偶然の多重列挙が災害になるようなデータベースを多用するコードを書いているのであれば、あなたは別のトレードオフを好むかもしれません。

4
Gabriel Morin

このような状況では、通常IEnumerableとIListでメソッドをオーバーロードします。

public static IEnumerable<T> Method<T>( this IList<T> source ){... }

public static IEnumerable<T> Method<T>( this IEnumerable<T> source )
{
    /*input checks on source parameter here*/
    return Method( source.ToList() );
}

IEnumerableを呼び出すと.ToList()が実行されることになるメソッドのコメントをまとめて説明するように注意します。

複数の操作が連結されている場合、プログラマーはより高いレベルで.ToList()を選択してからIListオーバーロードを呼び出すか、またはIEnumerableオーバーロードにそれを処理させることができます。

4
Mauro Sampietro

最初の要素だけをチェックする必要がある場合は、コレクション全体を繰り返すことなくそれを覗くことができます。

public List<object> Foo(IEnumerable<object> objects)
{
    object firstObject;
    if (objects == null || !TryPeek(ref objects, out firstObject))
        throw new ArgumentException();

    var list = DoSomeThing(firstObject);
    var secondList = DoSomeThingElse(objects);
    list.AddRange(secondList);

    return list;
}

public static bool TryPeek<T>(ref IEnumerable<T> source, out T first)
{
    if (source == null)
        throw new ArgumentNullException(nameof(source));

    IEnumerator<T> enumerator = source.GetEnumerator();
    if (!enumerator.MoveNext())
    {
        first = default(T);
        source = Enumerable.Empty<T>();
        return false;
    }

    first = enumerator.Current;
    T firstElement = first;
    source = Iterate();
    return true;

    IEnumerable<T> Iterate()
    {
        yield return firstElement;
        using (enumerator)
        {
            while (enumerator.MoveNext())
            {
                yield return enumerator.Current;
            }
        }
    }
}
0
Madruel