2008-09-25 5 views
29

Ich würde gerne selbst herausfinden, aber ich frage mich, grob, was ist der Algorithmus zum Konvertieren einer Funktion mit Yield-Anweisungen in eine Zustandsmaschine für einen Enumerator? Zum Beispiel, wie funktioniert C# drehen dies:Algorithmus zur Implementierung von C# yield statement

IEnumerator<string> strings(IEnumerable<string> args) 
{ IEnumerator<string> enumerator2 = getAnotherEnumerator();  
    foreach(var arg in arg) 
    { enumerator2.MoveNext(); 
     yield return arg+enumerator.Current; 
    } 
} 

in dieser:

bool MoveNext() 
{ switch (this.state) 
    { 
     case 0: 
      this.state = -1; 
      this.enumerator2 = getAnotherEnumerator(); 
      this.argsEnumerator = this.args.GetEnumerator(); 
      this.state = 1; 
      while (this.argsEnumerator.MoveNext()) 
      { 
       this.arg = this.argsEnumerator.Current; 
       this.enumerator2.MoveNext(); 
       this.current = this.arg + this.enumerator2.Current; 
       this.state = 2; 
       return true; 

       state1: 
       this.state = 1; 
      } 
      this.state = -1; 
      if (this.argsEnumerator != null) this.argsEnumerator.Dispose(); 
      break; 

     case 2: 
      goto state1; 
    } 
    return false; 
} 

Natürlich kann das Ergebnis je nach dem ursprünglichen Code völlig unterschiedlich sein.

Antwort

58

Das besondere Codebeispiel sind Sie bei ihnen eine Reihe von Transformationen beinhaltet. Bitte beachten Sie, dass dies eine ungefähre Beschreibung des Algorithmus ist. Die tatsächlichen Namen, die vom Compiler verwendet werden, und der genaue Code, der generiert wird, können unterschiedlich sein. Dann ist die Idee jedoch dieselbe.

Die erste Transformation ist die "foreach" Transformation, die diesen Code transformiert:

foreach (var x in y) 
{ 
    //body 
} 

in diesen Code:

var enumerator = y.GetEnumerator(); 
while (enumerator.MoveNext()) 
{ 
    var x = enumerator.Current; 
    //body 
} 

if (y != null) 
{ 
    enumerator.Dispose(); 
} 

Die zweite Transformation findet alle yield return-Anweisungen in dem Funktionskörper , weist jedem eine Zahl zu (ein Zustandswert) und erstellt direkt nach der Ausbeute eine "Sprungmarke".

Die dritte Transformation hebt alle lokalen Variablen und Funktionsargumente im Methodenrumpf in ein Objekt namens Closure auf.

der Code in Ihrem Beispiel gegeben, damit so aussehen würde:

class ClosureEnumerable : IEnumerable<string> 
{ 
    private IEnumerable<string> args; 
    private ClassType originalThis; 
    public ClosureEnumerator(ClassType origThis, IEnumerable<string> args) 
    { 
     this.args = args; 
     this.origianlThis = origThis; 
    } 
    public IEnumerator<string> GetEnumerator() 
    { 
     return new Closure(origThis, args); 
    } 
} 

class Closure : IEnumerator<string> 
{ 
    public Closure(ClassType originalThis, IEnumerable<string> args) 
    { 
     state = 0; 
     this.args = args; 
     this.originalThis = originalThis; 
    } 

    private IEnumerable<string> args; 
    private IEnumerator<string> enumerator2; 
    private IEnumerator<string> argEnumerator; 

    //- Here ClassType is the type of the object that contained the method 
    // This may be optimized away if the method does not access any 
    // class members 
    private ClassType originalThis; 

    //This holds the state value. 
    private int state; 
    //The current value to return 
    private string currentValue; 

    public string Current 
    { 
     get 
     { 
      return currentValue; 
     } 
    } 
} 

Der Körper Methode wird aus dem ursprünglichen Verfahren dann bewegt, sich auf ein Verfahren innerhalb „Closure“ genannt Movenext, das eine Bool zurückgibt und implementiert IEnumerable.MoveNext. Jeder Zugriff auf alle lokalen Benutzer wird über "this" weitergeleitet, und jeder Zugriff auf alle Klassenmitglieder wird durch diese.originalThis weitergeleitet.

Jede ist "yield return expr" Übersetzung:

currentValue = expr; 
state = //the state number of the yield statement; 
return true; 

Jede Aussage yield break in übersetzt:

state = -1; 
return false; 

Es gibt eine "implizite" yield break-Anweisung am Ende der Funktion. Am Anfang der Prozedur wird dann eine switch-Anweisung eingefügt, die die Zustandsnummer betrachtet und zum zugehörigen Label springt.

ist die ursprüngliche Methode dann in etwa wie folgt übersetzt:

IEnumerator<string> strings(IEnumerable<string> args) 
{ 
    return new ClosureEnumerable(this,args); 
} 

Die Tatsache, dass der Zustand des Verfahrens alle in ein Objekt gedrückt wird, und dass die Movenext-Methode verwendet eine switch-Anweisung/Zustandsvariablen, was Erlaubt es dem Iterator, sich so zu verhalten, als würde die Kontrolle bei dem nächsten Aufruf von "MoveNext" unmittelbar nach der letzten Anweisung "yield return" an den Punkt zurückgegeben.

Es ist jedoch wichtig, darauf hinzuweisen, dass die vom C# -Compiler verwendete Umwandlung nicht der beste Weg ist, dies zu tun. Es leidet an einer schlechten Leistung, wenn versucht wird, "Ausbeute" mit rekursiven Algorithmen zu verwenden. Es ist ein gutes Papier, das einen besseren Weg skizziert dies hier zu tun:

http://research.microsoft.com/en-us/projects/specsharp/iterators.pdf

Es ist lesenswert, wenn Sie es noch nicht gelesen haben.

+2

Wow. Ausgezeichnete und umfassende Antwort. Ich wünschte, ich könnte dies mehr als einmal abstimmen. –

7

Raymond Chen antwortet darauf; http://blogs.msdn.com/b/oldnewthing/archive/2008/08/12/8849519.aspx

(herausgegeben zu Teil 1 der Serie zu zeigen, die nicht Teil 4)

+1

Raymond Chen sagt nur, dass "es magisch ist" –

+0

Ich denke, marxidad möchte herausfinden, welcher Algorithmus vom Compiler verwendet wird, um zu interpretieren und zu transformieren der C# -Code des Iterators blockt in die IL, die einer Zustandsmaschine entspricht. –

+0

ja Romain ist korrekt –

7

Gerade habe ich diese Frage - I wrote an article vor kurzem entdeckt. Ich werde die anderen hier genannten Links zum Artikel hinzufügen müssen ...