Download code for this article
Setting the stage
.NET Framework comes with a rich barrage of built-in data types and also provides the developer a fertile platform for building custom types. .NET Framework offers 5 (6 in VB.NET) types to build these custom types; Class, Structure, Enum, Delegate, Interface (and Module in VB.NET).
When modeling real life problems, Classes (and to a lesser degree Structures) are what is predominately used to build types that map to the problem domain. Two main types of classes that most developers have come to know and expect to build are Business Object classes and Collection classes.
Business Object classes
Business Object classes are types that present singular entities like Person, Employee or Order. An example of a simple Business Object class is the Person type.
Public Class Person
Public Sub New(ByVal firstName As String, ByVal lastName As String)
m_firstName = firstName
m_LastName = lastName
End Sub
Private m_firstName As String
Public Property FirstName() As String
Get
Return m_firstName
End Get
Set(ByVal value As String)
m_firstName = value
End Set
End Property
Private m_LastName As String
Public Property LastName() As String
Get
Return m_LastName
End Get
Set(ByVal value As String)
m_LastName = value
End Set
End Property
Public ReadOnly Property FullName() As String
Get
Return String.Format("{0} {1}", m_firstName, m_LastName)
End Get
End Property
Public ReadOnly Property ReverseName() As String
Get
Return String.Format("{1}, {0}", m_firstName, m_LastName)
End Get
End Property
End Class
The Person type class has four read-only properties; FirstName, LastName, FullName and ReverseName.
Collection classes
Collection classes are types that present groupings of objects that need to be considered as a whole.
The Array type is an example of a collection type that requires predefining its size (the number of elements it will hold) before it can store objects.
Dim peopleArr As Person()
ReDim peopleArr(3)
peopleArr(0) = New Person("Omar", "Shraim")
peopleArr(1) = New Person("Bashar", "Lulu")
peopleArr(2) = New Person("Fadi", "Atteya")
peopleArr(3) = New Person("Mohammad", "Abdol-Haleem")
.NET brings with it a rich offering of collection types that do not require defining their sizes. Examples of these collection types are ArrayList, Hashtable, SortedList, Queue, Stack, Bitarray, BitVector32.
With the exception of BitArray and BitVector32 (each of which holds values of Boolean type) all these collection types can hold objects of only one type; System.Object (which means they can hold any object of any type).
Dim peopleList As ArrayList = New ArrayList()
peopleList.Add(New Person("Omar", "Shraim"))
peopleList.Add(New Person("Bashar", "Lulu"))
peopleList.Add(New Person("Fadi", "Atteya"))
peopleList.Add(New Person("Mohammad", "Abdol-Haleem"))
If the developer needs to build collection types that hold values of types other than System.Object, .NET provides abstract collection types that can be extended to create Strongly Typed collections (collections that can only hold the type they are extended for).
Examples of abstract collection types are CollectionBase, ReadOnlycollectionBase, DictionayBase, NameObjectCollectionbase.
.NET Framework 2.0 brought with it Generic types that offered yet another richer and safer model of defining strongly typed collections with much less code.
Dim peopleList2 As List(Of Person) = New List(Of Person)
peopleList2.Add(New Person("Omar", "Shraim"))
peopleList2.Add(New Person("Bashar", "Lulu"))
peopleList2.Add(New Person("Fadi", "Atteya"))
peopleList2.Add(New Person("Mohammad", "Abdol-Haleem"))
What’s missing?
Though it seems that our Person class models our real life person entity and we have an ample choice of collection types to hold Person type objects, we soon find out that our Person collections are lifeless because they don’t support Searching and Sorting on the Person type objects they hold.
It sounds to reason that if an array or a collection of a built-in .NET type (like Integer or String) can be searched and sorted, so should an array or a collection of Person objects. Yet for any array or collection of Person type objects, the IndexOf() method (of the Array type) and Contains() method (of any collection type) would always return empty handed. The simple reason for this is that the array and collection types do not know how to compare a Person object.
Dim peopleArr As Person()
ReDim peopleArr(3)
peopleArr(0) = New Person2.Person("Omar", "Shraim")
peopleArr(1) = New Person2.Person("Bashar", "Lulu")
peopleArr(2) = New Person2.Person("Fadi", "Atteya")
peopleArr(3) = New Person2.Person("Mohammad", "Abdol-Haleem")
' IndexOf can never find the requested object
If Array.IndexOf(peopleArr, New Person2.Person("Fadi", "Atteya")) >= 0 Then
Console.WriteLine("FADI Found")
Else
Console.WriteLine("FADI Not Found")
End If
To add this capability to our Person type we need to override the Equals() method (that all .NET types implicitly inherit from the System.Object type).
Public Overrides Function Equals(ByVal obj As Object) As Boolean
If (obj Is Nothing) OrElse (Not TypeOf obj Is Person) Then
Return False
End If
Return String.Compare(Me.FullName, DirectCast(obj, Person).FullName, ignoreCase:=True) = 0
End Function
Now the IndexOf() and Contains() methods can search for a Person object by comparing the FullName property of the objects in the collection.
Not the same but equal
The GetHashCode() method (all .NET types also implicitly inherit from the System.Object type) is responsible for generating an object’s hash code. For two distinct objects to be equal, they also should have the same hash code. A customary manner to generate a hash code is to combine the hash codes of two or more immutable fields.
Private m_hashCode As Integer
Public Sub New(ByVal firstName As String, ByVal lastName As String)
m_firstName = firstName
m_LastName = lastName
'Person objects that have the same FirstName and LastName
'also have the same hash codes.
'use bitwise Xor to avoid overflow!
m_hashCode = firstName.GetHashCode Xor lastName.GetHashCode
End Sub
Public Overrides Function GetHashCode() As Integer
Return m_hashCode
End Function
Now that our Person objects can be compared for equality, we need to give arrays and collections of Person objects the capability to sort their objects. This leads the way to the first of our Fantastic Four.
Enter the Fantastic Four
Our heroes are four interfaces that when implemented by a .NET class (or structure) would add some extra functionality. For example, collections of built-in .NET types (like Integer, String …) have the capability to sort the objects they hold out of the box because these built-in .NET types implement the first of our fantastic four interfaces; the IComparable interface.
IComparable
This interface exposes only one method, CompareTo(), which has the following signature:
Function CompareTo(ByVal obj As Object) As Integer
The CompareTo() method is expected to return -1, 0, or 1 depending whether the object on which CompareTo() is called is less than, equal to, or greater than the object passed as an argument.
' this function adds sorting capabilities to the class
Private Function CompareTo(ByVal obj As Object) As Integer Implements System.IComparable.CompareTo
If obj Is Nothing Then Return 1
Dim hold As Person = DirectCast(obj, Person)
Return String.Compare(Me.FullName, hold.FullName, ignoreCase:=True)
End Function
The implementation of CompareTo() in the Person type simply compares the FullName string and returns the result accordingly.
Now that the Person type implements ICompareTo, any collection of Person objects would magically become sortable.
'sorting peopleArr from previous code
Array.Sort(peopleArr)
For i As Integer = 0 To 3
Console.WriteLine(peopleArr(i).FullName)
Next
IComparer
Collections of Person objects can now be sorted by the FullName property because that is the property used by the CompareTo() method. But what if we wanted collections of Person objects to be storable by other properties like the ReverseName property and the FirstName property?
It is very common to have Business Objects that need to be compared and sorted on different properties or even property combinations.
The second interface of our fantastic four solves this issue elegantly. The IComparer interface exposes only one method, Compare, which receives two object references and returns -1, 0 or 1 depending on whether the first object is less than, equal to or greater than the second object.
So if our Person type needs to be sorted on ReverseName and on FirstName, we need to build two comparer classes, ReverseNameComparer and FirstNameComparer.
A Comparer class is a class the implements the IComparer interface to compare objects of a specific type (which in our case is the Person type).
Public Class ComparerByFirstName
Implements IComparer
Public Function Compare(ByVal x As Object, ByVal y As Object) As Integer Implements System.Collections.IComparer.Compare
' two null objects are equal!
If x Is Nothing AndAlso y Is Nothing Then
Return 0
End If
' a non-null object is always greater than a null object
If x Is Nothing Then
Return 1
End If
If y Is Nothing Then
Return -1
End If
'if passed objects are not of Person type,
'an exception is thrown
Dim p1 As Person = DirectCast(x, Person)
Dim p2 As Person = DirectCast(y, Person)
Return String.Compare(p1.FirstName, p2.FirstName, ignoreCase:=True)
End Function
End Class
Public Class ComparerByReverseName
Implements IComparer
Public Function Compare(ByVal x As Object, ByVal y As Object) As Integer Implements System.Collections.IComparer.Compare
' two null objects are equal!
If x Is Nothing AndAlso y Is Nothing Then
Return 0