Delegates
There are times when it would be nice to be able to pass a
procedure as a
parameter to a method. The classic case is when building a generic sort
routine, where we not only need to provide the data to be sorted, but we need
to provide a comparison routine appropriate for the specific data.
It is easy enough to write a sort routine that sorts Person
objects by name, or to write a sort routine that sorts SalesOrder
objects by sales date. However, if we want to write a sort routine that can
sort any type of object based on arbitrary sort criteria, that gets pretty
difficult. At the same time, it would be nice to do, since some sort routines
can get very complex and it would be nice to reuse that code without having to
copy-and-paste it for each different sort scenario.
By using delegates, we can create
such a generic routine for sorting and in so doing we can see how delegates
work and can be used to create many other types of generic routines.
The concept of a delegate formalizes the
process of declaring a routine to be called and calling that routine.
The underlying
mechanism used by the .NET environment for callback methods is the delegate.
VB.NET uses delegates behind the scenes as it implements the Event,
RaiseEvent,
WithEvents,
and Handles
keywords.
Declaring a Delegate
In our code, we can declare what a
delegate procedure must look like from an interface standpoint. This is done
using the Delegate keyword. To see how this can work, let's create a routine to
sort any kind of data.
To do this, we'll declare a delegate that defines a method
signature for a method that compares the value of two objects and returns a
Boolean indicating whether the first object has a larger value that the second
object. We'll then create a sort algorithm that uses this generic comparison
method to sort data. Finally, we'll create an actual method that implements
the comparison and we'll pass
the address of that method to the sort routine.
Add a new module to our project by choosing the Project | Add
Module menu option. Name the module Sort.vb
and then add the following code:
Module Sort
Public Delegate Function Compare(ByVal v1 As Object, ByVal v2 As Object)
_
As Boolean
End Module
This line of code does something interesting. It actually
defines a method signature as a data type.
This new data type is named Compare
and it can be used within our
code to declare variables or parameters that will be accepted by our methods. A
variable or parameter declared using this data type can actually hold the
address of a method that matches the defined method signature and we can then
invoke that method by using the variable.
Any method with the signature:
f(Object, Object)
Can be viewed as being of type Compare.
Using the Delegate Data Type
We can write a routine that accepts this data type as a
parameter meaning that anyone calling our routine must pass us the address of
a method that conforms to this interface. Add the following sort routine to the
code module:
Public Sub DoSort(ByVal theData() As Object, ByVal GreaterThan As
Compare)
Dim outer As Integer
Dim inner As Integer
Dim temp As Object
For outer = 0 To UBound(theData)
For inner = outer + 1 To UBound(theData)
If GreaterThan.Invoke(theData(outer), theData(inner)) Then
temp = theData(outer)
theData(outer) = theData(inner)
theData(inner) = temp
End If
Next
Next
End
Sub
The GreaterThan
parameter is a variable
that holds the address of a method matching the method signature defined by our
Compare
delegate. The address of any method with a matching signature can be passed as
a parameter to our Sort routine.
Note the use of the Invoke
method, which is the way a
delegate is called from our code. Also note that the routine deals entirely
with the generic System.Object data type rather than with any specific
type of data. The specific comparison of one object to another is left to the
delegate routine that is passed in as a parameter.
Implementing a Delegate Method
All that remains is to actually create the implementation of
the delegate routine
and call our sort method. On a very basic level, all we need to do is create a
method that has a matching method signature. For instance, we could create a
method such as:
Public Function PersonCompare(ByVal Person1 As Object, _
ByVal Person2 As Object) As Boolean
End
Function
The method signature of this method exactly matches that
which we defined by our delegate earlier:
In both cases, we're defining two parameters of type Object.
Of course, there's more to it than simply creating the stub
of a method. We know that the method needs to return a value of True
if its first parameter is greater than the second parameter, but otherwise
should be written to deal with some specific type of data.
The Delegate
statement defines a data
type based on a specific method interface. To call a routine that expects a
parameter
of this new data type, it must pass us the address of a method that conforms to
the defined interface.
To conform to the interface, a method must have the same
number of
parameters with the same data types as we've defined in our Delegate
statement. Additionally, the method must provide the same return type as
defined. The actual name of the method doesn't matter it is the number,
order, and data type of the parameters and return value that count.
To find the address of a
specific method, we can use the AddressOf
operator. This operator
returns the address of any procedure or method, allowing us to pass that value
as a parameter to any routine that expects a delegate as a parameter.
Our Person
class already has a shared
method named CompareAge
that generally does what we want. Unfortunately, it accepts parameters of type Person
rather than of type Object as required by the Compare
delegate. We can use method overloading to solve this problem.
Create a second implementation of CompareAge
that accepts parameters of type Object
as required by the delegate,
rather than of type Person as we have in the existing implementation:
Public Shared Function CompareAge(ByVal Person1 As Object, _
ByVal Person2 As Object) As Boolean
Return CType(Person1, Person).Age > CType(Person2, Person).Age
End
Function
This method simply returns True
if the first Person
object's age is greater than the second. The routine accepts two Object
parameters rather than specific Person
type parameters, so we have to
use the CType()
method to access those objects as type Person.
We accept the parameters as
type Object
because that is what is defined by the Delegate
statement. We are matching
its method signature:
f(Object, Object)
Since this method's parameter data types and return value
match the delegate, we can use it when calling the sort routine. Place a button
on the form and write the following code behind that button:
Private Sub Button2_Click(ByVal
sender As System.Object, _
ByVal e As System.EventArgs) Handles button2.Click
Dim myPeople(4) As Person
myPeople(0) = New Person("Fred", #7/9/1960#)
myPeople(1) = New Person("Mary", #1/21/1955#)
myPeople(2) = New Person("Sarah", #2/1/1960#)
myPeople(3) = New Person("George", #5/13/1970#)
myPeople(4) = New Person("Andre", #10/1/1965#)
DoSort(myPeople, AddressOf Person.CompareAge)
End
Sub
This code creates an array of Person
objects and populates them. It then calls the DoSort
routine from our module, passing the array as the first parameter and the
address of our shared CompareAge method as the second. To display the contents
of the sorted array in the IDE's output window, we can add the following code:
Private Sub button2_Click(ByVal
sender As System.Object, _
ByVal e As System.EventArgs) Handles button2.Click
Dim myPeople(4) As Person
myPeople(0) = New Person("Fred", #7/9/1960#)
myPeople(1) = New Person("Mary", #1/21/1955#)
myPeople(2) = New Person("Sarah", #2/1/1960#)
myPeople(3) = New Person("George", #5/13/1970#)
myPeople(4) = New Person("Andre", #10/1/1965#)
DoSort(myPeople, AddressOf Person.CompareAge)
Dim myPerson As Person
For Each myPerson In myPeople
System.Diagnostics.Debug.WriteLine(myPerson.Name & " "
& myPerson.Age)
Next
End
Sub
When we run the application and click the button, the output
window will display a list of the people, sorted by age:
What makes this whole thing very powerful is that we can
change the comparison routine without changing the sort mechanism. Simply add
another comparison routine to the Person
class:
Public Shared Function CompareName(ByVal Person1 As Object, _
ByVal Person2 As Object) As Boolean
Return CType(Person1, Person).Name > CType(Person2, Person).Name
End
Function
and then change the code behind the button on the form to
use that alternate comparison routine:
Private
Sub button2_Click(ByVal
sender As System.Object, _
ByVal e As System.EventArgs) Handles button2.Click
Dim myPeople(4) As Person
myPeople(0) = New Person("Fred", #7/9/1960#)
myPeople(1) = New Person("Mary", #1/21/1955#)
myPeople(2) = New Person("Sarah", #2/1/1960#)
myPeople(3) = New Person("George", #5/13/1970#)
myPeople(4) = New Person("Andre", #10/1/1965#)
DoSort(myPeople, AddressOf Person.CompareName)
Dim myPerson As Person
For Each myPerson In myPeople
System.Diagnostics.Debug.WriteLine(myPerson.Name & " "
& myPerson.Age)
Next
End
Sub
When we run this updated code, we'll find that our array
contains a set of data sorted by name rather than by age:
By simply creating a new compare routine and passing it as a
parameter, we can entirely change the way that the data is sorted. Better
still, this sort routine can operate on any type of object, as long as we
provide an appropriate delegate method that knows how to compare that type of
object.