vb@rchiv
VB Classic
VB.NET
ADO.NET
VBA
C#
Schützen Sie Ihre Software vor Software-Piraterie - mit sevLock 1.0 DLL!  
 vb@rchiv Quick-Search: Suche startenErweiterte Suche starten   Impressum  | Datenschutz  | vb@rchiv CD Vol.6  | Shop Copyright ©2000-2024
 
zurück
Rubrik: OOP / Tools   |   VB-Versionen: VB5, VB615.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 BorstelBewertung:     [ Jetzt bewerten ]Views:  20.697 

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.

Darstellung einer 1:N Beziehung
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

Dieser Workshop wurde bereits 20.697 mal aufgerufen.

Über diesen Workshop im Forum diskutieren
Haben Sie Fragen oder Anregungen zu diesem Workshop, können Sie gerne mit anderen darüber in unserem Forum diskutieren.

Neue Diskussion eröffnen

nach obenzurück


Anzeige

Kauftipp Unser Dauerbrenner!Diesen und auch alle anderen Workshops finden Sie auch auf unserer aktuellen vb@rchiv  Vol.6

Ein absolutes Muss - Geballtes Wissen aus mehr als 8 Jahren vb@rchiv!
- nahezu alle Tipps & Tricks und Workshops mit Beispielprojekten
- Symbol-Galerie mit mehr als 3.200 Icons im modernen Look
Weitere Infos - 4 Entwickler-Vollversionen (u.a. sevFTP für .NET), Online-Update-Funktion u.v.m.
 
   

Druckansicht Druckansicht Copyright ©2000-2024 vb@rchiv Dieter Otter
Alle Rechte vorbehalten.
Microsoft, Windows und Visual Basic sind entweder eingetragene Marken oder Marken der Microsoft Corporation in den USA und/oder anderen Ländern. Weitere auf dieser Homepage aufgeführten Produkt- und Firmennamen können geschützte Marken ihrer jeweiligen Inhaber sein.

Diese Seiten wurden optimiert für eine Bildschirmauflösung von mind. 1280x1024 Pixel