IObject - Why is it Important?
Why would you want to implement the IObject interface in new classes? The
IObject interface may only have 3 methods, but the methods can play
a tremendous role when using the new class with VBCorLib. Many of the classes in
VBCorLib will utilize the IObject interface to access objects when needed.
By implementing this interface, your class becomes a root type object that VBCorLib
can use when necessary. Classes that do not implement this interface have a limited
use within VBCorLib. VBCorLib even comes with a class template with the basic
interface already implemented. The name of the template class is IObjectBase.
By starting out with this class makes it extremely easy to create a VBCorLib
ready class.
In this tutorial, we will incrementally implement the IObject interface on a
Person class. With each incremental change, we will show how to make
VBCorLib treat our objects in a manner we want.
IObject.Equals
To begin, we will create a simple Person class in the normal manner.
Option Explicit
Private mName As String
Public Property Get Name() As String
Name = mName
End Property
Public Property Let Name(ByVal RHS As String)
mName = RHS
End Property
This is a simple class for our Person. We will use this class to represent
some people and keep track of them in a couple of different classes to show how
implementing the IObject interface provides the functionality we want.
The first example will be to place 3 Person objects in to an ArrayList
collection and how we can search for a specific person in the list.
Dim Kelly As New Person
Dim Scott As New Person
Dim John As New Person
Dim list As New ArrayList
Kelly.Name = "Kelly"
Scott.Name = "Scott"
John.Name = "John"
list.Add Kelly
list.Add Scott
list.Add John
Debug.Print "List contains Scott: " & list.Contains(Scott)
' output
' List contains Scott: True
This is a simple list of Person objects, each identified with a different
name. Checking if the list contains a specific person is requested, and True
is returned. The ArrayList finds Scott in the list by comparing each object
in the list to the value being sought using the 'Is' operator. This works fine
as long as we use the original object instance to perform the search. What if
a dynamic search is needed. We want to be able to define a person to search for
and then use that object instance to find if list contains the person.
Dim find As New Person
find.Name = "Scott"
Debug.Print "List contains Scott: " & list.Contains(find)
' output
' List contains Scott: False
The problem here is that the 'Is' operator is used to compare each object instance
with the 'find' object. Since 'find' was never added to the list, it is not found.
That is where the IObject interface comes into play. It allows our classes
to be smart enough to perform comparisons on behalf of such things as the ArrayList.
Lets re-implement our Person starting with the IObject template class
included with VBCorLib. The name of the template class is IObjectBase and it
has the basic implementation of the IObject interface already written.
Option Explicit
Implements IObject
Private mName As String
' original Person class implementation
Public Property Get Name() As String
Name = mName
End Property
Public Property Let Name(ByVal RHS As String)
mName = RHS
End Property
' IObject interface included in the public interface
Public Function ToString() As String
ToString = App.Title & "." & TypeName(Me)
End Function
Public Function Equals(ByRef value As Variant) As Boolean
If IsObject(value) Then
Equals = (value Is Me)
End If
End Function
Public Function GetHashCode() As Long
GetHashCode = ObjPtr(Me)
End Function
' IObject Interface
Private Function IObject_Equals(value As Variant) As Boolean
IObject_Equals = Equals(value)
End Function
Private Function IObject_GetHashcode() As Long
IObject_GetHashcode = GetHashCode
End Function
Private Function IObject_ToString() As String
IObject_ToString = ToString
End Function
We will concentrate on the 'Equals' method for this part of the example.
The default implementation of IObject will make the class behave exactly
as without the interface when it comes to comparing one object to another. As you
can see in the 'Equals' method, the 'Is' operator is ultimately used to determine
if the two objects are equal. This is no different than what ArrayList did,
so there is no advantange in using the IObject interface if that's all that
is needed.
We originally wanted to be able to dynamically search for a person in the list,
and to do that we wanted to make our class smart enough to perform the comparison
on behalf of the ArrayList. We need to change the behaviour of the 'Equals'
method and make it compare what we want, instead of using the 'Is' operator. We
want the Person class to compare the Names of the value against its own name
for equality. Since any kind of value can be passed into the 'Equals' methods,
we will need to check to make sure we are dealing with another Person object.
Once we have another Person, we can then compare the names.
Public Function Equals(ByRef value As Variant) As Boolean
If IsObject(value) Then
If TypeOf value Is Person Then
Dim p As Person
Set p = value
Equals = (p.Name = mName)
End If
End If
End Function
The re-implementation of the 'Equals' method checks that it is dealing with
a Person object. Once it is satified, it sets the value to a local
Person variable so the 'Name' property can be accessed. Finally, the
equality of the names is returned from the function.
Now that we have the Person class performing equality comparisons the
way we want, we can re-run the original example searching for a person in the list.
Debug.Print "List contains Scott: " & list.Contains(Scott)
' output
' List contains Scott: True
Dim find As New Person
find.Name = "Scott"
Debug.Print "List contains Scott: " & list.Contains(find)
' output
' List contains Scott: True
Now the ArrayList calls the 'Equals' method on each object in the list,
passing in the 'find' object. ArrayList has simply delegated the comparison
of values to the objects in the list.
IObject.GetHashCode
Now we have made our Person smarter. It's not as smart as it could be,
and we will encounter this drawback in the next example.
In this example we want to keep a simple In/Out board used to tell if someone
is at work or not. We will use a Hashtable to keep the current status
for each person we are tracking.
Dim Kelly As New Person
Dim Scott As New Person
Dim John As New Person
Dim board As New Hashtable
Kelly.Name = "Kelly"
Scott.Name = "Scott"
John.Name = "John"
board(Kelly) = "In"
board(Scott) = "In"
board(John) = "In"
Debug.Print "Kelly is " & board(Kelly)
' output
' Kelly is In
Each Person object is used as the key for the status in the Hashtable.
We then use the existing 'Kelly' object to retrieve the status from the board and
display the result. Now we want to dynamically retrieve the status of a specific
person.
In dynamically retrieving the status of a person, we need to create a new Person
object with the name of the person we are searching for.
Dim find As New Person
find.Name = "Kelly"
Debug.Print "Kelly is " & board(find)
' output
' Kelly is
Wait a second. We got nothing, not even an exception! Remember, if the key is
not found in a Hashtable, then Empty is returned. No exception is
thrown. The key isn't found? But, we overrode the 'Equals' function in our Person
class. Well, the Hashtable uses a hashcode to help search for a value very
quickly. Once it narrows down the values with a hashcode, it then will test for
equality for the remaining values.
A hashcode is used by a Hashtable, and there happens to be a function named
'GetHashCode'. The default implementation simply returns the pointer to the current
object on which the function is being called. If IObject was not being implemented,
then the Hashtable would also use the ObjPtr function. This number will be unique across
all objects. That is where our problem is. We want two different objects to generate
the same hashcode if they are equal to eachother. In this case, the Person property
'Name' is what we use to test equality, so that is what we will use to generate
our hashcode. So we will override the default implementation with our own.
Public Function GetHashCode() As Long
Dim i As Long
Dim ch() As Integer
ch = cString.ToCharArray(mName)
For i = 0 To Len(mName) - 1
GetHashCode = ((GetHashCode * 16) + ch(i)) And &H3FFFFFF
Next i
End Function
This will generate a hashcode based on the Name property. The number is not unique
across all possible values, but will be unique enough to create a uniform set
of hashcodes.
Ok, now we have overridden the 'GetHashCode' function and the 'Equals' function.
Lets try checking the In/Out board again based on the original 'Kelly' object
and the dynamic 'find' object.
Debug.Print "Kelly is " & board(Kelly)
' output
' Kelly is In
Dim find As New Person
find.Name = "Kelly"
Debug.Print "Kelly is " & board(find)
' output
' Kelly is In
By also overriding 'GetHashCode' we made our Person even smarter than before
by returning a hashcode that makes sense for our class.
Now we can easily use any Person as a key just as any other datatype. This is
a very powerful technique. There is no need to determine outside of the Person
class on how to create a key value for the object.
Note
In order for our Person class to be able to be used in
binary searches or sorting methods, we still need to make the class smarter than
what IObject can provide. Check out IComparable and any tutorials
about how to implement it to make our class very smart.
IObject.ToString
The final method of IObject we will look at is the ToString method. This
method is used to return a string representation of the object's value. Most of
the time this value simply returns the name of the class itself. This is the
default implementation in the IObjectBase template class.
What can we use it for when using VBCorLib? Many programs need to output information
for display. VB will convert the intrinsic datatypes to a string representation
automatically. However, VB has no idea how to deal with our classes when it comes
to displaying them on the screen, except for a default method. The default method
may be used for other reasons, not allowing for simple string conversion capabilities.
VBCorLib's cString class is one class that will use an object's
IObject.ToString method to retrieve a string representation of an object.
This gives a powerful way for treating our objects as datatypes in many situation.
Dim p As Person
' the list is from the Equals example at the beginning
For Each p In list
Debug.Print cString.Format("The name of this Person is {0}.", p)
Next p
' output
' The name of this Person is Project1.Person.
' The name of this Person is Project1.Person.
' The name of this Person is Project1.Person.
Well, we outputted something, not exactly what we wanted. As you can see, the
name of the class was displayed. By looking at the default implementation of
ToString we see that it returns the project and class name.
By overriding the default implementation we can make our Person class
behave similar to an intrinsic datatype when it comes to converting it to a string.
Public Function ToString() As String
ToString = mName
End Function
The override for our needs is trivial. We simply return the name of the person
instead of the name of the class.
Now when we re-run the example we get an output that is more appropriate.
Dim p As Person
' the list is from the Equals example at the beginning
For Each p In list
Debug.Print cString.Format("The name of this Person is {0}.", p)
Next p
' output
' The name of this Person is Kelly.
' The name of this Person is Scott.
' The name of this Person is John.
Since VBCorLib will use the ToString method in methods that need a value of
a class, it treats our class as a VB intrinsic datatype. This makes it easy
to convert an object's value to something that can be displayed.
Note
Our class can become even smarter if we wanted to have custom formatting
commands supplied, such as the cDateTime uses. Check out the IFormattable
interface to learn how to include formatting commands for our class to process.
Summary
IObject only provides 3 methods to implement. However, those methods
provide a very powerful way of making classes smarter and better utilized by VBCorLib.
By implementing the IObject interface, a class can be made smart enough to be
used in many of the collection classes provided by VBCorLib. Since the logic
is fully encapsulated, a developer could call the functions on their own without
worrying about how to perform equality checks.
There is a template class named IObjectBase that is included with VBCorLib.
This class should be used to easily start a smart and integratable class when
working with VBCorLib.
|