Working with Objects
In the .NET environment, and within VB in particular, we use
objects all the time without even thinking about it. Every control on a form
and, in fact, every form is an object. When we open a file or interact with a
database we are using objects to do that work.
Objects are created using the New
keyword indicating that we want a new instance of a particular class. There
are a number of variations on how or where we can use the New
keyword in our code. Each one provides different advantages in terms of code
readability or
flexibility.
Unlike
previous versions of VB, VB.NET doesn't use the CreateObject
statement for object creation. CreateObject
was an outgrowth of VB's
relationship with COM and, since VB.NET doesn't use COM, it has no use for CreateObject.
The CreateObject
method still exists to
support COM interoperability, but is not used to access .NET objects.
The most obvious way to create an object is to declare an
object
variable and then create an instance of the object:
Dim obj As TheClass
obj = New TheClass()
The result of this code is that we have a
new instance of TheClass
ready for our use. To
interact with this new object, we will use the obj
variable that we declared. The obj
variable contains a reference to
the object a concept we'll explore more later.
We can shorten this by combining the
declaration of the variable with the creation of the instance:
Dim obj As New TheClass()
In
previous versions of VB this was a very poor thing to do, as it had both
negative performance and maintainability effects. However, in VB.NET, there is
no difference between our first example and this one, other than that our code
is shorter.
This code both declares the variable obj
as data type TheClass
and also creates an instance
of the class immediately creating an object that we can use from our code.
Another variation on this theme is:
Dim obj As TheClass = New TheClass()
Again, this
both declares a variable of data type TheClass and creates an instance of the class for our use.
This third syntax provides a great deal of
flexibility while remaining compact. Though it is a single line of code, it
separates the declaration of the variable's data type from the creation of the
object.
Such flexibility is very useful when working
with inheritance or with multiple interfaces. We might declare the variable to
be of one type say an interface and instantiate the object based on a class
that implements that interface. We'll cover interfaces in detail in Chapter 6
but as an example
here, let's create an interface named ITheInterface:
Public Interface ITheInterface
Sub
DoSomething()
End Interface
Our class can then implement that interface,
meaning that our class now has its own native interface and also has a
secondary interface ITheInterface:
Public Class TheClass
Implements ITheInterface
Public Sub DoSomething() Implements ITheInterface.DoSomething
'
implementation goes here
End
Sub
End Class
We can now create an instance of TheClass,
but reference it via the secondary interface by declaring the variable to be of
type ITheInterface:
Dim obj As ITheInterface = New TheClass()
We can also do this using two separate lines
of code:
Dim obj As ITheInterface
obj = New TheClass()
Either
technique works fine and achieves the same result, which is that we have a new
object of type TheClass, being accessed via its secondary interface.
We'll discuss multiple interfaces in more detail in Chapter 6.
So far we've been declaring a variable for
our new objects. However, sometimes we may simply need to pass an object as a
parameter to a method
in which case we can create an instance of the object right in the call to
that method:
DoSomething(New TheClass())
This calls the DoSomething
method, passing a new instance of TheClass
as a parameter.
This can be even more complex. Perhaps,
instead of needing an object reference, our method needs an Integer.
We can provide that Integer
value from a method on our
object:
Public Class TheClass
Public Function GetValue() As Integer
Return 42
End
Function
End Class
We can then instantiate the object and call
the method all in one shot, thus passing the value returned from the method as
a parameter:
DoSomething(New TheClass().GetValue())
Obviously, we need to carefully weigh the
readability of such code against its compactness at some point, having more
compact code can detract from readability rather than enhancing it.
Notice that nowhere do we use the Set
statement when working with objects. In VB6, any time we worked with an object
reference we had to use the Set
command differentiating objects
from any other data type in the language.
In
VB.NET, objects are not treated differently from any other data type, and so we
can use direct assignment for objects just like we do with Integer
or String
data types. The Set
command is no longer valid in VB.NET.
Typically, when we work with an
object we are using a reference
to that object. On the other
hand, when we are working with simple data types such as Integer,
we are working with the actual value rather than a reference. Let's explore
these concepts and see how they work and interact.
When we create a new object using the New
keyword, we store a reference to that object in a variable. For instance:
Dim obj As New TheClass()
This code creates a new instance of TheClass.
We gain access to this new object via the obj
variable. This variable holds a reference to the object. We might then do
something like this:
Dim another As TheClass
another = obj
Now we have a second variable, another,
which also has a reference to that same object. We can use either variable
interchangeably, since they both reference the exact same object. The thing we
need to remember is that the variable we have is not the object itself but,
rather, is just a reference or pointer to the object itself.
When we are done working with an
object, we can indicate that we're through with it by dereferencing the object.
To dereference an object, we need to simply
set our object reference to Nothing:
Dim obj As TheClass
obj = New TheClass()
obj = Nothing
This code has no impact on our object
itself. In fact, the object may remain blissfully unaware that it has been
dereferenced for some time.
Once any and all variables that reference an
object are set to Nothing,
the .NET runtime can tell
that we no longer need that object. At some point, the runtime will destroy the
object and reclaim the memory and resources consumed by the object.
Between the time that we dereference the
object and the time that .NET gets around to actually destroying it, the object
simply sits in memory unaware that it has been dereferenced. Right before
.NET does destroy the object, the framework will call the Finalize
method on the object (if it has one). We discussed the Finalize
method in Chapter 3.
One of the strengths of Visual Basic has
long been that we had access to both early and late binding when interacting
with objects.
Early binding
means that our code directly
interacts with the object, by directly calling its methods. Since the VB
compiler knows the object's data type ahead of time, it can directly compile
code to invoke the methods on the object. Early binding also allows the IDE to
use IntelliSense to aid our development efforts; it allows the compiler to
ensure that we are referencing methods that do exist and that we are providing
the proper parameter values.
In previous
versions of VB, early binding was also known as vtable binding. The vtable was
an artifact of COM, providing a list of the addresses for all the methods on an
object's interface. In .NET, things are simpler and there is no real vtable.
Instead, the compiler is able to generate code to directly invoke the methods
on an object. From a VB coding perspective this makes no difference, but it is
quite a change behind the scenes.
Late binding
means that our code interacts with an object dynamically at run-time. This
provides a
great deal of flexibility since our code literally doesn't care what type of
object it is interacting with as long as the object supports the methods we
want to call. Because the type of the object isn't known by the IDE or
compiler,
neither IntelliSense nor compile-time syntax checking is possible but we get
unprecedented flexibility in exchange.
If we enable strict type
checking by using OptionStrictOn
at the top of our code modules, then the IDE and compiler will enforce early
binding behavior. By default, OptionStrict is turned off and so we have easy access to the use
of late binding within our code. We discussed OptionStrict in Chapter 4.
Implementing Late Binding
Late binding occurs when the compiler can't determine the type of
object that we'll be calling. This level of ambiguity is achieved
through the use of the Object data type. A variable of data type Object can hold virtually any value including a
reference to any type of object. Thus, code such as the following could be run
against any object that implements a DoSomething method that accepts no parameters:
Option Strict Off
Module LateBind
Public Sub DoWork(ByVal obj As Object)
obj.DoSomething()
End
Sub
End Module
If the object passed into this routine does not have a DoSomething
method that accepts no parameters, then a run-time error will result. Thus, it
is recommended that any code that uses late binding always provides error
trapping:
Option Strict Off
Module LateBind
Public Sub DoWork(ByVal obj As Object)
Try
obj.DoSomething()
Catch ex As Exception When Err.Number = 438
' do something appropriate given failure to call the method
End Try
End
Sub
End Module
Here, we've put the call to the DoSomething
method in a Try
block. If it works then the code in the Catch
block is ignored but, in the
case of a failure, the code in the Catch
block is run. We would need to
write code in the Catch block to handle the case that the object did not
support the
DoSomething method call. This Catch
block, in fact, only catches
error number 438, which is the error indicating that the method doesn't exist
on the object.
While late binding is flexible, it can be error prone and it
is slower than early bound code. To make a late bound method call, the .NET
runtime must dynamically determine if the target object actually has a method
that matches the one we're calling, and then it must invoke that method on our
behalf. This takes more time and effort than an early bound call where the
compiler knows ahead of time that the method exists and can compile our code to
make the call directly. With a late bound call, the compiler has to generate
code to make the call dynamically at runtime.
Whether we are using late binding or not, it can be useful to
pass object references around using
the Object
data type converting them to an appropriate type when we need to interact
with them. This is particularly useful when working with objects that use
inheritance or implement multiple interfaces concepts that we'll discuss in
Chapter 6.
If Option Strict is turned off, which is the default, we
can write code that allows us to use a variable of type Object
to make an early bound method call:
Module LateBind
Public Sub DoWork(obj As Object)
Dim local As TheClass
local = obj
local.DoSomething()
End Sub
End
Module
We are using a strongly typed variable, local,
to reference what was a generic object value. Behind the scenes, VB.NET
converts the generic type to a specific type so it can be assigned to the
strongly typed variable. If the conversion can't be done we'll get a trappable
runtime error.
The same thing can be done using the CType
function. If Option
Strict is enabled, then the previous approach will not compile
and the CType
function must be used. Here is the same code making use of CType:
Module LateBind
Public Sub DoWork(obj As Object)
Dim local As TheClass
local = CType(obj, TheClass)
local.DoSomething()
End
Sub
End Module
Here, we've declared a variable of type TheClass,
which is an early bound data type that we want to use. The parameter we're
accepting, though, is of the generic Object
data type, and so we use the CType()
method to gain an early bound reference to the object. If the object isn't of
type TheClass,
the call to CType()
will fail with a trappable error.
Once we have a reference to the object, we
can call methods by using the early bound variable, local.
Since all the
method calls with CType() are early bound, this code will work even if we
override the default and set Option Strict
On.
This code can be shortened to avoid the use of the
intermediate variable. Instead, we can simply call methods directly from the
data type:
Module LateBind
Public Sub DoWork(obj As Object)
CType(obj, TheClass).DoSomething()
End
Sub
End Module
Even though the variable we're working with is of type Object
and, thus, any calls to it will be late bound, we are using the CType
method to temporarily convert the variable into a +specific type in this
case, the type TheClass.
If the object passed as a parameter is not of
type TheClass,
we will get a trappable error, so it is always wise to wrap this code in a Try
Catch
block.
The CType
function can be very useful
when working with objects that implement multiple interfaces, since we can
reference a single object variable through the appropriate type as needed. For
instance, as we discussed earlier, if we have an object of type TheClass
that also implements ITheInterface, we can use that interface with the
following code:
Dim obj As TheClass
obj = New TheClass
CType(obj, ITheInterface).DoSomething()
In this way, we can make early bound calls to other
interfaces on an object without needing to declare a new variable of the
interface type. We'll discuss multiple interfaces in detail in Chapter 6.