[ Team LiB ] |
12.1 DelegatesIn 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. The delegate can then be used to invoke that encapsulated method. 12.1.1 Using Delegates to Specify Methods at RuntimeDelegates uncouple the class that declares the delegate from the class that uses the delegate. For example, suppose that you want to create a simple 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 delegate Comparison WhichIsFirst(object obj1, object 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 class Pair { // hold both objects private object[]thePair = new object[2]; // two objects, added in order received public Pair(object firstObject, object secondObject) { thePair[0] = firstObject; thePair[1] = secondObject; } 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) { 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 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) { object 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 (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 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: 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 10. 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 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.
Example 12-1 is the complete program, which illustrates how the delegate methods are invoked. Example 12-1. -1. Working with delegatesusing System; namespace Programming_CSharp { public enum Comparison { theFirstComesFirst = 1, theSecondComesFirst = 2 } // a simple collection to hold 2 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 take two objects, // added in order received public Pair( object firstObject, object 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) { object 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) { 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( ); } } // 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( 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( ); } } // end class Dog public class Student { private string name; public Student(string name) { this.name = name; } // students are ordered 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; } } // 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 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( )); } } } 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 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 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( )); 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. We 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 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 12.1.2 Delegates and Instance MethodsIn Example 12-1, the delegates encapsulate static methods, as in the following: public static Comparison WhichStudentComesFirst(Object o1, Object o2) The delegate is then instantiated using the class rather than an instance: Pair.WhichIsFirst theStudentDelegate = new Pair.WhichIsFirst( Student.WhichStudentComesFirst); You can just as easily encapsulate instance methods: public Comparison WhichStudentComesFirst(Object o1, Object o2) in which case you will instantiate the delegate by passing in the instance method as invoked through an instance of the class, rather than through the class itself: Pair.WhichIsFirst theStudentDelegate = new Pair.WhichIsFirst( Jesse.WhichStudentComesFirst); 12.1.3 Static DelegatesOne disadvantage of Example 12-1 is that it forces the calling class (in this case Test) to instantiate the delegates it needs in order to sort the objects in a Pair. It would be nice to get the delegate from the Student or Dog class itself. You can do this by giving each class its own static delegate. Thus, you can modify Student to add this: public static readonly Pair.WhichIsFirst OrderStudents = new Pair.WhichIsFirst(Student.WhichStudentComesFirst); This creates a static, read-only delegate named OrderStudents.
You can create a similar delegate within the Dog class: public static readonly Pair.WhichIsFirst OrderDogs = new Pair.WhichIsFirst(Dog.WhichDogComesFirst); These are now static fields of their respective classes. Each is prewired 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); Console.WriteLine("After Sort studentPair\t\t: {0}", studentPair.ToString( )); studentPair.ReverseSort(Student.OrderStudents); Console.WriteLine("After ReverseSort studentPair\t: {0}", studentPair.ToString( )); 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( )); The output after these changes is identical to Example 12-1. 12.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 12-1. You can improve these classes by changing the static delegate fields to properties. For Student, you 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, you replace the static Dog 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 test 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. 12.1.5 Setting Order of Execution with Delegate ArraysDelegates can help you create a system in which the user can dynamically decide the order in which operations are performed. For instance, suppose you have an image processing system in which an image can be manipulated in a number of well-defined ways, such as blurring, sharpening, rotating, filtering, and so forth. Assume, as well, that the order in which these effects are applied to the image is important. The user wishes to choose from a menu of effects, applying all that he likes, and then telling the image processor to run the effects, one after the other in the order that he has specified. You can create delegates for each operation and add them to an ordered collection, such as an array, in the order you'd like them to execute. Once all the delegates are created and added to the collection, you simply iterate over the array, invoking each delegated method in turn. You begin by creating a class, Image, to represent the image that will be processed by the ImageProcessor: public class Image { public Image( ) { Console.WriteLine("An image created"); } } You can imagine that this stands in for a .gif or .jpeg file or other image. The ImageProcessor then declares a delegate. You can of course define your delegate to return any type and take any parameters you like. For this example, you'll define the delegate to encapsulate any method that returns void and takes no arguments: public delegate void DoEffect( ); The ImageProcessor then declares a number of methods, each of which processes an image and each of which matches the return type and signature of the delegate: public static void Blur( ) { Console.WriteLine("Blurring image"); } public static void Filter( ) { Console.WriteLine("Filtering image"); } public static void Sharpen( ) { Console.WriteLine("Sharpening image"); } public static void Rotate( ) { Console.WriteLine("Rotating image"); }
The ImageProcessor class needs an array to hold the delegates that the user picks, a variable to hold the running total of how many effects are in the array, and of course a variable for the image itself: DoEffect[] arrayOfEffects; Image image; int numEffectsRegistered = 0; The ImageProcessor also needs a method to add delegates to the array: public void AddToEffects(DoEffect theEffect) { if (numEffectsRegistered >= 10) { throw new Exception("Too many members in array"); } arrayOfEffects[numEffectsRegistered++] = theEffect; } It needs another method to actually call each method in turn: public void ProcessImages( ) { for (int i = 0;i < numEffectsRegistered;i++) { arrayOfEffects[i]( ); } } Finally, you need to declare the static delegates that the client can call, hooking them to the processing methods: public DoEffect BlurEffect = new DoEffect(Blur); public DoEffect SharpenEffect = new DoEffect(Sharpen); public DoEffect FilterEffect = new DoEffect(Filter); public DoEffect RotateEffect = new DoEffect(Rotate);
The client code would normally have an interactive user-interface component, but we'll simulate that by choosing the effects, adding them to the array, and then calling ProcessImage, as shown in Example 12-2. Example 12-2. Using an array of delegatesnamespace Programming_CSharp { using System; // the image which we'll manipulate public class Image { public Image( ) { Console.WriteLine("An image created"); } } public class ImageProcessor { // declare the delegate public delegate void DoEffect( ); // create various static delegates tied to member methods public DoEffect BlurEffect = new DoEffect(Blur); public DoEffect SharpenEffect = new DoEffect(Sharpen); public DoEffect FilterEffect = new DoEffect(Filter); public DoEffect RotateEffect = new DoEffect(Rotate); // the constructor initializes the image and the array public ImageProcessor(Image image) { this.image = image; arrayOfEffects = new DoEffect[10]; } // in a production environment we'd use a more // flexible collection. public void AddToEffects(DoEffect theEffect) { if (numEffectsRegistered >= 10) { throw new Exception( "Too many members in array"); } arrayOfEffects[numEffectsRegistered++] = theEffect; } // the image processing methods... public static void Blur( ) { Console.WriteLine("Blurring image"); } public static void Filter( ) { Console.WriteLine("Filtering image"); } public static void Sharpen( ) { Console.WriteLine("Sharpening image"); } public static void Rotate( ) { Console.WriteLine("Rotating image"); } public void ProcessImages( ) { for (int i = 0;i < numEffectsRegistered;i++) { arrayOfEffects[i]( ); } } // private member variables... private DoEffect[] arrayOfEffects; private Image image; private int numEffectsRegistered = 0; } // test driver public class Test { public static void Main( ) { Image theImage = new Image( ); // no ui to keep things simple, just pick the // methods to invoke, add them in the required // order, and then call on the image processor to // run them in the order added. ImageProcessor theProc = new ImageProcessor(theImage); theProc.AddToEffects(theProc.BlurEffect); theProc.AddToEffects(theProc.FilterEffect); theProc.AddToEffects(theProc.RotateEffect); theProc.AddToEffects(theProc.SharpenEffect); theProc.ProcessImages( ); } } } Output: An image created Blurring image Filtering image Rotating image Sharpening image In the Test class of Example 12-2, the ImageProcessor is instantiated, and effects are added. If the user chooses to blur the image before filtering the image, it is a simple matter to add the delegates to the array in the appropriate order. Similarly, any given operation can be repeated as often as the user desires, just by adding more delegates to the collection. You can imagine displaying the order of operations in a listbox that might allow the user to reorder the methods, moving them up and down the list at will. As the operations are reordered, you need only change their sort order in the collection. You might even decide to capture the order of operations to a database and then load them dynamically, instantiating delegates as dictated by the records you've stored in the database. Delegates provide the flexibility to determine dynamically which methods will be called, in what order, and how often. |
[ Team LiB ] |