Filtern von foreach-Schleifen mit einer where-Bedingung gegen continue-Schutzklauseln

24

Ich habe gesehen, dass einige Programmierer dies verwenden:

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

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

    // code
}

anstatt wo ich normalerweise verwenden würde:

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

Ich habe sogar eine Kombination von beiden gesehen. Ich mag die Lesbarkeit mit 'Fortfahren' sehr, besonders bei komplexeren Bedingungen. Gibt es überhaupt einen Leistungsunterschied? Bei einer Datenbankabfrage gehe ich davon aus, dass dies der Fall sein würde. Was ist mit regulären Listen?

Paprik
quelle
3
Für reguläre Listen klingt es nach Mikrooptimierung.
Apokalypse
2
@zgnilec: ... aber welche der beiden Varianten ist eigentlich die optimierte Version? Ich habe natürlich eine Meinung dazu, aber wenn man sich nur den Code ansieht, ist das nicht für alle von Natur aus klar.
Doc Brown
2
Natürlich geht es schneller weiter. Verwenden von linq. Hier erstellen Sie einen zusätzlichen Iterator.
Apokalypse
1
@zgnilec - Gute Theorie. Möchtest du eine Antwort schreiben, in der erklärt wird, warum du das denkst? Beide Antworten, die derzeit existieren, sagen das Gegenteil aus.
Bobson
2
... das Fazit lautet also: Die Leistungsunterschiede zwischen den beiden Konstrukten sind vernachlässigbar, und sowohl die Lesbarkeit als auch die Debuggbarkeit können für beide erreicht werden. Es ist einfach eine Geschmackssache, welche Sie bevorzugen.
Doc Brown

Antworten:

63

Ich würde dies als einen geeigneten Ort betrachten, um die Trennung von Befehlen und Abfragen zu verwenden . Beispielsweise:

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

Auf diese Weise können Sie dem Abfrageergebnis auch einen guten, selbstdokumentierenden Namen geben. Es hilft Ihnen auch dabei, Möglichkeiten für die Umgestaltung zu erkennen, da es viel einfacher ist, Code umzugestalten, der nur Daten abfragt oder nur Daten mutiert, als gemischten Code, der versucht, beides zu tun.

Beim Debuggen können Sie eine Pause einlegen, foreachum schnell zu überprüfen, ob der Inhalt der validItemsLösung Ihren Erwartungen entspricht. Sie müssen nicht ins Lambda steigen, es sei denn, Sie müssen. Wenn Sie in das Lambda einsteigen müssen, dann schlage ich vor, es in eine separate Funktion zu zerlegen und diese stattdessen durchzugehen.

Gibt es einen Leistungsunterschied? Wenn die Abfrage von einer Datenbank gesichert wird, kann die LINQ-Version möglicherweise schneller ausgeführt werden, da die SQL-Abfrage möglicherweise effizienter ist. Wenn es sich um LINQ to Objects handelt, werden Sie keinen echten Leistungsunterschied feststellen. Profilieren Sie Ihren Code wie immer und beheben Sie die tatsächlich gemeldeten Engpässe, anstatt im Voraus Optimierungen vorherzusagen.

Christian Hayter
quelle
1
Warum würde ein extrem großer Datensatz einen Unterschied machen? Nur weil sich die winzigen Kosten der Lambdas irgendwann summieren würden?
BlueRaja - Danny Pflughoeft
1
@ BlueRaja-DannyPflughoeft: Ja, Sie haben Recht, dieses Beispiel beinhaltet keine zusätzliche algorithmische Komplexität, die über den ursprünglichen Code hinausgeht. Ich habe den Satz entfernt.
Christian Hayter
Führt dies nicht zu zwei Iterationen der Sammlung? Natürlich ist der zweite kürzer, da nur gültige Elemente enthalten sind. Sie müssen ihn jedoch noch zweimal ausführen, einmal, um die Elemente herauszufiltern, und zum zweiten Mal, um mit den gültigen Elementen zu arbeiten.
Andy
1
@DavidPacker Nein. Das IEnumerablewird nur von der foreachSchleife gesteuert .
Benjamin Hodgson
2
@ DavidPacker: Genau das macht es. Die meisten LINQ to Objects-Methoden werden mithilfe von Iteratorblöcken implementiert. Der obige Beispielcode durchläuft die Auflistung genau einmal und führt das WhereLambda und den Schleifenkörper (wenn das Lambda true zurückgibt) einmal pro Element aus.
Christian Hayter
7

Natürlich gibt es einen Unterschied in der Leistung, was dazu .Where()führt, dass für jedes einzelne Element ein Delegiertenanruf durchgeführt wird. Um die Performance würde ich mir jedoch überhaupt keine Sorgen machen:

  • Die beim Aufrufen eines Delegaten verwendeten Taktzyklen sind im Vergleich zu den Taktzyklen, die vom Rest des Codes verwendet werden, der die Auflistung durchläuft und die Bedingungen überprüft, vernachlässigbar.

  • Der Leistungsnachteil beim Aufrufen eines Delegaten liegt in der Größenordnung einiger Taktzyklen, und glücklicherweise sind wir längst über die Tage hinausgegangen, an denen wir uns um einzelne Taktzyklen kümmern mussten.

Wenn aus irgendeinem Grund die Leistung auf Taktebene für Sie wirklich wichtig ist, verwenden Sie List<Item>stattdessen IList<Item>, damit der Compiler direkte (und inlinierbare) Aufrufe anstelle von virtuellen Aufrufen verwenden kann, und damit der Iterator von List<T>, der tatsächlich ist a struct, muss nicht eingepackt werden. Aber das ist wirklich Kleinigkeit.

Eine Datenbankabfrage stellt eine andere Situation dar, da (zumindest theoretisch) die Möglichkeit besteht, den Filter an das RDBMS zu senden, was die Leistung erheblich verbessert: Nur übereinstimmende Zeilen lösen die Reise vom RDBMS zu Ihrem Programm aus. Aber dafür müssten Sie wahrscheinlich linq verwenden. Ich glaube nicht, dass dieser Ausdruck so wie er ist an das RDBMS gesendet werden kann.

Sie werden die Vorteile sofort erkennen, if(x) continue;wenn Sie diesen Code debuggen müssen: Das einfache Überschreiten von if()s und continues funktioniert einwandfrei. Ein einziger Schritt in den Filter-Delegierten ist ein Schmerz.

Mike Nakis
quelle
In diesem Fall stimmt etwas nicht und Sie möchten alle Elemente anzeigen und im Debugger überprüfen, welche Field! = Null und welche State! = Null haben. das könnte schwierig bis unmöglich sein mit foreach ... wo.
gnasher729
Guter Punkt beim Debuggen. Das Betreten eines Where ist in Visual Studio nicht so schlimm, aber Sie können Lambda-Ausdrücke beim Debuggen nicht neu schreiben, ohne sie neu zu kompilieren. Dies vermeiden Sie bei der Verwendung if(x) continue;.
Paprik
Genau genommen wird es .Wherenur einmal aufgerufen. Was bei jeder Iteration aufgerufen wird , ist der Filter delegiert (und MoveNextund Currentauf dem Enumerator, wenn sie nicht bekommen , optimiert out)
CodesInChaos
@CodesInChaos Ich habe ein bisschen nachgedacht, um zu verstehen, wovon du sprichst, aber natürlich, wenn du recht hast, wird das genau genommen .Wherenur einmal aufgerufen. Behoben.
Mike Nakis