IEnumerableの複数の列挙の可能性に関する警告の処理


359

私のコードでは、IEnumerable<>数回使用する必要があるため、「可能性のある複数の列挙」のResharperエラーが発生しますIEnumerable

サンプルコード:

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();
    // ...
 }

しかし、これはただ厄介なことです

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

回答:


471

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

オブジェクトパラメータをListに変更して、複数の列挙を回避できますが、処理できる最も高いオブジェクトを取得できません。

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

ここでセマンティックスが欠落しているのは、メソッドの詳細を読むのに時間がかからない呼び出し元は、一度しかイテレートしないと想定する可能性があるため、高価なオブジェクトが渡されるためです。メソッドシグネチャはどちらの方法も示していません。

メソッドのシグネチャをIList/に変更ICollectionすることで、少なくとも呼び出し側に期待値を明確にし、コストのかかる間違いを回避できます。

それ以外の場合、メソッドを確認するほとんどの開発者は、一度しか反復しないと想定するかもしれません。を取ることIEnumerableが非常に重要な場合.ToList()は、メソッドの開始時にを行うことを検討する必要があります。

残念です。.NETには、IEnumerable + Count + Indexerのインターフェイスがなく、Add / Removeなどのメソッドがないため、この問題を解決できると思います。


32
ReadOnlyCollection <T>は、必要なインターフェイス要件を満たしていますか? msdn.microsoft.com/en-us/library/ms132474.aspx
DanはFirelightによっていじられています

74
@DanNeely私はIReadOnlyCollection(T)(.net 4.5の新機能として)IEnumerable(T)複数回列挙することを意図したものであるという考えを伝えるための最良のインターフェースとして提案します。この回答が述べているように、IEnumerable(T)それ自体は非常に一般的であり、副作用なしで再度列挙することができないリセットできないコンテンツを参照することさえあります。ただし、IReadOnlyCollection(T)反復可能性を意味します。
binki 2013

1
.ToList()メソッドの開始時にを行う場合は、最初にパラメーターがnullでないかどうかを確認する必要があります。つまり、ArgumentExceptionをスローする場所は2つあります。パラメータがnullの場合と、項目がない場合です。
comecme、2015年

私はvs のセマンティクスに関するこの記事を読んだ後、パラメーターとしての使用、およびその場合についての質問を投稿しまし。私が最終的に得た結論は従うべき論理だと思います。列挙が期待される場所で使用する必要があります。必ずしも複数の列挙がある可能性がある場所ではありませんIReadOnlyCollection<T>IEnumerable<T>IReadOnlyCollection<T>
2016

32

データが常に反復可能である場合、おそらくそれについて心配する必要はありません。ただし、アンロールすることもできます。これは、受信データが大きい可能性がある場合(たとえば、ディスク/ネットワークからの読み取り)に特に役立ちます。

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返されるリストに追加するのではなく、取得したアイテムを取得します。


4
+1まあ、それは非常に素晴らしいコードですが、私はコードの可愛らしさと可読性を失っています。
gdoronは

5
@gdoronすべてがきれいではありません; pただし、上記は判読できません。特にそれがイテレータブロックにされた場合-かなりかわいいIMO。
Marc Gravell

「var objectList = objects.ToList();」と書くだけです。列挙子の処理をすべて保存します。
gdoronはモニカをサポートしています

10
@gdoronがあなたが本当にそれをしたくなかったと明示的に言った質問の中で; pまた、ToList()アプローチは、データが非常に大きい場合(潜在的に、無限-シーケンスが有限である必要はないが、まだ繰り返されます)。結局、私はオプションを提示しているだけです。完全なコンテキストを知っているのはあなただけです。データが繰り返し可能な場合、別の有効なオプションは次のとおりです。何も変更しないでください!代わりにR#を無視してください...それはすべてコンテキストに依存します。
Marc Gravell

1
マルクの解決策はとても良いと思います。1.「リスト」がどのように初期化されるか、リストが反復される方法、およびリストのどのメンバーが操作されるかが非常に明確です。
キースホフマン

9

メソッドシグネチャでの代わりに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>常に互換性があるため、リファクタリング時にどこにでも手動で追加する必要があることに注意してください。)

これは絶対的なものではないため、偶発的な複数の列挙が障害となるデータベースを多用するコードを作成している場合は、別のトレードオフを好むかもしれません。


2
+1古い投稿に来て、.NETプラットフォームが提供する新機能(IReadOnlyCollection <T>)でこの問題を解決する新しい方法に関するドキュメントを追加した場合
Kevin Avignon

4

Marc Gravellによる回答が複数の列挙を防ぐことを本当に目的とする場合は、読み取る必要がありますが、同じセマンティクスを維持するには、冗長なAnyとを削除してFirst、次のようにします。

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が一般的ではないか、少なくとも参照型に制限されていることを前提としています。


2
objectsリストを返すDoSomethingElseにまだ渡されているため、2回列挙している可能性があります。どちらの方法でも、コレクションがLINQ to SQLクエリの場合、DBを2回ヒットします。
ポールストーベル

1
@PaulStovell、これを読んでください:「もし目標が本当に複数の列挙を防ぐことであるなら、マークグラベルの答えが読むべきものである」=)
gdoronはモニカをサポートしています

空のリストの例外のスローに関する他のスタックオーバーフローの投稿で提案されていますが、ここで提案します。なぜ空のリストで例外をスローするのですか?空のリストを取得し、空のリストを与えます。パラダイムとして、それは非常に簡単です。呼び出し元が空のリストを渡していることを知らない場合、それは問題です。
キースホフマン

@Keith Hoffman、サンプルコードは、OPがポストしたコードと同じ動作を維持します。ここで、を使用するとFirst、空のリストに対して例外がスローされます。空のリストで例外をスローしない、または常に空のリストで例外をスローすることはできないと思います。状況によって異なります
–JoãoAngelo

十分に公正なジョアン。投稿に対する批判よりも、考えるための食べ物のほうが多い。
キース・ホフマン

3

通常、この状況では、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オーバーロードにそれを処理させることができます。


20
これは恐ろしいことです。
Mike Chamberlain

1
@MikeChamberlainええ、それは本当に恐ろしいと同時に完全にきれいです。残念ながら、いくつかの重いシナリオでは、このアプローチが必要です。
Mauro Sampietro 2017年

1
ただし、IEnumerableパラメータ化されたメソッドに渡すたびに配列を再作成します。これに関して@MikeChamberlainに同意します。これはひどい提案です。
Krythic

2
@Krythicリストを再作成する必要がない場合は、別の場所でToListを実行し、IListオーバーロードを使用します。それがこのソリューションのポイントです。
Mauro Sampietro

0

最初の要素のみをチェックする必要がある場合は、コレクション全体を反復せずにそれを覗くことができます。

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;
            }
        }
    }
}

-3

まず、その警告は必ずしもそれほど意味がないわけではありません。パフォーマンスのボトルネックではないことを確認した後、私は通常それを無効にしました。これは単にIEnumerableが2回評価されることを意味しevaluationます。通常、それ自体に長い時間がかからない限り、問題はありません。時間がかかる場合でも、この場合、最初に使用するのは1つの要素だけです。

このシナリオでは、強力なlinq拡張メソッドをさらに活用することもできます。

var firstObject = objects.First();
return DoSomeThing(firstObject).Concat(DoSomeThingElse(objects).ToList();

IEnumerable面倒な場合、この場合は一度だけを評価することができますが、最初にプロファイルを作成して、それが本当に問題かどうかを確認します。


15
私はIQueryableを使用して多くの作業を行っており、そのような警告をオフにすることは非常に危険です。
gdoronがモニカをサポートしている

5
二度評価することは私の本では悪いことです。メソッドがIEnumerableを取る場合、LINQ to SQLクエリをそのメソッドに渡そうとする可能性があり、その結果、複数のDB呼び出しが発生します。メソッドシグネチャは、2回列挙される可能性があることを示していないため、シグネチャを変更する(IList / ICollectionを受け入れる)か、1回だけ繰り返す必要があります
Paul Stovell

4
私の本では、早期に最適化して読みやすさを台無しにし、それによって後で差異を生じさせる最適化を実行する可能性は、はるかに悪くなっています。
vidstige

あなたは一度確認してください、それが唯一の評価を行っています作り、データベースクエリを持っている場合は、すでに違いになります。これは理論的な将来の利益だけでなく、現在測定可能な実際の利益でもあります。それだけでなく、1回だけ評価することは、パフォーマンスにとって有益であるだけでなく、正確さのためにも必要です。データベースクエリを2回実行すると、2つのクエリ評価の間に誰かが更新コマンドを発行した可能性があるため、異なる結果が生成される可能性があります。

1
@hvd私はそのようなことをお勧めしませんでした。もちろん、IEnumerablesそれを反復するたびに異なる結果が得られることや、反復に非常に長い時間がかかることを列挙しないでください。マークグラベルの回答を参照-彼はまた、何よりもまず「心配しないでください」と述べています。事は-あなたがこれを目にすることが多いので、心配することは何もありません。
vidstige 2015年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.