it-swarm.com.de

Verursacht "foreach" eine wiederholte Ausführung von Linq?

Ich habe zum ersten Mal mit dem Entity Framework in .NET gearbeitet und LINQ-Abfragen geschrieben, um Informationen aus meinem Modell zu erhalten. Ich möchte von Anfang an gute Gewohnheiten programmieren, daher habe ich nachgeforscht, wie diese Abfragen am besten geschrieben und ihre Ergebnisse erzielt werden können. Leider habe ich beim Durchsuchen von Stack Exchange anscheinend zwei widersprüchliche Erklärungen gefunden, wie die verzögerte/sofortige Ausführung mit LINQ funktioniert:

  • Ein foreach bewirkt, dass die Abfrage in jeder Iteration der Schleife ausgeführt wird:

Demonstriert in Frage Langsames Foreach () bei einer LINQ-Abfrage - ToList () steigert die Leistung immens - warum ist das so? bedeutet, dass "ToList ()" aufgerufen werden muss, um die Abfrage sofort auszuwerten, da foreach die Abfrage in der Datenquelle wiederholt auswertet, was den Vorgang erheblich verlangsamt.

Ein weiteres Beispiel ist die Frage Foreaching durch gruppierte Linq-Ergebnisse ist unglaublich langsam, irgendwelche Tipps? , wobei die akzeptierte Antwort auch impliziert, dass das Aufrufen von "ToList ()" für die Abfrage die Leistung verbessert.

  • Ein foreach führt dazu, dass eine Abfrage einmal ausgeführt wird, und ist für die Verwendung mit LINQ sicher

Demonstriert in Frage Führt foreach die Abfrage nur einmal aus? , die Implikation ist, dass foreach eine Aufzählung erstellt und die Datenquelle nicht jedes Mal abfragt.

Das fortgesetzte Durchsuchen der Website hat viele Fragen aufgeworfen, bei denen "wiederholte Ausführung während einer foreach-Schleife" die Ursache für die Leistungsproblematik ist, und viele andere Antworten, die besagen, dass foreach eine einzelne Abfrage aus einer Datenquelle ordnungsgemäß abruft, was bedeutet, dass beide Erklärungen scheinen Gültigkeit zu haben. Wenn die Hypothese "ToList ()" falsch ist (wie die meisten aktuellen Antworten vom 05.06.2013, 1:51 UhrPM EST zu implizieren scheinen), woher kommt dieses Missverständnis? Gibt es eine dieser Erklärungen, die zutreffend ist und eine, die nicht zutreffend ist, oder gibt es verschiedene Umstände, die dazu führen können, dass eine LINQ-Abfrage unterschiedlich ausgewertet wird?

Bearbeiten: Zusätzlich zu der unten akzeptierten Antwort habe ich die folgende Frage zu Programmierern gestellt, die mir das Verständnis der Abfrageausführung sehr erleichtert hat, insbesondere die Fallstricke, die während einer Schleife zu mehreren Datenquellentreffern führen können Ich denke, dies ist hilfreich für andere, die sich für diese Frage interessieren: https://softwareengineering.stackexchange.com/questions/178218/for-vs-foreach-vs-linq

26
Mejwell

Im Allgemeinen verwendet LINQ die verzögerte Ausführung. Wenn Sie Methoden wie First() und FirstOrDefault() verwenden, wird die Abfrage sofort ausgeführt. Wenn du etwas machst wie;

foreach(string s in MyObjects.Select(x => x.AStringProp))

Die Ergebnisse werden auf Streaming-Weise abgerufen, dh nacheinander. Jedes Mal, wenn der Iterator MoveNextaufruft, wird die Projektion auf das nächste Objekt angewendet. Wenn Sie einen Wherehätten, würde er zuerst den Filter anwenden, dann die Projektion.

Wenn du etwas machst wie;

List<string> names = People.Select(x => x.Name).ToList();
foreach (string name in names)

Dann halte ich das für eine verschwenderische Operation. ToList() erzwingt die Ausführung der Abfrage, listet die Peoplename__-Liste auf und wendet die x => x.Name-Projektion an. Anschließend werden Sie die Liste erneut aufzählen. Wenn Sie also keinen guten Grund haben, die Daten in einer Liste zu haben (anstatt IEnumerale), verschwenden Sie nur CPU-Zyklen.

Im Allgemeinen hat die Verwendung einer LINQ-Abfrage für die Auflistung, die Sie mit foreach auflisten, keine schlechtere Leistung als alle anderen ähnlichen und praktischen Optionen.

Beachten Sie auch, dass Benutzer, die LINQ-Anbieter implementieren, aufgefordert werden, die allgemeinen Methoden so zu verwenden, wie sie bei den von Microsoft bereitgestellten Anbietern funktionieren, dies jedoch nicht erforderlich ist. Wenn ich einen LINQ to HTML- oder LINQ to My Proprietary Data Format-Anbieter schreiben würde, gäbe es keine Garantie dafür, dass er sich auf diese Weise verhält. Vielleicht würde die Art der Daten eine sofortige Ausführung zur einzig praktikablen Option machen.

Auch endgültige Bearbeitung; Wenn Sie daran interessiert sind, ist Jon Skeets C # In Depth sehr informativ und eine großartige Lektüre. Meine Antwort fasst ein paar Seiten des Buches zusammen (hoffentlich mit angemessener Genauigkeit), aber wenn Sie mehr darüber erfahren möchten, wie LINQ im Hintergrund funktioniert, ist dies ein guter Ort, um nachzuschauen.

16
evanmcdonnal

versuchen Sie dies auf LinqPad

void Main()
{
    var testList = Enumerable.Range(1,10);
    var query = testList.Where(x => 
    {
        Console.WriteLine(string.Format("Doing where on {0}", x));
        return x % 2 == 0;
    });
    Console.WriteLine("First foreach starting");
    foreach(var i in query)
    {
        Console.WriteLine(string.Format("Foreached where on {0}", i));
    }

    Console.WriteLine("First foreach ending");
    Console.WriteLine("Second foreach starting");
    foreach(var i in query)
    {
        Console.WriteLine(string.Format("Foreached where on {0} for the second time.", i));
    }
    Console.WriteLine("Second foreach ending");
}

Jedes Mal, wenn der Stellvertreter ausgeführt wird, wird eine Konsolenausgabe angezeigt, sodass die Linq-Abfrage jedes Mal ausgeführt wird. Wenn wir uns nun die Konsolenausgabe ansehen, sehen wir, dass die zweite foreach-Schleife immer noch das "Doing where on" ausgibt, was zeigt, dass die zweite Verwendung von foreach tatsächlich dazu führt, dass die where-Klausel erneut ausgeführt wird ... was möglicherweise zu einer Verlangsamung führt .

First foreach starting
Doing where on 1
Doing where on 2
Foreached where on 2
Doing where on 3
Doing where on 4
Foreached where on 4
Doing where on 5
Doing where on 6
Foreached where on 6
Doing where on 7
Doing where on 8
Foreached where on 8
Doing where on 9
Doing where on 10
Foreached where on 10
First foreach ending
Second foreach starting
Doing where on 1
Doing where on 2
Foreached where on 2 for the second time.
Doing where on 3
Doing where on 4
Foreached where on 4 for the second time.
Doing where on 5
Doing where on 6
Foreached where on 6 for the second time.
Doing where on 7
Doing where on 8
Foreached where on 8 for the second time.
Doing where on 9
Doing where on 10
Foreached where on 10 for the second time.
Second foreach ending
6
Aron

Dies hängt davon ab, wie die Linq-Abfrage verwendet wird.

var q = {some linq query here}

while (true)
{
    foreach(var item in q)
    {
    ...
    }
}

Der obige Code führt die Linq-Abfrage mehrmals aus. Nicht wegen foreach, sondern weil foreach sich in einer anderen Schleife befindet und foreach selbst mehrmals ausgeführt wird.

Wenn alle Konsumenten einer Linq-Abfrage sie "vorsichtig" verwenden und dumme Fehler wie die oben genannten verschachtelten Schleifen vermeiden, sollte eine Linq-Abfrage nicht unnötigerweise mehrmals ausgeführt werden.

Es gibt Fälle, in denen eine linq-Abfrage mit ToList () auf eine speicherinterne Ergebnismenge reduziert werden muss, aber meiner Meinung nach wird ToList () viel zu oft verwendet. ToList () wird fast immer zu einer Giftpille, wenn große Datenmengen betroffen sind, da die gesamte Ergebnismenge (möglicherweise Millionen von Zeilen) in den Speicher abgerufen und zwischengespeichert wird, selbst wenn der äußerste Consumer/Enumerator nur 10 Zeilen benötigt. Vermeiden Sie ToList (), es sei denn, Sie haben eine ganz bestimmte Begründung und Sie wissen, dass Ihre Daten niemals umfangreich sein werden.

4
dthorpe

Manchmal könnte es eine gute Idee sein, eine LINQ-Abfrage mit ToList() oder ToArray() zu "cachen", wenn in Ihrem Code mehrmals auf die Abfrage zugegriffen wird.

Denken Sie jedoch daran, dass beim "Cachen" immer noch ein foreachaufgerufen wird.

Die Grundregel für mich lautet also:

  • wenn eine Abfrage einfach in einer foreachverwendet wird (und das ist es) - dann kann ich die Abfrage nicht zwischenspeichern
  • wenn eine Abfrage in einem foreachname__ und an einigen anderen Stellen im Code verwendet wird, speichere ich sie in einem var mit ToList/ToArray
3
jazzcat

foreach selbst durchläuft seine Daten nur einmal. Tatsächlich läuft es spezifisch einmal durch. Sie können weder nach vorne noch nach hinten schauen oder den Index mit einer for-Schleife so ändern, wie Sie es können.

Wenn Ihr Code jedoch mehrere foreach enthält, die alle mit derselben LINQ-Abfrage ausgeführt werden, wird die Abfrage möglicherweise mehrmals ausgeführt. Dies hängt jedoch vollständig von data ab. Wenn Sie eine LINQ-basierte IEnumerable/IQueryable durchlaufen, die eine Datenbankabfrage darstellt, wird diese Abfrage jedes Mal ausgeführt. Wenn Sie eine List oder eine andere Sammlung von Objekten durchlaufen, wird die Liste jedes Mal durchlaufen, Ihre Datenbank jedoch nicht wiederholt.

Mit anderen Worten, dies ist eine Eigenschaft vonLINQ, keine Eigenschaft von foreach .

2
Bobson

Der Unterschied liegt im zugrunde liegenden Typ. Da LINQ auf IEnumerable (oder IQueryable) aufbaut, kann derselbe LINQ-Operator völlig unterschiedliche Leistungsmerkmale aufweisen.

Eine Liste wird immer schnell zu beantworten sein, es ist jedoch eine Vorarbeit erforderlich, um eine Liste zu erstellen.

Ein Iterator ist auch IEnumerable und kann jedes Mal, wenn er das "nächste" Element abruft, einen beliebigen Algorithmus verwenden. Dies ist schneller, wenn Sie nicht den gesamten Artikelbestand durchgehen müssen.

Sie können jeden IEnumerable in eine Liste verwandeln, indem Sie ToList () aufrufen und die resultierende Liste in einer lokalen Variablen speichern. Dies ist ratsam, wenn

  • Sie sind nicht auf eine verzögerte Ausführung angewiesen.
  • Sie müssen auf mehr Gesamtelemente als den gesamten Satz zugreifen.
  • Sie können die Vorabkosten für das Abrufen und Speichern aller Artikel bezahlen.
1
Tormod

Wenn Sie LINQ auch ohne Entitäten verwenden, wird die verzögerte Ausführung wirksam. Nur durch Erzwingen einer Iteration wird der tatsächliche linq-Ausdruck ausgewertet. In diesem Sinne wird der linq-Ausdruck jedes Mal ausgewertet, wenn Sie ihn verwenden.

Bei Entities ist dies immer noch das Gleiche, aber hier ist nur mehr Funktionalität am Werk. Wenn das Entity Framework den Ausdruck zum ersten Mal sieht, sieht es so aus, als ob er diese Abfrage bereits ausgeführt hat. Andernfalls wird es in die Datenbank gehen und die Daten abrufen, das interne Speichermodell einrichten und die Daten an Sie zurückgeben. Wenn das Entitäts-Framework erkennt, dass es die Daten bereits zuvor abgerufen hat, wird es nicht in die Datenbank gehen und das zuvor eingerichtete Speichermodell verwenden, um Daten an Sie zurückzugeben.

Dies kann Ihr Leben leichter machen, aber es kann auch ein Schmerz sein. Zum Beispiel, wenn Sie alle Datensätze aus einer Tabelle mit einem Linq-Ausdruck anfordern. Das Entity Framework lädt alle Daten aus der Tabelle. Wenn Sie später denselben Linq-Ausdruck auswerten, selbst wenn Datensätze zum Zeitpunkt des Löschens oder Hinzufügens hinzugefügt wurden, erhalten Sie dasselbe Ergebnis.

Das Entity-Framework ist eine komplizierte Sache. Es gibt natürlich Möglichkeiten, die Abfrage erneut ausführen zu lassen, wobei die Änderungen berücksichtigt werden, die sie an ihrem eigenen Speichermodell und dergleichen vorgenommen hat.

Ich schlage vor, "Programming Entity Framework" von Julia Lerman zu lesen. Es werden viele Probleme angesprochen, wie Sie sie gerade haben.

0
Philip Stuyck