where条件とcontinueガード句を使用したforeachループのフィルタリング


24

私はこれを使用するプログラマを見てきました:

foreach (var item in items)
{
    if (item.Field != null)
        continue;

    if (item.State != ItemStates.Deleted)
        continue;

    // code
}

私が通常使用する代わりに:

foreach (var item in items.Where(i => i.Field != null && i.State != ItemStates.Deleted))
{
    // code
}

私は両方の組み合わせを見てきました。「継続」、特により複雑な条件での読みやすさが本当に好きです。パフォーマンスに違いはありますか?データベースクエリを使用すると、あると想定しています。通常のリストはどうですか?


3
通常のリストの場合、それはマイクロ最適化のように聞こえます。
黙示録

2
@zgnilec:...しかし、実際には2つのバリアントのどちらが最適化されたバージョンですか?もちろんそれについては意見がありますが、コードを見るだけでは、これは誰にとっても本質的に明確ではありません。
Doc Brown

2
もちろん継続は速くなります。linqを使用します。追加のイテレータを作成する場所。
黙示録

1
@zgnilec-良い理論。どうしてそう思うのを説明する回答を投稿してくれませんか?現在存在する両方の答えは反対を言います。
ボブソン

2
...結論として、2つの構成要素のパフォーマンスの違いは無視でき、読みやすさとデバッグ可能性の両方を実現できます。それは単に好みの問題です。
Doc Brown

回答:


63

これは、コマンド/クエリの分離を使用する適切な場所と見なします。例えば:

// query
var validItems = items.Where(i => i.Field != null && i.State != ItemStates.Deleted);
// command
foreach (var item in validItems) {
    // do stuff
}

これにより、クエリ結果に適切な自己文書化名を付けることもできます。また、両方を実行しようとする混合コードよりも、データのみを照会するか、データのみを変更するコードをリファクタリングする方がはるかに簡単なので、リファクタリングの機会を見つけるのにも役立ちます。

デバッグするときは、前foreachに中断して、内容がvalidItems期待どおりに解決するかどうかをすばやく確認できます。必要な場合を除き、ラムダにステップインする必要はありません。ラムダにステップインする必要がある場合は、それを別の関数にファクタリングし、代わりにステップスルーすることをお勧めします。

パフォーマンスに違いはありますか?クエリがデータベースによってバックアップされている場合は、LINQのバージョンがあり潜在的な SQLクエリがあるため、より高速に実行することがより効率的にします。LINQ to Objectsの場合、実際のパフォーマンスの違いは見られません。いつものように、事前に最適化を予測しようとするのではなく、コードのプロファイルを作成し、実際に報告されるボトルネックを修正します。


1
なぜ非常に大きなデータセットが違いを生むのでしょうか?ラムダのごくわずかなコストが最終的に加算されるという理由だけで?
BlueRaja-ダニーPflughoeft

1
@ BlueRaja-DannyPflughoeft:はい、そのとおりです。この例には、元のコードを超える追加のアルゴリズムの複雑さは含まれていません。フレーズを削除しました。
クリスチャンヘイター

これにより、コレクションに対して2回の反復が発生しませんか?当然、有効なアイテムのみが含まれていることを考慮すると、2番目の要素は短くなりますが、要素をフィルターで除外するために1回、有効なアイテムで作業するために2回行う必要があります。
アンディ

1
@DavidPackerいいえ。IEnumerableforeachループによってのみ駆動されます。
ベンジャミンホジソン

2
@DavidPacker:それがまさにそれです。ほとんどのLINQ to Objectsメソッドは、イテレーターブロックを使用して実装されます。上記のサンプルコードは、コレクションを1回だけ繰り返し、Whereラムダとループボディ(ラムダがtrueを返す場合)を要素ごとに1回実行します。
クリスチャンヘイター

7

もちろん、パフォーマンスには違いがあり、.Where()すべてのアイテムに対してデリゲート呼び出しが行われます。ただし、パフォーマンスについてはまったく心配しません。

  • デリゲートの呼び出しで使用されるクロックサイクルは、コレクションを反復処理して条件をチェックする残りのコードで使用されるクロックサイクルと比較して無視できます。

  • デリゲートを呼び出すことによるパフォーマンスの低下は、数クロックサイクルのオーダーであり、幸いなことに、個々のクロックサイクルを心配しなければならなかった時代はずっと過ぎています。

何らかの理由でパフォーマンスがある場合は、本当にクロックサイクルレベルでのあなたのために重要な、そして使いList<Item>の代わりをIList<Item>するので、コンパイラは、直接(とINLINABLE)を利用できることは、仮想呼び出しの代わりに呼び出す、とのイテレータているのでList<T>、実際に、 、structボックス化する必要はありません。しかし、それは本当に些細なことです。

データベースクエリは、(少なくとも理論的には)RDBMSにフィルターを送信する可能性があるため、パフォーマンスが大幅に向上するため、状況は異なります。一致する行のみがRDBMSからプログラムに移動します。しかし、そのためにはlinqを使用する必要があると思いますが、この式をそのままRDBMSに送信できるとは思いません。

if(x) continue;このコードをデバッグする必要がある瞬間の利点が本当にわかります。s if()continues のシングルステップ実行はうまく機能します。フィルタリングデリゲートへのシングルステップは苦痛です。


それは何かが間違っていて、すべての項目を調べて、Field!= nullを持つものとState!= nullを持つものをデバッガーでチェックする場合です。これはforeachでは難しいことから不可能なことかもしれません...どこで。
gnasher729

デバッグの良い点。Visual Studioでwhereに足を踏み入れることはそれほど悪いことではありませんが、デバッグ中に再コンパイルせずにラムダ式を書き換えることはできませんif(x) continue;
パプリク

厳密に言えば、.Where一度だけ呼び出されます。どのような各反復で呼び出されると、フィルタデリゲートである(とMoveNextCurrent彼らが最適化されません列挙子、上)
CodesInChaos

@CodesInChaosそれは私があなたが話していることを理解するために少し考えましたが、もちろん、あなたは正しい、厳密に言えば、wh00ps .Whereは一度だけ呼び出されます。それを修正しました。
マイクナキス
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.