Artikel

Februar 2006 | Artikel

yield - enträtselt

(Link zum Artikel: http://www.it-republik.de/dotnet/artikel/0798)

Geheimnissen in C# 2.0 auf der Spur

Text: Von Christian Gross
  • Teilen
  • kommentieren
  • empfehlen
  • Bookmark and Share
Das Schlüsselwort yield ist neu in .NET 2.0. Es vereinfacht die Implementierung von aufzählbaren Klassen. Bislang musste man eine Reihe von Schnittstellenmethoden implementieren, bevor man mit der foreach-Schleifenkonstruktion arbeiten konnte.

Das folgende Beispiel definiert eine Klasse, die in einer foreach-Anweisung aufgezählt werden kann:

  1. public class ExampleIterator : IEnumerable {
  2. public IEnumerator GetEnumerator() {
  3. yield return 1;
  4. }
  5. }
Die Klasse ExampleIterator implementiert die Schnittstelle IEnumerable und muss deshalb die Methode GetEnumerator implementieren. Die Methode GetEnumerator gibt eine IEnumerator-Instanz zurück. Die Implementierung von GetEnumerator liefert aber den Rückgabewert 1 und keine Instanz der IEnumerator-Schnittstelle. Man fragt sich, wie der Wert 1 in eine IEnumerator-Instanz konvertiert wird. Das Geheimnis liegt im Schlüsselwort yield, das den fehlenden Code in Form von generierter IL liefert. Das Schlüsselwort yield ist eine Compiler-Direktive, die einen ziemlich großen IL-Codeblock hervorbringt. Mit dem Programm Reflector lässt sich per Reverse Engineering untersuchen, was der Compiler erzeugt hat. Abpictureung 1 zeigt den generierten Code.

Wie Abpictureung 1 zeigt, besitzt die Klasse WhatDoesTheYieldReallyGenerate.ExampleIterator einen eingebetteten Klassenaufruf GetEnumerator>d__0. Etwas eigentümlich ist die Benennung der eingebetteten Klasse. Es scheint, als ob der eigentliche Klassenname d__0 lautet und <GetEnumerator> auf einen generischen .NET-Typ verweist. Tatsächlich aber ist der Bezeichner <GetEnumerator> Teil des Klassenbezeichners. Mit einem derartigen Bezeichner hätten Sie in C# oder VB.NET eine Fehlermeldung des Compilers geerntet. Die Namenskonvention ist absichtlich so festgelegt, damit niemals die Situation eintreten kann, dass ein Programmierer eine Klasse definiert, die mit der generierten Klasse kollidiert. Die Klasse <GetEnumerator>d__0 besitzt darüber hinaus das Attribut CompilerGenerated und sie ist versiegelt (sealed), weshalb man von diesem Typ im Code keine Klasse ableiten kann.
Das Schlüsselwort yield generiert eine private Klasse, die die Schnittstelle IEnumerator implementiert. Diesen Code müssten Sie vor .NET 2.0 selbst schreiben, jetzt aber wird er automatisch generiert. Weiterhin ist interessant, wie eine Instanz der privaten Klasse an den Client zurückgegeben wird. Damit Sie die Abläufe besser verstehen können, wurde das yield-Beispiel umgeschrieben und veranschaulicht eine Aufrufsequenz, die sich im "reverse-engineered" Code genau festmachen lässt. Das modifizierte yield-Beispiel sieht wie in Listing 1 aus.

Listing 1
  1. public class ExampleIterator : IEnumerable {
  2. int _param;
  3. private int Method1( int param) {
  4. return param + param;
  5. }
  6. private int Method2( int param) {
  7. return param * param;
  8. }
  9. public IEnumerator GetEnumerator() {
  10. Console.WriteLine( "before");
  11. _param = 10;
  12. yield return Method1( _param);
  13. yield return Method2( _param);
  14. Console.WriteLine( "after");
  15. }
  16. }
Im modifizierten yield-Beispiel ruft die GetEnumerator-Implementierung die Funktion Console.WriteLine am Anfang und am Ende der Methode auf. Die als param deklarierte Variable wird an die Methoden Method1 und Method2 übergeben. Die Methoden Method1 und Method2 geben modifizierte Werte der Variablen param zurück. Diese Variablendeklarationen und Methodenaufrufe sollen Verfolgungspunkte bereitstellen, damit sich nachvollziehen lässt, wie der C#-Compiler seinen IL-Code generiert. Durch Reverse Engineering des IL-Codes erhält man den Quellcode aus Listing 2 für die Methode GetEnumerator.

Listing 2
  1. public IEnumerator GetEnumerator() {
  2. ExampleIterator.<GetEnumerator>d__0 d__1 =
  3. new ExampleIterator.<GetEnumerator>d__0(0);
  4. d__1.<>4__this = this;
  5. return d__1;
  6. }

Die Details in der IL-Implementierung von GetEnumerator sind für das, was in C# kodiert ist, überhaupt nicht relevant. Ein genauer Blick auf den Code zeigt, dass die private Klasse instanziiert und der Variablen d__1 zugewiesen wird. Der Konstruktorwert 0 ist für private Zwecke vorgesehen und wird vom Code nicht beeinflusst. Auffällig ist die Zuweisung des Datenelements d__1.<>4__this an die this-Instanz. Vom strukturellen Gesichtspunkt her bedeutet das, dass der privaten Klasse eine Referenz auf die übergeordnete Klasse zugewiesen wird. Schließlich gibt der letzte Schritt die erzeugte IEnumerator-Instanz zurück. Eines der Rätsel ist nun gelöst - wie der Wert 1 des ursprünglichen yield-Beispiels in eine IEnumerator-Instanz konvertiert wird. Der Compiler entfernt den C#-Code, der die Methode GetEnumerator implementiert, und ersetzt ihn durch Code, der die private Klasse instanziiert und diese Instanz zurückgibt.
So weit so gut, doch stellt sich die Frage, was mit dem ursprünglichen GetEnumerator-Code passiert ist. Und wie ist es möglich, dass der yield-Code scheinbar aus der GetEnumerator-Methode heraus und dann zurück in die Methode GetEnumerator springt? Beide Fragen lassen sich klären, wenn man sich die in IL generierte IEnumerator.MoveNext-Methode ansieht, die der Codeausschnitt in Listing 3 zeigt.

Listing 3
  1. private bool MoveNext()
  2. {
  3. switch (this.<>1__state)
  4. {
  5. case 0:
  6. {
  7. this.<>1__state = -1;
  8. Console.WriteLine("before");
  9. this.<>4__this._param = 10;
  10. this.<>2__current =
  11. this.<>4__this.Method1(this.<>4__this._param);
  12. this.<>1__state = 1;
  13. return true;
  14. }
  15. case 1:
  16. {
  17. this.<>1__state = -1;
  18. this.<>2__current =
  19. this.<>4__this.Method2(this.<>4__this._param);
  20. this.<>1__state = 2;
  21. return true;
  22. }
  23. case 2:
  24. {
  25. this.<>1__state = -1;
  26. Console.WriteLine("after");
  27. break;
  28. }
  29. }
  30. return false;
  31. }
Im Beispielcode verkörpern die fett gedruckten Teile die fehlende Funktionalität. Diese Teile ziehen sich durch die gesamte MoveNext-Implementierung. Die yield-Implementierung wird mit jedem Teil des generierten Codes rätselhafter. Der Compiler hat den Code in Blöcke aufgeteilt, deren Anzahl darauf basiert, wie oft das Schlüsselwort yield verwendet wird. Dazu kommt noch eine Instanz. Das Schlüsselwort yield dient als Markierung, um einen Block vom anderen zu trennen. Bevor Sie weiterlesen, sollten Sie die generierte MoveNext-Implementierung mit der modifizierten GetEnumerator-Implementierung vergleichen und beachten, wie der Compiler das Schlüsselwort yield als Compiler-Trennzeichen verwendet.Das Schlüsselwort yield dient deshalb als Begrenzer, weil jede Instanz einen Zustand definiert, den die switch-Anweisung auswertet. Kommt also das Schlüsselwort yield in Ihrer GetEnumerator-Implementierung 15-mal vor, gibt es 15 Zustände. Wenn Sie aber eine einzelne yield-Anweisung im Kontext einer Schleife verwenden, wissen Sie nicht, was der Compiler generiert. Der Code funktioniert, doch der resultierende Code sieht mehr nach Spagetti-Code aus - wenn Sie solchen Code selbst schreiben würden, könnten Sie keinen Preis für Programmierkunst erwarten. Für jeden Durchlauf der foreach-Anweisung wird die Methode MoveNext aufgerufen und die Variable this.<>1__state inkrementiert. Beachten Sie aber, dass dem Zustand in einer case-Anweisung zuerst der Wert -1 zugewiesen, dann der benutzerdefinierte Code aufgerufen und danach der Zustand inkrementiert wird. Vermutlich gibt es Situationen der Codegenerierung, die dies als Notausgang für die foreach-Schleife nutzen. Die Variable this.<>2__current speichert den Wert, der beim Aufruf der Eigenschaft IEnumerator.Current zurückgegeben wird. Wenn Sie das Schlüsselwort yield verwenden, sollten Sie die folgenden Punkte berücksichtigen:

Das Schlüsselwort yield ist ein Element der .NET-Umgebung, das die Sprache erweitert, um die Implementierung bestimmter Codeabschnitte einfacher zu gestalten. Das Schlüsselwort yield müssen Sie nicht verwenden, wo Sie bislang eine aufzählbare Auflistung implementiert haben. Selbst wenn es scheint, dass das Schlüsselwort yield funktioniert, hat das generierte Ergebnis große Ähnlichkeit mit Spagetti-Code. Das macht neugierig, ob der erzeugte Code immer noch funktioniert, wenn man ihn mit mehreren komplizierteren und esoterischen Sprachkonstrukten kombiniert. Bisher ist nichts Gegenteiliges bekannt, doch man muss sich fragen, ob nicht irgendwo versteckte Bugs lauern und nur darauf warten, im denkbar ungünstigsten Moment zuzuschlagen.
Kann .NET ein Abenteuer sein? Wir finden ja und haben den definitiven Guide für das Abenteuer .NET-Applikationen entwickelt. Dieses Buch fängt da an, wo andere aufhören und zeigt Ihnen die unbekanntesten, aber effektivsten Tricks, wie Sie Ihren .NET-Code verbessern können.

.NET Live!
Real-Life-Programming
Christian Gross
April 2006
250 Seiten, Pocket
ISBN: 3-935042-80-9
Preis: 24,90 €
 
 
Christian Gross lebt als selbstständiger Software-Entwickler, Buchautor und Künstler in der Schweiz. Wenn Sie Fragen haben, schicken Sie eine E-Mail an christianhgross@gmail.com. Dieser .NET-Knigge ist Teil des im April erscheinenden Buches ".NET live - Beste Tipps für besten Code" von Christian Gross.


Anzeige

Kommentare

zurück zum Seitenanfang