Entwickler-Ecke
Basistechnologien - Wie arbeitet "yield"?
Christian S. - Fr 25.11.05 17:37
Titel: Wie arbeitet "yield"?
Hallo!
Ich habe mich heute mit ein paar der neuen Features in C# 2.0 beschäftigt und bin auf die wirklich nette Möglichkeit gestoßen, mittels eines Konstruktes wie diesem hier
C#-Quelltext
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20:
| public class myList<T> { private List<T> elements;
public myList() { elements = new List<T>(); }
public void add(T el) { elements.Add(el); }
public IEnumerator GetEnumerator() { foreach (T el in elements) yield return el; } } |
einer Klasse einen Enumerator zu verpassen. Gegenüber der "alten" Methode dafür erst noch eine eigene Klasse zu schreiben ein wirklich toller Fortschritt. (Dass der Code oben ansich Quatsch ist, ist mir bewusst, aber ist gut zum testen ;-))
Nun stellt sich mir aber die Frage: Wie funktioniert das genau? Wenn ich nun hergehe und folgenden Code aufrufe:
C#-Quelltext
1: 2: 3: 4: 5: 6: 7: 8:
| myList<int> ml = new myList<int>();
ml.add(3); ml.add(4); ml.add(5);
foreach (int i in ml) MessageBox.Show(i.ToString()); |
"Merkt" sich der Compiler dann, dass er beim dritten Element die ersten zwei yield-return-Anweisungen ignorieren muss? :gruebel:
Grüße
Christian
Robert_G - Fr 25.11.05 23:29
Christian S. hat folgendes geschrieben: |
"Merkt" sich der Compiler dann, dass er beim dritten Element die ersten zwei yield-return-Anweisungen ignorieren muss? :gruebel: |
Hihi, nö. ... Naja so ähnlich jedenfalls...
Wenn du in einer Methode yield benutzt wird der Compiler eine private nested class anlegen, die selbst IEnumerable[meta]wenn du IEnumerable<T> als rückgabewert hast auch IEnumerable<T>[/meta] implementiert und im Konstruktor die Parameter deiner Methode bekommt.
Jedes yield innerhalb deines Iterators entspricht nachher einem Wert den der ,ebenfalls angelegte, Enumerator zurückgibt.
Ist das Ende deiner Methode erreicht wird MoveNext des Enumerators false liefern.
Auf die Art kann man zum Beispiel direkt durch Dateien iterieren
ohne die einzelnen Objekte erst in einen Container werfen zu müssen. :)
Du musst also immer nur ein Objekt im Speicher halten.
Der code deiner Methode wird in die iterator Klasse kopiert und etwas umgebaut. Alle Bezüge auf this/self werden auf ein Feld des iterators umgebogen. Außerdem darfst du leider keine using statements innerhalb des Iterators nehmen... :-/
Hier ein kleiner Bleistift um eine CSV zu durchlaufen:
Pascal:
Delphi-Quelltext
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22:
| class method ConsoleApp.ReadPersonsFrom(reader : StreamReader) : IEnumerable<Person>; begin while reader.Peek() <> -1 do begin var splittedLine := reader.ReadLine().Split([';']);
var person := new Person(splittedLine[0], splittedLine[1], Integer.Parse(splittedLine[2])); yield (person); end; end;
class method ConsoleApp.Main; begin using reader := new StreamReader('Persons.txt', Encoding.Default) do for person : Person in ReadPersonsFrom(reader) do Console.WriteLine('{0}, {1} ({2})', Person.Name, Person.FirstName, Person.Age); end; |
C# port:
C#-Quelltext
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23:
| static IEnumerable<Person> ReadPersonsFrom(StreamReader reader) { while (reader.Peek() != -1) { string[] splittedLine = reader.ReadLine().Split(new char[1] {';'});
Person person = new Person(splittedLine[0], splittedLine[1], int.Parse(splittedLine[2])); yield return person; } }
static void Main(string[] args) { using (StreamReader reader = new StreamReader("Persons.txt", Encoding.Default)) foreach (Person person in ReadPersonsFrom(reader)) Console.WriteLine("{0}, {1} ({2})", person.Name, person.FirstName, person.Age);
} |
btw: Iteratoren verwendet man eigentlich nicht in GetEnumerator...
Nein, man kann leider keinen Iterator in einen XmlSerializer werfen...
Christian S. - Sa 26.11.05 01:36
Hm. Ganz verstanden habe ich den Mechanismus noch nicht. Die Methode GetEnumerator aus meinem Beispiel wird also nur einmal aufgerufen, richtig?
Und es werden dann alle Rückgabewerte irgendwo gespeichert und mittels foreach durchlaufen? :gruebel: Wenn das stimmt, was passiert, wenn sich nach Aufruf von GetEnumerator nochmal ein Element ändert?
Robert_G - Sa 26.11.05 01:49
Christian S. hat folgendes geschrieben: |
Hm. Ganz verstanden habe ich den Mechanismus noch nicht. Die Methode GetEnumerator aus meinem Beispiel wird also nur einmal aufgerufen, richtig? |
Jupp, zurückgegeben wird die Instanz der Iterator klasse.
Diese wiederum enthält den Code, den du reingeschrieben hast.
Christian S. hat folgendes geschrieben: |
Und es werden dann alle Rückgabewerte irgendwo gespeichert und mittels foreach durchlaufen? :gruebel: Wenn das stimmt, was passiert, wenn sich nach Aufruf von GetEnumerator nochmal ein Element ändert? |
Nope...
Machen wir es gaaanz simpel:
C#-Quelltext
1: 2: 3: 4: 5: 6: 7: 8: 9: 10:
| IEnumerable<Integer> CountTil3() { int countDings = 0
while (countDings < 3) { countDings++; yield return countDings; } } |
Die Iterator klasse bekommt nun ein Feld für countDings, welches mit 0 initialisiert wird.
Bei jedem MoveNext wird geprüft, ob countDings < 3 ist. Current liefert den aktuellen Wert von CountDings.
Du hast somit die Möglichkeit durch eine Liste zu iterieren, die gar keine ist. ;)
Christian S. - Sa 26.11.05 01:59
Okay, und die Schleife wird bei jedem "yield" angehalten und beim nächsten Aufruf von MoveNext an genau diesem Punkt fortgesetzt? Sozusagen "eingefroren" und "fortgesetzt". countDings bleibt als Feld des Iterators ja sowieso beim zuletzt zugewiesenen Wert.
Robert_G - Sa 26.11.05 02:15
Christian S. hat folgendes geschrieben: |
Okay, und die Schleife wird bei jedem "yield" angehalten und beim nächsten Aufruf von MoveNext an genau diesem Punkt fortgesetzt? Sozusagen "eingefroren" und "fortgesetzt". |
Ich glaube jetzt habe ich dich falsch verstanden...
Deine "Scheife" existiert nicht mehr, die einzige Schleife, die noch existiert ist das foreach, das den Iterator zum Schluss benutzt.
Du hast jetzt folgenden Pseudo code.
- Konstruktor: this.countDings = 0
- MoveNext:
C#-Quelltext
1: 2: 3: 4: 5: 6:
| if (countDings < 3) { countDings++; return true; } return fals; |
- Current: return countDings:
Christian S. - Sa 26.11.05 02:21
Ah, jetzt habe ich es verstanden! :think:
Da muss der Compiler (je nach Komplexität des Codes, der in einen Iterator verwandelt werden soll) allerdings schon einiges leisten!
Robert_G - Sa 26.11.05 02:59
Klappt aber prima (siehe Anhang). :)
Gibt's übrigens auch in hübsch:
Chrome-Quelltext
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14:
| class method ConsoleApp.CountSomething : IEnumerable<Integer>; begin for a : Integer := 1 to 3 do begin yield (a); for b : Integer := 10 to 30 step 10 do yield (b); end; end;
class method ConsoleApp.Main; begin var values := new LinkedList<Integer>(CountSomething()); end; |
Entwickler-Ecke.de based on phpBB
Copyright 2002 - 2011 by Tino Teuber, Copyright 2011 - 2025 by Christian Stelzmann Alle Rechte vorbehalten.
Alle Beiträge stammen von dritten Personen und dürfen geltendes Recht nicht verletzen.
Entwickler-Ecke und die zugehörigen Webseiten distanzieren sich ausdrücklich von Fremdinhalten jeglicher Art!