Was ist der Unterschied zwischen der Verwendung von IDisposable und einem Destruktor in C #?

101

Wann würde ich IDispose in einer Klasse im Gegensatz zu einem Destruktor implementieren? Ich habe diesen Artikel gelesen , aber ich vermisse immer noch den Punkt.

Ich gehe davon aus, dass ich IDispose, wenn ich es für ein Objekt implementiere, explizit "zerstören" kann, anstatt darauf zu warten, dass der Garbage Collector dies tut. Ist das richtig?

Bedeutet das, dass ich Dispose für ein Objekt immer explizit aufrufen sollte? Was sind einige gängige Beispiele dafür?

Jordan Parmer
quelle
5
In der Tat sollten Sie Dispose für jedes Disposable-Objekt aufrufen. Sie können dies einfach mit dem usingKonstrukt tun .
Luc Touraille
Ah, das macht Sinn. Ich hatte mich immer gefragt, warum die Anweisung "using" für Dateistreams verwendet wurde. Ich weiß, dass es etwas mit dem Umfang des Objekts zu tun hat, aber ich habe es nicht mit der IDisposable-Schnittstelle in Zusammenhang gebracht.
Jordan Parmer
5
Ein wichtiger Punkt, den Sie beachten sollten, ist, dass ein Finalizer niemals auf verwaltete Mitglieder einer Klasse zugreifen sollte , da diese Mitglieder möglicherweise keine gültigen Referenzen mehr sind.
Dan Bryant

Antworten:

126

Ein Finalizer (auch Destruktor genannt) ist Teil der Garbage Collection (GC) - es ist unbestimmt, wann (oder sogar wenn) dies geschieht, da GC hauptsächlich aufgrund des Speicherdrucks erfolgt (dh mehr Speicherplatz benötigt). Finalizer werden normalerweise nur zum Bereinigen nicht verwalteter Ressourcen verwendet, da verwaltete Ressourcen über eine eigene Sammlung / Entsorgung verfügen.

Daher IDisposablewird verwendet, um Objekte deterministisch zu bereinigen, dh jetzt. Es sammelt nicht den Speicher des Objekts (der noch zu GC gehört), sondern wird beispielsweise zum Schließen von Dateien, Datenbankverbindungen usw. verwendet.

Es gibt viele frühere Themen zu diesem Thema:

Beachten Sie schließlich, dass es nicht ungewöhnlich ist, dass ein IDisposableObjekt auch einen Finalizer hat. In diesem Fall werden Dispose()normalerweise Aufrufe ausgeführt GC.SuppressFinalize(this), was bedeutet, dass GC den Finalizer nicht ausführt - es wirft einfach den Speicher weg (viel billiger). Der Finalizer wird weiterhin ausgeführt, wenn Sie Dispose()das Objekt vergessen haben .

Marc Gravell
quelle
Vielen Dank! Das macht durchaus Sinn. Ich schätze die großartige Resonanz sehr.
Jordan Parmer
27
Eine zusätzliche Sache zu sagen. Fügen Sie Ihrer Klasse keinen Finalizer hinzu, es sei denn, Sie brauchen wirklich einen. Wenn Sie einen Finalizer (Destruktor) hinzufügen, muss der GC ihn aufrufen (sogar einen leeren Finalizer), und um ihn aufzurufen, überlebt das Objekt immer eine Müllsammlung der 1. Generation. Dies wird den GC behindern und verlangsamen. Das ist es, was Marc sagt, um SuppressFinalize im obigen Code aufzurufen
Kevin Jones
1
Finalize besteht also darin, nicht verwaltete Ressourcen freizugeben. Aber Dispose könnte verwendet werden, um verwaltete und nicht verwaltete Ressourcen freizugeben?
Dark_Knight
2
@ Dark ja; weil 6 Ebenen in der Verwaltungskette eine nicht verwaltete sein könnten, die sofort bereinigt werden muss
Marc Gravell
1
@ KevinJones Objekte mit einem Finalizer überleben garantiert Gen 0, nicht 1, oder? Ich habe das in einem Buch namens .NET Performance gelesen.
David Klempfner
25

Die Finalize()Methode besteht darin, sicherzustellen, dass ein .NET-Objekt nicht verwaltete Ressourcen bereinigen kann, wenn Müll gesammelt wird . Objekte wie Datenbankverbindungen oder Dateihandler sollten jedoch so bald wie möglich freigegeben werden, anstatt sich auf die Speicherbereinigung zu verlassen. Dazu sollten Sie die IDisposableSchnittstelle implementieren und Ihre Ressourcen in der Dispose()Methode freigeben .

Igal Tabachnik
quelle
9

Es gibt eine sehr gute Beschreibung auf MSDN :

Die Hauptverwendung dieser Schnittstelle besteht darin , nicht verwaltete Ressourcen freizugeben . Der Garbage Collector gibt den einem verwalteten Objekt zugewiesenen Speicher automatisch frei, wenn dieses Objekt nicht mehr verwendet wird. Es ist jedoch nicht möglich vorherzusagen, wann eine Speicherbereinigung stattfinden wird . Darüber hinaus kennt der Garbage Collector keine nicht verwalteten Ressourcen wie Fensterhandles oder geöffnete Dateien und Streams.

Verwenden Sie die Dispose-Methode dieser Schnittstelle, um nicht verwaltete Ressourcen in Verbindung mit dem Garbage Collector explizit freizugeben . Der Verbraucher eines Objekts kann diese Methode aufrufen, wenn das Objekt nicht mehr benötigt wird.

abatishchev
quelle
1
Eine große Schwäche dieser Beschreibung besteht darin, dass MS Beispiele für nicht verwaltete Ressourcen enthält, aber nach dem, was ich gesehen habe, den Begriff nie wirklich definiert. Da verwaltete Objekte im Allgemeinen nur innerhalb von verwaltetem Code verwendet werden können, könnte man denken, dass in nicht verwaltetem Code verwendete Dinge nicht verwaltete Ressourcen sind, aber das stimmt nicht wirklich. Viele nicht verwaltete Codes verwenden keine Ressourcen, und einige Arten von nicht verwalteten Ressourcen, z. B. Ereignisse, sind nur im Universum für verwalteten Code vorhanden.
Superkatze
1
Wenn ein kurzlebiges Objekt ein Ereignis von einem langlebigen Objekt abonniert (z. B. um Änderungen gebeten zu werden, die während der Lebensdauer des kurzlebigen Objekts auftreten), sollte ein solches Ereignis als nicht verwaltete Ressource betrachtet werden, da dies nicht der Fall ist Wenn Sie das Ereignis abbestellen, wird die Lebensdauer des kurzlebigen Objekts auf die des langlebigen Objekts verlängert. Wenn viele tausend oder Millionen kurzlebiger Objekte ein Ereignis abonniert haben, aber ohne Abmeldung abgebrochen wurden, kann dies zu einem Speicher- oder CPU-Verlust führen (da sich die für die Verarbeitung jedes Abonnements erforderliche Zeit erhöhen würde).
Supercat
1
Ein weiteres Szenario mit nicht verwalteten Ressourcen in verwaltetem Code wäre die Objektzuweisung aus Pools. Insbesondere wenn Code in .NET Micro Framework ausgeführt werden muss (dessen Garbage Collector viel weniger effizient ist als der auf Desktop-Computern), kann es hilfreich sein, wenn Code z. B. über ein Array von Strukturen verfügt, von denen jede als "verwendet" gekennzeichnet sein kann. oder "frei". Eine Zuweisungsanforderung sollte eine Struktur finden, die derzeit als "frei" markiert ist, sie als "verwendet" markieren und einen Index an sie zurückgeben. Eine Freigabeanforderung sollte eine Struktur als "frei" markieren. Wenn eine Zuweisungsanforderung zB 23 zurückgibt, dann ...
Supercat
1
... wenn der Code den Eigentümer des Arrays niemals darüber informiert, dass er Element Nr. 23 nicht mehr benötigt, kann dieser Array-Steckplatz niemals von einem anderen Code verwendet werden. Eine solche manuelle Zuweisung aus Array-Slots wird im Desktop-Code nicht sehr häufig verwendet, da der GC ziemlich effizient ist, aber im Code, der auf dem Micro Framework ausgeführt wird, kann dies einen großen Unterschied bewirken.
Superkatze
8

Das einzige, was in einem C # -Destruktor enthalten sein sollte, ist diese Zeile:

Dispose(False);

Das ist es. Nichts anderes sollte jemals in dieser Methode sein.

Jonathan Allen
quelle
3
Dies ist das von Microsoft in der .NET-Dokumentation vorgeschlagene Entwurfsmuster. Verwenden Sie es jedoch nicht, wenn Ihr Objekt nicht identifizierbar ist. msdn.microsoft.com/en-us/library/fs2xkftw%28v=vs.110%29.aspx
Zbyl
1
Ich kann mir keinen Grund vorstellen, eine Klasse mit einem Finalizer anzubieten, der auch keine Dispose-Methode hat.
Jonathan Allen
4

Ihre Frage, ob Sie immer anrufen sollten oder nicht, Disposeist normalerweise eine hitzige Debatte. In diesem Blog finden Sie eine interessante Perspektive von angesehenen Personen in der .NET-Community.

Persönlich denke ich, dass Jeffrey Richters Position, dass Anrufe Disposenicht obligatorisch sind, unglaublich schwach ist. Er gibt zwei Beispiele, um seine Meinung zu rechtfertigen.

Im ersten Beispiel sagt er, dass das Aufrufen Disposevon Windows Forms-Steuerelementen in Mainstream-Szenarien mühsam und unnötig ist. Er erwähnt jedoch nicht, dass DisposeSteuercontainer in diesen Mainstream-Szenarien tatsächlich automatisch aufgerufen werden.

Im zweiten Beispiel gibt er an, dass ein Entwickler fälschlicherweise annehmen könnte, dass die Instanz von IAsyncResult.WaitHandleaggressiv entsorgt werden sollte, ohne zu bemerken, dass die Eigenschaft das Wartehandle träge initialisiert, was zu einer unnötigen Leistungseinbuße führt. Das Problem bei diesem Beispiel ist jedoch, dass das IAsyncResultselbst nicht den von Microsoft veröffentlichten Richtlinien für den Umgang mit IDisposableObjekten entspricht. Wenn eine Klasse einen Verweis auf einen IDisposableTyp enthält, sollte die Klasse selbst implementiert werden IDisposable. Wenn IAsyncResultdiese Regel befolgt wird, kann die eigene DisposeMethode entscheiden, welches ihrer Mitglieder entsorgt werden muss.

Wenn also nicht jemand ein überzeugenderes Argument hat, werde ich im Lager "Immer anrufen" entsorgen bleiben, mit dem Verständnis, dass es einige Randfälle geben wird, die hauptsächlich aus schlechten Designentscheidungen resultieren.

Brian Gideon
quelle
3

Es ist wirklich ziemlich einfach. Ich weiß, dass es beantwortet wurde, aber ich werde es erneut versuchen, aber ich werde versuchen, es so einfach wie möglich zu halten.

Ein Destruktor sollte im Allgemeinen niemals verwendet werden. Es wird nur ausgeführt .net möchte, dass es ausgeführt wird. Es wird nur nach einem Müllsammelzyklus ausgeführt. Es wird möglicherweise nie während des Lebenszyklus Ihrer Anwendung ausgeführt. Aus diesem Grund sollten Sie niemals Code in einen Destruktor einfügen, der ausgeführt werden muss. Sie können sich auch nicht darauf verlassen, dass vorhandene Objekte in der Klasse vorhanden sind, wenn sie ausgeführt werden (sie wurden möglicherweise bereits bereinigt, da die Reihenfolge, in der Destruktoren ausgeführt werden, nicht garantiert ist).

IDisposible sollte immer dann verwendet werden, wenn Sie ein Objekt haben, das Ressourcen erstellt, die bereinigt werden müssen (z. B. Datei- und Grafikhandles). Tatsächlich argumentieren viele, dass alles, was Sie in einen Destruktor einfügen, aus den oben genannten Gründen nicht verfügbar sein sollte.

Die meisten Klassen rufen dispose auf, wenn der Finalizer ausgeführt wird, aber dies dient lediglich als Schutz und sollte niemals als verlässlich angesehen werden. Sie sollten explizit alles entsorgen, was IDisposable implementiert, wenn Sie damit fertig sind. Wenn Sie IDisposable implementieren, sollten Sie dispose im Finalizer aufrufen. Ein Beispiel finden Sie unter http://msdn.microsoft.com/en-us/library/system.idisposable.aspx .

DaEagle
quelle
Nein, der Garbage Collector ruft niemals Dispose () auf. Es wird nur der Finalizer aufgerufen.
Marc Gravell
Das wurde behoben. Klassen sollen Dispose in ihrem Finalizer aufrufen, müssen es aber nicht.
DaEagle