Object Lifecycle
In VB6, objects had a clearly defined and well-understood life cycle – a set of events that we always knew would occur over the life of an object. We were guaranteed the following:
Event |
Description |
Sub Main |
Would run as the component was loaded, before an object was created (optional) |
|
Would run before any other code in our object; called by the runtime as the object was being created |
Event |
Description |
|
Would run after any other code in our object; called by the runtime as the object was being destroyed |
With VB.NET, objects also have a lifecycle,
but things are not quite the same as in the past. In particular, we no longer
have the same concept of a component-level Sub
Main
that runs as a DLL is loaded, and the concept
of the Class
_Terminate
event changes rather substantially. However, the concept behind the
Class
_Initialize
event is morphed into a full-blown constructor method that accepts parameters.
Thus, in VB.NET, we are only guaranteed the following:
Event |
Description |
New |
Would run before any other code in our object; called by the runtime as the object was being created |
This is quite a change so let’s discuss the details further.
Construction
Object
construction is triggered any time we create a new instance of a class. This
is done using the New
keyword – a level of consistency that didn’t exist with VB6 where we got to
choose between New
and CreateObject
.
Sub Main
Since
VB6 was based on COM, creating an object could trigger a Sub
Main
procedure to be run. This would happen the first
time an object was created from a given component – often a DLL. Before even
attempting to create the object, the VB6 runtime would load the DLL and run
the Sub
Main
procedure.
The .NET Common Language Runtime doesn’t
treat components quite the same way, and so neither does VB.NET. This means
that no Sub
Main
procedure is called as a component is loaded.
In fact, Sub
Main
is only used once – when an application itself
is first started. As further components are loaded by the application, only
code within the classes we invoke is called.
It wasn’t that wise to rely on Sub
Main
even in VB6, since that code would run prior
to all the error handling infrastructure being in place. Bugs in Sub
Main
were notoriously difficult to debug in VB6. If
we do have to use code that relies heavily on the Sub
Main
concept for initialization, we’ll need to implement
a workaround in VB.NET.
This can be done easily by calling a central method from the constructor method in each class. For instance, we might create a centrally available method in a module such as:
Public
Module CentralCode
Private blnHasRun As Boolean
Public Sub Initialize()
If Not blnHasRun Then
blnHasRun = True
‘ Do initialization here
End If
End Sub
End
Module
This routine is designed to only run one time, no matter how often it is called. We can then use this method from within each constructor of our classes. For example:
Public
Class TheClass
Public Sub New()
CentralCode.Initialize()
‘ regular class code goes here
End Sub
End
Class
While this is a bit of extra work on our
part, it does accomplish the same effect we’re used to with a VB6-style Sub
Main
routine.
New Method
Like the situation
with Sub
Main
, Class
_Initialize
is called before any other code in a VB6 class. Again, it is called before the
error handling mechanism is fully in place, making debugging very hard; errors
show up at the client as a generic failure to instantiate the object. Additionally,
Class
_Initialize
accepts no parameters – meaning there is no way in VB6 to initialize an object
with data as it is created.
VB.NET eliminates Class
_Initialize
in favor of full-blown constructor methods, which have full error handling capabilities
and do accept parameters. This means we can initialize our objects as we create
them – a very important and powerful feature. The constructor method in VB.NET
is Sub
New
.
The simplest constructor method for a class is one that accepts no parameters
– quite comparable to Class
_Initialize
:
Public Class TheClass
Public Sub New()
‘ initialize
object here
End Sub
End Class
With this type of constructor, creating an instance of our class is done as follows:
Dim obj As New TheClass()
This example is directly analogous to
creating a VB6 object with code in Class
_Initialize.
However, more often than not we’d prefer to actually initialize our object with data as it is created. Perhaps we want to have the object load some data from a database, or perhaps we want to provide it with the data directly. Either way, we want to provide some data to the object as it is being created.
This is done by adding a parameter list
to the New
method:
Public Class TheClass
Public Sub New(ByVal
ID As Integer)
‘ use the
ID value to initialize the object
End Sub
End Class
Now, when we go to create an instance of the class, we can provide data to the object:
Dim obj As New TheClass(42)
To increase flexibility we might want
to optionally accept the parameter value. This can be done in two ways – through
the use of the Optional
keyword to declare an optional parameter, or by overloading
the New
method. To use the Optional
keyword, we simply declare the parameter as optional:
Public Sub New(Optional
ByVal ID As Integer = -1)
If ID = -1 Then
‘ initialize object here
Else
‘ use the ID value to initialize the object
End If
End
Sub
This approach is far from ideal, however, since we have to check to see if the parameter was or wasn’t provided, and then decide how to initialize the object. It would be clearer to just have two separate implementations of the New method – one for each type of behavior. This is accomplished through overloading:
Public Overloads Sub
New()
‘
initialize object here
End
Sub
Public Overloads Sub
New(ByVal ID As Integer)
‘ use the ID value to initialize the object
End
Sub
Not only does this approach avoid the
conditional check and simplify our code, but it also makes the use of our object
clearer to any client code. The overloaded New
method is shown by IntelliSense in the VS.NET IDE, making it clear that New
can be called both with and without a parameter.
In fact, through overloading we can create many different constructors if needed – allowing our object to be initialized in a number of different ways.
Constructor methods are optional in VB.NET. The only exception being when we’re using inheritance and the parent class has only constructors that require parameters. We’ll discuss inheritance later in the chapter.
Termination
In VB6 an object was destroyed when its last reference was removed. In other words, when no other code had any reference to an object, the object would be automatically destroyed – triggering a call to its Class_Terminate event. This approach was implemented through reference counting – keeping a count of how many clients had a reference to each object – and was a direct product of VB’s close relationship with COM.
While this behavior was nice – since we
always knew an object would be destroyed immediately and we could count on Class
_Terminate
to know when – it had its problems. Most notably, it was quite easy to create
circular references between two objects, which could leave them running in memory
forever. This was one of the few (but quite common) ways to create a memory
leak in VB6.
To be fair, the problem was worse prior to VB6. In VB6, circular references are only a problem across components. Objects created from classes within the same component would be automatically destroyed in VB6, even if they had a circular reference. Still, the circular reference problem exists any time objects come from different components. The issue is non-trivial and has created a lot of headaches for VB developers over the years.
The clear termination scheme used in VB6 is an example of deterministic finalization. It was always very clear when an object would be terminated.
Unlike COM, the .NET runtime does not use reference counting to determine when an object should be terminated. Instead it uses a scheme known as garbage collection to terminate objects. This means that in VB.NET we do not have deterministic finalization, so it is not possible to predict exactly when an object will be destroyed. Let’s discuss garbage collection and the termination of VB.NET objects in more detail.
Garbage Collection
In .NET, reference counting is not part of the infrastructure. Instead, objects are destroyed through a garbage collection mechanism. At certain times (based on specific rules), a task will run through all of our objects looking for those that no longer have any references. Those objects are then terminated; the garbage collected.
This means that we can’t tell exactly when an object will really be finally destroyed. Just because we eliminate all references to an object doesn’t mean it will be terminated immediately. It will just hang out in memory until the garbage collection process gets around to locating and destroying it. This is an example of nondeterministic finalization.
The major benefit of garbage collection is that it eliminates the circular reference issues found with reference counting. If two objects have references to each other, and no other code has any references to either object, the garbage collector will discover and terminate them, whereas in COM these objects would have sat in memory forever.
There is also a potential performance benefit from garbage collection. Rather than expending the effort to destroy objects as they are dereferenced, with garbage collection this destruction process typically occurs when the application is otherwise idle – often decreasing the impact on the impact on the user. However, garbage collection may also occur with the application is active in the case that the system starts running low on resources.
We can manually trigger the garbage collection process through code:
System.GC.Collect()
This process takes time however, so it is not the sort of thing that should be done each time we want to terminate an object. It is far better to design our applications in such a way that it is acceptable for our objects to sit in memory for a time before they are finally terminated.
Finalize Method
The
garbage collection mechanism does provide some functionality comparable to the
VB6 Class
_Terminate
event. As an object is being terminated, the garbage collection code will call
its Finalize
method – allowing us to take care of any final cleanup that might be required:
Protected
Overrides Sub Finalize()
‘ clean up code goes here
End
Sub
This code uses both the Protected
scope and Overrides
keyword – concepts we’ll discuss later as we cover inheritance. For now it is
sufficient to know that this method will be called just prior to the object
being terminated by the garbage collection mechanism – somewhat like Class
_Terminate
.
However, it is critical to remember that this method may be called long after the object is dereferenced by the last bit of client code (perhaps even minutes later).
Implementing a Dispose Method
In some cases the Finalize behavior is not acceptable. If we have an object that is using some expensive or limited resource – such as a database connection, a file handle, or a system lock – we might need to ensure that the resource is freed as soon as the object is no longer in use.
To accomplish this, we can implement a
method to be called by the client code to force our object to clean up and release
its resources. This is not a perfect solution, but it is workable. By convention,
this method is typically named Dispose
:
Public
Sub Dispose()
‘ clean up code goes here
End
Sub
It is up to our client code to call this
method at the appropriate time to ensure cleanup occurs.
Again, the specific name of this method is up to us,
though within the .NET system class libraries the convention is to use the name
Dispose
.
At this point we’ve largely covered the changes in behavior between VB6 and VB.NET in terms of creating classes and objects. Let’s move on and see how the substantial new inheritance feature works.
Comments