Self Cleaning CollectionsThe ProblemA common mistake made when creating collections of objects is to design the collection in such a way that an instance of it can never be deleted. This generally happens where the objects within the collection have backlinks to the collection itself, so that they can access its properties and methods. Once you have made, and populated, a collection like this it is impossible to get rid of it because its reference count never gets to zero, so VB always thinks it is still in use. Figure 1 illustrates this.
Each member of the collection has a property, such as Parent, which returns a reference to the collection itself, shown by the dotted lines. When it is added to the collection this is set by the collection object eg (in the Doofers class) Private mColl As New Collection Public Function Add(ByVal Name As String) as Doofer
Set Add = New Doofer
Add.Init Name, Me
mColl.Add Add, Name
End Function
and in the Doofer class Private mParent As Doofers Private mName As String Public Sub Init(ByVal Name As String, ByVal Parent As Doofers)
mName = Name
Set mParent = Parent
End Sub
Public Property Get Parent() As Doofers
Set Parent = mParent
End Property
The External Ref in Figure 1 is the object reference (or references) created in the application, eg Set MyDoofers = New Doofers You might expect that when MyDoofers is set to Nothing, either explicitly, when the program exits, or when the object containing it is terminated, that the Doofers collection would be terminated, together which all of the Doofer objects, and that the memory used by them all would be freed up. Unfortunately, this won't happen. Visual Basic objects use the OLE reference counting mechanism, and they don't get terminated/deleted until no variables reference them. Checking Figure 1 you will see that after MyDoofers (External Ref) is removed there are still 3 variables with references to it (the mParent variables in each Doofer) represented by the dotted lines. Those references won't be cleared until all the Doofer objects are deleted, but that won't happen until the Doofers object is terminated, which won't happen until all the Doofer objects are deleted... A Solution - DIYThere's an obvious way out of this loop, which is to give the Doofers collection a method telling it to remove all of its members. You just call this immediately before removing the last external reference to it, eg MyDoofers.Cleanup Set MyDoofers = Nothing This works, so long as everything is that simple, everyone who uses Doofers knows how important it is to call Cleanup, remembers to call it, and there is no chance that there is never any other reference to the object held elsewhere. That last one is the real stopper. If references to MyDoofers have been passed around to other objects how can be sure that you know when you need to call Cleanup. In general, you can't. A better solutionIt would be so much easier if the collection knew when its external reference count had hit zero, and it terminated itself automatically, the way it was always meant to. There isn't any way it can distinguish between external and internal references, and there isn't any (half-way safe) way we can fiddle with its reference count. There is, however, a way we can have the Doofer objects call a routine in their parent collection object, without adding to its reference count - by using Events. An event allows one object to call a routine in another object without having a reference to it. You might think of events as being calls that controls make to forms when something happens, such as the user clicking the mouse on the control, but they are a lot more flexible than that. Have a look at the BabLogFile class in the Babbacombe Error Handler. This has an event called SupplyBackupName. It raises this event when it wants to create a backup of a log file in case the application want to give it a specific name for the backup file. If the app has a WithEvents reference to a BabLogFile object it can trap the event and supply the name, if it wants to. Eg Private WithEvents mLogFile As BabLogFile Private Sub mLogFile_SupplyBackupName(BackupName As String)
BackupName = "MyBackup.log"
End Sub
The BabLogFile object doesn't take out a reference to the object containing this code. In fact, it doesn't know or care what type of object it is, or even whether it exists (sort of). We could do something similar with the Doofer objects so that, rather than having mParent keeping a reference to the collection open the whole time, the Parent property could raise an event in the Doofers collection something like (in the Doofers collection class) Private WithEvents mDoofers() As Doofer Private Sub mDoofers_SupplyParent(Index As Long, Parent As Doofers)
Set Parent = Me
End Sub
and in the Doofer Class Public Event SupplyParent(Parent As Doofers) Public Property Get Parent() as Doofers
Dim P as Doofers
RaiseEvent SupplyParent(P)
Set Parent = P
End Sub
Don't bother pasting that into VB and trying it, because it's not valid. You can't have a WithEvents array variable. But we are getting there. The solution is to have another object between the collection and its members, which relays messages between the members and the collection. Although the collection and each collection member will need to have a reference to it, the relay object can use an event to save it from needing to have a reference to the collection, as in Figure 2
As you can see the Doofers object now has only one reference to it. When that is removed it will terminate, when it can remove its reference to the relay object and also remove all the Doofer objects from the collection. This removes the references from each of them (the arrows on the left), causing them in turn to terminate, removing the remaining references to the relay object (the dotted lines), which then causes it to terminate. This Doofers collection has thus cleaned itself up, hence the name of this article. Here's the code that manages all this In the Doofers collection class Private WithEvents mRelay As DoofersRelay Private mDoofers As Collection Private Sub Class_Initialize()
Set mRelay = New DoofersRelay
Set mDoofers = New Collection
End Sub
Private Sub Class_Terminate()
Set mDoofers = Nothing
Set mRelay = Nothing
End Sub
Public Function Add(ByVal Name As String) As Doofer
Set Add = New Doofer
Add.Init Name, Relay
mDoofers.Add Add, Name
End Function
Private Sub mRelay_SupplyParent(Parent As Doofers)
Set Parent = Me
End Sub
In the DoofersRelay class Public Event SupplyParent(Parent As Doofers) Public Function GetParent() As Doofers
Dim Parent As Doofers
RaiseEvent SupplyParent(Parent)
Set GetParent = Parent
End Function
And in the Doofer class Private mName As String Private mRelay As DoofersRelay Private Sub Class_Terminate()
Set mRelay = Nothing
End Sub
Friend Sub Init(ByVal Name As String, Relay As DoofersRelay)
mName = Name
Set mRelay = Relay
End Sub
Public Property Get Parent() As Doofer
Set Parent = mRelay.GetParent()
End Property
NotesI've made the Init method of the Doofer class Friend. I usually implement object hierarchies in DLLs, so the Relay objects are always Private, and can't therefore appear in the parameter list of any exported routine. The code above assumes that the only way to create a Doofer is to add it to a Doofers collection, in which case the Doofer class will be Public Not Createable. In theory, you could just create one Relay class and use it for everything. I like to use strong typing, so I always create type specific collection classes, and type specific relay classes. It is perfectly reasonable to give a relay class some extra method/event pairs if you feel like it. One useful one is for implementing an Index property for the collection members. You could implement it using just the Parent property and a Friend function in the collection, but it's up to you. |
|
|