When a head of state dies, the President of the United States typically does not have time to attend the funeral personally. Instead, he dispatches a delegate. Often this delegate is the Vice President, but sometimes the VP is unavailable and the President must send someone else, such as the Secretary of State or even the First Lady. He does not want to “hardwire” his delegated authority to a single person; he might delegate this responsibility to anyone who is able to execute the correct international protocol.
The President defines in advance what responsibility will be delegated (attend the funeral), what parameters will be passed (condolences, kind words), and what value he hopes to get back (good will). He then assigns a particular person to that delegated responsibility at “runtime” as the course of his presidency progresses.
In programming, you are often faced with situations where you need to execute a particular action, but you don’t know in advance which method, or even which object, you’ll want to call upon to execute it. For example, a button might know that it must notify some object when it is pushed, but it might not know which object or objects need to be notified. Rather than wiring the button to a particular object, you will connect the button to a delegate and then resolve that delegate to a particular method when the program executes.
In the early, dark, and primitive days of computing, a program would begin execution and then proceed through its steps until it completed. If the user was involved, the interaction was strictly controlled and limited to filling in fields.
Today’s Graphical User Interface (GUI) programming model requires a different approach, known as event-driven programming. A modern program presents the user interface and waits for the user to take an action. The user might take many different actions, such as choosing among menu selections, pushing buttons, updating text fields, clicking icons, and so forth. Each action causes an event to be raised. Other events can be raised without direct user action, such as events that correspond to timer ticks of the internal clock, email being received, file-copy operations completing, and so forth.
An event is the encapsulation of the idea that “something happened” to which the program must respond. Events and delegates are tightly coupled concepts because flexible event handling requires that the response to the event be dispatched to the appropriate event handler. An event handler is typically implemented in C# via a delegate .
Delegates are also used as callbacks so that one class can say to another “do this work and when you’re done, let me know.”
In C#, delegates are first-class objects, fully supported by the language. Technically, a delegate is a reference type used to encapsulate a method with a specific signature and return type. You can encapsulate any matching method in that delegate.
A delegate is created with the delegate
keyword, followed by a return type
and the signature of the methods that can be delegated to it, as in the
following:
public delegate int WhichIsFirst(object obj1, object obj2);
This declaration defines a delegate named WhichIsFirst
, which will encapsulate any
method that takes two objects as parameters and that returns an int
.
Once the delegate is defined, you can encapsulate a member method with that delegate by instantiating the delegate, passing in a method that matches the return type and signature. As an alternative, you can use anonymous methods as described below. In either case, the delegate can then be used to invoke that encapsulated method.
Delegates decouple the class that
declares the delegate from the class that uses the delegate. For
example, suppose that you want to create a simple generic container
class called a Pair
that can hold
and sort any two objects passed to it. You can’t know in advance what
kind of objects a Pair
will hold,
but by creating methods within those objects to which the sorting task
can be delegated, you can delegate responsibility for determining
their order to the objects themselves.
Different objects will sort differently (for example, a Pair
of Counter
objects might sort in numeric order,
while a Pair
of Buttons
might sort alphabetically by their
name). As the author of the Pair
class, you want the objects in the pair to have the responsibility of
knowing which should be first and which should be second. To
accomplish this, you will insist that the objects to be stored in the
Pair
must provide a method that
tells you how to sort the objects.
You can define this requirement with interfaces, as well.
Delegates are smaller and of finer granularity than interfaces. The
Pair
class does not need to
implement an entire interface; it just needs to define the signature
and return type of the method it wants to invoke. That is what
delegates are for: they define the return type and signature of
methods that can be invoked through the interface.
In this case, the Pair
class
will declare a delegate named WhichIsFirst
. When the Pair
needs to know how to order its objects,
it will invoke the delegate passing in its two member objects as
parameters. The responsibility for deciding which of the two objects
comes first is delegated to the method encapsulated by the
delegate:
public delegateComparison WhichIsFirst<T>
( T obj1, T obj2 );
In this definition, WhichIsFirst
is defined to encapsulate a
method that takes two objects as parameters, and that returns an
object of type Comparison
. Comparison
turns out to be an enumeration
you will define:
public enum Comparison { TheFirstComesFirst = 1, TheSecondComesFirst = 2 }
To test the delegate, you will create two classes: a Dog
class and a Student
class. Dogs
and Students
have little in common, except that
they both implement methods that can be encapsulated by WhichComesFirst
, and thus both Dog
objects and Student
objects are eligible to be held
within Pair
objects.
In the test program, you will create a couple of Students
and a couple of Dogs
, and store them each in a Pair
. You will then create instances of
WhichIsFirst
to encapsulate their
respective methods that will determine which Student
or which Dog
object should be first, and which
second. Let’s take this step by step.
You begin by creating a Pair
constructor that takes two objects and stashes them away in a private
array:
public Pair( T firstObject, T secondObject ) { thePair[0] = firstObject; thePair[1] = secondObject; }
Note that the Pair<T>
class uses generics, as discussed in Chapter 14. Therefore, firstObject
and secondObject
in the constructor above are of
the generic type T, and the actual types will be assigned at
runtime.
Next, you override ToString( )
to obtain the string value of the two objects:
public override string ToString( ) { return thePair[0].ToString( ) + ", " + thePair[1].ToString( ); }
You now have two objects in your Pair
and you can print out their values.
You’re ready to sort them and print the results of the sort. You can’t
know in advance what kind of objects you will have, so you delegate
the responsibility of deciding which object comes first in the sorted
Pair
to the objects
themselves.
Both the Dog
class and the
Student
class implement methods
that can be encapsulated by WhichIsFirst
. Any method that takes two
objects and returns a Comparison
can be encapsulated by this delegate at runtime.
You can now define the Sort( )
method for the Pair
class:
public void Sort(WhichIsFirst theDelegatedFunc) { if (theDelegatedFunc(thePair[0],thePair[1]) == Comparison.theSecondComesFirst) { T temp = thePair[0]; thePair[0] = thePair[1]; thePair[1] = temp; } }
This method takes a parameter: a delegate of type WhichIsFirst
named theDelega-tedFunc
. The Sort( )
method delegates responsibility for deciding which of the two objects in
the Pair
comes first to the method
encapsulated by that delegate. In the body of the Sort( )
method, it invokes the delegated
method and examines the return value, which will be one of the two
enumerated values of Comparison
.
If the value returned is theSecondComesFirst
, the objects within the
pair are swapped; otherwise, no action is taken.
This is analogous to how the other parameters work. If you had a
method that took an int
as a
parameter:
int SomeMethod (int myParam){//...}
The parameter name is myParam
, but you can pass in any int
value or variable. Similarly, the
parameter name in the delegate example is theDelegatedFunc
, but you can pass in any
method that meets the return value and signature defined by the
delegate WhichIsFirst
.
Imagine you are sorting Students
by name. You write a method that
returns theFirstComesFirst
if the
first student’s name comes first, and theSecondComesFirst
if the second student’s
name does. If you pass in “Amy, Beth,” the method will return theFirstComesFirst
, and if you pass in
“Beth, Amy,” it will return theSecondComesFirst
. If you get back
theSecondComesFirst
, the Sort( )
method reverses the items in its
array, setting Amy to the first position and Beth to the
second.
Now add one more method, ReverseSort( )
, which will force the items in the array into the reverse
of their normal order:
public void ReverseSort(WhichIsFirst theDelegatedFunc) { if (theDelegatedFunc(thePair[0], thePair[1]) == Comparison.theFirstComesFirst) { T temp = thePair[0]; thePair[0] = thePair[1]; thePair[1] = temp; } }
The logic here is identical to Sort( )
, except that this method performs the swap if the
delegated method says that the first item comes first. Because the
delegated function thinks the first item comes first, and this is a
reverse sort, the result you want is for the second item to come
first. This time, if you pass in “Amy, Beth,” the delegated function
returns theFirstComesFirst
(Amy
should come first), but because this is a reverse
sort, it swaps the values, setting Beth first. This allows you to use
the same delegated function as you used with Sort( )
, without forcing the object to
support a function that returns the reverse sorted value.
Now all you need are some objects to sort. You’ll create two
absurdly simple classes: Student
and Dog
. Assign Student
objects a name at creation:
public class Student { public Student(string name) { this.name = name; }
The Student
class requires
two methods: one to override ToString( )
and the other to be encapsulated as the delegated
method.
Student
must override
ToString( )
so that the ToString( )
method in Pair
, which invokes ToString( )
on the contained objects, will
work properly; the implementation does nothing more than return the
student’s name (which is already a string object):
public override string ToString( ) { return name; }
It must also implement a method to which Pair.Sort( )
can delegate the responsibility
of determining which of two objects comes first, called WhichStudentComesFirst( )
in this
case:
public static Comparison WhichStudentComesFirst( Student s1, Student s2 ) { return (String.Compare(s1.name, s2.name) < 0 ? Comparison.theFirstComesFirst : Comparison.theSecondComesFirst); }
String.Compare( )
is a .NET
Framework method on the String
class that compares two strings and returns less than zero if the
first is smaller, greater than zero if the second is smaller, and zero
if they are the same. This method is discussed in some detail in Chapter 15. Notice that the logic
here returns theFirstComesFirst
only if the first string is smaller; if they are the same or the
second is larger, this method returns theSecondComesFirst
.
Notice that the WhichStudentComesFirst( )
method takes two objects as parameters and returns a
Comparison
. This qualifies it to be
a Pair.WhichIsFirst
delegated
method, whose signature and return value it matches.
The second class is Dog
. For
our purposes, Dog
objects will be
sorted by weight, lighter dogs before heavier. Here’s the complete
declaration of Dog
:
public class Dog { public Dog(int weight) { this.weight=weight; } // dogs are ordered by weight public static Comparison WhichDogComesFirst( Object o1, Object o2) { Dog d1 = (Dog) o1; Dog d2 = (Dog) o2; return d1.weight > d2.weight ? Comparison.theSecondComesFirst : Comparison.theFirstComesFirst; } public override string ToString( ) { return weight.ToString( ); } private int weight; }
The Dog
class also overrides
ToString
and implements a static
method called Which-DogComesFirst( )
with the correct signature for the delegate. Notice also
that the Dog
and Student
delegate methods do not have the
same name. They do not need to have the same name, as they will be
assigned to the delegate dynamically at runtime.
You can call your delegated method names anything you like,
but creating parallel names (such as WhichDogComesFirst
and WhichStudent-ComesFirst
) makes the code
easier to read, understand, and maintain.
Example 17-1 is the complete program, which illustrates how the delegate methods are invoked.
Example 17-1. Working with delegates
using System; using System.Collections.Generic; using System.Text; namespace WorkingWithDelegates { public enum Comparison { TheFirstComesFirst = 1, TheSecondComesFirst = 2 } // a simple collection to hold 2 items public class Pair<T> { // private array to hold the two objects private T[] thePair = new T[2]; // the delegate declaration public delegate Comparison WhichIsFirst( T obj1, T obj2 ); // passed in constructor take two objects, // added in order received public Pair( T firstObject, T secondObject ) { thePair[0] = firstObject; thePair[1] = secondObject; } // public method which orders the two objects // by whatever criteria the object likes! public void Sort( WhichIsFirst theDelegatedFunc ) { if ( theDelegatedFunc( thePair[0], thePair[1] ) == Comparison.TheSecondComesFirst ) { T temp = thePair[0]; thePair[0] = thePair[1]; thePair[1] = temp; } } // public method which orders the two objects // by the reverse of whatever criteria the object likes! public void ReverseSort( WhichIsFirst theDelegatedFunc ) { if ( theDelegatedFunc( thePair[0], thePair[1] ) == Comparison.TheFirstComesFirst ) { T temp = thePair[0]; thePair[0] = thePair[1]; thePair[1] = temp; } } // ask the two objects to give their string value public override string ToString( ) { return thePair[0].ToString( ) + ", " + thePair[1].ToString( ); } } // end class Pair public class Dog { private int weight; public Dog( int weight ) { this.weight = weight; } // dogs are ordered by weight public static Comparison WhichDogComesFirst( Dog d1, Dog d2 ) { return d1.weight > d2.weight ? Comparison.TheFirstComesFirst : Comparison.TheSecondComesFirst ); } public override string ToString( ) { return weight.ToString( ); } } // end class Dog public class Student { private string name; public Student( string name ) { this.name = name; } // students are ordered alphabetically public static Comparison WhichStudentComesFirst( Student s1, Student s2 ) { return ( String.Compare( s1.name, s2.name ) < 0 ? Comparison.theFirstComesFirst : Comparison.theSecondComesFirst ); } public override string ToString( ) { return name; } } // end class Student public class Test { public static void Main( ) { // create two students and two dogs // and add them to Pair objects Student Jesse = new Student( "Jesse" ); Student Stacey = new Student( "Stacey" ); Dog Milo = new Dog( 65 ); Dog Fred = new Dog( 12 ); Pair<Student> studentPair = new Pair<Student>( Jesse, Stacey ); Pair<Dog> dogPair = new Pair<Dog>( Milo, Fred ); Console.WriteLine( "studentPair : {0}", studentPair.ToString( ) ); Console.WriteLine( "dogPair : {0}", dogPair.ToString( ) ); // Instantiate the delegates Pair<Student>.WhichIsFirst theStudentDelegate = new Pair<Student>.WhichIsFirst( Student.WhichStudentComesFirst ); Pair<Dog>.WhichIsFirst theDogDelegate = new Pair<Dog>.WhichIsFirst( Dog.WhichDogComesFirst ); // sort using the delegates studentPair.Sort( theStudentDelegate ); Console.WriteLine( "After Sort studentPair : {0}", studentPair.ToString( ) ); studentPair.ReverseSort( theStudentDelegate ); Console.WriteLine( "After ReverseSort studentPair : {0}", studentPair.ToString( ) ); dogPair.Sort( theDogDelegate ); Console.WriteLine( "After Sort dogPair : {0}", dogPair.ToString( ) ); dogPair.ReverseSort( theDogDelegate ); Console.WriteLine( "After ReverseSort dogPair : {0}", dogPair.ToString( ) ); } } }
The output looks like this:
studentPair : Jesse, Stacey dogPair : 65, 12 After Sort studentPair : Jesse, Stacey After ReverseSort studentPair : Stacey, Jesse After Sort dogPair : 12, 65 After ReverseSort dogPair : 65, 12
The Test
program creates two
Student
objects and two Dog
objects and then adds them to Pair
containers. The student
constructor takes a string
for the student’s name and the
dog
constructor takes an int
for the dog’s weight.
Student Jesse = new Student( "Jesse" ); Student Stacey = new Student( "Stacey" ); Dog Milo = new Dog( 65 ); Dog Fred = new Dog( 12 ); Pair<Student> studentPair = new Pair<Student>( Jesse, Stacey ); Pair<Dog> dogPair = new Pair<Dog>( Milo, Fred ); Console.WriteLine( "studentPair : {0}", studentPair.ToString( ) ); Console.WriteLine( "dogPair : {0}", dogPair.ToString( ) );
It then prints the contents of the two Pair
containers to see the order of the
objects. The output looks like this:
studentPair : Jesse, Stacey dogPair : 65, 12
As expected, the objects are in the order in which they were
added to the Pair
containers. You
next instantiate two delegate objects:
Pair<Student>.WhichIsFirst theStudentDelegate = new Pair<Student>.WhichIsFirst( Student.WhichStudentComesFirst ); Pair<Dog>.WhichIsFirst theDogDelegate = new Pair<Dog>.WhichIsFirst( Dog.WhichDogComesFirst );
The first delegate, theStudentDelegate
, is created by passing in
the appropriate static method from the Student
class. The second delegate, theDogDelegate
, is passed a static method
from the Dog
class.
The delegates are now objects that can be passed to methods. You
pass the delegates first to the Sort( )
method of the Pair
object, and then to the ReverseSort( )
method. The results are printed to the console:
After Sort studentPair : Jesse, Stacey After ReverseSort studentPair : Stacey, Jesse After Sort dogPair : 12, 65 After ReverseSort dogPair : 65, 12