Rubrik: OOP / Tools | VB-Versionen: VB5, VB6 | 15.01.03 |
Zirkuläre Verweise vermeiden: WeakReference in VB-Classic In VB.NET gibt es eine schöne neue Einrichtung namens "WeakReference". Hiermit kann eine Referenz auf ein Objekt gespeichert werden, ohne dass dadurch die Zerstörung des Objektes verhindert wird. Dieser Artikel soll zeigen, wie "weiche Referenzen" in VB5/6 implementiert werden können. Wozu soll das gut sein? | ||
Autor: Harry von Borstel | Bewertung: | Views: 20.722 |
In VB.NET gibt es eine schöne neue Einrichtung namens "WeakReference". Hiermit kann eine Referenz auf ein Objekt gespeichert werden, ohne dass dadurch die Zerstörung des Objektes verhindert wird. Dieser Artikel soll zeigen, wie "weiche Referenzen" in VB5/6 implementiert werden können. Wozu soll das gut sein?
Problem: Zirkuläre Verweise
Es gebe zwei Klassen Abteilung und Mitarbeiter. Jedes Abteilung-Objekt hat eine Aufzählung Mitarbeiter, in der alle Mitarbeiter-Objekte verzeichnet sind, die zu der Abteilung gehören. Umgekehrt hat jedes Mitarbeiter-Objekt eineAbteilung-Eigenschaft, die auf das Abteilung-Objekt verweist, das die Abteilung des Mitarbeiters repräsentiert. Dies ist eine klassische 1:N-Beziehung, zu jeweils einer Abteilung gibt es N Mitarbeiter.
Abb. 1: Darstellung einer 1:N Beziehung
Eine Implementierung in VB5/6 könnte so aussehen:
Abteilung.cls
Option Explicit Public Name As String Public Mitarbeiter As New Collection Public Sub AddMitarbeiter(ByVal strName As String) Dim m As New Mitarbeiter m.Name = strName Set m.Abteilung = Me Mitarbeiter.Add m End Sub Private Sub Class_Terminate() Debug.Print "Terminate Abteilung "; Me.Name End Sub Public Sub DebugPrint() On Error Resume Next Dim m As Mitarbeiter Debug.Print "Abteilung "; Me.Name For Each m In Mitarbeiter m.DebugPrint Next End Sub
Mitarbeiter.cls
Option Explicit Public Name As String Public Abteilung As Abteilung Private Sub Class_Terminate() Debug.Print "Terminate Mitarbeiter "; Me.Name End Sub Public Sub DebugPrint() On Error Resume Next Debug.Print "Abteilung "; Me.Abteilung.Name, "Mitarbeiter "; Me.Name End Sub
Jetzt kann man in einem VB-Programm Abteilungen und Mitarbeiter hinzufügen:
MainMod.bas
Option Explicit Sub Main() Dim Einkauf As New Abteilung Dim Verkauf As New Abteilung With Einkauf .Name = "Einkauf" .AddMitarbeiter "Meier" .AddMitarbeiter "Schulze" End With With Verkauf .Name = "Verkauf" .AddMitarbeiter "Müller" .AddMitarbeiter "Schmidt" End With Einkauf.DebugPrint Verkauf.DebugPrint Set Einkauf = Nothing Set Verkauf = Nothing Stop End Sub
Blendet man jetzt das Direktfenster ein und startet das Programm (Ctrl+G / F5), so wird das Programm durchgeführt und bei der Stop-Anweisung angehalten, ohne dass im Direktfenster bereits Terminate-Ereignisse protokolliert wären:
Abteilung Einkauf Abteilung Einkauf Mitarbeiter Meier Abteilung Einkauf Mitarbeiter Schulze Abteilung Verkauf Abteilung Verkauf Mitarbeiter Müller Abteilung Verkauf Mitarbeiter Schmidt
Erst wenn man das Programm mit F5 fortsetzt, werden die Objekte beendet:
Terminate Abteilung Verkauf Terminate Abteilung Einkauf Terminate Mitarbeiter Schmidt Terminate Mitarbeiter Müller Terminate Mitarbeiter Schulze Terminate Mitarbeiter Meier
Was hat das zu bedeuten? Obwohl in unserer Routine Main keine Referenzen mehr auf die Abteilung-Klasse bestehen, werden die entsprechenden Objekte offenbar noch nicht freigegeben, d.h. sie sind weiterhin im Hauptspeicher vorhanden. Grund ist, dass das Abteilung-Objekt nicht beendet werden kann, weil es noch Verweise darauf in den Mitarbeiter-Objekten gibt. Näheres findet sich in der VB-Dokumentation (MSDN) unter "Dealing with Circular References" bzw. "Umgang mit Zirkelbezügen".
Es gibt Programme, bei denen dieses Verhalten nicht weiter stört. In anderen Fällen kann es zu Problemen wie Speicherfraß mit anschließendem Programmabsturz oder dazu führen, dass das Programm unfähig wird, sich selbst zu beenden.
Lösung: Weiche Referenzen
Eine schöne Lösung für dieses Problem bieten "weiche Referenzen". So gibt es in .NET eine WeakReference Klasse, mit deren Hilfe man Referenzen auf Objekte halten kann, ohne dass dies die Terminierung der referenzierten Objekte behindert. Die Kehrseite der Medaille: Es ist nicht gewährleistet, dass zu der Referenz auch noch ein Objekt besteht, wenn man sie benutzen will.
So schön diese neue .NET-Welt auch ist, für VB5/6 braucht man eine eigene Lösung.
Im neuen Modul WeakReference werden die Routinen WrefFromAbteilung, AbteilungFromWref und ReleaseAbteilung definiert, die weiche Referenzen auf Abteilung-Objekte ermöglichen:
WeakReference.bas
Option Explicit Private Declare Sub LongFromObject Lib "kernel32" _ Alias "RtlMoveMemory" ( _ dest As Long, _ src As Object, _ ByVal nbytes As Long) Private Declare Sub ObjectFromLong Lib "kernel32" _ Alias "RtlMoveMemory" ( _ dest As Object, _ src As Long, _ ByVal nbytes As Long) Private Declare Sub CopyMemory Lib "kernel32" _ Alias "RtlMoveMemory" ( _ dest As Any, _ src As Any, _ ByVal nbytes As Long) Private mWrefAbteilungen As Collection
' Hier sammeln wir alle weichen Referenzen. Wenn ein Objekt ' terminiert, wird seine Referenz hier entfernt. So kann ' überprüft werden, ob es zu einer extern gespeicherten Wref ' noch ein existierendes Objekt gibt. ' Public Function WrefFromAbteilung(ByVal a As Abteilung) As Long ' Liefert eine weiche Referenz auf ein Abteilung-Objekt On Error Resume Next Dim wref1 As Long Dim wref2 As Long If Not a Is Nothing Then Call LongFromObject(wref1, a, 4) ' Objektzeiger holen Err.Clear wref2 = mWrefAbteilungen(Hex(wref1)) ' Schon gespeichert? If Err Then ' Referenz noch nicht gespeichert Call mWrefAbteilungen.Add(wref1, Hex(wref1)) ' Referenz speichern End If WrefFromAbteilung = wref1 End If End Function
Public Function AbteilungFromWref(ByVal wref As Long) As Abteilung ' Liefert das Objekt zur weichen Referenz, oder Nothing, ' wenn das Objekt nicht (mehr) existiert On Error Resume Next Dim a As Abteilung If wref <> 0 Then Call ObjectFromLong(a, wref, 4) ' Objekt holen IncRefCount a ' Referenzzähler erhöhen End If Set AbteilungFromWref = a End Function
Public Sub ReleaseAbteilung(ByVal a As Abteilung) ' Baut weiche Referenzen zum Abteilung-Objekt ab On Error Resume Next Dim wref As Long If Not a Is Nothing Then Call LongFromObject(wref, a, 4) mWrefAbteilungen.Remove Hex(wref) End If End Sub
Private Sub IncRefCount(obj As IUnknown) ' Erhöht den Referenzzähler (simuliert IUnkown.AddRef) If obj Is Nothing Then Exit Sub Dim lngRc As Long ' Referenzzählerwert holen... CopyMemory lngRc, ByVal (ObjPtr(obj)) + 4, 4 ' ...und fortschalten CopyMemory ByVal (ObjPtr(obj)) + 4, (lngRc + 1), 4 End Sub
Jetzt können die "harten" Referenzen in der Klasse Mitarbeiter durch weiche Referenzen ersetzt werden:
Mitarbeiter.cls
Option Explicit Public Name As String ' Public Abteilung As Abteilung (ehemalige "harte" Referenz) Private mWrefAbteilung As Long Public Property Get Abteilung() As Abteilung Set Abteilung = AbteilungFromWref(mWrefAbteilung) End Property Public Property Set Abteilung(ByVal vNewValue As Abteilung) mWrefAbteilung = WrefFromAbteilung(vNewValue) End Property Private Sub Class_Terminate() Debug.Print "Terminate Mitarbeiter "; Me.Name End Sub Public Sub DebugPrint() On Error Resume Next Debug.Print "Abteilung "; Me.Abteilung.Name, "Mitarbeiter "; Me.Name End Sub
In der Klasse Abteilung wird jetzt in der Ereignisprozedur Class_Terminate die Routine ReleaseAbteilung aufgerufen:
Abteilung.cls
... Private Sub Class_Terminate() Debug.Print "Terminate Abteilung "; Me.Name ReleaseAbteilung Me End Sub ...
Als Lohn dieser Mühe wird uns im Direktfenster bestätigt, dass jetzt alle Objekte schon an der Stop-Marke korrekt abgeräumt wurden:
Abteilung Einkauf Abteilung Einkauf Mitarbeiter Meier Abteilung Einkauf Mitarbeiter Schulze Abteilung Verkauf Abteilung Verkauf Mitarbeiter Müller Abteilung Verkauf Mitarbeiter Schmidt Terminate Abteilung Einkauf Terminate Mitarbeiter Meier Terminate Mitarbeiter Schulze Terminate Abteilung Verkauf Terminate Mitarbeiter Müller Terminate Mitarbeiter Schmidt
Hintergrund: COM-Referenzzähler
Wozu dient die Routine IncRefCount? Nun, wie der Kommentar schon sagt, wird damit der Referenzzähler des als Argument übergebenen Objekts erhöht. Was aber hat es damit auf sich?
Alle Objekte in VB-Classic sind COM-Objekte - und die haben die angenehme Eigenschaft, dass sie ihre Entsorgung selbst erledigen. Dies ist überhaupt der Schlüssel zum Erfolg von Visual Basic. Wer sich schon mal in Allokation und Freigabe von Speicherbereichen z.B. in C oder C++ geübt hat, weiß das zu schätzen. COM hat also für jedes Objekt einen Referenzzähler. Sobald eine neue Referenz auf ein Objekt erzeugt wird (in COM: IUnknown.AddRef), wird dieser Zähler erhöht, wenn eine Referenz abgebaut wird (COM: IUnknown.Release), wird der Zähler erniedrigt. Ist der Zähler bei 0 angekommen, so entsorgt IUnknown.Release das Objekt, weil es dann ja von niemandem mehr verwendet wird. In VB wird eine Referenz bei der Zuweisung eines Objekts zu einer Variablen erzeugt. Die Referenz wird in VB abgebaut, wenn der Variablen ein anderer Wert (z.B. Nothing) zugewiesen wird.
Somit dürfte auch klar werden, wie wir hier "weiche Referenzen" implementiert haben: Es sind einfach Referenzen, die nicht im Referenzzähler gezählt werden. Im ersten Beispiel (mit den "harten" Referenzen) hat sich der Referenzzähler des Abteilung-Objektes erhöht, wenn wir der Abteilung-Variablen im Mitarbeiter-Objekt ein Abteilung-Objekt zugewiesen haben. Im zweiten Beispiel (mit den "weichen" Referenzen) wurde aus dem Abteilung-Objekt nur ein Long-Wert (die Adresse des Objektes) erzeugt und abgespeichert. Da wir kein Objekt (im VB-Sinn) gespeichert haben, wurde auch der Referenzzähler nicht erhöht. Wenn wir aber (in der Routine AbteilungFromWref) aus der "weichen Referenz" wieder ein Objekt gemacht haben, musste der Referenzzähler des COM-Objektes (mit der Routine IncRefCount) erhöht werden.
Es lohnt sich, bei beiden Beispielen einmal den Referenzzähler im Einzelschrittmodus des VB-Debuggers zu beobachten. Dazu fügen wir zunächst folgende Routine hinzu:
WeakReference.bas
... Public Function RefCount(obj As IUnknown) As Long ' liefert den Referenzzähler von obj If obj Is Nothing Then Exit Function Dim lngRc As Long ' Referenzzählerwert holen... CopyMemory lngRc, ByVal (ObjPtr(obj)) + 4, 4 RefCount = lngRc - 2 End Function
Jetzt wechseln wir zur Routine Main, machen einen Rechtmausklick und wählen "Überwachung hinzufügen...". Unter "Ausdruck" geben wir RefCount(Einkauf) ein und bestätigen mit OK. Jetzt gehen wir mit F8 im Einzelschrittmodus Schritt für Schritt durch den Code und beobachten dabei den Referenzzähler im Überwachungsfenster.
Zum Abschluss noch eine Preisfrage:
Könnte man sich nicht das alles sparen, es bei der harten Referenz belassen und stattdessen in Abteilung.Class_Terminate statt "ReleaseAbteilung Me" einfach "Set Abteilung = Nothing" schreiben?
1. (und einziger) Preis ist eine vertiefte Erkenntnis in das Problem der zirkulären Referenzen unter VB.
Download Demoprojekt: WeakReference.zip (15 KB) | Harry von Borstel www.blueshell.com/ hvbblueshell.com |