2016-06-16 27 views
4

Lets sagen, dass ich eine unendliche Zustandsmaschine habe zufälliges md5 Hashes zu generieren:unendliche Zustandsmaschine mit einer IDisposable

public static IEnumerable<string> GetHashes() 
{ 
    using (var hash = System.Security.Cryptography.MD5.Create()) 
    { 
     while (true) 
      yield return hash.ComputeHash(Guid.NewGuid().ToByteArray()); 
    } 
} 

Im obigen Beispiel verwende ich eine using Aussage. Wird die .Dispose() Methode jemals aufgerufen? CQ, werden die nicht verwalteten Ressourcen jemals freigegeben?

Zum Beispiel, wenn ich die Maschine wie folgt verwenden:

public static void Test() 
{ 
    int counter = 0; 
    var hashes = GetHashes(); 
    foreach(var md5 in hashes) 
    { 
     Console.WriteLine(md5); 
     counter++; 
     if (counter > 10) 
      break; 
    } 
} 

Da die hashes Variable den Gültigkeitsbereich verlassen (und ich nehme an Müll gesammelt) wird die dispose-Methode aufgerufen werden, um die Ressourcen freizugeben, die von System.Security.Cryptography.MD5 oder ist das ein Speicherleck?

+3

Versuchen Sie es mit etwas Code, der zuerst kompiliert. Wie, würde tatsächlich eine 'Rendite-Return-Anweisung 'helfen. :-) –

+3

Die kurze Antwort auf Ihre Frage lautet "Ja,' Dispose 'wird aufgerufen" - Sie können dies selbst leicht testen, indem Sie 'MD5.Create' durch eine Klasse ersetzen, die beim Entladen etwas auf die Konsole druckt. Die lange Antwort ist viel interessanter, da sie erklärt, zu welchen Iterator-Methoden kompiliert wird (und was "foreach" unter den Abdeckungen mit Iteratoren tut). Ich werde es selbst aufschreiben, wenn niemand es später getan hat, aber das ist Stack Overflow, also ... nicht viel Zufall. –

+0

@JeroenMostert mein schlechtes, schrieb es einfach schnell von der Spitze meines Kopfes. – SynerCoder

Antwort

3

Lassen Sie uns Ihre ursprünglichen Code-Blöcke ein wenig ändern, um sie auf das Wesentliche zu reduzieren, während Sie sie immer noch interessant genug zum Analysieren halten. Dies entspricht nicht genau dem, was Sie gepostet haben, aber wir verwenden immer noch den Wert des Iterators.

class Disposable : IDisposable { 
    public void Dispose() { 
     Console.WriteLine("Disposed!"); 
    } 
} 

IEnumerable<int> CreateEnumerable() { 
    int i = 0; 
    using (var d = new Disposable()) { 
     while (true) yield return ++i; 
    } 
} 

void UseEnumerable() { 
    foreach (int i in CreateEnumerable()) { 
     Console.WriteLine(i); 
     if (i == 10) break; 
    } 
} 

Dadurch werden die Zahlen von 1 bis 10 drucken, bevor Disposed!

Drucken Was die Decke geschieht unter eigentlich? Eine ganze Menge mehr. Lassen Sie uns zuerst die äußere Schicht angehen, UseEnumerable. Die foreach ist syntaktischer Zucker für die folgenden:

var e = CreateEnumerable().GetEnumerator(); 
try { 
    while (e.MoveNext()) { 
     int i = e.Current; 
     Console.WriteLine(i); 
     if (i == 10) break; 
    } 
} finally { 
    e.Dispose(); 
} 

Die genauen Details (denn auch dies vereinfacht, ein wenig) verweise ich Sie auf the C# language specification, Abschnitt 8.8.4. Das wichtige Bit hier ist, dass ein foreach einen impliziten Aufruf an die Dispose des Enumerators beinhaltet.Die using Anweisung in CreateEnumerable ist syntaktischer Zucker. In der Tat, lassen Sie sich das Ganze in primitiven Aussagen schreiben, so dass wir mehr Sinn für die Übersetzung später machen können:

in Abschnitt 10.14 der Sprachspezifikation
IEnumerable<int> CreateEnumerable() { 
    int i = 0; 
    Disposable d = new Disposable(); 
    try { 
     repeat: 
     i = i + 1; 
     yield return i; 
     goto repeat; 
    } finally { 
     d.Dispose(); 
    } 
} 

Die genauen Regeln für die Umsetzung der Iterator Blöcke sind detailliert. Sie beziehen sich auf abstrakte Operationen, nicht auf Code. Eine gute Diskussion darüber, welche Art von Code durch den C# -Compiler erzeugt wird und was jeder Teil tut, ist in C# in Depth angegeben, aber ich werde stattdessen eine einfache Übersetzung geben, die immer noch der Spezifikation entspricht. Um es noch einmal zu wiederholen, dies wird nicht der Compiler eigentlich produzieren, aber es ist eine gut genug Approximation, um zu veranschaulichen, was passiert und lässt die mehr haarigen Bits, die Threading und Optimierung betreffen.

class CreateEnumerable_Enumerator : IEnumerator<int> { 
    // local variables are promoted to instance fields 
    private int i; 
    private Disposable d; 

    // implementation of Current 
    private int current; 
    public int Current => current; 
    object IEnumerator.Current => current; 

    // State machine 
    enum State { Before, Running, Suspended, After }; 
    private State state = State.Before; 

    // Section 10.14.4.1 
    public bool MoveNext() { 
     switch (state) { 
      case State.Before: { 
        state = State.Running; 
        // begin iterator block 
        i = 0; 
        d = new Disposable(); 
        i = i + 1; 
        // yield return occurs here 
        current = i; 
        state = State.Suspended; 
        return true; 
       } 
      case State.Running: return false; // can't happen 
      case State.Suspended: { 
        state = State.Running; 
        // goto repeat 
        i = i + 1; 
        // yield return occurs here 
        current = i; 
        state = State.Suspended; 
        return true; 
       } 
      case State.After: return false; 
      default: return false; // can't happen 
     } 
    } 

    // Section 10.14.4.3 
    public void Dispose() { 
     switch (state) { 
      case State.Before: state = State.After; break; 
      case State.Running: break; // unspecified 
      case State.Suspended: { 
        state = State.Running; 
        // finally occurs here 
        d.Dispose(); 
        state = State.After; 
       } 
       break; 
      case State.After: return; 
      default: return; // can't happen 
     } 
    } 

    public void Reset() { throw new NotImplementedException(); } 
} 

class CreateEnumerable_Enumerable : IEnumerable<int> { 
    public IEnumerator<int> GetEnumerator() { 
    return new CreateEnumerable_Enumerator(); 
    } 

    IEnumerator IEnumerable.GetEnumerator() { 
    return GetEnumerator(); 
    } 
} 

IEnumerable<int> CreateEnumerable() { 
    return new CreateEnumerable_Enumerable(); 
} 

Das wesentliche Bit hierbei ist, dass der Codeblock an dem Vorkommen einer yield return oder yield break Anweisung aufgeteilt, mit dem Iterator verantwortlich für die Erinnerung an der Zeit der Unterbrechung „wo wir waren“. Alle finally Blöcke im Körper sind bis zum Dispose zurückgestellt. Die Endlosschleife in Ihrem Code ist wirklich keine Endlosschleife mehr, da sie von periodischen yield return Anweisungen unterbrochen wird. Beachten Sie, dass , weil der finally Block ist nicht eigentlich ein finally Block mehr, es ausgeführt wird, ist ein wenig weniger sicher, wenn Sie mit Iteratoren beschäftigen. Aus diesem Grund ist die Verwendung von foreach (oder einer anderen Methode, die sicherstellt, dass die Dispose Methode des Iterators in einem finally Block aufgerufen wird) wesentlich.

Dies ist ein vereinfachtes Beispiel; Die Dinge werden viel interessanter, wenn Sie die Schleife komplexer machen, Ausnahmen einführen und so weiter. Die Last, "diese Arbeit einfach zu machen", liegt beim Compiler.

+0

Awesome Antwort, und wie versprochen die angenommene Antwort und ein +1 – SynerCoder

1

Hauptsächlich hängt es davon ab, wie Sie es codieren. Aber in Ihrem Beispiel wird Dispose aufgerufen werden.

Hier ist ein explanation on how iterators get compiled.

Und speziell, im Gespräch über finally:

Iteratoren stellen ein unangenehmes Problem. Anstatt die gesamte Methode auszuführen, bevor der Stapelrahmen aufgerufen wird, pausiert die Ausführung effektiv jedes Mal, wenn ein Wert zurückgegeben wird. Es gibt keine Möglichkeit zu garantieren, dass der Aufrufer den Iterator jemals wieder verwenden wird, in irgendeiner Form oder Form. Wenn Sie zu einem bestimmten Zeitpunkt nach Ausführung des Werts noch mehr Code ausführen möchten, befinden Sie sich in Schwierigkeiten: Sie können nicht garantieren, dass dies geschieht. Um auf den Punkt zu kommen, code in einen finally-Block, der normalerweise in fast allen Umständen ausgeführt werden würde, bevor man die Methode verlässt, kann man sich nicht ganz darauf verlassen.

...

Die Zustandsmaschine, dass schließlich so gebaut Blöcke ausgeführt werden, wenn ein Iterator ordnungsgemäß jedoch verwendet wird. Das liegt daran, dass IEnumerator IDisposable implementiert und die C# -Foreach-Schleife Dispose für Iteratoren aufruft (selbst die nicht-generischen IEnumerator-Einsen, wenn sie IDisposable implementieren). Die IDisposable Implementierung im generierten Iterator ermittelt, welche Blöcke letztendlich für die aktuelle Position relevant sind (basierend auf dem Zustand wie immer) und den entsprechenden Code ausführen.

+0

Diese Antwort hat mir auch geholfen :) :) thx und +1 – SynerCoder