[ Previous ]
[ Index ]
[ Next ]
2
|
Construction and Demolition
|
Isn't software programming great? Not only do we get to build things, but we also get to break and demolish them as well. Granted, we typically break things by accident, but nevertheless it's fun and we can always write it up as "great exercise". Therefore, this chapter starts with constructors and creating objects and then concludes with how to destroy them.
Let's attempt to cover constructors without boring ourselves. We'll start by looking at normal constructors, then make things more interesting by taking a look at inheritance and constructors, and then conclude with static constructors.
Constructors in VB.NET have the name New, and constructors in C# have the same name as the type, just as they do in C++ or Java. I say "type" because structures can have constructors as well (though they cannot have default, parameterless constructors as was mentioned previously). Constructors can be overloaded in both VB.NET and C#. The following is a VB.NET class with two constructors:
Public Class AnsweringMachine
Public Sub New()
Console.WriteLine("Default constructor called.")
Console.WriteLine("Leave a message after the beep.")
Microsoft.VisualBasic.Beep()
End Sub
Public Sub New(ByVal message As String)
Me.New()
Console.WriteLine("Received message: " + message)
End Sub
End Class
The single-argument constructor calls the default constructor, which would not be called by the VB.NET compiler otherwise. Here is almost the same example using a C# class:
public class AnsweringMachine
{
public AnsweringMachine()
{
Console.WriteLine("Default constructor called.");
Console.WriteLine("Leave a message after the silence.");
}
public AnsweringMachine(string message) : this()
{
Console.WriteLine("Received message: " + message);
}
}
First, if you were paying attention then you probably are wondering where the Beep went to. It's part
of the Visual Basic .NET Runtime and not available in C#. So if you're building a component for R2D2,
consider using VB.NET. Next, you'll notice the this() initializer used by the single-argument
constructor in order to call the default constructor. When using this(), the default constructor
will be invoked before the body of the single-argument constructor. You can also pass arguments
within the initializer in order to call a non-default constructor.
Chapter 5 will cover inheritance in more detail, but there are some inheritance topics involving constructors and destructors that we are going to cover in this and in the next section. Let's add a parent class to our VB.NET example as follows:
Public Class MotherOfAllMachines
Public Sub New()
Console.WriteLine("Mama constructor called.")
End Sub
End Class
Public Class AnsweringMachine
Inherits MotherOfAllMachines
Public Sub New()
Console.WriteLine("Default constructor called.")
Console.WriteLine("Leave a message after the beep.")
Microsoft.VisualBasic.Interaction.Beep()
End Sub
Public Sub New(ByVal message As String)
Me.New()
Console.WriteLine("Received message: " + message)
End Sub
End Class
If an AnsweringMachine instance is created using the following:
Dim machine As New AnsweringMachine("Constructor construction junction")
Then the parent's constructor will be invoked automatically first, followed by the
AnsweringMachine's default constructor, and then the single-argument constructor. The output resembles the following:
Mama constructor called.
Default constructor called.
Leave a message after the beep.
Received message: Constructor construction junction
The parent's constructor is automatically called only if there is a default constructor.
If a default constructor is not explicitly defined, the compiler will generate one for you
(as is the case in C++ and Java). A default constructor will not be generated
if a base class implements a non-default constructor. In such a case, a derived class must
explicitly call its parent's constructor, supplying the appropriate arguments, in order
for the initialization of the object to proceed. In VB.NET this is done
using MyBase.New(...) in the first line of the child's non-default
constructor as shown below:
Public Class MotherOfAllMachines
Public Sub New(ByVal parameter As Integer)
Console.WriteLine("Mama constructor called.")
End Sub
End Class
Public Class AnsweringMachine
Inherits MotherOfAllMachines
Public Sub New()
MyBase.New(42)
Console.WriteLine("Default constructor called.")
Console.WriteLine("Leave a message after the beep.")
Microsoft.VisualBasic.Interaction.Beep()
End Sub
Public Sub New(ByVal message As String)
MyBase.New(42)
Console.WriteLine("Received message: " + message)
End Sub
End Class
The code above would not compile if both of the AnsweringMachine's constructors did not
explicitly call the non-default constructor of their parent class. Also notice that the
Me.New() call is now missing from AnsweringMachine's non-default constructor. This is an
interesting battle. Both MyBase.New and Me.New must
be the very first line within a
constructor, so you have a choice between either one or the other. Realistically,
you would call MyBase.New, which is required in a case such as that above,
and move any other shared construction logic into a separate method callable by
all constructors within the class.
All of the above, in principle, is the same in C#. If a parameterless base class constructor
exists, it will be called for you. Otherwise, you need to specify a base(...) initializer
similarly to how the this() initializer was used in our previous C# example. Here is the
C# equivalent of the VB.NET code above:
public class MotherOfAllMachines
{
public MotherOfAllMachines(int parameter)
{
Console.WriteLine("Mama constructor called.");
}
}
public class AnsweringMachine : MotherOfAllMachines
{
public AnsweringMachine() : base(42)
{
Console.WriteLine("Default constructor called.");
Console.WriteLine("Leave a message after the silence.");
}
public AnsweringMachine(string message) : base(42)
{
Console.WriteLine("Received message: " + message);
}
}
If you are a refugee from Java (or perhaps you now consider yourself a double-agent) you are
likely familiar with static initializers. Static initializers (which also go by the following
aliases: type constructors, static constructors, or type initializers) are used to
initialize static variables within a class.
Now, let's play a game. All of the following rules apply to static initializers except one. Which one is it?
- A class can have only one static constructor.
- A static constructor will be called only once for the class.
- A static constructor may not have any parameters.
- A static constructor is run before any instance of the type is created.
- A static constructor is run before any of the type's static members are referenced.
- A static constructor is run before any types that derived from the type are loaded.
- A static constructor is run before any of its code is ever written.
- A static constructor will not be run more than once during a single execution of a program (technically, per lifetime of an AppDomain).
Unlike all the other major programming languages, VB.NET uses the keyword Shared instead of static,
but don't let that throw you. It's the same thing. Shared constructors within VB.NET have
implicit Public access whereas static
constructors in C# have implicit private access.
Not that it matters being that the compiler calls all static constructors for you.
In fact, you cannot explicitly specify any access level for static constructors in
either VB.NET or C#.
Static constructors are used to initialize static member variables. They are also sometimes used to write or initialize log files as shown in this shocking example:
Public Class StaticElectricity
Shared Sub New()
Debug.WriteLine("StaticElectricity class has been loaded and charged.")
End Sub
End Class
If the above class had a static member variable, you might decide to initialize it inline as follows:
Public Class StaticElectricity
Private Shared mCharge As Double = 545.32
Shared Sub New()
Debug.WriteLine("StaticElectricity class has been loaded and charged.")
End Sub
End Class
When the compiler works its magic on this code example, it will move the initialization
of m_Charge from its inline initialization to the very top of the Shared Sub New()
initializer. If you have not defined a static initializer for a class, the compiler
generates one for you and moves the intermediate language for the initialization of any
static members into it. The same applies to C#. Here is a similar example that
illustrates the C# syntax:
public class StaticElectricity
{
private static double mCharge;
static StaticElectricity()
{
mCharge = ScootAroundRoomOnCarpetWithSocks();
Debug.WriteLine("StaticElectricity class has been loaded and charged.");
}
private static double ScootAroundRoomOnCarpetWithSocks()
{
// Do real-world logic here.
return 545.32;
}
}
Further above, when we were discussing how to code an answering machine, you might have took note of the following syntax, especially if you have a lot of experience with Visual Basic 6.0:
Dim machine As New AnsweringMachine("Constructor construction junction")
This "inline" method of creating objects was actively discouraged in VB 6.0 via punishment
enforced by members of the Code Review Gestapo (with echo). The reason being that the keywords
As New in VB 6.0 actually initialized an object to Nothing. Then, every time the object variable
was encountered it was first evaluated before the code executed. If the object was Nothing,
the object was created before the code that used it executed. In other words, As New was
Ass Stupid. Instead, developers were encouraged to declare an object separately from its
instantiation and to explicitly instantiate it immediately before it is used.
Thankfully, there is no implicit object creation in VB.NET and objects are explicitly
created using As New. In addition, all constructors in VB 6.0 had to be parameterless
and that is no longer a limitation.
In case you haven't yet discovered why Visual Studio .NET licenses are so expensive, it's because Microsoft is throwing in lifetime garbage collection for all your code. And we both know that's a lot of garbage! Since this e-Book is not written for total newbies, I assume that you're already aware that in a garbage collected language the memory for objects is automatically reclaimed once your application is finished with it. Garbage collection in .NET, as it is in Java, is non-deterministic, meaning that Bill Gates will clean up after you whenever he sees fit.
Since garbage collection is non-deterministic, you have no control over when a class's
destructor (C#) or finalizer (VB.NET) is executed or whether it even runs at all. You
will see both the destructor and finalizer terminology used interchangeably in .NET
literature; however, finalization and finalizer are technically more accurate terms, being that
finalization occurs after an object has been destructed as far as your code is concerned.
The VB.NET and C# compilers both emit Finalize methods into the Intermediate Language code.
C# does so by looking for a finalizer written using the C++ destructor syntax, an arguable
and possibly confusing choice, as demonstrated in this simple example:
public class BornToDie
{
public static void Main()
{
BornToDie deadCode = new BornToDie();
}
~BornToDie()
{
System.Diagnostics.Trace.WriteLine("I have been finalized. Finally!");
}
}
The example above creates an instance of BornToDie and then simply exists. The garbage
collector is invoked as the application exists (though there is no guarantee of this) and
BornToDie's finalizer is called. Here is the same example in VB.NET, notice the Finalize syntax:
Public Class BornToDie
Public Shared Sub Main()
Dim deadCode As New BornToDie()
End Sub
Protected Overrides Sub Finalize()
System.Diagnostics.Trace.WriteLine("I have been finalized. Finally!")
End Sub
End Class
For now, take note of the Overrides keyword used on the
Finalize method declaration.
We will cover this later in Chapter 6 on inheritance.
Let me reiterate that there are no guarantees concerning when a finalizer will execute.
If you have a class that includes member classes that in turn hold pointers to other
member classes, and all of these classes implement finalizers, the finalizers
implemented by these classes could be called in any order. (It's just like after a
snowstorm during the holiday season, the garbage collector may run or he may not.
When he does, who knows which neighborhood will be first?) Therefore, within a
finalizer be very careful about accessing inner, member objects, and in fact,
try to avoid doing so completely. Finalizers are intended to free unmanaged
resources, not other managed objects which can implement their own finalizers.
Microsoft .NET includes a large assortment of tools and utilities, most of which are invoked from the command-line, and a few of which are even useful. I strongly encourage you to lookup the phrase ". NET Framework Tools" in the .NET Framework SDK Documentation to view a list of these. Among the tools is the Intermediate Language Disassembler (ILDasm.exe). The IL Disassembler displays the IL of a DLL or EXE in human-readable format (or more correctly, nerd-readable format) either through its GUI or by outputting a text file that can be sent to its counterpart, the IL Assembler. Note that unless an assembly has already been converted to a native image (by using the Native Image Generator Tool, ngen.exe), it is already in IL form. The IL Disassembler simply formats the IL into a readable form. It's technically not "disassembling" anything.
If you have been reading along so far without typing in any of the examples,
now is a perfect time to get more involved. Create a Console Application in either
VB.NET or C# (or both!) using the New Project wizard and type in one of the tiny
BornToDie code examples further above. Test the code to your satisfaction first.
Then launch the Visual Studio .NET Command Prompt in your Programs menu beneath
Microsoft Visual Studio .NET\Visual Studio .NET Tools. Run ildasm.exe and browse
to your EXE. You should see something similar to the window below. This is displayed
after browsing to the VB.NET example:
By double-clicking on the Finalize method we can view the IL for the method, which will be displayed in a separate window like that below:
This is not a tutorial on intermediate language. I believe the code above is fairly
self-explanatory. Now, let's examine the IL emitted for the C# example. Remember that
the logic of the two BornToDie examples are exactly identical. The following is displayed
after browsing to the C# EXE and double-clicking its finalizer:
C# has generated into the IL try and finally blocks for us as well as a call to our parent
class's Finalize method within the finally block! How cool, yet totally inconsistent! In fact,
in C# if you attempt to explicitly call your base class's Finalize method from your destructor,
you will get a compiler error informing you that it is called for you automatically.
Both of these steps are extremely important. They ensure that the parent class's
finalizer is always called. Unfortunately, a brief look back at the IL generated by the
VB.NET compiler shows that the same is not true of VB.NET. So you may be thinking, "I just
need to be sure to always write the same try/finally logic myself in my VB.NET classes,
right?" Unfortunately, you can't because you cannot call your base class's finalizer in VB.NET.
If you're beginning to think that finalizers are more trouble than they're worth, then
you're getting the hang of it. (Though I still prefer garbage collection to the reference
counting nightmares of COM.) Here's "da rule": If your class uses an unmanaged resource
(i.e. you have a Win32 handle to a file, socket, or something similar attained via an API call),
then implement a finalizer to guarantee that it is cleaned up appropriately. However, whenever
possible use the .NET Framework classes for files, sockets, etc. because they manage resources
for you and implement their own finalizers. Then, you will only need to call a Close
or Dispose
method on the object, which brings us to our next topic. Use finalizers only as a last resort.
In many cases you need resources held by your objects to be cleaned up when you want
them to be, not when the stinky garbage collector wants them to be. Luckily, there is a
buzzword design pattern in .NET for performing just such a thing. It's the use of the
IDisposable interface to implement the dispose pattern.
The IDisposable interface looks
like this:
public interface IDisposable
{
public void Dispose();
}
Why is this a design pattern? The interface itself is not a design patter per se;
it's the common use of the interface throughout many of the .NET Framework classes
offering explicit cleanup of resources. You will also see the Close method used often.
It is part of the design pattern as well, and often the Close method simply
calls Dispose.
For instance, it seems more natural to "close" a file rather than "dispose of" it.
The SqlConnection class implements both methods.
Chapter 5 will cover implementing interfaces in more detail, but let's skip ahead and take
a look at implementing the IDisposable interface. The following is a simple example of how you
might do so in VB.NET for a class that manages a FileStream:
Imports System.IO
Public Class UseMeAndDisposeMe
Implements IDisposable
Private mFileStream As FileStream
Public Sub Open()
' Note: This can throw 8 different kinds of
' exceptions, but that's beyond the scope of this example.
mFileStream = New FileStream("temp.txt", FileMode.Create)
End Sub
' ... methods that do something with the FileStream would be here ...
Public Sub Dispose() Implements IDisposable.Dispose
If Not mFileStream Is Nothing Then
mFileStream.Flush()
mFileStream.Close()
mFileStream = Nothing
End If
End Sub
End Class
This class does not implement a finalizer; however, if it did you could call the Dispose method
from the finalizer "just to be safe". Then, you would want to call GC.SuppressFinalize(this)
within the Dispose method. This prevents finalization from occurring since the object's
resources have already been cleaned up. When garbage collection occurs, the object's
memory will simply be reclaimed without the finalizer being executed. In instances
where you have a large number of objects, perhaps a few hundred or thousand in an
array, suppressing finalization can be a significant performance boost.
Here is the C# equivalent to the VB.NET code above:
using System;
using System.IO;
public class UseMeAndDisposeMe : IDisposable
{
private FileStream mFileStream;
public void Open()
{
// Note: This can throw 8 different kinds of
// exceptions, but that's beyond the scope of this example.
mFileStream = new FileStream("temp.txt", FileMode.Create);
}
// ... methods that do something with the FileStream would be here ...
public void Dispose()
{
if (mFileStream != null)
{
mFileStream.Flush();
mFileStream.Close();
mFileStream = null;
}
}
}
If you were creating, using, and closing a FileStream within a single method, then the logic would look like this:
public void StreamAByte()
{
FileStream fileStream = null;
try
{
fileStream = new FileStream("temp.txt", FileMode.Create);
fileStream.WriteByte( (byte)42 );
fileStream.Flush();
}
finally
{
if (fileStream != null)
{
((IDisposable)fileStream).Dispose();
}
}
}
With C# you can reduce the method above down to the following by using the using statement:
public void StreamAByte()
{
using (FileStream fileStream = new FileStream("temp.txt", FileMode.Create))
{
fileStream.WriteByte( (byte)42 );
fileStream.Flush();
}
}
The using statement generates the try and finally blocks along with a call to Dispose in
the finally block. Variables specified between using's parenthesis must implement
IDisposable and must be initialized within the parenthesis as well. You can initialize
multiple variables by separating them with commas.
Unfortunately, the using statement is not available in VB.NET. In this particular
example, there is another problem as well. Opening and writing to files can generate
any of a number of exceptions. You cannot attach catch statements to the end of the
using statement and embedding the using block
within another try/catch would defeat
its purpose. So, in this particular instance, I would not use the shortcut method and
opt for the long form instead by implementing try/catch/finally exception handling.
[ Previous ]
[ Index ]
[ Next ]