IDisposable handlich verpackt

Objekte, die Ressourcen halten, sollten diese Ressourcen schnellstmöglich wieder freigeben. Das gilt insbesondere für Unmanaged Resources wie File Handle usw. Eine Ressourcenfreigabe ausschließlich im „Destruktor“ von Klassen erfüllt diese Forderung aber nicht, da ungewiss ist, wann die Garbage Collection ihn aufruft. Trotzdem müssen aber natürlich auch dort allerspätestens die Ressourcen freigegeben werden.

Public Class MyResourceClass
    …
    Protected Overrides Sub Finalize()
        ' Ressourcen freigeben
    End Sub
End Class

Für eine frühere, explizite Freigabe empfiehlt der .NET Framework eine Dispose()-Methode:

Public Class MyResourceClass
    …
    Public Sub Dispose()
        ' Ressourcen freigeben
    End Sub
  
    Protected Overrides Sub Finalize()
        Dispose()
    End Sub
End Class
...
Dim r As New MyResourceClass
...
r.Dispose()

Um eine zumindest halbautomatische frühe Ressourcenfreigabe zu realisieren, bietet C# die using-Anweiung, die am Ende selbstständig Dispose() der in ihrer „Parameterliste“ erzeugten Objekte aufruft:

using(MyResourceClass r = new MyResourceClass())
{
    ...
}

Dafür ist allerdings nötig, dass using ohne viel Aufwand erkennen kann, ob eines seiner Objeke eine Dispose()-Methode hat.  Für using zählt daher nur die Dispose()-Methode des Interface IDisposable:

Interface IDisposable
    Sub Dispose()
End Interface

Damit das obige using-Statement funktioniert, muss MyResourceClass also wie folgt aussehen:

Public Class MyResourceClass
    Implements IDisposable
    …
    Public Sub Dispose() Implements IDisposable.Dispose
        ' Ressourcen freigeben
    End Sub
  
    Protected Overrides Sub Finalize()
        Dispose()
    End Sub
End Class

Leider ist es damit aber nicht getan. Bei dieser Implementation besteht die Gefahr, dass Ressourcen mehrfach freigegeben werden, denn Dispose() wird ja immer auch im „Destruktor“ aufgerufen. Dispose() müsste also feststellen, dass es schon die Ressourcen freigegeben hat; und Dispose() sollte darüber hinaus auch noch einen zukünftigen Aufruf des „Destruktors“ – oder genauer: des Finalisierers – unterdrücken, denn er ist ja nicht mehr nötig.

Die Implementation des IDisposable-Patterns gestaltet sich damit recht kompliziert. Wie kann man es einfacher machen? Durch eine hübsche Basisklasse für alle Ressourcen haltenden Objekte.

Public MustInherit Class ResourceObject
    Implements IDisposable

    Private _disposed As Boolean = False

    Public Overloads Sub Dispose() Implements IDisposable.Dispose
        Dispose(True)
        GC.SuppressFinalize(Me)
    End Sub

    Private Overloads Sub Dispose(ByVal calledFromCode As Boolean)
        If Not Me._disposed Then
            If calledFromCode Then
                Me.DisposeManagedResources()
            End If
            Me.DisposeUnmanagedResources()
        End If
        Me._disposed = True
    End Sub

    Protected NotOverridable Overrides Sub Finalize()
        Dispose(False)
        MyBase.Finalize()
    End Sub

    Protected MustOverride Sub DisposeManagedResources()
    Protected MustOverride Sub DisposeUnmanagedResources()
End Class

Die Klasse folgt weitgehend dem Pattern, wie es auch in der Microsoft Dokumentation beschrieben ist.

• Dispose(calledFromCode) implementiert die Freigabe der Ressourcen. Ressourcen werden nur freigegeben, wenn die Methode noch nicht aufgerufen wurde. Das merkt sie sich im Flag _disposed.
• Ihr Code ruft die Methode Dispose() auf. Sie delegiert die Arbeit an Dispose(calledFromCode) und sorgt dafür, dass der Finalisierer Finalize() nicht mehr aufgerufen wird, denn das ist ja nicht nötig, weil die Ressourcen schon freigegeben wurde.
• Falls auf einem Objekt Dispose() nicht aufgerufen wurde, läuft am Ende kurz vor Objektzerstörung automatisch Finalize(). In dem Dispose(calledFromCode) dann mit False aufgerufen wird, signalisiert der Finalisierer, dass nur noch Unmanaged Resources (z.B. GDI-Objekte, File Handle) freigegeben werden dürfen. Managed Resources, d.h. „normale“ .NET Framework Objekte dürfen dann nicht mehr angefasst werden, weil unsicher ist, ob ihre Finalisierer – falls vorhanden – schon aufgerufen wurden.

Kleine Abweichungen der Klasse im Vergleich zu Microsofts Pattern:

• Ich habe die Finalize()-Methode (den „Destruktor“) als NotOverridable markiert, damit abgeleitete Klassen nicht mit der Arbeitsweise des Pattern interferieren können.
• Der Parameter der Dispose()-Methode, die die Ressourcen freigibt, heißt calledFromCode und nicht disposing. Der Name zeigt damit deutlicher an, wann er True enthält, nämlich dann, wenn Dispose() von Ihrem Code aus explizit aufgerufen wurde. (Noch lieber hätte ich ihn calledFromFinalizer genannt, aber dann hätte die Bedingung der If-Anweisung eine Negation enthalten (If Not calledFromFinanlizer...).)

Der Nutzen der Klasse liegt darin, dass Sie sich um die Details – erinnern, ob Dispose() schon aufgerufen wurde, Finalisierung unterdrücken usw. – nicht mehr kümmern müssen. Ressourcen haltende Klasse leiten einfach von ResourceObject ab und implementieren zwei Methoden:

Public Class MyResourceClass
    Inherits ResourceObject
    …
    Protected Overrides Sub DisposeManagedResources()
        …
    End Sub

    Protected Overrides Sub DisposeUnmanagedResources()
        …
    End Sub
End Class

In DisposeManagedResources() können Sie alle möglichen Ressourcen freigeben, in DisposeUnmanagedResources() jedoch keine “normalen” Objekte mehr. Beide Methoden sind als MustInherit definiert, um dem Implementierer einer abgeleiteten Klasse deutlich vor Augen zu halten, dass er sich dafür entschieden hat, Ressourcen in seinen Objekte zu halten und an welche Arten von Ressourcen er denken muss. (Sollten die Instanzen keine Ressourcen einer der beiden Arten enthalten, kann die Implementation der zugehörigen Dispose-Methode ja leer gelassen werden.)

Sind aber überhaupt zwei Dispose-Methoden nötig? Ja. Gäbe es nur DisposeManagedResources(), würde Code zur Freigabe von Unmanaged Resources nicht aufgerufen, wenn Ihr Code Dispose() nicht nutzt. Gäbe es nur DisposeUnmanagedResources() würden über den Finalisierer immer auch „normale“ Objekte angesprochen werden müssen. Die Aufteilung macht also Sinn und sorgt dafür, dass im schlimmsten Fall – d.h. wenn Dispose() nicht aufgerufen wurde – Managed Resources erst während der Garbage Collection freigegeben werden.

Ein letztes Wort: Bei mehrstufigen Vererbungsbäumen sollte am Ende jeder Dispose-Methoden sollte natürlich die der Basisklasse aufgerufen werden:

Protected Overrides Sub DisposeManagedResources()
    …
    MyBase.DisposeManagedResources()
End Sub

Anmerkung: Die Idee einer allgemeinen Ressourcenbasisklasse hatte ich allerdings nicht allein. Zumindest einen Artikel habe ich gefunden, als ich schon mit dem Implementation angefangen hatte: The IDisposable design pattern considered harmful. Dort fehlt allerdings eine zusammenhänge Implementation zum Herauskopieren und der Autor unterscheidet nicht wirklich, zu welchem Zeitpunkt Dispose() aufgerufen wird.

No Comments