LINQ und IEnumerable<T>
ist Pull basierte. Dies bedeutet, dass die Prädikate und Aktionen, die Teil der LINQ-Anweisung im Allgemeinen sind, erst ausgeführt werden, wenn Werte abgerufen werden. Darüber hinaus werden die Prädikate und Aktionen jedes Mal ausgeführt, wenn Werte abgerufen werden (z. B. gibt es kein geheimes Caching).
von einem IEnumerable<T>
Pulling wird durch die foreach
Aussage gemacht, die wirklich syntaktischen Zucker ist ein Enumerator für das Erhalten von IEnumerable<T>.GetEnumerator()
Aufruf und IEnumerator<T>.MoveNext()
Aufruf wiederholte die Werte zu ziehen.
LINQ-Operatoren wie ToList()
, ToArray()
, ToDictionary()
und ToLookup()
wickelt eine foreach
Aussage, so dass diese Methoden einen Zug tun. Dasselbe kann über Operatoren wie Aggregate()
, Count()
und First()
gesagt werden. Diese Methoden haben gemeinsam, dass sie ein einzelnes Ergebnis erzeugen, das durch Ausführen einer foreach
-Anweisung erstellt werden muss.
Viele LINQ-Operatoren erzeugen eine neue IEnumerable<T>
Sequenz. Wenn ein Element aus der resultierenden Sequenz gezogen wird, zieht der Operator ein oder mehrere Elemente aus der Quellsequenz. Der Operator Select()
ist das offensichtlichste Beispiel, aber andere Beispiele sind SelectMany()
, Where()
, Concat()
, Union()
, Distinct()
, Skip()
und Take()
. Diese Operatoren führen kein Caching durch. Wenn dann das N-te Element von einem Select()
gezogen wird, zieht es das N-te Element aus der Quellensequenz, wendet die Projektion unter Verwendung der bereitgestellten Aktion an und gibt es zurück. Nichts Geheimnis hier.
Andere LINQ-Operatoren produzieren auch neue IEnumerable<T>
Sequenzen, aber sie werden implementiert, indem die gesamte Quellsequenz tatsächlich gezogen wird, ihre Aufgabe erfüllt wird und dann eine neue Sequenz erzeugt wird. Diese Methoden umfassen Reverse()
, OrderBy()
und GroupBy()
. Der Pull, der vom Operator ausgeführt wird, wird jedoch nur ausgeführt, wenn der Operator selbst gezogen wird, was bedeutet, dass Sie noch eine foreach
-Schleife "am Ende" der LINQ-Anweisung benötigen, bevor irgendetwas ausgeführt wird. Sie könnten argumentieren, dass diese Operatoren einen Cache verwenden, weil sie sofort die gesamte Quellsequenz ziehen. Dieser Cache wird jedoch jedes Mal erstellt, wenn der Operator iteriert wird. Es handelt sich also wirklich um ein Implementierungsdetail und nicht um etwas, das auf magische Weise feststellt, dass Sie dieselbe Operation mehrmals in derselben Sequenz anwenden.
In Ihrem Beispiel wird die ToList()
ziehen. Die Aktion im äußeren Select
wird 100 Mal ausgeführt. Jedes Mal, wenn diese Aktion ausgeführt wird, führt die Aggregate()
einen weiteren Pull durch, bei dem die XML-Attribute analysiert werden. Insgesamt wird Ihr Code Int32.Parse()
200 Mal aufrufen.
Sie können dies verbessern, indem die Attribute einmal ziehen, anstatt auf jeder Iteration:
var X = XElement.Parse (@"
<ROOT>
<MUL v='2' />
<MUL v='3' />
</ROOT>
")
.Elements()
.Select (t => Int32.Parse (t.Attribute ("v").Value))
.ToList();
Enumerable.Range (1, 100)
.Select (s => x.Aggregate (s, (t, u) => t * u))
.ToList()
.ForEach (s => Console.WriteLine (s));
Jetzt wird Int32.Parse()
nur 2 mal aufgerufen. Die Kosten bestehen jedoch darin, dass eine Liste von Attributwerten zugeordnet, gespeichert und schließlich mit Müll gesammelt werden muss. (Keine große Sorge, wenn die Liste zwei Elemente enthält.)
Beachten Sie, dass wenn Sie die erste ToList()
vergessen, die die Attribute zieht, der Code weiterhin ausgeführt wird, aber mit den gleichen Leistungsmerkmalen wie der ursprüngliche Code. Es wird kein Leerzeichen zum Speichern der Attribute verwendet, aber sie werden bei jeder Iteration analysiert.
"Wie würde ich so etwas selbst herausfinden?" - das Beste ist, die IL zu studieren, die aus diesem Code generiert wird. – Andrey
Sie könnten einen Debugger-Haltepunkt für die Parse() -Methode festlegen und sehen, wie oft er trifft. –