2009-12-18 10 views
43

Ich glaube, ich habe ein ziemlich gutes Verständnis von Schließungen, wie man sie benutzt und wann sie nützlich sein können. Aber was ich nicht verstehe ist, wie sie hinter den Kulissen im Gedächtnis arbeiten. Einige Beispiel-Code:Wie funktionieren Verschlüsse hinter den Kulissen? (C#)

public Action Counter() 
{ 
    int count = 0; 
    Action counter =() => 
    { 
     count++; 
    }; 

    return counter; 
} 

Normalerweise, wenn {count} nicht durch die Schließung gefangen genommen wurde, würde seine Lebenszyklus der Zähler() -Methode scoped werden, und nachdem sie abgeschlossen würde es gehen weg mit dem Rest des Stapels Zuweisung für Counter(). Was passiert wenn es geschlossen wird? Bleibt die gesamte Stack-Zuweisung für diesen Aufruf von Counter() bestehen? Kopiert sie {count} in den Heap? Wird es niemals tatsächlich auf dem Stack zugewiesen, sondern vom Compiler als geschlossen erkannt und lebt daher immer auf dem Heap?

Für diese spezielle Frage interessiert mich hauptsächlich, wie dies in C# funktioniert, aber nicht gegen Vergleiche mit anderen Sprachen, die Schließungen unterstützen.

+0

Große Frage. Ich bin mir nicht sicher, aber ja, Sie können den Stapelrahmen in C# behalten. Generatoren verwenden es ständig (Sache LINQ für Datenstrukturen), die auf Ausbeute unter der Haube angewiesen sind. Hoffentlich bin ich nicht daneben. wenn ich bin, werde ich sehr viel lernen. –

+2

Ausbeute macht die Methode in eine separate Klasse mit einer Zustandsmaschine. Der Stack selbst wird nicht beibehalten, aber der Stack-Status wird in eine vom Compiler generierte Klasse – thecoop

+0

@thecoop in den Klassenstatus verschoben. Haben Sie einen Link, der das erklärt? –

Antwort

31

Der Compiler (im Gegensatz zur Laufzeit) erstellt eine andere Klasse/Typ. Die Funktion mit Ihrer Schließung und alle Variablen, die Sie geschlossen/gehoben/gefangen genommen haben, werden in Ihrem Code als Mitglieder dieser Klasse neu geschrieben. Eine Schließung in .NET wird als eine Instanz dieser versteckten Klasse implementiert.

Das bedeutet, dass Ihre Zählvariable vollständig Mitglied einer anderen Klasse ist und die Lebensdauer dieser Klasse wie jedes andere CLR-Objekt funktioniert; Es ist nicht für Garbage Collection geeignet, bis es nicht mehr rooted ist. Das bedeutet, solange Sie eine aufrufbare Referenz auf die Methode haben, die nirgendwohin geht.

+4

Überprüfen Sie den betreffenden Code mit Reflektor, um ein Beispiel für diese – Greg

+5

zu sehen ... einfach suchen die hässlichste benannte Klasse in Ihrer Lösung. – Will

+2

Bedeutet dies, dass eine Schließung zu einer neuen Heap-Zuweisung führt, auch wenn der Wert, der geschlossen wird, ein Primitiv ist? – Matt

45

Ihre dritte Vermutung ist richtig. Der Compiler generiert Code wie folgt:

Sinn machen?

Auch Sie haben nach Vergleichen gefragt. VB und JScript erzeugen beide Schließungen auf ziemlich ähnliche Weise.

+14

Warum sollten wir dir glauben? Oh, das stimmt ... – ChaosPandion

0

Dank @HenkHolterman. Da es bereits von Eric erklärt wurde, fügte ich den Link nur hinzu, um zu zeigen, welche tatsächliche Klasse der Compiler zum Schließen generiert. Ich möchte hinzufügen, dass die Erstellung von Display-Klassen durch C# -Compiler zu Speicherlecks führen kann. Zum Beispiel gibt es innerhalb einer Funktion eine int-Variable, die von einem Lambda-Ausdruck erfasst wird, und dort eine andere lokale Variable, die einfach einen Verweis auf ein großes Byte-Array enthält. Der Compiler würde eine Anzeigeklasseninstanz erzeugen, die die Verweise sowohl auf die Variablen, d. H. Int, als auch auf das Byte-Array enthält. Aber das Byte-Array wird nicht Müll gesammelt werden, bis das Lambda referenziert wird.

0

Eric Lipperts Antwort trifft wirklich den Punkt. Es wäre jedoch nett, ein Bild davon zu erstellen, wie Stack-Frames und Captures im Allgemeinen funktionieren. Um dies zu tun, hilft es, ein etwas komplexeres Beispiel zu betrachten. Hier

ist das Erfassungscode:

public class Scorekeeper { 
    int swish = 7; 

    public Action Counter(int start) 
    { 
     int count = 0; 
     Action counter =() => { count += start + swish; } 
     return counter; 
    } 
} 

Und hier ist das, was ich denke, das Äquivalent wäre (wenn wir Glück haben Eric Lippert auf Kommentar wird, ob dies tatsächlich korrekt ist oder nicht):

private class Locals 
{ 
    public Locals(Scorekeeper sk, int st) 
    { 
     this.scorekeeper = sk; 
     this.start = st; 
    } 

    private Scorekeeper scorekeeper; 
    private int start; 

    public int count; 

    public void Anonymous() 
    { 
    this.count += start + scorekeeper.swish; 
    } 
} 

public class Scorekeeper { 
    int swish = 7; 

    public Action Counter(int start) 
    { 
     Locals locals = new Locals(this, start); 
     locals.count = 0; 
     Action counter = new Action(locals.Anonymous); 
     return counter; 
    } 
} 

Der Punkt ist, dass die lokale Klasse den gesamten Stack-Frame ersetzt und jedes Mal, wenn die Counter-Methode aufgerufen wird, entsprechend initialisiert wird. In der Regel enthält der Stack-Frame einen Verweis auf 'this' sowie Methodenargumente und lokale Variablen. (Der Stapelrahmen wird auch tatsächlich erweitert, wenn ein Steuerblock eingegeben wird.)

Folglich haben wir nicht nur ein Objekt, das dem erfassten Kontext entspricht, stattdessen haben wir tatsächlich ein Objekt pro erfasstem Stapelrahmen.

Basierend darauf können wir das folgende mentale Modell verwenden: Stapelrahmen werden auf dem Heap (statt auf dem Stapel) gehalten, während der Stapel selbst nur Zeiger auf die Stapelrahmen enthält, die sich auf dem Heap befinden. Lambda-Methoden enthalten einen Zeiger auf den Stapelrahmen. Dies geschieht über verwalteten Speicher, so dass der Frame auf dem Heap bleibt, bis er nicht mehr benötigt wird.

Offensichtlich kann der Compiler dies implementieren, indem er nur den Heap verwendet, wenn das Heap-Objekt zur Unterstützung eines Lambda-Abschlusses benötigt wird.

Was ich an diesem Modell mag ist, dass es ein integriertes Bild für "Rendite" bietet. Wir können uns eine Iterator-Methode vorstellen (mit yield return), als wäre ihr Stack-Frame auf dem Heap und der referenzierende Pointer in einer lokalen Variablen im Aufrufer zur Verwendung während der Iteration erstellt worden.

+0

Es ist nicht korrekt; Wie kann man von außerhalb der Klasse "Scorekeeper" auf den privaten 'Swish' zugreifen? Was passiert, wenn 'start' mutiert ist? Aber mehr auf den Punkt: Was ist der Wert bei der Beantwortung einer acht Jahre alten Frage mit einer akzeptierten Antwort? –

+0

Wenn Sie wissen möchten, was der echte Codegen ist, verwenden Sie ILDASM oder einen IL-to-Source-Disassembler. –

+0

Ein besserer Weg, um darüber nachzudenken, ist es, nicht mehr an "Stack Frames" zu denken. Der Stack ist einfach eine Datenstruktur, die zwei Dinge implementiert: ** Aktivierung ** und ** Fortsetzung **. Das heißt: Welche Werte sind mit der Aktivierung einer Methode verbunden und welcher Code wird ausgeführt, nachdem diese Methode zurückgegeben wurde? Der Stapel ist jedoch nur eine geeignete Datenstruktur zum Speichern von Aktivierungs-/Fortsetzungsinformationen **, wenn die Aktivierungszeiten der Methode logisch einen Stapel bilden **. –

Verwandte Themen