Lassen Sie uns Ihre ursprünglichen Code-Blöcke ein wenig ändern, um sie auf das Wesentliche zu reduzieren, während Sie sie immer noch interessant genug zum Analysieren halten. Dies entspricht nicht genau dem, was Sie gepostet haben, aber wir verwenden immer noch den Wert des Iterators.
class Disposable : IDisposable {
public void Dispose() {
Console.WriteLine("Disposed!");
}
}
IEnumerable<int> CreateEnumerable() {
int i = 0;
using (var d = new Disposable()) {
while (true) yield return ++i;
}
}
void UseEnumerable() {
foreach (int i in CreateEnumerable()) {
Console.WriteLine(i);
if (i == 10) break;
}
}
Dadurch werden die Zahlen von 1 bis 10 drucken, bevor Disposed!
Drucken Was die Decke geschieht unter eigentlich? Eine ganze Menge mehr. Lassen Sie uns zuerst die äußere Schicht angehen, UseEnumerable
. Die foreach
ist syntaktischer Zucker für die folgenden:
var e = CreateEnumerable().GetEnumerator();
try {
while (e.MoveNext()) {
int i = e.Current;
Console.WriteLine(i);
if (i == 10) break;
}
} finally {
e.Dispose();
}
Die genauen Details (denn auch dies vereinfacht, ein wenig) verweise ich Sie auf the C# language specification, Abschnitt 8.8.4. Das wichtige Bit hier ist, dass ein foreach
einen impliziten Aufruf an die Dispose
des Enumerators beinhaltet.Die using
Anweisung in CreateEnumerable
ist syntaktischer Zucker. In der Tat, lassen Sie sich das Ganze in primitiven Aussagen schreiben, so dass wir mehr Sinn für die Übersetzung später machen können:
in Abschnitt 10.14 der Sprachspezifikation
IEnumerable<int> CreateEnumerable() {
int i = 0;
Disposable d = new Disposable();
try {
repeat:
i = i + 1;
yield return i;
goto repeat;
} finally {
d.Dispose();
}
}
Die genauen Regeln für die Umsetzung der Iterator Blöcke sind detailliert. Sie beziehen sich auf abstrakte Operationen, nicht auf Code. Eine gute Diskussion darüber, welche Art von Code durch den C# -Compiler erzeugt wird und was jeder Teil tut, ist in C# in Depth angegeben, aber ich werde stattdessen eine einfache Übersetzung geben, die immer noch der Spezifikation entspricht. Um es noch einmal zu wiederholen, dies wird nicht der Compiler eigentlich produzieren, aber es ist eine gut genug Approximation, um zu veranschaulichen, was passiert und lässt die mehr haarigen Bits, die Threading und Optimierung betreffen.
class CreateEnumerable_Enumerator : IEnumerator<int> {
// local variables are promoted to instance fields
private int i;
private Disposable d;
// implementation of Current
private int current;
public int Current => current;
object IEnumerator.Current => current;
// State machine
enum State { Before, Running, Suspended, After };
private State state = State.Before;
// Section 10.14.4.1
public bool MoveNext() {
switch (state) {
case State.Before: {
state = State.Running;
// begin iterator block
i = 0;
d = new Disposable();
i = i + 1;
// yield return occurs here
current = i;
state = State.Suspended;
return true;
}
case State.Running: return false; // can't happen
case State.Suspended: {
state = State.Running;
// goto repeat
i = i + 1;
// yield return occurs here
current = i;
state = State.Suspended;
return true;
}
case State.After: return false;
default: return false; // can't happen
}
}
// Section 10.14.4.3
public void Dispose() {
switch (state) {
case State.Before: state = State.After; break;
case State.Running: break; // unspecified
case State.Suspended: {
state = State.Running;
// finally occurs here
d.Dispose();
state = State.After;
}
break;
case State.After: return;
default: return; // can't happen
}
}
public void Reset() { throw new NotImplementedException(); }
}
class CreateEnumerable_Enumerable : IEnumerable<int> {
public IEnumerator<int> GetEnumerator() {
return new CreateEnumerable_Enumerator();
}
IEnumerator IEnumerable.GetEnumerator() {
return GetEnumerator();
}
}
IEnumerable<int> CreateEnumerable() {
return new CreateEnumerable_Enumerable();
}
Das wesentliche Bit hierbei ist, dass der Codeblock an dem Vorkommen einer yield return
oder yield break
Anweisung aufgeteilt, mit dem Iterator verantwortlich für die Erinnerung an der Zeit der Unterbrechung „wo wir waren“. Alle finally
Blöcke im Körper sind bis zum Dispose
zurückgestellt. Die Endlosschleife in Ihrem Code ist wirklich keine Endlosschleife mehr, da sie von periodischen yield return
Anweisungen unterbrochen wird. Beachten Sie, dass , weil der finally
Block ist nicht eigentlich ein finally
Block mehr, es ausgeführt wird, ist ein wenig weniger sicher, wenn Sie mit Iteratoren beschäftigen. Aus diesem Grund ist die Verwendung von foreach
(oder einer anderen Methode, die sicherstellt, dass die Dispose
Methode des Iterators in einem finally
Block aufgerufen wird) wesentlich.
Dies ist ein vereinfachtes Beispiel; Die Dinge werden viel interessanter, wenn Sie die Schleife komplexer machen, Ausnahmen einführen und so weiter. Die Last, "diese Arbeit einfach zu machen", liegt beim Compiler.
Versuchen Sie es mit etwas Code, der zuerst kompiliert. Wie, würde tatsächlich eine 'Rendite-Return-Anweisung 'helfen. :-) –
Die kurze Antwort auf Ihre Frage lautet "Ja,' Dispose 'wird aufgerufen" - Sie können dies selbst leicht testen, indem Sie 'MD5.Create' durch eine Klasse ersetzen, die beim Entladen etwas auf die Konsole druckt. Die lange Antwort ist viel interessanter, da sie erklärt, zu welchen Iterator-Methoden kompiliert wird (und was "foreach" unter den Abdeckungen mit Iteratoren tut). Ich werde es selbst aufschreiben, wenn niemand es später getan hat, aber das ist Stack Overflow, also ... nicht viel Zufall. –
@JeroenMostert mein schlechtes, schrieb es einfach schnell von der Spitze meines Kopfes. – SynerCoder