Dispose-Muster

Hinweis

Diese Inhalte wurden mit Genehmigung von Pearson Education, Inc. aus Framework Design Guidelines nachgedruckt: Conventions, Idioms, and Patterns for Reusable .NET Libraries, 2nd Edition. Diese Ausgabe wurde 2008 veröffentlicht, und das Buch wurde seitdem in der dritten Ausgabe vollständig überarbeitet. Einige der Informationen auf dieser Seite sind möglicherweise veraltet.

Alle Programme rufen im Laufe ihrer Ausführung mindestens eine Systemressource ab (z. B. Speicher, Systemhandles oder Datenbankverbindungen). Entwickler*innen müssen bei der Verwendung solcher Systemressourcen vorsichtig sein, da sie freigegeben werden müssen, nachdem sie abgerufen und verwendet wurden.

Die Common Language Runtime (CLR) bietet Unterstützung für die automatische Speicherverwaltung. Verwalteter Speicher (mit dem C#-Operator new zugeordneter Speicher) muss nicht explizit freigegeben werden. Er wird automatisch vom Garbage Collector (GC) freigegeben. Für Entwickler*innen entfällt damit die mühsame und schwierige Aufgabe, Speicher freizugeben – einer der wichtigsten Gründe für die beispiellose Produktivität, die .NET Framework bietet.

Leider ist der verwaltete Speicher nur einer von vielen Systemressourcentypen. Andere Ressourcen als der verwaltete Speicher müssen weiterhin explizit freigegeben werden und werden als nicht verwaltete Ressourcen bezeichnet. Der GC bzw. die Garbage Collection (automatische Speicherbereinigung) wurde nicht speziell dafür konzipiert, solche nicht verwalteten Ressourcen zu verwalten. Die Verantwortung für die Verwaltung dieser Ressourcen liegt daher bei den Entwickler*innen.

Die CLR erleichtert die Freigabe nicht verwalteter Ressourcen. System.Object deklariert eine virtuelle Methode Finalize (auch als Finalizer bezeichnet), die vom GC aufgerufen wird, bevor er den Speicher des Objekts freigibt, und zum Freigeben nicht verwalteter Ressourcen überschrieben werden kann. Typen, die den Finalizer außer Kraft setzen, werden als finalisierbare Typen bezeichnet.

Obwohl Finalizer in einigen Bereinigungsszenarien effektiv sind, haben sie zwei erhebliche Nachteile:

  • Der Finalizer wird aufgerufen, wenn der GC erkennt, dass ein Objekt für die Garbage Collection freigegeben ist. Dies geschieht nach einem unbestimmten Zeitraum, wenn die Ressource nicht mehr benötigt wird. Die Verzögerung zwischen dem Zeitpunkt, zu dem Entwickler*innen die Ressource freigeben können oder möchten, und dem Zeitpunkt, zu dem die Ressource tatsächlich vom Finalizer freigegeben wird, kann in Programmen, die viele knappe Ressourcen abrufen (d. h. Ressourcen, die leicht aufgebraucht werden können), oder in Fällen, in denen die weitere Verwendung von Ressourcen kostspielig ist (z. B. große nicht verwaltete Speicherpuffer), inakzeptabel sein.

  • Wenn die CLR einen Finalizer aufrufen muss, muss sie die automatische Bereinigung des Speichers des Objekts bis zur nächsten Ausführung der Garbage Collection zurückstellen (die Finalizer werden zwischen Garbage Collections ausgeführt). Dies bedeutet, dass der Speicher des Objekts (und aller Objekte, auf die es verweist) für einen längeren Zeitraum nicht freigegeben wird.

In vielen Szenarien ist es daher möglicherweise nicht empfehlenswert, sich ausschließlich auf Finalizer zu verlassen. Dies ist z. B. der Fall, wenn nicht verwaltete Ressourcen so schnell wie möglich freigegeben werden müssen, die Ressourcen knapp sind oder eine hohe Leistung erforderlich ist und der zusätzliche Mehraufwand durch die Garbage Collection bei der Finalisierung (Bereinigung) inakzeptabel ist.

Das Framework stellt die System.IDisposable-Schnittstelle bereit, die implementiert werden sollte, damit Entwickler*innen nicht verwaltete Ressourcen manuell freigeben können, sobald sie nicht mehr benötigt werden. Außerdem stellt sie die GC.SuppressFinalize-Methode bereit, die dem GC mitteilen kann, dass ein Objekt manuell verworfen (freigegeben) wurde und nicht mehr finalisiert (bereinigt) werden muss. In diesem Fall kann der Speicher des Objekts früher freigegeben werden. Typen, die die IDisposable-Schnittstelle implementieren, werden als verwerfbare Typen bezeichnet.

Das Dispose-Muster soll die Verwendung und Implementierung von Finalizern sowie der IDisposable-Schnittstelle standardisieren.

In erster Linie dient das Muster dazu, die Komplexität der Implementierung der Methoden Finalize und Dispose zu reduzieren. Die Komplexität ergibt sich aus der Tatsache, dass die Methoden einige, aber nicht alle Codepfade gemeinsam haben (die Unterschiede werden weiter unten erläutert). Darüber hinaus gibt es historische Gründe für einige Elemente des Musters, die mit der Entwicklung der Sprachunterstützung für die deterministische Ressourcenverwaltung zusammenhängen.

✓ DO: Implementieren Sie das grundlegende Dispose-Muster für Typen, die Instanzen von verwerfbaren Typen enthalten. Ausführliche Informationen zum grundlegenden Muster finden Sie im Abschnitt Grundlegendes Dispose-Muster.

Wenn ein Typ für die Lebensdauer anderer verwerfbarer Objekte verantwortlich ist, benötigen Entwickler*innen auch eine Möglichkeit, diese zu verwerfen. Die Verwendung der Dispose-Methode des Containers ist eine einfache Möglichkeit, um dies zu erreichen.

✓ DO: Implementieren Sie das grundlegende Dispose-Muster, und stellen Sie einen Finalizer für Typen bereit, die Ressourcen belegen, die explizit freigegeben werden müssen, und nicht über Finalizer verfügen.

Das Muster sollte beispielsweise für Typen implementiert werden, die nicht verwaltete Speicherpuffer speichern. Im Abschnitt Finalisierbare Typen werden Richtlinien für die Implementierung von Finalizern erläutert.

✓ DO: Erwägen Sie die Implementierung des grundlegenden Dispose-Musters für Klassen, die selbst keine nicht verwalteten Ressourcen oder verwerfbaren Objekte belegen, aber wahrscheinlich Untertypen aufweisen, bei denen dies der Fall ist.

Ein gutes Beispiel hierfür ist die System.IO.Stream-Klasse. Dies ist eine abstrakte Basisklasse, die keine Ressourcen hält, deren Unterklassen größtenteils aber Ressourcen belegen, weshalb sie das Muster implementiert.

Grundlegendes Dispose-Muster

Die grundlegende Implementierung des Musters umfasst das Implementieren der System.IDisposable-Schnittstelle und das Deklarieren der Dispose(bool)-Methode, die die gesamte Ressourcenbereinigungslogik implementiert, die von der Dispose-Methode und dem optionalen Finalizer gemeinsam genutzt werden soll.

Das folgende Beispiel zeigt eine einfache Implementierung des grundlegenden Musters:

public class DisposableResourceHolder : IDisposable {

    private SafeHandle resource; // handle to a resource

    public DisposableResourceHolder() {
        this.resource = ... // allocates the resource
    }

    public void Dispose() {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing) {
        if (disposing) {
            if (resource!= null) resource.Dispose();
        }
    }
}

Der boolesche Parameter disposing gibt an, ob die Methode über die IDisposable.Dispose-Implementierung oder über den Finalizer aufgerufen wurde. Vor dem Zugriff auf andere Referenzobjekte (z. B. das Ressourcenfeld im vorherigen Beispiel) muss die Dispose(bool)-Implementierung den Parameter überprüfen. Auf solche Objekte sollte nur zugegriffen werden, wenn die Methode über die IDisposable.Dispose-Implementierung aufgerufen wird (wenn der disposing-Parameter „true“ ist). Wird die Methode über den Finalizer aufgerufen (disposing ist „false“), sollte nicht auf andere Objekte zugegriffen werden. Der Grund hierfür ist, dass Objekte in einer unvorhersehbaren Reihenfolge finalisiert werden, sodass sie oder eine ihrer Abhängigkeiten möglicherweise bereits finalisiert wurden.

Dieser Abschnitt gilt auch für Klassen mit einer Basis, die das Dispose-Muster noch nicht implementiert. Wenn Sie von einer Klasse erben, die das Muster bereits implementiert, überschreiben Sie einfach die Dispose(bool)-Methode, um zusätzliche Ressourcenbereinigungslogik bereitzustellen.

✓ DO: Deklarieren Sie eine protected virtual void Dispose(bool disposing)-Methode, um die gesamte Logik im Zusammenhang mit der Freigabe nicht verwalteter Ressourcen zu zentralisieren.

Die gesamte Ressourcenbereinigung sollte in dieser Methode erfolgen. Die Methode wird sowohl vom Finalizer als auch von der IDisposable.Dispose-Methode aufgerufen. Beim Aufruf innerhalb eines Finalizers ist der Parameter „false“. Er sollte verwendet werden, um sicherzustellen, dass während der Finalisierung ausgeführter Code nicht auf andere finalisierbare Objekte zugreift. Details zur Implementierung von Finalizern finden Sie im nächsten Abschnitt.

protected virtual void Dispose(bool disposing) {
    if (disposing) {
        if (resource!= null) resource.Dispose();
    }
}

✓ DO: Implementieren Sie die IDisposable-Schnittstelle, indem Sie einfach Dispose(true) gefolgt von GC.SuppressFinalize(this) aufrufen.

Der Aufruf von SuppressFinalize sollte nur erfolgen, wenn Dispose(true) erfolgreich ausgeführt wird.

public void Dispose(){
    Dispose(true);
    GC.SuppressFinalize(this);
}

X DON‘T: Machen Sie die parameterlose Dispose-Methode nicht zu einer virtuellen Methode.

Die Dispose(bool)-Methode ist die Methode, die von Unterklassen überschrieben werden sollte.

// bad design
public class DisposableResourceHolder : IDisposable {
    public virtual void Dispose() { ... }
    protected virtual void Dispose(bool disposing) { ... }
}

// good design
public class DisposableResourceHolder : IDisposable {
    public void Dispose() { ... }
    protected virtual void Dispose(bool disposing) { ... }
}

X DON‘T: Deklarieren Sie außer Dispose() und Dispose(bool) keine anderen Überladungen der Dispose-Methode.

Dispose sollte als reserviertes Wort betrachtet werden, um die Programmierung dieses Musters zu erleichtern und Verwirrung bei Ausführenden, Benutzer*innen und Compilern zu vermeiden. Einige Programmiersprachen können dieses Muster automatisch für bestimmte Typen implementieren.

✓ DO: Lassen Sie mehrere Aufrufe der Dispose(bool)-Methode zu. Nach dem ersten Aufruf führt die Methode möglicherweise keinerlei Aktionen aus.

public class DisposableResourceHolder : IDisposable {

    bool disposed = false;

    protected virtual void Dispose(bool disposing) {
        if (disposed) return;
        // cleanup
        ...
        disposed = true;
    }
}

X DON‘T: Vermeiden Sie mit Ausnahme von kritischen Situationen, in denen der enthaltende Prozess beschädigt wurde (Verluste, inkonsistenter freigegebener Zustand usw.), das Auslösen einer Ausnahme innerhalb von Dispose(bool).

Benutzer*innen erwarten, dass ein Aufruf von Dispose keine Ausnahme auslöst.

Wenn Dispose eine Ausnahme auslösen könnte, wird weitere Finally-Block-Bereinigungslogik nicht ausgeführt. Um dieses Problem zu umgehen, müssten die Benutzer*innen jeden Aufruf von Dispose (innerhalb des Finally-Blocks) in einem Try-Block umschließen, was zu sehr komplexen Bereinigungshandlern führt. Lösen Sie beim Ausführen einer Dispose(bool disposing)-Methode niemals eine Ausnahme aus, wenn der disposing-Parameter auf „false“ gesetzt ist. Andernfalls wird der Prozess beendet, wenn er in einem Finalizerkontext ausgeführt wird.

✓ DO: Lösen Sie eine ObjectDisposedException für jeden Member aus, der nicht verwendet werden kann, nachdem das Objekt verworfen wurde.

public class DisposableResourceHolder : IDisposable {
    bool disposed = false;
    SafeHandle resource; // handle to a resource

    public void DoSomething() {
        if (disposed) throw new ObjectDisposedException(...);
        // now call some native methods using the resource
        ...
    }
    protected virtual void Dispose(bool disposing) {
        if (disposed) return;
        // cleanup
        ...
        disposed = true;
    }
}

✓ DO: Erwägen Sie, zusätzlich zur Close()-Methode eine Dispose()-Methode bereitzustellen, wenn „close“ im jeweiligen Bereich der Standardbegriff ist.

Dabei ist es wichtig, dass die Close-Implementierung mit Dispose identisch ist. Erwägen Sie außerdem, die IDisposable.Dispose-Methode explizit zu implementieren.

public class Stream : IDisposable {
    IDisposable.Dispose() {
        Close();
    }
    public void Close() {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

Finalisierbare Typen

Finalisierbare Typen sind Typen, die das grundlegende Dispose-Muster erweitern, indem sie den Finalizer außer Kraft setzen und den Codepfad für die Finalisierung in der Dispose(bool)-Methode bereitstellen.

Die korrekte Implementierung von Finalizern ist bekanntermaßen schwierig. Dies liegt vor allem daran, dass Sie während ihrer Ausführung keine bestimmten (normalerweise gültigen) Annahmen über den Zustand des Systems treffen können. Die folgenden Richtlinien sollten sorgfältig berücksichtigt werden.

Beachten Sie, dass einige der Richtlinien nicht nur für die Finalize-Methode gelten, sondern auch für jeden Code, der über einen Finalizer aufgerufen wird. Im Fall des zuvor beschriebenen grundlegenden Dispose-Musters ist dies Logik, die innerhalb von Dispose(bool disposing) ausgeführt wird, wenn der disposing-Parameter „false“ ist.

Wenn die Basisklasse bereits finalisierbar ist und das grundlegende Dispose-Muster implementiert, sollten Sie Finalize nicht erneut überschreiben. Stattdessen sollten Sie einfach die Dispose(bool)-Methode überschreiben, um zusätzliche Ressourcenbereinigungslogik bereitzustellen.

Der folgende Code zeigt ein Beispiel für einen finalisierbaren Typ:

public class ComplexResourceHolder : IDisposable {

    private IntPtr buffer; // unmanaged memory buffer
    private SafeHandle resource; // disposable handle to a resource

    public ComplexResourceHolder() {
        this.buffer = ... // allocates memory
        this.resource = ... // allocates the resource
    }

    protected virtual void Dispose(bool disposing) {
        ReleaseBuffer(buffer); // release unmanaged memory
        if (disposing) { // release other disposable objects
            if (resource!= null) resource.Dispose();
        }
    }

    ~ComplexResourceHolder() {
        Dispose(false);
    }

    public void Dispose() {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

X DON‘T: Vermeiden Sie es, Typen als „finalisierbar“ zu definieren.

Überdenken Sie sorgfältig jeden Fall, in dem Ihrer Meinung nach ein Finalizer erforderlich ist. Instanzen mit Finalizern stellen sowohl im Hinblick auf die Leistung als auch die Codekomplexität einen echten Kostenfaktor dar. Verwenden Sie stattdessen Ressourcenwrapper wie SafeHandle, um nicht verwaltete Ressourcen nach Möglichkeit zu kapseln. In diesem Fall ist kein Finalizer erforderlich, da der Wrapper selbst für die Ressourcenbereinigung verantwortlich ist.

X DON‘T: Definieren Sie Werttypen nicht als „finalisierbar“.

Nur Verweistypen werden tatsächlich von der CLR finalisiert. Jeder Versuch, einen Finalizer für einen Werttyp zu verwenden, wird daher ignoriert. Die C#- und C++-Compiler erzwingen diese Regel.

✓ DO: Definieren Sie einen Typ als „finalisierbar“, wenn dieser für die Freigabe einer nicht verwalteten Ressource verantwortlich ist, die nicht über einen eigenen Finalizer verfügt.

Rufen Sie beim Implementieren des Finalizers einfach Dispose(false) auf, und platzieren Sie die gesamte Ressourcenbereinigungslogik in der Dispose(bool disposing)-Methode.

public class ComplexResourceHolder : IDisposable {

    ~ComplexResourceHolder() {
        Dispose(false);
    }

    protected virtual void Dispose(bool disposing) {
        ...
    }
}

✓ DO: Implementieren Sie das grundlegende Dispose-Muster für jeden finalisierbaren Typ.

Dadurch erhalten Benutzer*innen des Typs die Möglichkeit, explizit eine deterministische Bereinigung der Ressourcen durchzuführen, für die der Finalizer verantwortlich ist.

X DON‘T: Greifen Sie nicht auf finalisierbare Objekte im Codepfad des Finalizers zu, da eine große Wahrscheinlichkeit besteht, dass diese bereits finalisiert wurden.

Beispielsweise kann ein finalisierbares Objekt A, das einen Verweis auf ein anderes finalisierbares Objekt B enthält, das Objekt B nicht zuverlässig im Finalizer von Objekt A verwenden und umgekehrt. Finalizer werden in einer zufälligen Reihenfolge aufgerufen (abgesehen von einer schwachen Garantie der Reihenfolge für die kritische Finalisierung).

Beachten Sie außerdem, dass in statischen Variablen gespeicherte Objekte an bestimmten Punkten während des Entladens der Anwendungsdomäne oder beim Beenden des Prozesses bereinigt werden. Der Zugriff auf eine statische Variable, die auf ein finalisierbares Objekt verweist, (oder das Aufrufen einer statischen Methode, die u. U. in statischen Variablen gespeicherte Werte verwendet) ist möglicherweise nicht sicher, wenn Environment.HasShutdownStarted „true“ zurückgibt.

✓ DO: Schützen Sie Ihre Finalize-Methode.

C#-, C++- und VB.NET-Entwickler*innen müssen sich darüber keine Gedanken machen, da die Compiler das Erzwingen dieser Richtlinie unterstützen.

X DON‘T: Lassen Sie außer systemkritischen Fehlern keine Ausnahmen von der Finalisierungslogik auslösen.

Wenn eine Ausnahme von einem Finalizer ausgelöst wird, beendet die CLR den gesamten Prozess (ab .NET Framework-Version 2.0), wodurch verhindert wird, dass andere Finalizer ausgeführt und Ressourcen kontrolliert freigegeben werden.

✓ DO: Erwägen Sie das Erstellen und Verwenden eines kritischen finalisierbaren Objekts (ein Typ mit einer Typhierarchie, die CriticalFinalizerObject enthält) für Situationen, in denen ein Finalizer auch dann ausgeführt werden muss, wenn das Entladen der Anwendungsdomäne und Threadabbrüche erzwungen werden.

Teile ©2005, 2009 Microsoft Corporation. Alle Rechte vorbehalten.

Nachdruck mit Genehmigung von Pearson Education, Inc aus Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries, 2nd Edition von Krzysztof Cwalina und Brad Abrams, veröffentlicht am 22. Oktober 2008 durch Addison-Wesley Professional als Teil der Microsoft Windows Development Series.

Weitere Informationen