- Mar 1, 2002
Using Constructors and Destructors
As with VB 6.0 classes, the classes in VB.NET support the inclusion of initialization and termination code that runs when a new instance of the class is created and deallocated. However, the names and behavior of these methods differ dramatically.
A constructor in VB.NET is defined as a procedure that has the name New (rather than Initialize as in VB 6.0) and can accept arguments to allow clients to pass data into the instance to assist with initialization. Constructors do not return values and therefore are always declared as a Sub. The constructor can also be overloaded and accept varying arguments to promote polymorphic use of the class.
When the first instance of a class is constructed using the New operator, the run-time will initially attempt to find and execute the shared constructor (defined as Shared Sub New()) on the class. Note that shared constructors are not defined with the Public keyword, cannot accept arguments, and hence cannot be overloaded with another version of the shared constructor. In other words, the following code is illegal because it attempts to overload a shared constructor:
Overloads Shared Sub New() End Sub Overloads Shared Sub New(ByVal ID As Integer) End Sub
After dealing with the shared constructor, the runtime executes any virtual constructor (a constructor that is not shared), passing in the optional arguments, for the specific instance of the class. After that is completed, the instance is ready for use.
Inherited classes can also have their own shared and instance constructors. In this case, when a new instance of a derived class is created, the runtime first looks for a shared constructor of the base class, and then the shared constructor for the derived class, followed by an instance constructor for the derived class, and finally the instance constructor for the base class. This arrangement makes sense because the shared constructor of the derived class can alter the contents of other shared data and therefore must execute after the shared constructor of the base class.
If the derived class does not contain a shared constructor, the shared constructor of the base class is executed, followed by the instance constructor of the base class, and finally the instance constructor of the derived class.
This discussion implies that constructors cannot be overridden and will always execute regardless of whether a derived class has a constructor as well. As a result, you do not need to use the MyBase keyword to explicitly call the New method of the base class. However, you can invoke MyBase.New() as the first line in a instance constructor if you want to execute the constructor in the base class before that in the derived class.
The ability to pass data directly into the constructor is referred to as parameterized construction. VB.NET allows this by expanding the syntax for the New operator. For example, the Course class discussed earlier supports construction using the following syntax:
Protected Class Course Public Sub New(ByVal courseID As Integer) ' Initialize with a specific course in mind End Sub Public Sub New() ' Initialize without a course End Sub End Class
A client then has the option of creating the class using either constructor by simply providing parameters to the New statement in the variable declaration:
Dim objCourse As New Course(intCourseID)
or separately in a New statement:
Dim objCourse As Course objCourse = New Course(intCourseID)
Note that as an additional bonus, the IntelliSense feature of VS.NET shows both constructors in a drop-down list as shown in Figure 4.2.
Figure 4.2. Calling constructors. This screen shot from VS.NET shows how IntelliSense provides a drop-down list for all constructors of a class.
As with other overloaded methods, overloaded constructors must differ in either the number of arguments or their data types; argument names are not considered. Of course, in this simple example you could also have specified the courseID argument as Optional, although this would have required you to specify a default value and the IntelliSense feature would not show the varying constructors in a drop-down list. In addition, using overloaded constructors rather than optional arguments frees you from the limitation that each argument following the first optional must also be optional.
Although it might seem confusing the Overloads keyword is not allowed on overloaded constructors.
By creating classes with numerous (overloaded) constructors, you also run the risk of duplicating code. You can alleviate duplication in one of two ways. First, you can create private procedures that abstract the non-specific initialization code required and simply invoke that procedure from each constructor. Second, you can place the non-specific initialization code in a shared constructor, although remember that shared constructors can reference only other shared members of the class.
Analogous to the Terminate method in VB 6.0, each class in VB.NET can implement a Finalize method that is called when the object instance is deallocated. The Finalize method is a Sub procedure that should always be protected and overrides the Finalize method of the base class. However, unlike a constructor, the Finalize method of the base classes in the hierarchy do not run unless explicitly called with MyBase.Finalize in the derived class as follows:
Overrides Protected Sub Finalize ' Call the base class Finalize MyBase.Finalize End Sub
Perhaps the most important difference between Terminate and Finalize, as mentioned in Chapter 1, is that by default, the Finalize method is actually executed by the runtime on a special thread allocated by the Garbage Collector (GC). Subsequent to the object instance no longer being referenced (reachable), the Finalize method is executed whenever the runtime feels it is appropriate, such as when a low-resource condition occurs. In other words, you don't necessarily have control of when the Finalize method executes. This situation is often referred to as non-deterministic finalization. VB.NET still supports the Nothing keyword and you are encouraged to set an object instance to Nothing when you are finished using it. This information actually helps the GC detect when an object is no longer reachable and allows it to be marked as requiring deallocation.
Using the Finalize method for code that frees resources such as file handles and database connections is tempting. However, because of non-deterministic finalization, you'll probably also want to free system resources much sooner than whenever the GC gets around to finalizing your objects.
In fact, your classes do not have to implement a Finalize method at all. Not doing so actually increases performance for the runtime because the GC does not have to queue up the execution of the Finalize method, as mentioned in Chapter 1. However, in some cases you might want to make sure that all resources are cleaned up gracefully, and implementing a Finalize method is the only way to ensure that this happens.
The standard technique for allowing objects to clean up their resources in a timely fashion is to implement a Close or Dispose method in the class. Typically, a Close method is used when the object can be "opened" again by calling some other method, whereas Dispose is used if the object instance is not to be used after calling Dispose. Keep in mind that these methods will not automatically be called and so the documentation for your class must adequately describe its proper use. To provide both explicit and implicit cleanup, you can place your cleanup code in the Finalize method and then call Finalize from the Dispose method as shown in the following code:
Public Class Instructors Public Sub Dispose ' Dispose() calls Finalize so that you can include all ' the cleanup code in one place. Me.Finalize() ' Tell the GC that the object doesn't require any cleanup GC.SuppressFinalize(Me) End Sub Overrides Protected Sub Finalize ' Clean up system resources End Sub End Class
Note that the Finalize method can be called using the Me keyword because it is a member of the same object instance. In addition, the Services Framework makes available the System.GC class that you can use to interact with the Garbage Collector through its shared methods. In this case, the SuppressFinalize method is called to notify the GC that it does not have to Finalize this object because it has already been finalized. This saves the GC from having to do extra work when the object becomes unreachable.
One other aspect of the GC that you need to be aware of is that it does not guarantee that objects will be deallocated in any particular order. In other words, the order in which you set them to Nothing is not necessarily the order in which they'll be cleaned up.
This presents an interesting situation when you create one class that references another. For example, assume that the Registration class accepts an argument in its constructor that refers to an instance of the Instructors class defined in the previous code snippet as shown here:
Public Class Registration Private mIns As Instructors Public Sub New(ByRef obj As Instructors) mIns = obj End Sub Protected Overrides Sub Finalize() mIns.Dispose() End Sub End Class
Obviously, as long as the Registration instance is reachable, the Instructors object will also be reachable because Registration retains a reference to it in the mIns local variable. However, let's assume that both instances become unreachable (are set to Nothing). Because the order of finalization is not guaranteed, the runtime could finalize the Instructors object referred to by the Registration object before the Registration object. In this case, when the finalization occurs on Registration and it attempts to call the Dispose method of Instructors, an exception is thrown. As a result, in designs like these you should either be prepared to catch the resulting exception or not implement a Finalize method in classes like Registration. Optionally, if the Instructors object really does need to be disposed, you can implement a Dispose method in Registration that calls the Dispose method of Instructors.
When you are dealing with classes that use external resources such as file handles, window handles, database connections, and other resources outside of the runtime, make sure that those resources are released with a finalizer and a Dispose method. However, if your classes simply manipulate other managed classes, either custom or through the Services Framework, you typically do not need to create a finalizer.
In order for other classes to call Dispose methods polymorphically in an early-bound fashion, the Services Framework exposes the IDisposable interface that your classes can implement. By implementing the interface, any client code can query for the interface and call an object's Dispose method without knowing anything else about the object. Windows Forms uses this technique to cleanup controls that are placed on a form. The Instructors class implementing IDisposable would look like Listing 4.10.
Listing 4.10 The Instructors class implementing the IDisposable interface to allow clients to call Dispose polymorphically.
Public Class Instructors Implements IDisposable Private mflDisposed As Boolean = False Public Sub Dispose Implements IDisposable.Dispose ' Dispose() calls Finalize so that you can include ' all the cleanup code in one place. Me.Finalize() ' Tell the GC that the object doesn't require any cleanup GC.SuppressFinalize(Me) mflDisposed = True End Sub Overrides Protected Sub Finalize ' Clean up system resources End Sub Public Function GetInstructors() As DataSet If mflDisposed Then Throw ObjectDisposedException("Instructors") End If End Function End Class
You'll also notice in Listing 4.10 that the class tracks a private Boolean variable to determine when the object was disposed. If any other method is subsequently called, such as GetInstructors, the System.ObjectDisposedException is thrown indicating to the client that the object is not usable.
Interacting with the GC
As the call to SuppressFinalize in the previous section makes clear, you can programmatically interact with the GC through the System.GC class through its shared methods.
The first method you can use to affect the GC is Collect. Basically, this method forces the GC to collect all unreachable objects and queue them up for finalization. Although it's possible to include this call in your code so that resources are cleaned up at a specific point in time, it is recommended that you design your classes to do explicit cleanup as discussed previously and then leave the actual collection process to the GC. In this way, resources that need to be freed early will be, and those that are less critical won't take up the extra CPU cycles being deallocated unnecessarily.
You can also call the KeepAlive method of System.GC and pass it a reference to an object you wish to not have garbage collected in the current method. When the runtime sees a call to KeepAlive, it assumes that the object is reachable up to the point where the KeepAlive method is called (in execution order within the current procedure, not necessarily in line order). In other words, the KeepAlive method assures that the object will be kept alive until at least directly after the call to KeepAlive. This method is sometimes used when referring to COM objects or other unmanaged objects so that the runtime does not inadvertently lose references to it.
Finally, you can also call the WaitForPendingFinalizers method, which suspends the current thread and waits for all Finalize methods that have been queued up to run. This is seldom used because this operation could take some time and block the current thread. In fact, WaitForPendingFinalizers is not assured ever to return (because the finalization process could trigger another garbage collection, ad infinitum) and so should be left safely alone.
As mentioned in Chapter 1, the runtime also allows objects to be re-referenced from within their Finalize methods. If this occurs, the object is said to be resurrected because it will not be deallocated and is now once again reachable. For example, assume that the Finalize method of the Registration class includes a line of code that passes a reference to the object using the Me keyword to another class or to a public variable within the application. In this case, the original variable used to refer to the Registration object would be set to Nothing; however, the object itself would still be alive and could be referenced through the new class or variable. The object is then said to be resurrected and can be used normally with the exception that the Finalize method will not run a second time. To allow it to run again, you must call the ReRegisterForFinalize method of the GC, passing in the Me keyword. The code for implementing the Registration class with resurrection is as follows:
Dim objHolder as Registration Public Class Registration Private mIns As Instructors Public Sub New(ByRef obj As Instructors) mIns = obj End Sub Protected Overrides Sub Finalize() mIns.Dispose() ' Resurrect objHolder = Me GC.ReRegisterForFinalize(Me) End Sub End Class
As a final note, keep in mind that as in this case shown here with mIns, if the resurrected class contains references to other objects, those references will also be resurrected.
Obviously, the situations in which you'd want to use resurrection are very limited, and as a general rule it should be avoided.
The final way in which you can interact with the GC is to utilize weak references. Basically, as I discussed in Chapter 1, a weak reference is a reference to an object instance that can be collected by the GC if it deems that resources are scarce. In other words, by creating a weak reference, you are giving permission to the GC to collect the object even though you are still holding a reference to it, albeit a weak one. Of course, weak references imply the existence of strong references. A strong reference is simply one that is created with a standard variable using the New operator.
The idea behind weak references is to allow your application to hold references to many easily reconstructable objects without permanently consuming large amounts of memory. Obviously, the higher the cost of re-creating the object, the less likely it will be that you'll choose to use weak references. However, when you use a weak reference, there are a few coding conventions that must be followed.
To create a weak reference, you create an instance of the System.WeakReference class and pass it a strong reference to the object you want to hold. For example, the following code snippet creates a strong reference to the Instructors class followed by a weak reference:
Dim oIns As New Instructors Dim wrIns As New WeakReference(oIns,False)
At this point, the oIns variable can go out of scope or otherwise become unreachable and the Instructors object will still be alive as long as the GC has not determined that it needs to reclaim memory by deallocating it. In the meantime, the wrIns variable can be passed around and perhaps stored in a collection for later use. The second argument to the WeakReference constructor shown earlier indicates whether the object should be available until the object is collected (False) or until it is actually finalized (True).
When the weakly referenced object needs to be used, you'll need to cast it to the correct type; however, you can first check to be sure that the object still exists by checking the Target property of the object. If the property is not Nothing, you can use the CType function to once again create a strong reference to the object.
Dim obj As Instructors If Not wrIns.Target Is Nothing Then obj = CType(wrIns.Target, Instructors) End If
Alternatively, you can check the IsAlive property to determine whether the object is still available, although doing so is discouraged because there is no guarantee that the object will still be alive even as soon as the very next statement.
At this point, the GC will not collect the object since it is reachable with a strong reference, and you can continue to work with it.