Mit dem yield
Schlüsselwort können Sie ein IEnumerable<T>
Formular in einem Iteratorblock erstellen . Dieser Iteratorblock unterstützt die verzögerte Ausführung. Wenn Sie mit dem Konzept nicht vertraut sind, kann es fast magisch erscheinen. Letztendlich ist es jedoch nur Code, der ohne seltsame Tricks ausgeführt wird.
Ein Iteratorblock kann als syntaktischer Zucker beschrieben werden, bei dem der Compiler eine Zustandsmaschine generiert, die verfolgt, wie weit die Aufzählung der Aufzählung fortgeschritten ist. Um eine Aufzählung aufzulisten, verwenden Sie häufig eine foreach
Schleife. Eine foreach
Schleife ist jedoch auch syntaktischer Zucker. Sie sind also zwei Abstraktionen, die aus dem realen Code entfernt wurden, weshalb es anfangs schwierig sein kann zu verstehen, wie alles zusammenarbeitet.
Angenommen, Sie haben einen sehr einfachen Iteratorblock:
IEnumerable<int> IteratorBlock()
{
Console.WriteLine("Begin");
yield return 1;
Console.WriteLine("After 1");
yield return 2;
Console.WriteLine("After 2");
yield return 42;
Console.WriteLine("End");
}
Echte Iteratorblöcke haben häufig Bedingungen und Schleifen, aber wenn Sie die Bedingungen überprüfen und die Schleifen abrollen, werden sie immer noch als yield
Anweisungen ausgegeben, die mit anderem Code verschachtelt sind.
Um den Iteratorblock aufzulisten, wird eine foreach
Schleife verwendet:
foreach (var i in IteratorBlock())
Console.WriteLine(i);
Hier ist die Ausgabe (keine Überraschungen hier):
Start
1
Nach 1
2
Nach 2
42
Ende
Wie oben angegeben foreach
ist syntaktischer Zucker:
IEnumerator<int> enumerator = null;
try
{
enumerator = IteratorBlock().GetEnumerator();
while (enumerator.MoveNext())
{
var i = enumerator.Current;
Console.WriteLine(i);
}
}
finally
{
enumerator?.Dispose();
}
Um dies zu entwirren, habe ich ein Sequenzdiagramm mit den entfernten Abstraktionen erstellt:
Die vom Compiler generierte Zustandsmaschine implementiert auch den Enumerator, aber um das Diagramm klarer zu machen, habe ich sie als separate Instanzen gezeigt. (Wenn die Zustandsmaschine von einem anderen Thread aufgezählt wird, erhalten Sie tatsächlich separate Instanzen, aber dieses Detail ist hier nicht wichtig.)
Jedes Mal, wenn Sie Ihren Iteratorblock aufrufen, wird eine neue Instanz der Zustandsmaschine erstellt. Ihr Code im Iteratorblock wird jedoch erst ausgeführt, wenn er enumerator.MoveNext()
zum ersten Mal ausgeführt wird. So funktioniert die verzögerte Ausführung. Hier ist ein (ziemlich dummes) Beispiel:
var evenNumbers = IteratorBlock().Where(i => i%2 == 0);
Zu diesem Zeitpunkt wurde der Iterator noch nicht ausgeführt. Die Where
Klausel erstellt eine neue IEnumerable<T>
, die die IEnumerable<T>
zurückgegebenen von IteratorBlock
umschließt, aber diese Aufzählung muss noch aufgelistet werden. Dies geschieht, wenn Sie eine foreach
Schleife ausführen :
foreach (var evenNumber in evenNumbers)
Console.WriteLine(eventNumber);
Wenn Sie die Aufzählung zweimal auflisten, wird jedes Mal eine neue Instanz der Zustandsmaschine erstellt, und Ihr Iteratorblock führt denselben Code zweimal aus.
Beachten Sie, dass LINQ Methoden wie ToList()
, ToArray()
, First()
, Count()
wird usw. , eine Verwendung foreach
Schleife die enumerable aufzuzählen. Zum Beispiel ToList()
werden alle Elemente der Aufzählung aufgelistet und in einer Liste gespeichert. Sie können jetzt auf die Liste zugreifen, um alle Elemente der Aufzählung abzurufen, ohne dass der Iteratorblock erneut ausgeführt wird. Es gibt einen Kompromiss zwischen der Verwendung der CPU, um die Elemente der Aufzählung mehrfach zu erzeugen, und dem Speicher, um die Elemente der Aufzählung zu speichern, um mehrmals auf sie zuzugreifen, wenn Methoden wie verwendet werden ToList()
.