Let’s Code: “yield” in C#
C# gehört, so wie ich es erlebt habe, leider zu den Sprachen, die an Universitäten zu kurz kommen. Die Gründe hierfür (einer ist sicherlich Microsoft) sind vielseitig, sollen aber nicht Thema dieses Artikels sein. Ist man dann einmal fertig, wird man schnell feststellen, dass man ohne C# Kenntnisse leider einen recht großen Teil der Arbeitgeber nicht zufrieden stellen können wird. Ich selbst komme (aufgrund der Inhalte an der Uni) aus der Java Ecke, habe mich aber seit .NET 1.1 auch mit C# beschäftigt; seit einer ganzen Weile nun auch beruflich.
Ich möchte nun in einer kleinen Serie – mit offenem Ziel – kleine, coole und eventuell nicht jedem bekannte C# Features vorstellen. In mundgerechten Häppchen die in einen Feierabend passen: Let’s code :)
Nehmen wir nun kurz an wir wollen eine Liste erstellen. Der Inhalt spielt vorerst keine Rolle, wir müssen ihn nur irgendwie berechnen können. Um in diesem Beispiel mit irgendwas arbeiten zu können, erzeugen wir eine Reihe von Objekten, die durch folgendes Interface definiert sind
internal interface IShortTuple { ushort A {get;set;} ushort B {get;set;} }
Sehr kreativ, ja. Wir erzeugen die Objekte wie folgt:
internal static IEnumerable<T> CreateTestCollection<T>(ushort size) where T : IShortTuple, new() { IList<T> tmp = new List<T>(); for (ushort i = 0; i < size; i++) { for (ushort j = 0; j < size; j++) { tmp.Add(new T() { A = i, B = j }); } } return tmp; }
Die Einschränkung bezüglich der Größe ist willkürlich. Wer will aber schon bei solchen Beispielen lange auf Ergebnisse warten? Nun ist die Art wie die Objekte erzeugt werden nicht grade optimal. Wir erzeugen eine Liste, füllen diese von A bis Z und geben sie anschließend an einem Stück zurück. Sprich: der gesamte Thread wartet auf unsere Berechnung. Wenn wir für dieses Beispiel ferner annehmen, dass diese Daten einfach in die Konsole geschrieben werden sollen, wird klar, warum das Vorgehen ungeschickt ist: die erste Ausgabe erfolgt nachdem alles berechnet wurde obwohl wir die ersten Teile bereits kennen.
Nun steht in der Methodensignatur IEnumerable<T>, wir könnten also dieses Interface implementieren und dann auch gleich noch IEnumerator<T> um unser gewünschtes Verhalten zu erreichen. Insgesamt etwas aufwändig, denn es geht viel einfacher:
internal static IEnumerable<T> CreateDebugCollection<T>(ushort size) where T : IShortTuple, new() { for (ushort i = 0; i < size; i++) { for (ushort j = 0; j < size; j++) { yield return new T() { A = i, B = j }; } } }
Durch das yield Keyword wird dem Compiler mitgeteilt, dass es sich bei dieser Funktion um einen Iterator-Block handelt. Hierbei werden die einzelnen Werte zurückgegeben sobald sie verfügbar sind, in etwa wie in einem Stream. Iteriert man nun über dieses IEnumerable und schreibt dabei alle Werte in die Konsole wird man merken, dass es keine merkbare Wartezeit gibt bis die ersten Zeilen erscheinen.
In den folgenden Artikeln werde ich verschiedene Aspekte dieser Code-Snippets detailierter betrachten. Unter anderem wie man das Interface IShortTuple am geschicktesten implementiert, einige Messungen mit dem Profiler und einer Priese PLINQ.
Kommentare
[...] vergangenen Beitrag habe ich gezeigt wie man mit yield return ein IEnumerable<T> als eine Art Stream erzeugen [...]
#1206, geschrieben von Let’s Code: StructLayout @ XLKSTYLE.com 23.07.2011 @ 21:49:21
Kleiner Nachtrag:
Zwar hängt es vom Einsatzgebiet ab, jedoch ist es definitiv eine Überlegung wert den Rückgabewert der obigen Methode von
IEnumerable<T> zu IEnumerable<IShortTuple>
zu ändern. Erst nach einer solchen Änderung kann man folgendes schreiben:
IEnumerable<IShortTuple> a = CreateDebugCollection<ShortTuple>(100).
Wobei das wie gesagt vom Fall abhängt und von der Richtung die man eher bevorzugt: generische Programmierung oder Interfaceprogrammierung.
#1205, geschrieben von Paul Rupek 23.07.2011 @ 18:52:17