Setzen eines Objekts auf null vs Dispose ()

108

Ich bin fasziniert von der Funktionsweise von CLR und GC (ich arbeite daran, mein Wissen darüber zu erweitern, indem ich CLR über C #, Jon Skeets Bücher / Beiträge und mehr lese).

Was ist der Unterschied zwischen den Worten:

MyClass myclass = new MyClass();
myclass = null;

Oder indem Sie MyClass IDisposable und einen Destruktor implementieren lassen und Dispose () aufrufen?

Wenn ich einen Codeblock mit einer using-Anweisung habe (z. B. unten), wenn ich durch den Code gehe und den using-Block verlasse, wird das Objekt dann entsorgt oder wenn eine Garbage Collection auftritt? Was würde passieren, wenn ich Dispose () im using-Block aufrufe?

using (MyDisposableObj mydispobj = new MyDisposableObj())
{

}

Stream-Klassen (z. B. BinaryWriter) haben eine Finalize-Methode? Warum sollte ich das nutzen wollen?

GurdeepS
quelle

Antworten:

210

Es ist wichtig, die Entsorgung von der Müllabfuhr zu trennen. Es sind völlig getrennte Dinge, mit einem gemeinsamen Punkt, auf den ich gleich noch eingehen werde.

Dispose, Speicherbereinigung und Finalisierung

Wenn Sie eine usingAnweisung schreiben , ist dies einfach syntaktischer Zucker für einen try / finally-Block, der Disposeauch dann aufgerufen wird, wenn der Code im Hauptteil der usingAnweisung eine Ausnahme auslöst. Dies bedeutet nicht , dass es sich bei dem Objekt um Müll handelt, der am Ende des Blocks gesammelt wird.

Bei der Entsorgung handelt es sich um nicht verwaltete Ressourcen (Nicht-Speicherressourcen). Dies können UI-Handles, Netzwerkverbindungen, Dateihandles usw. sein. Dies sind begrenzte Ressourcen. Sie möchten sie daher im Allgemeinen so bald wie möglich freigeben. Sie sollten IDisposableimmer dann implementieren, wenn Ihr Typ eine nicht verwaltete Ressource "besitzt", entweder direkt (normalerweise über a IntPtr) oder indirekt (z. B. über a Stream, a SqlConnectionusw.).

Bei der Speicherbereinigung selbst geht es nur um Erinnerung - mit einer kleinen Wendung. Der Garbage Collector kann Objekte finden, auf die nicht mehr verwiesen werden kann, und diese freigeben. Es wird jedoch nicht ständig nach Müll gesucht - nur dann, wenn festgestellt wird, dass dies erforderlich ist (z. B. wenn einer "Generation" des Heaps der Speicherplatz ausgeht).

Die Wendung ist die Finalisierung . Der Garbage Collector führt eine Liste von Objekten, die nicht mehr erreichbar sind, aber einen Finalizer haben (geschrieben als~Foo() etwas verwirrend in C # - sie sind nichts anderes als C ++ - Destruktoren). Es führt die Finalizer für diese Objekte aus, nur für den Fall, dass sie eine zusätzliche Bereinigung durchführen müssen, bevor ihr Speicher freigegeben wird.

Finalizer werden fast immer verwendet, um Ressourcen zu bereinigen, wenn der Benutzer des Typs vergessen hat, sie ordnungsgemäß zu entsorgen. Wenn Sie also ein öffnen, FileStreamaber vergessen, Disposeoder aufzurufen Close, gibt der Finalizer schließlich das zugrunde liegende Dateihandle für Sie frei. In einem gut geschriebenen Programm sollten Finalizer meiner Meinung nach fast nie feuern.

Setzen einer Variablen auf null

Ein kleiner Punkt beim Setzen einer Variablen auf null- dies ist für die Speicherbereinigung fast nie erforderlich. Vielleicht möchten Sie es manchmal tun, wenn es sich um eine Mitgliedsvariable handelt, obwohl es meiner Erfahrung nach selten vorkommt, dass ein "Teil" eines Objekts nicht mehr benötigt wird. Wenn es sich um eine lokale Variable handelt, ist die JIT normalerweise intelligent genug (im Release-Modus), um zu wissen, wann Sie keine Referenz mehr verwenden werden. Beispielsweise:

StringBuilder sb = new StringBuilder();
sb.Append("Foo");
string x = sb.ToString();

// The string and StringBuilder are already eligible
// for garbage collection here!
int y = 10;
DoSomething(y);

// These aren't helping at all!
x = null;
sb = null;

// Assume that x and sb aren't used here

Es kann sich lohnen, eine lokale Variable festzulegen, nullwenn Sie sich in einer Schleife befinden und einige Zweige der Schleife die Variable verwenden müssen, aber Sie wissen, dass Sie einen Punkt erreicht haben, an dem Sie dies nicht tun. Beispielsweise:

SomeObject foo = new SomeObject();

for (int i=0; i < 100000; i++)
{
    if (i == 5)
    {
        foo.DoSomething();
        // We're not going to need it again, but the JIT
        // wouldn't spot that
        foo = null;
    }
    else
    {
        // Some other code 
    }
}

IDisposable / Finalizer implementieren

Sollten Ihre eigenen Typen Finalizer implementieren? Mit ziemlicher Sicherheit nicht. Wenn Sie nur indirekt über nicht verwaltete Ressourcen verfügen (z. FileStreamB. über eine Mitgliedsvariable), hilft das Hinzufügen eines eigenen Finalizers nicht weiter: Der Stream ist mit ziemlicher Sicherheit für die Speicherbereinigung geeignet, wenn sich Ihr Objekt befindet, sodass Sie sich einfach darauf verlassen können FileStreameinen Finalizer haben (falls erforderlich - er kann sich auf etwas anderes beziehen usw.). Wenn Sie eine nicht verwaltete Ressource "fast" direkt halten möchten, SafeHandleist Ihr Freund - es dauert ein bisschen, bis Sie damit beginnen, aber es bedeutet, dass Sie fast nie wieder einen Finalizer schreiben müssen . Normalerweise sollten Sie einen Finalizer nur benötigen, wenn Sie eine Ressource (an IntPtr) wirklich direkt im Griff haben und sich umsehen möchtenSafeHandle sobald Sie können. (Dort gibt es zwei Links - lesen Sie im Idealfall beide.)

Joe Duffy hat eine sehr lange Reihe von Richtlinien zu Finalisierern und IDisposable (zusammen mit vielen intelligenten Leuten geschrieben), die es wert sind, gelesen zu werden. Es ist zu beachten, dass das Versiegeln Ihrer Klassen das Leben erheblich erleichtert: Das Muster des Überschreibens Disposezum Aufrufen einer neuen virtuellen Dispose(bool)Methode usw. ist nur relevant, wenn Ihre Klasse für die Vererbung ausgelegt ist.

Dies war ein kleiner Streifzug, aber bitte fragen Sie nach, wo Sie welche haben möchten :)

Jon Skeet
quelle
Betreff "Das eine Mal, wenn es sich lohnt, eine lokale Variable auf Null zu setzen" - vielleicht auch einige der schwierigeren "Capture" -Szenarien (mehrere Captures derselben Variablen) -, aber es lohnt sich möglicherweise nicht, den Beitrag zu komplizieren! +1 ...
Marc Gravell
@Marc: Das stimmt - ich hatte noch nicht einmal an erfasste Variablen gedacht. Hmm. Ja, ich denke ich lasse das in Ruhe;)
Jon Skeet
Könnten Sie bitte sagen, was passieren wird, wenn Sie oben in Ihrem Code-Snippet "foo = null" setzen? Soweit ich weiß, löscht diese Zeile nur den Wert einer Variablen, die auf ein foo-Objekt im verwalteten Heap zeigt. Die Frage ist also, was mit dem Objekt dort passieren wird. sollten wir nicht darüber verfügen?
odiseh
@odiseh: Wenn das Objekt wegwerfbar wäre, dann ja - Sie sollten es entsorgen. Dieser Abschnitt der Antwort befasste sich jedoch nur mit der Speicherbereinigung, die völlig getrennt ist.
Jon Skeet
1
Ich suchte nach einer Klärung einiger IDisposable-Bedenken, googelte nach "IDisposable Skeet" und fand diese. Toll! : D
Maciej Wozniak
22

Wenn Sie ein Objekt entsorgen, werden die Ressourcen freigegeben. Wenn Sie einer Variablen null zuweisen, ändern Sie nur eine Referenz.

myclass = null;

Nachdem Sie dies ausgeführt haben, ist das Objekt, auf das sich myclass bezog, noch vorhanden und wird fortgesetzt, bis der GC damit fertig ist, es zu bereinigen. Wenn Dispose explizit aufgerufen wird oder sich in einem using-Block befindet, werden alle Ressourcen so schnell wie möglich freigegeben.

rekursiv
quelle
7
Es kann immer noch nicht existieren , nachdem diese Zeile ausgeführt wird - es war Müll gesammelt hat , bevor diese Linie. Die JIT ist intelligent - Linien wie diese sind fast immer irrelevant.
Jon Skeet
6
Das Setzen auf null kann bedeuten, dass die vom Objekt gehaltenen Ressourcen niemals freigegeben werden. Der GC entsorgt nicht, er finalisiert nur. Wenn das Objekt also direkt nicht verwaltete Ressourcen enthält und sein Finalizer nicht entsorgt (oder keinen Finalizer hat), gehen diese Ressourcen verloren. Etwas zu beachten.
LukeH
6

Die beiden Operationen haben nicht viel miteinander zu tun. Wenn Sie einen Verweis auf null setzen, geschieht dies einfach. Es hat an sich keinen Einfluss auf die Klasse, auf die überhaupt verwiesen wurde. Ihre Variable zeigt einfach nicht mehr auf das Objekt, das sie verwendet hat, sondern das Objekt selbst bleibt unverändert.

Wenn Sie Dispose () aufrufen, handelt es sich um einen Methodenaufruf für das Objekt selbst. Was auch immer die Dispose-Methode tut, wird jetzt für das Objekt ausgeführt. Dies hat jedoch keinen Einfluss auf Ihre Referenz auf das Objekt.

Der einzige Überlappungsbereich besteht darin, dass, wenn keine Verweise mehr auf ein Objekt vorhanden sind, möglicherweise Müll gesammelt wird. Und wenn die Klasse die IDisposable-Schnittstelle implementiert, wird Dispose () für das Objekt aufgerufen, bevor es mit Müll gesammelt wird.

Dies geschieht jedoch aus zwei Gründen nicht sofort, nachdem Sie Ihre Referenz auf null gesetzt haben. Erstens können andere Referenzen vorhanden sein, sodass noch kein Müll gesammelt wird, und zweitens passiert nichts, selbst wenn dies die letzte Referenz war, sodass es jetzt bereit ist, Müll zu sammeln, bis der Garbage Collector sich zum Löschen entscheidet das Objekt.

Das Aufrufen von Dispose () für ein Objekt "tötet" das Objekt in keiner Weise. Es wird allgemein aufzuräumen , so dass das Objekt verwendet kann sicher wieder gelöscht werden, aber schließlich gibt es nichts Magisches an entsorgen, es ist nur eine Klassenmethode.

jalf
quelle
Ich denke, diese Antwort ergänzt oder ist ein Detail der Antwort von "rekursiv".
Dance2die