19.1 DelegatesIn C#, a delegate is a reference type that encapsulates methods with a specified parameter list and return type. A delegate is created with the delegate keyword, followed by a return type and the parameter list 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 that encapsulates 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 and passing in as a parameter the name of a method that matches the return type and parameter list. 19.1.1 Using Delegates to Specify Methods at RuntimeSuppose, for example, that you want to create a simple collection class called 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. What a nightmare this could be for the creator of the Pair class. The class must know how each type of object sorts. If you add a Button, the Pair class must know to ask the Buttons for their names and sort them alphabetically. If you then add a Pair of Employees, the Pair class must know to ask the Employees for their date of hire, and sort by date. There must be a better way! The answer is to delegate this responsibility to the objects themselves. If the Button objects know which Button comes first and the Employee objects know which Employee comes first, then the Pair class can become much more flexible. This is the essence of good object-oriented design: delegate responsibility to the class that is in the best position to have the knowledge required for the task at hand. Delegating the responsibility for knowing how the objects are sorted to the objects themselves decouples the Pair class from the types contained in the Pair. The Pair no longer needs to know how the objects are sorted; it just needs to know that they can be sorted. Of course, the objects you put in the Pair container must know how to tell the Pair which object comes first. The Pair container must specify the method these objects need to implement. Rather than specifying a particular method, however, the Pair will just specify the signature and return type of the method. That is, the Pair will say, in essence, "I can hold any type of object that offers a method that takes two objects and returns an int signifying which comes first." Define the method you require by creating a delegate that specifies the parameter list and return type of the method the object (e.g., Button) must provide to allow the Pair to determine which object should be first and which should be second. The Pair class defines a delegate, WhichIsFirst. The Sort() method takes as a parameter an instance of the WhichIsFirst delegate. When the Pair needs to know how to order its objects, it invokes the delegate, passing in its two objects as parameters. The responsibility for deciding which of the two objects comes first is delegated to the method encapsulated by the delegate. To test the delegate, 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 Example 19-1, you will create a couple of Students and a couple of Dogs and store them in Pairs. You will then create delegate objects to encapsulate their respective methods that match the delegate signature and return type, and you'll ask the Pair objects to sort the Dog and Student objects. Example 19-1 shows a complete program illustrating the use of delegates. This is a long and somewhat complicated program that will be analyzed in detail following the output. Example 19-1. Delegatesusing System; namespace DelegatesAndEvents { public enum comparison { theFirstComesFirst = 1, theSecondComesFirst = 2 } // A simple collection to hold two items public class Pair { // Private array to hold the two objects private object[] thePair = new object[2]; // The delegate declaration public delegate comparison WhichIsFirst(object obj1, object obj2); // Passed in constructor takes two objects, // added in order received public Pair( object firstObject, object secondObject) { thePair[0] = firstObject; thePair[1] = secondObject; } // Public method that orders the // two objects by whatever criteria the objects like! public void Sort( WhichIsFirst theDelegatedFunc) { if (theDelegatedFunc(thePair[0],thePair[1]) == comparison.theSecondComesFirst) { object temp = thePair[0]; thePair[0] = thePair[1]; thePair[1] = temp; } } // Public method that orders the // two objects by the reverse of whatever criteria the // objects likes! public void ReverseSort( WhichIsFirst theDelegatedFunc) { if (theDelegatedFunc(thePair[0],thePair[1]) == comparison.theFirstComesFirst) { object 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(); } } public class Dog { private int weight; public Dog(int weight) { this.weight=weight; } // dogs are sorted 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(); } } public class Student { private string name; public Student(string name) { this.name = name; } // Students are sorted alphabetically public static comparison WhichStudentComesFirst(Object o1, Object o2) { Student s1 = (Student) o1; Student s2 = (Student) o2; return (String.Compare(s1.name, s2.name) < 0 ? comparison.theFirstComesFirst : comparison.theSecondComesFirst); } public override string ToString() { return name; } } class Tester { public void Run() { // 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 studentPair = new Pair(Jesse,Stacey); Pair dogPair = new Pair(Milo, Fred); Console.WriteLine("studentPair\t\t\t: {0}", studentPair.ToString()); Console.WriteLine("dogPair\t\t\t\t: {0}", dogPair.ToString()); // Instantiate the delegates Pair.WhichIsFirst theStudentDelegate = new Pair.WhichIsFirst( Student.WhichStudentComesFirst); Pair.WhichIsFirst theDogDelegate = new Pair.WhichIsFirst( Dog.WhichDogComesFirst); // Sort using the delegates studentPair.Sort(theStudentDelegate); Console.WriteLine("After Sort studentPair\t\t: {0}", studentPair.ToString()); studentPair.ReverseSort(theStudentDelegate); Console.WriteLine("After ReverseSort studentPair\t: {0}", studentPair.ToString()); dogPair.Sort(theDogDelegate); Console.WriteLine("After Sort dogPair\t\t: {0}", dogPair.ToString()); dogPair.ReverseSort(theDogDelegate); Console.WriteLine("After ReverseSort dogPair\t: {0}", dogPair.ToString()); } static void Main() { Tester t = new Tester(); t.Run(); } } } Output: 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 Let's consider Example 19-1, step by step. You begin by creating a Pair constructor that takes two objects and stashes them away in a private array (thePair): public class Pair { private object[] thePair = new object[2]; public Pair(object firstObject, object secondObject) { thePair[0] = firstObject; thePair[1] = secondObject; } 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 display their values. You're ready to sort them and display the results of the sort. Create the delegate WhichIsFirst that defines the signature for the sorting method. public delegate comparison WhichIsFirst(object obj1, object obj2); This delegate definition can be a bit confusing. Figure 19-1 illustrates the various parts of the definition. Figure 19-1. A delegate definitionThe return value is of type comparison, which is the enumeration defined earlier in the file: public enum comparison { theFirstComesFirst = 1, theSecondComesFirst = 2 } Any method that takes two objects and returns a comparison can be encapsulated by this delegate at runtime. Now define the Sort() method for the Pair class: public void Sort(WhichIsFirst theDelegatedFunc) { if (theDelegatedFunc(thePair[0],thePair[1]) == comparison.theSecondComesFirst) { object temp = thePair[0]; thePair[0] = thePair[1]; thePair[1] = temp; } } This method takes a parameter: a delegate of type WhichIsFirst named theDelegatedFunc. 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 is 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. Notice that theDelegatedFunc is the name of the parameter to represent the method encapsulated by the delegate. You can assign any method (with the appropriate return value and signature) to this parameter. It is as 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 returns theFirstComesFirst, and if you pass in "Beth, Amy", it returns 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 puts the items into the array in reverse order: public void ReverseSort(WhichIsFirst theDelegatedFunc) { if (theDelegatedFunc(thePair[0], thePair[1]) == comparison.theFirstComesFirst) { object temp = thePair[0]; thePair[0] = thePair[1]; thePair[1] = temp; } } The logic of the ReverseSort() method is the same as that of the Sort() method, except that ReverseSort() performs the swap of the items if the delegated method says that the first item comes first. In Example 19-1, 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 (i.e., 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 that 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. 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, works 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: return (String.Compare(s1.name, s2.name) < 0 ? comparison.theFirstComesFirst : comparison.theSecondComesFirst); As you saw in Chapter 17, 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. Notice also 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, with lighter dogs before heavier. Here's the complete declaration of Dog: public class Dog { private int weight; 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(); } } Notice that the Dog class also overrides ToString() and implements a static method 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 because they will be assigned to the delegate dynamically at runtime. You can call your delegated method names anything you like, but creating parallel names (e.g., WhichDogComesFirst and WhichStudentComesFirst) makes the code easier to understand and maintain. The Run() method 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. public void Run() { Student Jesse = new Student("Jesse"); Student Stacey = new Student ("Stacey"); Dog Milo = new Dog(65); Dog Fred = new Dog(12); Pair studentPair = new Pair(Jesse,Stacey); Pair dogPair = new Pair(Milo, Fred); Console.WriteLine("studentPair\t\t\t: {0}", studentPair.ToString()); Console.WriteLine("dogPair\t\t\t\t: {0}", dogPair.ToString()); Display 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. Next, instantiate two delegate objects: Pair.WhichIsFirst theStudentDelegate = new Pair.WhichIsFirst( Student.WhichStudentComesFirst); Pair.WhichIsFirst theDogDelegate = new Pair.WhichIsFirst( Dog.WhichDogComesFirst); The first delegate, theStudentDelegate, is created by passing in the appropriate method from the Student class. The second delegate, theDogDelegate, is passed a method from the Dog class. The delegates are now objects that can be passed to methods. Pass the delegates first to the Sort() method and then to the ReverseSort() method of the Pair object: studentPair.Sort(theStudentDelegate); studentPair.ReverseSort(theStudentDelegate); dogPair.Sort(theDogDelegate); dogPair.ReverseSort(theDogDelegate); The results are displayed on the console: After Sort studentPair : Jesse, Stacey After ReverseSort studentPair : Stacey, Jesse After Sort dogPair : 12, 65 After ReverseSort dogPair : 65, 12 19.1.2 Static Methods Versus Instance MethodsIn Example 19-1, the methods you encapsulated with the delegates were static methods of the Student and Dog classes: public static comparison WhichStudentComesFirst(Object o1, Object o2) You could declare the WhichStudentComesFirst() method to be an instance method instead: public comparison WhichStudentComesFirst(Object o1, Object o2) You can still encapsulate it as a delegate, but you must refer to it through an instance, rather than through the class: Pair.WhichIsFirst theStudentDelegate = new Pair.WhichIsFirst( Jesse.WhichStudentComesFirst); You decide whether to use instance or static methods. The advantage of static methods is that you don't need an instance of the class to create the delegate, but instance methods can be slightly more efficient when you are calling them many times. 19.1.3 Static DelegatesA disadvantage of Example 19-1 is that it forces the calling class, in this case Tester, to instantiate the delegates it needs in order to sort the objects in a Pair. Notice that in Example 19-1, within the Run() method of Tester, you see this code: Pair.WhichIsFirst theStudentDelegate = new Pair.WhichIsFirst( Student.WhichStudentComesFirst); What is going on here is that the Tester class needs to know that it must instantiate an instance of the WhichIsFirst delegate (declared in Pair) and that it must pass in the WhichStudentComesFirst method of the Student class. Once it has created this delegate, it can invoke the sort by passing in the delegate it just created: studentPair.Sort(theStudentDelegate); Tester then goes on to instantiate a second delegate, passing in the WhichDogComesFirst() method to create the delegate for the Dog objects and invoking Sort() with that delegate as well: Pair.WhichIsFirst theDogDelegate = new Pair.WhichIsFirst( Dog.WhichDogComesFirst); dogPair.Sort(theDogDelegate); Rather than forcing Tester to know which method Student and Dog must use to accomplish the sort, it would be better to get the delegate from the Student or Dog classes themselves. You can give the implementing classes (Student and Dog) the responsibility for instantiating the delegate by giving each implementing class its own static delegate. In that case, rather than knowing which method implements the sort for the Student, Tester would only need to know that the Student class has a static delegate named, for example, OrderStudents, and the author of the Tester class could then write: studentPair.Sort(Student.OrderStudents); Thus, you can modify Student to add this definition, which creates a static, read-only delegate named OrderStudents: public static readonly Pair.WhichIsFirst OrderStudents = new Pair.WhichIsFirst(Student.WhichStudentComesFirst);
You can create a similar delegate within Dog: public static readonly Pair.WhichIsFirst OrderDogs = new Pair.WhichIsFirst(Dog. WhichDogComesFirst); These are now static fields of their respective classes. Each is pre-wired to the appropriate method within the class. You can invoke delegates without declaring a local delegate instance. You just pass in the static delegate of the class: studentPair.Sort(Student.OrderStudents); studentPair.ReverseSort(Student.OrderStudents); Example 19-2 shows the complete listing. Note that after these changes, the output is identical to that of the previous example. Example 19-2. Static delegate membersusing System; namespace DelegatesAndEvents { public enum comparison { theFirstComesFirst = 1, theSecondComesFirst = 2 } // A simple collection to hold two items. public class Pair { // Private array to hold the two objects. private object[] thePair = new object[2]; // The delegate declaration. public delegate comparison WhichIsFirst(object obj1, object obj2); // Passed in constructor takes two objects, // added in order received. public Pair( object firstObject, object secondObject) { thePair[0] = firstObject; thePair[1] = secondObject; } // Public method that orders // the two objects by whatever criteria the objects like! public void Sort( WhichIsFirst theDelegatedFunc) { if (theDelegatedFunc(thePair[0],thePair[1]) == comparison.theSecondComesFirst) { object temp = thePair[0]; thePair[0] = thePair[1]; thePair[1] = temp; } } // Public method that orders // the two objects by the reverse of whatever criteria // the objects like! public void ReverseSort( WhichIsFirst theDelegatedFunc) { if (theDelegatedFunc(thePair[0],thePair[1]) == comparison.theFirstComesFirst) { object 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(); } } public class Dog { private int weight; // A static delegate. public static readonly Pair.WhichIsFirst OrderDogs = new Pair.WhichIsFirst(Dog. WhichDogComesFirst); public Dog(int weight) { this.weight=weight; } // Dogs are sorted 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(); } } public class Student { private string name; // A static delegate. public static readonly Pair.WhichIsFirst OrderStudents = new Pair.WhichIsFirst(Student.WhichStudentComesFirst); public Student(string name) { this.name = name; } // Students are sorted alphabetically. public static comparison WhichStudentComesFirst(Object o1, Object o2) { Student s1 = (Student) o1; Student s2 = (Student) o2; return (String.Compare(s1.name, s2.name) < 0 ? comparison.theFirstComesFirst : comparison.theSecondComesFirst); } public override string ToString() { return name; } } class Tester { public void Run() { // 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); // Create the Pair object. Pair studentPair = new Pair(Jesse,Stacey); Pair dogPair = new Pair(Milo, Fred); Console.WriteLine("studentPair\t\t\t: {0}", studentPair.ToString()); Console.WriteLine("dogPair\t\t\t\t: {0}", dogPair.ToString()); // Tell the student Pair to sort itself, // passing in the Student delegate. studentPair.Sort(Student.OrderStudents); Console.WriteLine("After Sort studentPair\t\t: {0}", studentPair.ToString()); studentPair.ReverseSort(Student.OrderStudents); Console.WriteLine("After ReverseSort studentPair\t: {0}", studentPair.ToString()); // Tell the Dog pair to sort itself, // passing in the Dog delegate. dogPair.Sort(Dog.OrderDogs); Console.WriteLine("After Sort dogPair\t\t: {0}", dogPair.ToString()); dogPair.ReverseSort(Dog.OrderDogs); Console.WriteLine("After ReverseSort dogPair\t: {0}", dogPair.ToString()); } [STAThread] static void Main() { Tester t = new Tester(); t.Run(); } } } 19.1.4 Delegates as PropertiesThe problem with static delegates is that they must be instantiated, whether or not they are ever used, as with Student and Dog in Example 19-2. You can improve these classes by changing the static delegate fields to properties. For Student, take out the declaration: public static readonly Pair.WhichIsFirst OrderStudents = new Pair.WhichIsFirst(Student.WhichStudentComesFirst); and replace it with: public static Pair.WhichIsFirst OrderStudents { get { return new Pair.WhichIsFirst(WhichStudentComesFirst); } } Similarly, replace the Dog static field with: public static Pair.WhichIsFirst OrderDogs { get { return new Pair.WhichIsFirst(WhichDogComesFirst); } } The assignment of the delegates is unchanged: studentPair.Sort(Student.OrderStudents); dogPair.Sort(Dog.OrderDogs); When the OrderStudent property is accessed, the delegate is created: return new Pair.WhichIsFirst(WhichStudentComesFirst); The key advantage is that the delegate is not created until it is requested. This allows the Tester class to determine when it needs a delegate but still allows the details of the creation of the delegate to be the responsibility of the Student (or Dog) class. |