Delegates

 
Chapter 4 - Advanced C# Topics
bySimon Robinsonet al.
Wrox Press 2002
  

Delegates can best be seen as a new type of object in C#, which has some similarities to classes. They exist for situations in which you want to pass methods around to other methods . To see what we mean by that, consider this line of code:

   int i = int.Parse("99");   

We are so used to passing data to methods as parameters, as above, that we don't consciously think about it, and for this reason the idea of passing methods around instead of data might sound a little strange . However, there are cases in which you have a method that does something, and rather than operating on data, the method might need to do something that involves invoking another method. To complicate things further, you do not know at compile-time what this second method is. That information is only available at run-time, and hence will need to be passed in as a parameter to the first method. That might sound confusing, but should be clearer with a couple of examples:

  • Starting Threads - It is possible in C# to tell the computer to start some new sequence of execution in parallel with what it is currently doing. Such a sequence is known as a thread, and starting one up is done using a method, Start() on an instance of one of the base classes, System.Threading.Thread . (We will look at threads more closely in Chapter 5.) Now, when your application first starts running, it has to start somewhere, and as we have just commented, the place it starts is Main() . Similarly, if you are going to tell the computer to start a new sequence of execution, you have got to tell it where to start that sequence. You have to supply it with the details of a method in which execution can start - in other words, the Thread.Start() method has to take a parameter that defines the method to be invoked by the thread.

  • Generic Library Classes - There are of course many libraries around that contain code to perform various standard tasks . It is usually possible for these libraries to be self-contained, in the sense that you know when you write to the library exactly how the task must be performed. However, sometimes the task contains some sub-task, which only the individual client code that uses the library knows how to perform. For example, say we want to write a class that takes an array of objects and sorts them into ascending order. Part of the sorting process involves repeatedly taking two of the objects in the array, and comparing them in order to see which one should come first. If we want to make the class capable of sorting arrays of any object, there is no way that it can tell in advance how to do this comparison. The client code that hands our class the array of objects will also have to tell our class how to do this comparison for the particular objects it wants sorted. In other words, the client code will have to pass our class details of an appropriate method that can be called, which does the comparison.

  • Chapter 7, the same principles will hold: our code will need to inform the .NET runtime of what methods handle what events.

So, we have established the principle that sometimes, methods need to take details of other methods as parameters. Next , we need to figure out how we can do that. The simplest way would appear to be to just pass in the name of a method as a parameter. To take our example from threading, suppose we are going to start a new thread, and we have a method called EntryPoint() , which is where we want our thread to start running:

   void EntryPoint()     {     // do whatever the new thread needs to do     }   

Could we perhaps start the new thread off with some code like this:

   Thread NewThread = new Thread();     Thread.Start(EntryPoint);                   // WRONG   

In fact, this is a simple way of doing it, and it is what some languages, such as C and C++, do in this kind of situation (in C and C++, the parameter EntryPoint is a function pointer). Incidentally, something like this is also going on behind the scenes in pre-.NET Visual Basic when you add event handlers, but the Visual Basic runtime is so good at shielding you from the details of what is happening, you would never realize that.

Unfortunately, this direct approach causes some problems with type safety, and it also neglects the fact that when we are doing object-oriented programming, methods rarely exist in isolation, but usually need to be associated with a class instance before they can be called. As a result of these problems, the .NET Framework does not syntactically permit this direct approach. Instead, if you want to pass methods around, you have to wrap up the details of the method in a new kind of object, a delegate. Delegates quite simply are a special type of object - special in the sense that, whereas all the objects we have defined up to now contain data, a delegate just contains the details of a method.

Using Delegates in C#

When we want to use a class in C#, there are two stages. First, we need to define the class - that is, we need to tell the compiler what fields and methods make up the class. Then (unless we are using only static methods), we instantiate an object of that class. With delegates it is the same thing. We have to start off by defining the delegates we want to use. In the case of a delegates, defining it means telling the compiler what kind of method a delegate of that type will represent. Then, we have to create one or more instances of that delegate.

The syntax for defining delegates looks like this:

   delegate void VoidOperation(uint x);   

In this case, we have defined a delegate called VoidOperation , and we have indicated that each instance of this delegate can hold a reference to a method that takes one uint parameter and returns void . The crucial point to understand about delegates is that they are very type-safe. When you define the delegate, you have to give full details of the signature of the method that it is going to represent.

Important 

One good way of understanding delegates is by thinking of a delegate as something that gives a name to a method signature.

Suppose we wanted to define a delegate called TwoLongsOp that will represent a function that takes two long s as its parameters and returns a double . We could do it like this:

   delegate double TwoLongsOp(long first, long second);   

Or, to define a delegate that will represent a method that takes no parameters and returns a string , we might write this:

   delegate string GetAString();   

The syntax is similar to that for a method definition, except that there is no method body, and the definition is prefixed with the keyword delegate . Since what we are doing here is basically defining a new class, we can define a delegate in any of the same places that we would define a class - that is to say either inside another class, or outside of any class and in a namespace as a top-level object. Depending on how visible we want our definition to be, we can apply any of the normal access modifiers to delegate definitions - public , private , protected , and so on:

   public delegate string GetAString();   

We do literally mean what we say when we describe 'defining a delegate' as 'defining a new class'. Delegates are implemented as classes derived from the base class, System.Delegate . The C# compiler is aware of this class, and uses its delegate syntax to shield us from the details of the operation of this class. This is another good example of how C# works in conjunction with the base classes to make programming as easy as practicable.

Once we have defined a delegate, we can create an instance of it so that we can use it to store details of a particular method.

There is an unfortunate problem with terminology here. With classes there are two distinct terms - 'class', which indicates the broader definition, and 'object', which means an instance of the class. Unfortunately, with delegates there is only the one term . When you create an instance of a delegate, what you have created is also referred to as 'a delegate'. You need to be aware of the context to know which meaning we are using when we talk about delegates.

The following code snippet demonstrates the use of a delegate. It is a rather long-winded way of calling the ToString() method on an int :

   private delegate string GetAString();     static void Main(string[] args)     {     int x = 40;     GetAString firstStringMethod = new GetAString(x.ToString);     Console.WriteLine("String is" + firstStringMethod());     // With firstStringMethod initialized to x.ToString(),     // the above statement is equivalent to saying     // Console.WriteLine("String is" + x.ToString());   

In this code, we instantiate a delegate of type GetAString , and we initialize it so that it refers to the ToString() method of the integer variable x . Delegates in C# always syntactically take a one-parameter constructor, the parameter being the method to which the delegate will refer. This method must match the signature with which we originally defined the delegate. So in this case, we would get a compilation error if we tried to initialize FirstStringMethod with any method that did not take no parameters and return a string. Notice that since int.ToString() is an instance method (as opposed to a static one) we need to specify the instance ( x ) as well as the name of the method to initialize the delegate properly.

The next line actually uses the delegate to display the string. In any code, supplying the name of a delegate instance, followed by brackets containing any parameters, has exactly the same effect as calling the method wrapped by the delegate. Hence, in the above code snippet, the Console.WriteLine() statement is completely equivalent to the commented-out line.

One feature of delegates is that they are type-safe to the extent that they ensure the signature of the method being called is correct. However, interestingly, they do not care what type of object the method is being called against, or even whether the method is a static method or an instance method.

Important 

An instance of a given delegate can refer to any instance or static method on any object of any type, provided that the signature of the method matches the signature of the delegate.

To see this, we will expand the above code snippet so that it uses the FirstStringMethod delegate to call a couple of other methods on another object - an instance method and a static method. For this, we will reuse the Currency struct that we defined earlier in the chapter. Recall that the Currency struct already has its own overload of ToString() . In order to demonstrate using delegates with static methods, we will also add a static method with the same signature to Currency :

   struct Currency     {     public static string GetCurrencyUnit()     {     return "Dollar";     }   

Now we can use our GetAString instance as follows .

 private delegate string GetAString();       static void Main(string[] args)       {          int x = 40;          GetAString firstStringMethod = new GetAString(x.ToString);          Console.WriteLine("String is " + firstStringMethod());   Currency balance = new Currency(34, 50);     firstStringMethod = new GetAString(balance.ToString);     Console.WriteLine("String is " + firstStringMethod());     firstStringMethod = new GetAString(Currency.GetCurrencyUnit);     Console.WriteLine("String is " + firstStringMethod());   

This code shows how you can call a method via a delegate, and subsequently reassign the delegate to refer to different methods on different instances of classes, even static methods or methods against instances of different types of class, provided that the signature of each method matches the delegate definition.

However, we still haven't demonstrated the process of actually passing a delegate to another method. Nor have we actually achieved anything particularly useful yet. It is possible to call the ToString() method of int and Currency objects in a much more straightforward way than using delegates! Unfortunately, it is in the nature of delegates that we need a fairly complex example before we can really appreciate their usefulness . We are now going to present two delegate examples. The first one simply uses delegates to call a couple of different operations. It illustrates how to pass delegates to methods, and how you can use arrays of delegates - although arguably it still doesn't do much that you couldn't do a lot more simply without delegates. Then, we will present a second, much more complex example of a BubbleSorter class, which implements a method to sort out arrays of objects into increasing order. This class would be difficult to write without delegates.

SimpleDelegate Example

For this example, we will define a MathOperations class that has a couple of static methods to perform two operations on doubles. Then, we will use delegates to call up these methods. The math class looks like this:

   class MathsOperations     {     public static double MultiplyByTwo(double value)     {     return value*2;     }     public static double Square(double value)     {     return value*value;     }     }   

and we call up these methods like this:

   using System;     namespace Wrox.ProCSharp.AdvancedCSharp     {     delegate double DoubleOp(double x);     class MainEntryPoint     {     static void Main()     {     DoubleOp [] operations =     {     new DoubleOp(MathsOperations.MultiplyByTwo),     new DoubleOp(MathsOperations.Square)     };     for (int i=0 ; i<operations.Length ; i++)     {     Console.WriteLine("Using operations[{0}]:", i);     ProcessAndDisplayNumber(operations[i], 2.0);     ProcessAndDisplayNumber(operations[i], 7.94);     ProcessAndDisplayNumber(operations[i], 1.414);     Console.WriteLine();     }     }     static void ProcessAndDisplayNumber(DoubleOp action, double value)     {     double result = action(value);     Console.WriteLine(     "Value is {0}, result of operation is {1}", value, result);     }   

In this code, we instantiate an array of DoubleOp delegates (remember that once we have defined a delegate class, we can basically instantiate instances just like we can with normal classes, so putting some into an array is no problem). Each element of the array gets initialized to refer to a different operation implemented by the MathOperations class. Then, we loop through the array, applying each operation to three different values. This illustrates one way of using delegates - that you can group methods together into an array using them, so that you can call several methods in a loop.

The key lines in this code are the ones in which we actually pass each delegate to the ProcessAndDisplayNumber() method, for example:

 ProcessAndDisplayNumber(operations[i], 2.0); 

Here, we are passing in the name of a delegate, but without any parameters. Given that operations[i] is a delegate, syntactically:

  • operations[i] means 'the delegate', in other words the method represented by the delegate

  • operations[i](2.0) means 'actually call this method, passing in the value in parentheses'

The ProcessAndDisplayNumber() method is defined to take a delegate as its first parameter:

 static void ProcessAndDisplayNumber(DoubleOp action, double value) 

Then, when in this method, we call:

 double result = action(value); 

This actually causes the method that is wrapped up by the action delegate instance to be called, and its return result stored in Result .

Running this sample gives the following:

  SimpleDelegate  Using operations[0]: Value is 2, result of operation is 4 Value is 7.94, result of operation is 15.88 Value is 1.414, result of operation is 2.828 Using operations[1]: Value is 2, result of operation is 4 Value is 7.94, result of operation is 63.0436 Value is 1.414, result of operation is 1.999396 

BubbleSorter Example

We are now ready for an example that will show delegates working in a situation in which they are very useful. We are going to write a class called BubbleSorter . This class implements a static method, Sort() , which takes as its first parameter an array of objects, and rearranges this array into ascending order. In other words, suppose we were to pass it this array of int s: {0, 5, 6, 2, 1} . It would rearrange this array into {0,   1,   2,   5,   6} .

The bubble-sorting algorithm is a well-known and very simple way of sorting numbers. It is best suited to small sets of numbers, since for larger sets of numbers (more than about 10) there are far more efficient algorithms available). It works by repeatedly looping through the array, comparing each pair of numbers and, if necessary, swapping them, so that the largest numbers progressively move to the end of the array. For sorting int s, a method to do a bubble sort might look like this:

   // Note that this isn't part of the sample     for (int i = 0; i < sortArray.Length; i++)     {     for (int j = i + 1; j < sortArray.Length; j++)     {     if (sortArray[j] < sortArray[i])   // problem with this test     {     int temp = sortArray[i];   // swap ith and jth entries     sortArray[i] = sortArray[j];     sortArray[j] = temp;     }     }     }   

This is all very well for int s, but we want our Sort() method to be able to sort any object. In other words, if some client code hands us an array of Currency structs or any other class or struct that it may have defined, we need to be able to sort the array. This gives us a problem with the line if(sortArray[j] < sortArray[i]) in the above code, since that requires us to compare two objects on the array to see which one is greater. We can do that for int s, but how are we to do it for some new class that is unknown or undecided until run-time? The answer is the client code that knows about the class will have to pass in a delegate wrapping a method that will do the comparison.

We define the delegate like this:

   delegate bool CompareOp(object lhs, object rhs);   

and give our Sort method this signature:

   static public void Sort(object [] sortArray, CompareOp gtMethod)   

The documentation for this method will state that gtMethod must refer to a static method that takes two arguments, and returns true if the value of the second argument is 'greater than' (in other words should come later in the array than) the first one.

Although we are using delegates here, it is possible to solve this problem alternatively, by using interfaces. .NET in fact makes the IComparer interface available for that purpose. However, we will use delegates here since this is still the kind of problem that lends itself to delegates.

Now we are all set. Here is the definition for the BubbleSorter class:

   class BubbleSorter     {     static public void Sort(object [] sortArray, CompareOp gtMethod)     {     for (int i=0 ; i<sortArray.Length ; i++)     {     for (int j=i+1 ; j<sortArray.Length ; j++)     {     if (gtMethod(sortArray[j], sortArray[i]))     {     object temp = sortArray[i];     sortArray[i] = sortArray[j];     sortArray[j] = temp;     }     }     }     }     }   

In order to use this class, we need to define some other class, which we can use to set up an array that needs sorting. For this example, we will assume that our Mortimer Phones mobile phone company has a list of employees, and wants them sorted according to salary. The employees are each represented by an instance of a class, Employee , which looks like this:

   class Employee     {     private string name;     private decimal salary;     public Employee(string name, decimal salary)     {     this.name = name;     this.salary = salary;     }     public override string ToString()     {     return string.Format(name + ", {0:C}", salary);     }     public static bool RhsIsGreater(object lhs, object rhs)     {     Employee empLhs = (Employee) lhs;     Employee empRhs = (Employee) rhs;     return (empRhs.salary > empLhs.salary) ? true : false;     }     }   

Notice that in order to match the signature of the CompareOp delegate, we have had to define RhsIsGreater in this class as taking two object references, rather than Employee references as parameters. This means that we have had to cast the parameters into Employee references in order to perform the comparison.

Now we are ready to write some client code to request a sort:

   using System;     namespace Wrox.ProCSharp.AdvancedCSharp     {     delegate bool CompareOp(object lhs, object rhs);     class MainEntryPoint     {     static void Main()     {     Employee [] employees =     {     new Employee("Karli Watson", 20000),     new Employee("Bill Gates", 10000),     new Employee("Simon Robinson", 25000),     new Employee("Mortimer", (decimal)1000000.38),     new Employee("Arabel Jones", 23000),     new Employee("Avon from 'Blake's 7'", 50000)};     CompareOp employeeCompareOp = new CompareOp(Employee.RhsIsGreater);     BubbleSorter.Sort(employees, employeeCompareOp);     for (int i=0 ; i<employees.Length ; i++)     Console.WriteLine(employees[i].ToString());     }     }   

Running this code shows that the Employees are correctly sorted according to salary:

  BubbleSorter  Bill Gates, 10,000.00 Karli Watson, 20,000.00 Arabel Jones, 23,000.00 Simon Robinson, 25,000.00 Avon from 'Blake's 7', 50,000.00 Mortimer, 1,000,000.38 

Note that the above output shows salaries in , because I'm in the UK, so my locale is set to UK , and this is picked up by the decimal struct when it formats each salary as a string. If you download and run this sample, will be replaced by the currency unit in your local currency. This might also make the salaries look a bit lower (10000 Yen, for example, aren't worth quite as much as GB10000!). A number like a 1000 is a pure int , not a currency amount. Unfortunately there is no formal way in C# to specify the locale value (to be associated with the int ) in a constructor and have the constructor invoke the proper converter delegate. If you were writing a Currency class in real life, and needed to be able to work with currencies other than the one associated with the computer's current culture setting, you would need to find some way round this. One possibility is for the Currency constructor to take strings allowing specification of the currency.

 new Currency ("1000"); 

Multicast Delegates

So far, each of the delegates we have used wraps just one single method call. Calling the delegate amounts to calling that method. If we want to call more than one method, we need to make an explicit call through a delegate more than once. However, it is possible for a delegate to wrap more than one method. Such a delegate is known as a multicast delegate . If a multicast delegate is called, it will successively call each method in order. For this to make sense, the delegate signature must return a void ( otherwise , where would all the return values go?), and in fact, if the compiler sees a delegate that returns a void , it automatically assumes you mean a multicast delegate. Consider this code, which is adapted from the SimpleDelegate example. Although the syntax is the same as before, it is actually a multicast delegate, Operations , that gets instantiated :

   delegate void DoubleOp(double value);     //   delegate double DoubleOp(double value);   // can't do this now   class MainEntryPoint    {       static void Main()       {   DoubleOp operations = new DoubleOp(MathOperations.MultiplyByTwo);     operations += new DoubleOp(MathOperations.Square);   

In our earlier example, we wanted to store references to two methods so we instantiated an array of delegates. Here, we simply add both operations into the same multicast delegate. Multicast delegates recognize the operators + and += . If we'd preferred, we could have expanded out the last two lines of the above code to this, which has the same effect:

   DoubleOp operation1 = new DoubleOp(MathOperations.MultiplyByTwo);     DoubleOp operation2 = new DoubleOp(MathOperations.Square);     DoubleOp operations = operation1 + operation2;   

Multicast delegates also recognize the operators - and -= to remove method calls from the delegate.

In terms of what's going on under the hood, a multicast delegate is a class derived from System.MulticastDelegate , which in turn is derived from System.Delegate . System.MulticastDelegate has additional members to allow chaining of method calls together into a list.

To illustrate the use of multicast delegates, we have recast the SimpleDelegate sample into a new sample, MulticastDelegate . Since we now need the delegate to refer to methods that return void , we have had to rewrite the methods in the MathOperations class, so they display their results instead of returning them:

   class MathOperations     {     public static void MultiplyByTwo(double value)     {     double result = value*2;     Console.WriteLine(     "Multiplying by 2: {0} gives {1}", value, result);     }     public static void Square(double value)     {     double result = value*value;     Console.WriteLine("Squaring: {0} gives {1}", value, result);     }     }   

To accommodate this change, we have rewritten ProcessAndDisplayNumber :

   static void ProcessAndDisplayNumber(DoubleOp action, double value)     {     Console.WriteLine("\nProcessAndDisplayNumber called with value = " +     value);     action(value);     }   

Now we can try out our multicast delegate like this:

   static void Main()     {     DoubleOp operations = new DoubleOp(MathOperations.MultiplyByTwo);     operations += new DoubleOp(MathOperations.Square);     ProcessAndDisplayNumber(operations, 2.0);     ProcessAndDisplayNumber(operations, 7.94);     ProcessAndDisplayNumber(operations, 1.414);     Console.WriteLine();     }   

Now, each time that ProcessAndDisplayNumber is called, it will display a message to say that it has been called. Then the following statement:

 action(value); 

will cause each of the method calls in the action delegate instance to be called in succession.

Running this code gives this result:

  MulticastDelegate  ProcessAndDisplayNumber called with value = 2 Multiplying by 2: 2 gives 4 Squaring: 2 gives 4 ProcessAndDisplayNumber called with value = 7.94 Multiplying by 2: 7.94 gives 15.88 Squaring: 7.94 gives 63.0436 ProcessAndDisplayNumber called with value = 1.414 Multiplying by 2: 1.414 gives 2.828 Squaring: 1.414 gives 1.999396 

If you are using multicast delegates, you should be aware that the order in which methods chained to the same delegate will be called is formally undefined. You should, therefore, avoid writing code that relies on such methods being called in any particular order.

  


Professional C#. 2nd Edition
Performance Consulting: A Practical Guide for HR and Learning Professionals
ISBN: 1576754359
EAN: 2147483647
Year: 2002
Pages: 244

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net