Chapter 15: Delegates and Events


This chapter examines two innovative C# features: delegates and events. A delegate provides a way to encapsulate a method. An event is a notification that some action has occurred. Delegates and events are related because an event is built upon a delegate. Both expand the set of programming tasks to which C# can be applied. Also examined are four new features related to delegates and events that have been added by C# 2.0: anonymous methods, covariance, contravariance, and method group conversions.

Delegates

Let’s begin by defining the term delegate. In straightforward language, a delegate is an object that can refer to a method. Thus, when you create a delegate, you are creating an object that can hold a reference to a method. Furthermore, the method can be called through this reference. Thus, a delegate can invoke the method to which it refers.

The idea of a reference to a method may seem strange at first because usually we think of references as referring to objects, but in reality there is little difference. As explained earlier in this book, a reference is essentially a memory address. Thus, a reference to an object is, essentially, the address of the object. Even though a method is not an object, it too has a physical location in memory, and the address of its entry point is the address called when the method is invoked. This address can be assigned to a delegate. Once a delegate refers to a method, the method can be called through that delegate.

Note 

If you are familiar with C/C++, then it will help to know that a delegate in C# is similar to a function pointer in C/C++.

It is important to understand that the same delegate can be used to call different methods during the runtime of a program by simply changing the method to which the delegate refers. Thus, the method that will be invoked by a delegate is not determined at compile time, but rather at runtime. This is the principal advantage of a delegate.

A delegate is declared using the keyword delegate. The general form of a delegate declaration is shown here:

 delegate ret-type name(parameter-list);

Here, ret-type is the type of value returned by the methods that the delegate will be calling. The name of the delegate is specified by name. The parameters required by the methods called through the delegate are specified in the parameter-list. A delegate can call only methods whose return type and parameter list match those specified by the delegate’s declaration.

A delegate can invoke either an instance method associated with an object or a static method associated with a class. All that matters is that the return type and signature of the method agree with that of the delegate.

To see delegates in action, let’s begin with the simple example shown here:

 // A simple delegate example. using System; // Declare a delegate. delegate string StrMod(string str); class DelegateTest {   // Replaces spaces with hyphens.   static string replaceSpaces(string a) {     Console.WriteLine("Replaces spaces with hyphens.");     return a.Replace(' ', '-');   }   // Remove spaces.   static string removeSpaces(string a) {     string temp = "";     int i;     Console.WriteLine("Removing spaces.");     for(i=0; i < a.Length; i++)       if(a[i] != ' ') temp += a[i];     return temp;   }   // Reverse a string.   static string reverse(string a) {     string temp = "";     int i, j;     Console.WriteLine("Reversing string.");     for(j=0, i=a.Length-1; i >= 0; i--, j++)       temp += a[i];     return temp;   }   public static void Main() {     // Construct a delegate.     StrMod strOp = new StrMod(replaceSpaces);     string str;     // Call methods through the delegate.     str = strOp("This is a test.");     Console.WriteLine("Resulting string: " + str);     Console.WriteLine();     strOp = new StrMod(removeSpaces);     str = strOp("This is a test.");     Console.WriteLine("Resulting string: " + str);     Console.WriteLine();     strOp = new StrMod(reverse);     str = strOp("This is a test.");     Console.WriteLine("Resulting string: " + str);   } }

The output from the program is shown here:

 Replaces spaces with hyphens. Resulting string: This-is-a-test. Removing spaces. Resulting string: Thisisatest. Reversing string. Resulting string: .tset a si sihT

Let’s examine this program closely. The program declares a delegate called StrMod that takes one string parameter and returns a string. In DelegateTest, three static methods are declared, each with a matching signature. These methods perform some type of string modification. Notice that replaceSpaces( ) uses one of string’s methods, called Replace( ), to replace spaces with hyphens.

In Main( ), a StrMod reference called strOp is created and assigned a reference to replaceSpaces( ). Pay close attention to this line:

 StrMod strOp = new StrMod(replaceSpaces);

Notice how the method replaceSpaces( ) is passed as a parameter. Only its name is used; no parameters are specified. This can be generalized. When instantiating a delegate, you specify only the name of the method to which you want the delegate to refer. Also, the method’s signature must match that of the delegate’s declaration. If it doesn’t, a compile-time error will result.

Next, replaceSpaces( ) is called through the delegate instance strOp, as shown here:

 str = strOp("This is a test.");

Because strOp refers to replaceSpaces( ), it is replaceSpaces( ) that is invoked.

Next, strOp is assigned a reference to removeSpaces( ), and then strOp is called again. This time, removeSpaces( ) is invoked.

Finally, strOp is assigned a reference to reverse( ) and strOp is called. This results in reverse( ) being called.

The key point of the example is that the invocation of strOp results in a call to the method referred to by strOp at the time at which the invocation occurred. Thus, the method to call is resolved at runtime, not compile time.

Delegate Method Group Conversion

C# 2.0 added an option that significantly simplifies the syntax that assigns a method to a delegate. This feature is called method group conversion, and it allows you simply to assign the name of a method to a delegate, without the use of new or explicitly invoking the delegate’s constructor.

For example, here is the Main( ) method of the preceding program rewritten to use method group conversions:

 public static void Main() {   // Construct a delegate using method group conversion.   StrMod strOp = replaceSpaces; // use method group conversion   string str;   // Call methods through the delegate.   str = strOp("This is a test.");   Console.WriteLine("Resulting string: " + str);   Console.WriteLine();   strOp = removeSpaces; // use method group conversion   str = strOp("This is a test.");   Console.WriteLine("Resulting string: " + str);   Console.WriteLine();   strOp = reverse; // use method group conversion   str = strOp("This is a test.");   Console.WriteLine("Resulting string: " + str); }

Pay special attention to the way that strOp is created and assigned the method replaceSpaces in this line:

 StrMod strOp = replaceSpaces; // use method group conversion

The name of the method is assigned directly to strOp. C# automatically provides a conversion from the method to the delegate. This syntax can be generalized to any situation in which a method is assigned to a delegate.

Because the method group conversion syntax is simpler than the old approach, it is used throughout the remainder of this book.

Using Instance Methods as Delegates

Although the preceding example used static methods, a delegate can also refer to instance methods. It must do so, however, through an object reference. For example, here is a rewrite of the previous example, which encapsulates the string operations inside a class called StringOps. Notice that the method group conversion syntax can also be applied in this situation.

 // Delegates can refer to instance methods, too. using System; // Declare a delegate. delegate string StrMod(string str); class StringOps {   // Replaces spaces with hyphens.   public string replaceSpaces(string a) {     Console.WriteLine("Replaces spaces with hyphens.");     return a.Replace(' ', '-');   }   // Remove spaces.   public string removeSpaces(string a) {     string temp = "";     int i;     Console.WriteLine("Removing spaces.");     for(i=0; i < a.Length; i++)       if(a[i] != ' ') temp += a[i];     return temp;   }   // Reverse a string.   public string reverse(string a) {     string temp = "";     int i, j;     Console.WriteLine("Reversing string.");     for(j=0, i=a.Length-1; i >= 0; i--, j++)       temp += a[i];     return temp;   } } class DelegateTest {   public static void Main() {     StringOps so = new StringOps(); // create an instance of StringOps     // Initialize a delegate.     StrMod strOp = so.replaceSpaces;     string str;     // Call methods through delegates.     str = strOp("This is a test.");     Console.WriteLine("Resulting string: " + str);     Console.WriteLine();     strOp = so.removeSpaces;     str = strOp("This is a test.");     Console.WriteLine("Resulting string: " + str);     Console.WriteLine();     strOp = so.reverse;     str = strOp("This is a test.");     Console.WriteLine("Resulting string: " + str);   } }

This program produces the same output as the first, but in this case, the delegate refers to methods on an instance of StringOps.

Multicasting

One of the most exciting features of a delegate is its support for multicasting. In simple terms, multicasting is the ability to create an invocation list, or chain, of methods that will be automatically called when a delegate is invoked. Such a chain is very easy to create. Simply instantiate a delegate, and then use the + or += operator to add methods to the chain. To remove a method, use or =. If the delegate returns a value, then the value returned by the last method in the list becomes the return value of the entire delegate invocation. Thus, a delegate that will make use of multicasting will usually have a void return type.

Here is an example of multicasting. It reworks the preceding examples by changing the string manipulation method’s return type to void and using a ref parameter to return the altered string to the caller.

 // Demonstrate multicasting. using System; // Declare a delegate. delegate void StrMod(ref string str); class MultiCastDemo {   // Replaces spaces with hyphens.   static void replaceSpaces(ref string a) {     Console.WriteLine("Replaces spaces with hyphens.");     a = a.Replace(' ', '-');   }   // Remove spaces.   static void removeSpaces(ref string a) {     string temp = "";     int i;     Console.WriteLine("Removing spaces.");     for(i=0; i < a.Length; i++)       if(a[i] != ' ') temp += a[i];     a = temp;   }   // Reverse a string.   static void reverse(ref string a) {     string temp = "";     int i, j;     Console.WriteLine("Reversing string.");     for(j=0, i=a.Length-1; i >= 0; i--, j++)       temp += a[i];     a = temp;   }   public static void Main() {     // Construct delegates.     StrMod strOp;     StrMod replaceSp = replaceSpaces;     StrMod removeSp = removeSpaces;     StrMod reverseStr = reverse;     string str = "This is a test";     // Set up multicast.     strOp = replaceSp;     strOp += reverseStr;     // Call multicast.     strOp(ref str);     Console.WriteLine("Resulting string: " + str);     Console.WriteLine();     // Remove replace and add remove.     strOp -= replaceSp;     strOp += removeSp;     str = "This is a test"; // reset string     // Call multicast.     strOp(ref str);     Console.WriteLine("Resulting string: " + str);     Console.WriteLine();   } }

Here is the output:

 Replaces spaces with hyphens. Reversing string. Resulting string: tset-a-si-sihT Reversing string. Removing spaces. Resulting string: tsetasisihT

In Main( ), four delegate instances are created. One, strOp, is null. The other three refer to specific string modification methods. Next, a multicast is created that calls removeSpaces( ) and reverse( ). This is accomplished via the following lines:

 strOp = replaceSp; strOp += reverseStr;

First, strOp is assigned a reference to replaceSp. Next, using +=, reverseStr is added. When strOp is invoked, both methods are invoked, replacing spaces with hyphens and reversing the string, as the output illustrates.

Next, replaceSp is removed from the chain, using this line:

 strOp -= replaceSp;

and removeSP is added using this line:

 strOp += removeSp;

Then, StrOp is again invoked. This time, spaces are removed and the string is reversed.

Delegate chains are a powerful mechanism because they allow you to define a set of methods that can be executed as a unit. This can increase the structure of some types of code. Also, as you will soon see, delegate chains have a special value to events.

Anonymous Methods

C# 2.0 added the ability to pass an anonymous method to a delegate. An anonymous method is, essentially, a block of code that is passed to a delegate. The main advantage to using an anonymous method is simplicity. In many cases, there is no need to actually declare a separate method whose only purpose is to be passed to a delegate. In this situation, it is easier to pass a block of code to the delegate than it is to first create a method and then pass that method to the delegate.

Here is a simple example that uses an anonymous method:

 // Demonstrate an anonymous method. using System; // Declare a delegate. delegate void CountIt(); class AnonMethDemo {   public static void Main() {     // Here, the code for counting is passed     // as an anonymous method.     CountIt count = delegate {       // This is the block of code passed to the delegate.       for(int i=0; i <= 5; i++)         Console.WriteLine(i);     }; // notice the semicolon     count();   } }

This program first declares a delegate type called CountIt that has no parameters and returns void. Inside Main( ), a CountIt delegate called count is created, and it is passed the block of code that follows the delegate keyword. This block of code is the anonymous method that will be executed when count is called. Notice that the block of code ends with a semicolon. The output from the program is shown here:

 0 1 2 3 4 5

Pass Arguments to an Anonymous Method

It is possible to pass one or more arguments to an anonymous method. To do so, specify a parameter list immediately before the block of code. For example, here is the preceding program rewritten so that the ending value for the count is passed:

 // Demonstrate an anonymous method that takes an argument. using System; // Notice that CountIt now has a parameter. delegate void CountIt(int end); class AnonMethDemo2 {   public static void Main() {     // Here, the ending value for the count     // is passed to the anonymous method.     CountIt count = delegate (int end) {       for(int i=0; i <= end; i++)         Console.WriteLine(i);     };     count(3);     Console.WriteLine();     count(5);   } }

In this version, CountIt now takes an integer argument. Notice how the parameter list is specified after the delegate keyword when the anonymous method is created. The code inside the anonymous method has access to the parameter end in just the same way it would if a “normal” method were being created. The output from this program is shown next:

 0 1 2 3 0 1 2 3 4 5

Return a Value from an Anonymous Method

An anonymous method can return a value. If it does so, the type of the return value must be compatible with the return type of the delegate. For example, here the code that performs the count also computes the summation of the count and returns the result:

 // Demonstrate an anonymous method that returns a value. using System; // This delegate returns a value. delegate int CountIt(int end); class AnonMethDemo3 {   public static void Main() {     int result;     // Here, the ending value for the count     // is passed to the anonymous method.     // A summation of the count is returned.     CountIt count = delegate (int end) {       int sum = 0;       for(int i=0; i <= end; i++) {         Console.WriteLine(i);         sum += i;       }       return sum; // return a value from an anonymous method     };     result = count(3);     Console.WriteLine("Summation of 3 is " + result);     Console.WriteLine();     result = count(5);     Console.WriteLine("Summation of 5 is " + result);   } }

In this version, the value of sum is returned by the code block that is associated with the count delegate instance. Notice that the return statement is used in an anonymous method in just the same way that it is used in a “normal” method. The output is shown here:

 0 1 2 3 Summation of 3 is 6 0 1 2 3 4 5 Summation of 5 is 15

Use Outer Variables with Anonymous Methods

Variables and parameters that are inside the scope that encloses an anonymous method are called outer variables. An anonymous method can use outer variables. When an outer variable is used by an anonymous method, that variable is said to be captured. A captured variable will stay in existence until the delegate that captures it is subject to garbage collection. Thus, even though a local variable will normally cease to exist when its block is exited, if that local variable is being used by an anonymous method, then that variable will stay in existence until that method ends.

The capturing of a local variable can lead to unexpected results. For example, consider this version of the counting program. As in the previous version, the summation of the count is computed. However, in this version, a CountIt object is constructed and returned by a static method called counter( ). This object uses the variable sum, which is declared in the enclosing scope provided by counter( ), rather than in the anonymous method, itself. Thus, sum is captured by the anonymous method. Inside Main( ), counter( ) is called to obtain a CountIt object. Thus, sum will not be destroyed until the program finishes.

 // Demonstrate a captured variable. using System; // This delegate returns int and takes an int argument. delegate int CountIt(int end); class VarCapture {   static CountIt counter() {     int sum = 0;     // Here, a summation of the count is stored     // in the captured variable sum.     CountIt ctObj = delegate (int end) {       for(int i=0; i <= end; i++) {         Console.WriteLine(i);         sum += i;       }       return sum;     };     return ctObj;   }   public static void Main() {     // Get a counter     CountIt count = counter();     int result;     result = count(3);     Console.WriteLine("Summation of 3 is " + result);     Console.WriteLine();     result = count(5);     Console.WriteLine("Summation of 5 is " + result);   } }

The output is shown here. Pay special attention to the summation value.

 0 1 2 3 Summation of 3 is 6 0 1 2 3 4 5 Summation of 5 is 21

As you can see, the count still proceeds normally. However, notice the summation value for 5. It shows 21 instead of 15! The reason for this is that sum is captured by ctObj when it is created by the counter( ) method. This means that it stays in existence until count is subject to garbage collection at the end of the program. Thus, its value is not destroyed when counter( ) returns.

Although captured variables can result in rather counter-intuitive situations, such as the one just shown, it makes sense if you think about it a bit. The key point is that when an anonymous method captures a variable, that variable cannot go out of existence until the delegate that captures it is no longer being used. If this were not the case, then the captured variable would be undefined.

Anonymous methods are of their greatest practical value when used with events. As you will see later in this chapter, often an anonymous method is the most efficient means of coding an event handler.

Covariance and Contravariance

Two other new delegate-related features added by C# 2.0 are covariance and contravariance. Normally, the method that you pass to a delegate must have the same return type and signature as the delegate. However, covariance and contravariance relax this rule slightly, as it pertains to derived types. Covariance enables a method to be assigned to a delegate when the method’s return type is a class derived from the class specified by the return type of the delegate. Contravariance enables a method to be assigned to a delegate when a method’s parameter type is a base class of the class specified by the delegate’s declaration.

Here is an example that illustrates both covariance and contravariance:

 // Demonstrate covariance and contravariance. using System; class X {   public int val; } // Y is derived from X. class Y : X { } // This delegate returns X and takes a Y argument. delegate X ChangeIt(Y obj); class CoContraVariance {   // This method returns X and has an X parameter.   static X incrA(X obj) {     X temp = new X();     temp.val = obj.val + 1;     return temp;   }   // This method returns Y and has a Y parameter.   static Y incrB(Y obj) {     Y temp = new Y();     temp.val = obj.val + 1;     return temp;   }   public static void Main() {     Y Yob = new Y();     // In this case, the parameter to incrA     // is X and the parameter to ChangeIt is Y.     // Because of contravariance, the following     // line is OK.     ChangeIt change = incrA;     X Xob = change(Yob);     Console.WriteLine("Xob: " + Xob.val);     // In the next case, the return type of     // incrB is Y and the return type of     // ChangeIt is X. Because of covariance,     // the following line is OK.     change = incrB;     Yob = (Y) change(Yob);     Console.WriteLine("Yob: " + Yob.val);   } }

The output from the program is shown here:

 Xob: 1 Yob: 1

In the program, notice that class Y is derived from class X. Next, notice that the delegate ChangeIt( ) is declared like this:

 delegate X ChangeIt(Y obj);

ChangeIt( ) returns X and has a Y parameter. Next, notice that the methods incrA( ) and incrB( ) are declared as shown here:

 static X incrA(X obj) static Y incrB(Y obj)

The incrA( ) method has an X parameter and returns X. The incrB( ) method has a Y parameter and returns Y. Given covariance and contravariance, either of these methods can be passed to ChangeIt, as the program illustrates.

Therefore, this line

 ChangeIt change = incrA;

uses contravariance to enable incrA( ) to be passed to the delegate because incrA( ) has an X parameter, but the delegate has a Y parameter. This works because with contravariance, if the parameter type of the method passed to a delegate is a base class of the parameter type used by the delegate, then the method and the delegate are compatible.

The next line is also legal, but this time it is because of covariance:

 change = incrB;

In this case, the return type of incrB( ) is Y, but the return type of ChangeIt( ) is X. However, because the return type of the method is a class derived from the return type of the delegate, the two are compatible.

System.Delegate

All delegates are classes that are implicitly derived from System.Delegate. You don’t normally need to use its members directly, and this book makes no explicit use of System.Delegate. However, its members may be useful in certain specialized situations.

Why Delegates

Although the preceding examples show the “how” behind delegates, they don’t really illustrate the “why.” In general, delegates are useful for two main reasons. First, as the next section will show, delegates support events. Second, delegates give your program a way to execute methods at runtime without having to know precisely what that method is at compile time. This ability is quite useful when you want to create a framework that allows components to be plugged in. For example, imagine a drawing program (a bit like the standard Windows Paint accessory). Using a delegate, you could allow the user to plug in special color filters or image analyzers. Furthermore, the user could create a sequence of these filters or analyzers. Such a scheme would be easily handled using a delegate.




C# 2.0(c) The Complete Reference
C# 2.0: The Complete Reference (Complete Reference Series)
ISBN: 0072262095
EAN: 2147483647
Year: 2006
Pages: 300

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