Dealing with Circular References
Why you get circular references in VB and two methods to prevent the problem.
VB and COM makes a lot of things easy to do, particularly using and dealing with objects:
you just create them and normally they clear themselves up automatically once you're finished
with them. However, one side effect of the way COM works is that it is possible to create objects
which cannot terminate properly: this problem is known as a "Circular Reference".
The effects of a circular reference can range from using more memory than you ought to
(possibly to the extent that you get an Out Of Memory error) to much harsher effects
when you are experimenting with API calls.
How VB Determines When to Terminate an Object
Normally, you don't need to worry about object termination. If you create an object
using the New operator, you know that VB when the object goes out of scope VB will
try to call its _Terminate method (if it has one) and clear up any memory
associated with it. You can also set any object reference you have
to Nothing which will do the same thing. So how does VB know when there are
no outstanding references to the object? After all, you can run code like this:
Dim m_c As New Collection
Private Sub Form_Load()
Dim c1 As New MyClass
c1.Name = "This Will Terminate At The End Of Form_Load"
Dim c2 As New MyClass
c2.Name = c1.Name
m_c.Add c2
End Sub ' c1 will terminate here, but c2 will not
The answer to this is provided by the fundamentals of COM objects. All VB objects
are either explicitly created as COM objects (if they are exposed objects compiled into
an ActiveX binary) or are created as such internally within VB. All COM objects must
follow the COM contract, which means they must implement at least three methods:
- QueryInterface
- AddRef
- Release
The second two of these are of interest to how VB knows when to terminate an object.
Whenever the AddRef method of a COM object is called, the object itself must
increment an internal reference count by 1. Whenever Release is called, the
object decrements the count. When the internal reference count of the object reaches
zero then it can be terminated and any resources associated with it cleared up.
A C++ coder using a COM object has complete control over when AddRef and
Release are called. This means that you can decide if a particular object always
lives within the lifetime of another object, and decide not to call AddRef. This
is considerably more flexibility than you have with VB. VB always
calls AddRef and Release for you at the correct moments. That is fine,
except it results in a problem called Circular References.
Creating a Circular Reference
A circular reference occurs when there are two objects which refer to each other.
Say you have a class which manages a series of Worker objects that perform specific tasks,
and the Worker objects need to notify their parent, or to interact with data held by
the parent. What happens is this:
- When you create the Parent object in VB, AddRef is called. Parent now has a reference
count of 1
- When Parent creates an instance of a Worker, AddRef is called on the Worker. Worker now
also has a reference count of 1
- Worker gets a reference to the Parent object to allow it to notify or interact with the
data. The Parent object reference count is incremented to 2.
- When Parent later goes out of scope, VB calls Release. This reduces the parent reference
count to 1 (the reference held by the Worker). However, this now means that Parent still
has a reference count of 1. Unless something can tell the Worker object to release its
reference, both Parent and Worker will remain with reference counts until the application
terminates. Remember that since the Parent does not enter the Class_Terminate method,
there is nothing that will occur in the code to allow you to know that its main reference
has been terminated and so it looks from Parent's point of view that it is still
required, even though it is only a child class which is requiring it!
The only way that these objects can go out of scope is when the VB process finally shuts
down. Then VB will terminate all the remaining objects it knows about, and the order in
which that occurs is not known. Fine if the order doesn't affect the object VB is terminating,
but not so good if you need to ensure if something terminates first (e.g. to close a handle).
The circular reference problem typically occurs when you're trying to create a
strongly typed collection of objects for a control. When you modify elements of the
object held in the collection typically this has to affect the control itself. However, if
the child object has a reference to the control to notify the control about the change, then
you have a circular reference.
Resolving Circular References
There are three ways you can go about preventing circular references. These are:
- Using a Late Bound events
- Providing a "Dispose" style method.
- Using Non-Counted References
Whilst the event method is a possible solution, it is not covered here. The problem with
using events is that the events have to be wired up at design time. If you don't know
how many child objects you're going to have then it is all but impossible to design a
solution to fix this.
The other two solutions are demonstrated in the example projects
and are covered in turn.
Providing a "Dispose" Style Method
The problem with a circular reference is there is no event which automatically occurs
when the owner of the Parent object releases its reference. Clearly, however, you can
provide a method in the Parent object which goes through any child Worker objects and
terminates the object reference. By doing this, the Worker objects release their references
on the parent, so the additional reference count is removed, and then the Parent object
can terminate as soon as it goes out of scope.
This method is demonstrated in the Fix 1 download. However, whilst it works ok,
it has two distinct disadvantages:
- The owner of the object needs to know when it releases its reference. That's fine for
a DLL object, but very difficult for something like a control, because VB's form engine
is actually in charge of when it wants to terminate the object.
- The user of the object needs to remember to call the "Dispose" style
method. This isn't a very natural way to code in VB and easily forgotten.
Ideally there would be a way of coding these objects which could be used in a normal
VB style by any user. Luckily there is, but you need to incorporate a few hacks.
Using Non-Counted References
This method is the equivalent of what a C++ coder would do when they knew they did
not have to call AddRef or Release on an object reference because its scope
could never go outside the bounds of the parent object. There are two ways you can
go about doing this:
-
Using object pointers.
The object pointer technique, first demonstrated by Bruce McKinney in his legendary
book Hardcore Visual Basic (a book that is still causing embarassment via my harddrive, as his
frequent use of the term "Hardcore" seems to convince others I've been up
to tricks on the Internet. Then again, things could be worse; it could have been called
Teenage Beaver Visual Basic, although I suppose that would have been somewhat unlikely), is
a tried and trusted technique on this site. The concept is simple: all COM objects
are accessed through a pointer in memory, and this is returned by VB's ObjPtr
function. Calling ObjPtr does not change the reference count. Therefore if you
can somehow reconstruct an object from its pointer, then you have a method to store
an object without changing its reference count.
Luckily there is a way, as follows:
' To Retrieve the Object From the Long Value:
Private Property Get ObjectFromPtr(ByVal lPtr As Long) As Object
Dim objT As Object
' Bruce McKinney's code for getting an Object from the
' object pointer:
CopyMemory objT, lPtr, 4
Set ObjectFromPtr = objT
CopyMemory objT, 0&, 4
End Property
The Fix 2 download demonstrates this technique.
-
Using IUnknown
If you have access to a Type Library which provides the basic IUnknown COM
object interface,
containing QueryInterface, AddRef and Release, you can cast your
object to that type and call the Release method on it as soon as you have copied
the object reference. This decrements the reference count by 1 and hence as soon as the
parent reference goes out of scope Class_Terminate can be called.
Note that VB itself appears to know about the IUnknown interface even though
you won't see it in the object browser. Declaring something as IUnknown is equivalent to
using As Object in VB, but VB does not allow you to call the AddRef or Release
methods through this route (you'll get an error). To use IUnknown properly
you need a Type Library, and you need to prefix the IUnknown type declaration
with the Type Library's name, otherwise you get VB's mindless version.
This version is demonstrated in the Fix 3 download. Looking at the code, it is a whole
lot more VB-like than the Fix 2 version and actually almost quite nice! However, there's
a downside. If the Worker classes are terminated prior to the Parent class, you get
a GPF. Whether you can be entirely sure which order VB will terminate its objects,
I'm not certain whether this a good idea.
Wrap Up
COM reference counting and the circular reference problem associated with it are
one of the major bugbears with all COM related projects. You will note that the .NET
Framework, like Java, does away with reference counting altogether and takes
a completely different approach to managing objects in memory. That means you do have
no choice but to consider the "Dispose" route if you need to be certain when
objects terminate in .NET, but at least it is obvious that this will be a problem and
there are certain constructs, such as the IDisposable interface and the
using language features that make it more likely that these will be used correctly.
This article demonstrates how you get a circular reference and a number of
techniques for removing the problem. Although all have a measure of danger, so do
circular references with no attempt at resolution. If used judiciously, you should find
the techniques presented here will stop these problems in their tracks.
|
|