2010-04-29 11 views
6

So wurde diese Frage nur so fragte auf:Kann jemand bitte diesen faulen Bewertungscode erklären?

How to handle an "infinite" IEnumerable?

Mein Beispielcode:

public static void Main(string[] args) 
{ 
    foreach (var item in Numbers().Take(10)) 
     Console.WriteLine(item); 
    Console.ReadKey(); 
} 

public static IEnumerable<int> Numbers() 
{ 
    int x = 0; 
    while (true) 
     yield return x++; 
} 

Kann jemand bitte erklären, warum diese faul ausgewertet? Ich habe diesen Code in Reflector nachgeschlagen, und ich bin mehr verwirrt als zu Beginn.

Reflector Ausgänge:

public static IEnumerable<int> Numbers() 
{ 
    return new <Numbers>d__0(-2); 
} 

Für die Zahlen Methode und sieht eine neue Art haben für diesen Ausdruck erzeugt:

[DebuggerHidden] 
public <Numbers>d__0(int <>1__state) 
{ 
    this.<>1__state = <>1__state; 
    this.<>l__initialThreadId = Thread.CurrentThread.ManagedThreadId; 
} 

Das macht mir keinen Sinn. Ich hätte angenommen, dass es eine Endlosschleife war, bis ich diesen Code zusammenstellte und ihn selbst ausführte.

EDIT: Also ich verstehe jetzt, dass .Nehmen() kann dem foreach sagt, dass die Aufzählung ‚beendet‘ hat, wenn es wirklich nicht hat, soll aber nicht Zahlen() aufgerufen wird in seiner Gesamtheit vor Verkettungs Vorwärts zum Take()? Das Take-Ergebnis ist das, was tatsächlich überzählt wird, richtig? Aber wie wird Take ausgeführt, wenn Numbers nicht vollständig ausgewertet wurde?

EDIT2: Ist dies also nur ein spezifischer Compiler-Trick, der durch das Schlüsselwort 'yield' erzwungen wird?

Antwort

1

Der Grund dafür, dass es sich nicht um eine Endlosschleife handelt, besteht darin, dass Sie nur 10 Mal nach der Verwendung von Linqs Take (10) -Aufruf aufzählen. Nun, wenn Sie den Code so etwas wie schrieben:

foreach (var item in Numbers()) 
{ 
} 

Nun ist dies eine Endlosschleife, weil Ihr enumerator immer einen neuen Wert zurückgeben. Der C# -Compiler nimmt diesen Code und wandelt ihn in eine Zustandsmaschine um. Wenn Ihr Enumerator keine Wächterklausel hat, um die Ausführung zu unterbrechen, muss der Aufrufer das in Ihrem Beispiel tun.

Der Grund, warum der Code faul ist, ist auch ein Grund, warum der Code funktioniert. Im Wesentlichen Take gibt das erste Element zurück, dann verbraucht Ihre Anwendung, dann dauert es ein weiteres, bis es 10 Elemente genommen hat.

bearbeiten

Das eigentlich nichts hat mit dem Zusatz von Take zu tun. Diese werden Iteratoren genannt. Der C# -Compiler führt eine komplizierte Transformation für Ihren Code durch und erstellt einen Enumerator aus Ihrer Methode. Ich empfehle es zu lesen, aber grundsätzlich (und das ist vielleicht nicht 100% genau), wird Ihr Code die Numbers-Methode eingeben, die Sie sich vorstellen könnten, die Zustandsmaschine zu initiieren.

Sobald Ihr Code eine Rendite-Rendite erreicht, sagen Sie im Wesentlichen, Zahlen() stoppen die Ausführung geben sie zurück dieses Ergebnis und dann, wenn sie das nächste Element fordern die Ausführung in der nächsten Zeile nach der Rendite zurück.

Erik Lippert has a great series auf misc Aspekte von Iteratoren

+0

Ja, aber würde Numbers() selbst nicht vollständig ausgewertet werden müssen, um mit dem Take Call fortzufahren? Ich verstehe, dass Numbers selbst eine Endlosschleife ist. Warum verhindert das Hinzufügen von .Take() plötzlich, dass Numbers in seiner Gesamtheit ausgewertet wird? – Tejs

+0

Interessant. Danke für den Link! Diese Frage (und der Code) hat mich dazu gebracht, einen Doppelklick zu machen, und mir ist jetzt klar, dass ich mehr über Enumerables erfahren muss! – Tejs

2

Dies muss mit:

  • Was IEnumerable macht, wenn bestimmte Methoden
  • Die Art der Aufzählung genannt werden und die Yield Aussage

Wenn Sie über eine beliebige Art von IEnumerable aufzählen, gibt Ihnen die Klasse die nächster Punkt, den es dir geben wird. Es tut nichts zu alle seine Artikel, es gibt Ihnen nur den nächsten Artikel. Es entscheidet, was dieser Gegenstand sein wird. (Einige Sammlungen sind zum Beispiel bestellt, andere nicht. Einige garantieren keine bestimmte Reihenfolge, sondern scheinen sie immer in der Reihenfolge zurückzugeben, in der Sie sie abgelegt haben.).

Die IEnumerable-Erweiterungsmethode Take() zählt 10 Mal auf und erhält die ersten 10 Elemente. Sie könnten Take(100000000) tun, und es würde Ihnen eine Menge Zahlen geben. Aber du machst gerade Take(10). Es fragt nur Numbers() für den nächsten Artikel. . . 10 mal.

Jeder dieser 10 Artikel, Numbers gibt den nächsten Artikel. Um zu verstehen, wie, müssen Sie auf der Yield-Anweisung nachlesen. Es ist syntactic sugar für etwas komplizierter. Yield ist sehr mächtig. (Ich bin ein VB-Entwickler und bin sehr verärgert, dass ich es immer noch nicht habe.) Es ist keine Funktion; Es ist ein Schlüsselwort mit gewissen Einschränkungen. Und es macht die Definition eines Enumerators viel einfacher als sonst.

Andere IEnumerable-Erweiterungsmethoden durchlaufen immer jedes einzelne Element. Calling .AsList würde es in die Luft jagen. Bei Verwendung der meisten LINQ-Abfragen würde dies explodieren.

+0

Das Schreiben von benutzerdefinierten Enumeratoren mit Ausbeute ist eine Menge Spaß. – JoshBerke

0

Im Grunde erstellt Ihre Numbers() -Funktion einen Enumerator.
Der Foreach überprüft in jeder Iteration, ob der Enumerator das Ende erreicht hat und wenn nicht, wird er fortgesetzt. Dein praktischer Enumerator wird niemals enden, aber das macht nichts. Dies wird faul ausgewertet.
Der Enumerator wird die Ergebnisse "live" generieren.
Was das bedeutet ist, wenn Sie schreiben würden. Nehmen Sie (3) dort, die Schleife wird nur dreimal ausgeführt. Der Enumerator würde immer noch einige Elemente "übrig" haben, aber sie würden nicht erzeugt werden, da sie zu diesem Zeitpunkt keine Methode benötigt.
Wenn Sie versuchen würden, alle Zahlen von 0 bis unendlich zu erzeugen, wie die Funktion impliziert, und sie alle auf einmal zurückgibt, wäre dieses Programm, das nur 10 davon verwendet, viel, viel langsamer. Das ist der Vorteil einer faulen Bewertung - was nie benutzt wurde, wird nie berechnet.

Verwandte Themen