IDisposable - dass deine Mutter nicht über die Freigabe von Ressourcen gesprochen hat. Teil 1

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:


  1. Unterbrechen Sie alle Threads.
  2. 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.
  3. Gehen Sie rekursiv alle Links der Objekte durch und markieren Sie sie als "erreichbar".
  4. 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:


  1. Nimmt ein Objekt aus der Warteschlange und startet seinen Finalizer. Es ist möglich, mehrere Finalizer verschiedener Objekte gleichzeitig auszuführen.
  2. 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:


  1. 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
  2. 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.

Source: https://habr.com/ru/post/de414873/


All Articles