Implementieren gemeinsamer Destruktoren in Visual Basic .NET

Einleitung

Die Programmiersprache Visual Basic .NET erlaubt, Klassen mit einem gemeinsamen Konstruktor auszustatten; Syntax für das Gegenstück, den gemeinsamen Destruktor, fehlt jedoch. Während gemeinsame Konstruktoren häufig eingesetzt werden, wird die Funktionalität, die ein gemeinsamer Destruktor bietet, verhältnismäßig selten benötigt und es lassen sich leicht andere Wege finden, um das gewünschte Verhalten zu erzielen. Dieser Artikel stellt eine Möglichkeit der Simulation gemeinsamer Destruktoren für Visual-Basic-.NET-Klassen mittels geschachtelter Klassen vor. Weiterhin wird eine Testanwendung entwickelt, die eine Assembly dynamisch lädt, darin enthaltene Methoden aufruft und sie daraufhin wieder entlädt. Abschließend erfolgt eine Einordnung der Rolle gemeinsamer Destruktoren hinsichtlich ihres Einsatzes in .NET-basierenden Anwendungen.

Gemeinsame Konstruktoren und Destruktoren

Gemeinsame Konstruktoren und Methoden werden in Visual Basic .NET mit dem Schlüsselwort Shared markiert, das von der Funktion her dem Schlüsselwort static der Programmiersprache C# entspricht. Wenngleich die Schlüsselwörter Shared und static einander aus technischer Sicht entsprechen, unterscheiden sie sich in ihrer Bedeutung: Gemeinsame Mitglieder in Visual Basic .NET werden zwischen allen Instanzen der enthaltenden Klasse geteilt (die Menge der Instanzen kann hierbei auch leer sein), statische Mitglieder in C# dagegen werden als zum enthaltenden Typ zugehörig angesehen. Die folgenden Ausführungen beziehen sich auf die Programmiersprache Visual Basic .NET und besitzen in übertragener Form auch Gültigkeit für C#.

In einem gemeinsamen Konstruktor enthaltener Code wird nach Programmstart und vor dem ersten Zugriff auf ein Mitglied der enthaltenden Klasse ausgeführt. Oft werden in gemeinsamen Konstruktoren Initialisierungen gemeinsamer Variablen vorgenommen. Vor Implementieren eines gemeinsamen Destruktors muß festgelegt werden, zu welchem Zeitpunkt dieser aufgerufen werden soll. Destruktoren von Objekten werden vor deren Finalisierung durch den Garbage Collector aufgerufen. Da gemeinsame Mitglieder an die Lebensdauer eines Typs gebunden sind, also an den Zeitraum, in dem der Typ in einer Anwendung bekannt ist und benutzt werden kann, ist es naheliegend, den Aufruf des gemeinsamen Destruktors direkt vor Entladen des Typs, also der ihn enthaltenden Assembly, zu erwarten.

Gemeinsame Destruktoren mittels geschachtelter Klassen

Betrachten wir als Beispiel eine Klasse, die über öffentliche gemeinsame Mitglieder Funktionalität bereitstellt und deren Implementierung intern mit einer unverwalteten Ressource arbeitet. Die Methode CreateObject dient zum Anlegen des unverwalteten Objekts und gibt eine Zugriffsnummer auf das Objekt zurück, ReleaseObject gibt das zur übergebenen Zugriffsnummer gehörende Objekt frei und Foo führt mit dem zur übergebenen Zugriffsnummer gehörenden Objekt eine Aufgabe aus. Die unverwaltete Ressource soll dabei nur ein Mal im gemeinsamen Konstruktor der Klasse angefordert und bei Entladen des Typs wieder freigegeben werden.

Die Idee zur Implementierung eines gemeinsamen Destruktors besteht darin, sich zu Nutze zu machen, daß Objekte, auf die gemeinsame Variablen verweisen, vom Garbage Collector finalisiert werden, wenn der sie enthaltende Typ entladen wird. Als Typ des Objekts, auf das von der privaten gemeinsamen Variablen verwiesen wird, bietet sich eine private geschachtelte Klasse an. Der Finalisierer dieser Klasse wird überschrieben und um den gemeinsamen Destruktorcode der umschließenden Klasse erweitert. Da es sich beim Finalisierer um die Methode einer geschachtelten Klasse handelt, besteht auch Zugriff auf private Mitglieder der umschließenden Klasse. Im folgenden Listing wird die beschriebene Vorgehensweise an einem konkreten Beispiel demonstriert.

Public Class Class1
    Private Shared m_UnmanagedHandle As Integer

    Shared Sub New()
        m_UnmanagedHandle = CreateObject(…)
    End Sub

    Private Shared Finalizer As New SharedFinalizer()

    Private Class SharedFinalizer
        Protected Overrides Sub Finalize()
            MyBase.Finalize()
            ReleaseObject(m_UnmanagedHandle)
        End Sub
    End Class

    Public Shared Sub DoSomething()
        Foo(m_UnmanagedHandle)
    End Sub
End Class

Vor dem ersten Zugriff auf ein Mitglied des Typs Class1 wird eine Instanz des geschachtelten Typs SharedFinalizer erstellt. Wenn der Garbage Collector nun die Assembly, welche den Typ Class1 enthält, entlädt, wird der Finalisierer der Instanz von SharedFinalizer aufgerufen. In diesem Finalisierer wird gemeinsamer Destruktorcode der Klasse Class1 plaziert; das ist im konkreten Fall der Code zur Freigabe des unverwalteten Objekts anhand seiner Zugriffsnummer. Der Code aus dem Finalisierer kann alternativ in einer zusätzlichen gemeinsamen Methode der Klasse Class1 abgelegt und diese Methode vom Finalisierer der statischen Klasse aus aufgerufen werden.

Dynamisches Entladen von Assemblies

Assemblies enthalten neben den eigentlichen Typen dazugehörige Metadaten. Während zur Laufzeit einer .NET-basierenden Anwendung nach und nach weitere Assemblies in die Anwendungsdomain geladen werden können, kann das Entladen einer Assembly nur durch Entladen der enthaltenden Anwendungsdomain erfolgen. In den meisten kleineren Anwendungen, die nur aus einer einzigen Anwendungsdomain bestehen, wird dies direkt vor dem Beenden der Anwendung stattfinden.

Um die Funktionsfähigkeit der zuvor beschriebenen Vorgehensweise zur Implementierung gemeinsamer Destruktoren zu überprüfen, soll eine Testanwendung erstellt werden, die eine Assembly lädt, gemeinsame Methoden einer der enthaltenen Klassen aufruft und anschließend die hinzugeladene Assembly wieder entlädt. Bei Entladen der Assembly wird der Typ mit dem gemeinsamen Destruktor der umschließenden Klasse ebenfalls entladen und dessen Destruktor aufgerufen.

Damit eine hinzugeladene Assembly zur Laufzeit vor Programmende entladen werden kann, ist es notwendig (aber nicht hinreichend), eine zweite Anwendungsdomain zu erstellen und die Assembly in die zusätzliche Anwendungsdomain zu laden. Zusätzlich muß bei Einsatz mehrerer Anwendungsdomain darauf geachtet werden, daß Zugriffe auf Typen und Metadaten einer hinzugeladenen Assembly nicht direkt von der Hauptanwendungsdomain der Anwendung aus erfolgen, da dies zur Folge hätte, daß die Assembly bei Entladen der zweiten Anwendungsdomain nicht entladen werden kann. Die Verwendung von Reflection zum Zugriff auf die hinzugeladene Assembly ist ebenfalls nicht ausreichend, da hierdurch auch die Assembly gesperrt werden würde.

Zur Umgehung des Sperrens einer Assembly bei Zugriff auf darin enthaltene Daten kann die im Artikel Executing Dynamic Code in .NET von Rick Strahl [MVP] vorgestellte Vorgehensweise gewählt werden. Dabei wird eine zusätzliche Proxyassembly RemoteLoader.dll eingeführt. Diese Assembly fungiert als Proxy, lädt die Assembly, welche den Typ Class1 enthält, in die von der Hauptanwendungsdomain entfernte Anwendungsdomain und gibt einen Schnittstellenzeiger an die Hauptanwendungsdomain zurück. Die Hauptanwendungdomain kommuniziert über diese Schnittstelle anschließend mit dem Anwendungsdomainproxy, um Daten an die dritte Assembly zu übergeben und von dieser zu empfangen. Die Proxyassembly kann direkt in der Hauptassembly benutzt werden, da deren Typinformationen bereits zur Kompilierungszeit in der Hauptassembly verfügbar sein dürfen.

Schlußwort

Visual Basic .NET und C# unterstützen zwar gemeinsame Destruktoren nicht als Sprachmerkmal, die Funktionalität läßt sich jedoch bei Bedarf mit geringem Implementierungsaufwand auf anderem Wege erzielen. Das Fehlen von Syntax für gemeinsame Destruktoren kann dadurch gerechtfertigt werden, daß derartige Funktionalität in der Praxis nur selten benötigt wird. Eine attraktive Alternative bietet das Singleton-Entwurfsmuster, das ein höheres Maß an Kontrolle über die Lebenszeit der einzigen Instanz einer Klasse gewährt. Eine Erweiterung des .NET Framework um die Fähigkeit, Assemblies getrennt von Anwendungsdomain zu entladen, würde die Entwicklung hin zu dynamisch erweiterbaren Anwendungen beschleunigen und könnte einen steigenden Bedarf nach Syntax für gemeinsame Destruktoren zur Folge haben.

Downloads

Beispielprojekt (SharedFinalizer.zip)

Projekt im Visual-Basic-.NET-2003-Format.