Dies ist eine Übersetzung des ersten Teils des Artikels. Der Artikel wurde 2008 geschrieben. Nach 10 Jahren fast seine Relevanz verloren.
Deterministische Freisetzung von Ressourcen - ein Bedarf
Im Laufe von mehr als 20 Jahren Erfahrung im Codieren habe ich manchmal meine eigenen Sprachen entwickelt, um Probleme zu lösen. Sie reichten von einfachen, zwingenden Sprachen bis zu speziellen regulären Ausdrücken für Bäume. Beim Erstellen von Sprachen gibt es viele Empfehlungen und einige einfache Regeln sollten nicht verletzt werden. Einer von ihnen:
Erstellen Sie niemals eine Ausnahmesprache, in der keine deterministische Freigabe von Ressourcen erfolgt.
Ratet mal, welchen Empfehlungen die .NET-Laufzeit nicht folgt und daher alle darauf basierenden Sprachen?
Der Grund für diese Regel ist, dass eine deterministische Freigabe von Ressourcen erforderlich ist, um unterstützte Programme zu erstellen . Die festgelegte Freigabe von Ressourcen bietet einen bestimmten Punkt, an dem der Programmierer sicher ist, dass die Ressource freigegeben ist. Es gibt zwei Möglichkeiten, zuverlässige Programme zu schreiben: Der traditionelle Ansatz besteht darin, Ressourcen so früh wie möglich freizugeben, und der moderne Ansatz besteht darin, Ressourcen auf unbestimmte Zeit freizugeben. Der Vorteil des modernen Ansatzes besteht darin, dass der Programmierer keine Ressourcen explizit freigeben muss. Der Nachteil ist, dass es viel schwieriger ist, eine zuverlässige Anwendung zu schreiben, es gibt viele subtile Fehler. Leider wurde die .NET-Laufzeit mit einem modernen Ansatz erstellt.
.NET unterstützt die nicht deterministische Freigabe von Ressourcen mithilfe der Finalize
Methode, die eine besondere Bedeutung hat. Für die deterministische Freigabe von Ressourcen hat Microsoft außerdem die IDisposable
Schnittstelle (und andere Klassen, auf die wir später noch IDisposable
werden) hinzugefügt. Trotzdem ist IDisposable
zur Laufzeit eine normale Schnittstelle, wie alle anderen auch. Dieser Status "zweitklassig" schafft einige Schwierigkeiten.
In C # kann "deterministische Freigabe für die Armen" mithilfe von try
and finally
try
oder using
(was fast dasselbe ist) implementiert werden. Microsoft hat lange darüber diskutiert, ob Linkzählungen durchgeführt werden sollen oder nicht, und es scheint mir, dass die falsche Entscheidung getroffen wurde. Daher müssen Sie für die deterministische Freigabe von Ressourcen das ungeschickte finally
\ using
Konstrukt oder einen direkten Aufruf von IDisposable.Dispose
, der mit Fehlern behaftet ist. Für einen C ++ - Programmierer, der es gewohnt ist, shared_ptr<T>
beide Optionen nicht attraktiv. (Der letzte Satz macht deutlich, wo der Autor eine solche Beziehung hat - ca.
IDisposable
IDisposable
ist eine Lösung für die deterministische Freigabe von Ressourcen, die von Misoftro angeboten werden. Einer ist für die folgenden Fälle:
- Jeder Typ, der verwaltete (
IDisposable
) Ressourcen besitzt. Ein Typ muss unbedingt besitzen , dh die Lebenszeit und die Ressourcen verwalten und sich nicht nur auf sie beziehen. - Jeder Typ, der nicht verwaltete Ressourcen besitzt.
- Jeder Typ, der sowohl verwaltete als auch nicht verwaltete Ressourcen besitzt.
- Jeder Typ, der von einer Klasse geerbt wurde, die
IDisposable
implementiert. Ich empfehle nicht, von Klassen zu erben, die nicht verwaltete Ressourcen besitzen. Verwenden Sie besser einen Anhang.
IDisposable
hilft, Ressourcen deterministisch IDisposable
, hat jedoch seine eigenen Probleme.
Schwierigkeiten IDisposable - Benutzerfreundlichkeit
IDisposable
Objekte sind IDisposable
um ziemlich umständlich zu verwenden. Die Verwendung eines Objekts muss in ein using
Konstrukt eingeschlossen werden. Die schlechte Nachricht ist, dass C # die using
mit einem Typ nicht zulässt, der IDisposable
nicht implementiert. Daher muss der Programmierer jedes Mal auf die Dokumentation zurückgreifen, um zu verstehen, ob es notwendig ist, using
oder nur using
überall zu schreiben, und dann zu löschen, wo der Compiler schwört.
Managed C ++ ist in dieser Hinsicht viel besser. Es unterstützt die Stapelsemantik für Referenztypen , die bei Bedarf nur für Typen verwendet wird. C # könnte von der Fähigkeit profitieren, mit jedem Typ zu schreiben.
Dieses Problem kann mit gelöst werden. Tools zur Code-Analyse. Wenn Sie die Verwendung vergessen, kann das Programm die Tests bestehen, stürzt jedoch ab, während Sie "auf den Feldern" arbeiten.
Anstatt Links zu zählen, hat IDisposable
ein anderes Problem - die Bestimmung des Besitzers. Wenn in C ++ die letzte Kopie von shared_ptr<T>
Gültigkeitsbereich shared_ptr<T>
, werden Ressourcen sofort freigegeben, ohne dass Sie überlegen müssen, wer freigegeben werden soll. IDisposable
zwingt den Programmierer zu bestimmen, wem das Objekt "gehört" und für dessen Freigabe verantwortlich ist. Manchmal ist das Eigentum offensichtlich: Wenn ein Objekt ein anderes kapselt und selbst IDisposable
implementiert, ist es daher für die Freigabe IDisposable
Objekte verantwortlich. Manchmal wird die Lebensdauer eines Objekts durch einen Codeblock bestimmt, und der Programmierer verwendet einfach die using
um diesen Block herum. Trotzdem gibt es viele Fälle, in denen ein Objekt an mehreren Orten verwendet werden kann und seine Lebensdauer schwer zu bestimmen ist (obwohl in diesem Fall die Referenzanzahl in Ordnung wäre).
Schwierigkeiten IDisposable - Abwärtskompatibilität
Das Hinzufügen von IDisposable
zur Klasse und das Entfernen von IDisposable
aus der Liste der implementierten Schnittstellen ist eine IDisposable
Änderung. Client-Code, der IDisposable
nicht erwartet, IDisposable
keine Ressourcen frei, wenn Sie IDisposable
zu einer Ihrer Klassen hinzufügen, die als Verweis auf eine Schnittstelle oder Basisklasse übergeben werden.
Microsoft selbst ist auf dieses Problem gestoßen. IEnumerator
nicht von IDisposable
geerbt, und IEnumerator<T>
geerbt. Wenn Sie IEnumerator<T>
Code übergeben, der IEnumerator
empfängt, wird Dispose
nicht aufgerufen.
Dies ist nicht das Ende der Welt, aber es gibt eine sekundäre Essenz von IDisposable
.
IDISposable Schwierigkeiten - Entwerfen einer Klassenhierarchie
Der größte Nachteil von IDisposable
im Bereich des Hierarchiedesigns besteht darin, dass jede Klasse und Schnittstelle vorhersagen muss, ob IDisposable
von ihren Nachkommen benötigt wird.
Wenn die Schnittstelle IDisposable
nicht erbt, die Klassen, die die Schnittstelle implementieren, jedoch auch IDisposable
implementieren, ignoriert der endgültige Code entweder die deterministische Version oder muss prüfen, ob das Objekt die IDisposable
Schnittstelle implementiert. Dafür ist es jedoch nicht möglich, das using-Konstrukt zu verwenden, und Sie müssen einen hässlichen try
schreiben und finally
.
Kurz gesagt, IDisposable
erschwert die Entwicklung wiederverwendbarer Software. Der Hauptgrund ist die Verletzung eines der Prinzipien des objektorientierten Designs - Trennung von Schnittstelle und Implementierung. Die Freigabe von Ressourcen sollte ein Implementierungsdetail sein. Microsoft hat beschlossen, die deterministische Freigabe von Ressourcen zu einer Schnittstelle zweiter Klasse zu machen.
Eine der nicht so schönen Lösungen besteht darin, alle Klassen IDisposable
implementieren zu IDisposable
, aber in der überwiegenden Mehrheit der Klassen wird IDisposable.Dispose
nichts tun. Das ist aber nicht zu schön.
Eine weitere Schwierigkeit bei IDisposable
sind Sammlungen. Einige Sammlungen „besitzen“ Objekte in ihnen, andere nicht. Die Sammlungen selbst implementieren jedoch kein IDisposable
. Der Programmierer muss daran denken, IDisposable.Dispose
für die Objekte in der Auflistung IDisposable.Dispose
oder eigene Nachkommen von Auflistungsklassen zu erstellen, die IDisposable
als Eigentümer implementieren.
Schwierigkeiten IDisposable - zusätzlicher "fehlerhafter" Zustand
IDisposable
kann jederzeit explizit aufgerufen werden, unabhängig von der Lebensdauer des Objekts. Das heißt, jedem Objekt wird ein "freigegebener" Status hinzugefügt, in dem empfohlen wird, eine ObjectDisposedException
. Das Überprüfen des Status und das Auslösen von Ausnahmen ist ein zusätzlicher Aufwand.
Anstatt nach jedem Niesen zu suchen, ist es besser, den Zugriff auf das Objekt im "freigegebenen" Zustand als "undefiniertes Verhalten" als Aufruf des freigegebenen Speichers zu betrachten.
Schwierigkeiten IDisposable - keine Garantien
IDisposable
ist nur eine Schnittstelle. Eine Klasse, die IDisposable
implementiert, unterstützt die deterministische Freigabe, garantiert sie jedoch nicht. Für Client-Code ist es in Ordnung, Dispose
nicht aufzurufen. Daher muss eine Klasse, die IDisposable
implementiert, sowohl deterministische als auch nicht deterministische Releases unterstützen.
Komplexitäten IDisposable - Komplexe Implementierung
Microsoft bietet ein Muster für die Implementierung von IDisposable
. (Früher gab es ein allgemein schreckliches Muster, aber vor relativ kurzer Zeit, nach dem Erscheinen von .NET 4, wurde die Dokumentation korrigiert, auch unter dem Einfluss dieses Artikels. In den alten Ausgaben von .NET-Büchern finden Sie die alte Version. - Ca. )
IDisposable.Dispose
möglicherweise überhaupt nicht aufgerufen, daher muss die Klasse einen Finalizer enthalten, um Ressourcen IDisposable.Dispose
.IDisposable.Dispose
kann mehrmals aufgerufen werden und sollte ohne sichtbare Nebenwirkungen funktionieren. Daher muss geprüft werden, ob die Methode bereits aufgerufen wurde oder nicht.- Finalizer werden in einem separaten Thread aufgerufen und können vor dem
IDisposable.Dispose
. Die Verwendung von GC.SuppressFinalize
, um solche "Rennen" zu vermeiden.
Außerdem:
- Finalizer werden aufgerufen, auch für Objekte, die im Konstruktor eine Ausnahme auslösen. Daher muss der Release-Code mit teilweise initialisierten Objekten funktionieren.
- Das Implementieren eines
IDisposable
in einer von CriticalFinalizerObject
geerbten Klasse erfordert nicht triviale Konstrukte. void Dispose(bool disposing)
ist eine virale Methode und muss in der eingeschränkten Ausführungsregion ausgeführt werden , für die ein Aufruf von RuntimeHelpers.PrepareMethod
erforderlich ist.
Schwierigkeiten IDisposable - Nicht für die Abschlusslogik geeignet
Herunterfahren eines Objekts - tritt häufig in Programmen in parallelen oder asynchronen Threads auf. Beispielsweise verwendet eine Klasse einen separaten Thread und möchte ihn mit ManualResetEvent
. Dies kann in IDisposable.Dispose
, kann jedoch zu einem Fehler führen, wenn der Code im Finalizer aufgerufen wird.
Um die Einschränkungen im Finalizer zu verstehen, müssen Sie wissen, wie der Garbage Collector funktioniert. Im Folgenden finden Sie ein vereinfachtes Diagramm, in dem viele Details zu Generationen, Schwachstellen, Wiederbelebung von Objekten, Hintergrund-Garbage-Collection usw. weggelassen werden.
Der .NET-Garbage Collector verwendet den Mark-and-Sweep-Algorithmus. Im Allgemeinen sieht die Logik folgendermaßen aus:
- Unterbrechen Sie alle Threads.
- Nehmen Sie alle
GCHandle
: Variablen auf dem Stapel, statische Felder, GCHandle
Objekte, Finalisierungswarteschlange. Beim Entladen der Anwendungsdomäne (Programmbeendigung) wird davon ausgegangen, dass die Variablen im Stapel und in den statischen Feldern keine Roots sind. - Gehen Sie rekursiv alle Links der Objekte durch und markieren Sie sie als "erreichbar".
- Durchsuchen Sie alle anderen Objekte mit Destruktoren (Finalisierern), deklarieren Sie sie als erreichbar und stellen Sie sie in die Finalisierungswarteschlange (
GC.SuppressFinalize
weist GC an, dies nicht zu tun). Objekte werden in einer unvorhersehbaren Reihenfolge in die Warteschlange gestellt.
Im Hintergrund funktioniert ein Stream (oder mehrere) der Finalisierung:
- Nimmt ein Objekt aus der Warteschlange und startet seinen Finalizer. Es ist möglich, mehrere Finalizer verschiedener Objekte gleichzeitig auszuführen.
- Das Objekt wird aus der Warteschlange entfernt, und wenn niemand anderes darauf verweist, wird es bei der nächsten Speicherbereinigung gelöscht.
Jetzt sollte klar sein, warum es unmöglich ist, vom Finalizer aus auf verwaltete Ressourcen zuzugreifen - Sie wissen nicht, in welcher Reihenfolge die Finalizer aufgerufen werden. Selbst das Aufrufen von IDisposable.Dispose
anderen Objekts aus dem Finalizer kann zu einem Fehler führen, da der Ressourcenfreigabecode möglicherweise in einem anderen Thread funktioniert.
Es gibt einige Ausnahmen, wenn Sie von einem Finalizer aus auf verwaltete Ressourcen zugreifen können:
- Die Finalisierung von Objekten, die von
CriticalFinalizerObject
geerbt wurden, erfolgt nach der Finalisierung von Objekten, die nicht von dieser Klasse geerbt wurden. Dies bedeutet, dass Sie ManualResetEvent
vom ManualResetEvent
aus aufrufen ManualResetEvent
, bis die Klasse von CriticalFinalizerObject
geerbt wird - Einige Objekte und Methoden sind speziell, z. B. die Konsolen- und einige Thread-Methoden. Sie können von Finalisierern aufgerufen werden, auch wenn das Programm endet.
Im Allgemeinen ist es besser, nicht über Finalizer auf verwaltete Ressourcen zuzugreifen. Trotzdem ist die Logik der Fertigstellung für nicht triviale Software erforderlich. Unter Windows.Forms
enthält Windows.Forms
die Abschlusslogik in der Application.Exit
Methode. Wenn Sie Ihre Komponentenbibliothek entwickeln, ist es am besten, die Abschlusslogik mit IDisposable
zu IDisposable
. Normale Kündigung bei Anruf IDisposable.Dispose
. IDisposable.Dispose
und sonst Notfall.
Microsoft ist auch auf dieses Problem gestoßen. Die StreamWriter
Klasse besitzt ein Stream
Objekt (abhängig von den Konstruktorparametern in der neuesten Version - ca. Per. ). StreamWriter.Close
den Puffer und ruft Stream.Close
(tritt auch auf, wenn using
- ca. Per. Stream.Close
). Wenn StreamWriter
nicht geschlossen ist, wird der Puffer nicht StreamWriter
und der StreamWriter
geht verloren. Microsoft hat den Finalizer einfach nicht neu definiert und damit das Abschlussproblem "gelöst". Ein gutes Beispiel für die Notwendigkeit einer Vervollständigungslogik.
Ich empfehle zu lesen
Viele Informationen zu .NET-Interna in diesem Artikel stammen von Jeffrey Richters CLR über C #. Wenn Sie es noch nicht haben, kaufen Sie es . Im Ernst. Dies ist das notwendige Wissen für jeden C # -Programmierer.
Fazit des Übersetzers
Die meisten .NET-Programmierer werden niemals auf die in diesem Artikel beschriebenen Probleme stoßen. .NET wird weiterentwickelt, um den Abstraktionsgrad zu erhöhen und den Bedarf an "Jonglieren" nicht verwalteter Ressourcen zu verringern. Trotzdem ist dieser Artikel insofern nützlich, als er die tiefen Details einfacher Dinge und ihre Auswirkungen auf das Code-Design beschreibt.
Im nächsten Teil wird anhand einer Reihe von Beispielen ausführlich erläutert, wie mit verwalteten und nicht verwalteten Ressourcen in .NET gearbeitet wird.