Delegates


The common blueprint for using delegates includes steps to define, create, and invoke a delegate.

  1. Define a classification of a delegate using the delegate keyword:

     public delegate int DelegateClass(string info); 

  2. Create an instance of a delegate using the new keyword. In the constructor, initialize the delegate with a function pointer. You can also create and initialize a delegate implicitly without the new keyword:

     DelegateClass obj=new DelegateClass(MethodA); DelegateClass obj2=MethodA; // implicit 

  3. Invoke the delegate with the call operator "()". The call operator is convenient, but makes it harder to discern a delegate invocation from a regular function call. Alternatively, invoke the delegate with the Invoke method, which clearly distinguishes invoking a delegate from a normal function call:

     obj("1"); obj.Invoke("2"); // Alternative. 

  4. As with any object, when the delegate is no longer needed, set the delegate to null:

     obj=null; 

The complete list of the example application follows:

 using System; namespace Donis.CSharpBook{  public delegate int DelegateClass(string info);      public class Steps{          public static void Main(){              DelegateClass obj=new DelegateClass(MethodA);              DelegateClass obj2=MethodA; // implicit              obj("1");              obj.Invoke("2"); // Alternative              obj=null;              obj2=null;          }          public static int MethodA(string info) {              Console.WriteLine("Steps.MethodA");              return int.Parse(info);          }      } } 

Define a Delegate

The delegate keyword is for defining new delegates. The delegate statement looks like a function signature. Rather, it defines a new delegate type. Function pointers matching the delegate signature and return type can be stored into the delegate. Therefore, only functions with similar signatures can be called through the delegate. This code defines a new delegate, which internally becomes a class named DelegateClass:

 public delegate int DelegateClass(string info); 

This is the syntax for defining a new delegate class:

  • accessibility delegate return delegatename(parameterslist)

Accessibility is limited to the valid accessibility of classes, such as public or private. The remainder of the syntax defines the signature and return type of the delegate. delegatename is the name of the delegate classification.

Because defining a delegate creates a new class, delegates can be defined anywhere a class is appropriate. A delegate can be defined in a namespace as a namespace member and within a class as a nested class, but not as a class field or a local variable within a method.

Create a Delegate

As a class, use the new keyword to create an instance of a delegate. As mentioned, delegates are derived from the System.MulticastDelegate reference type. Multicast delegates are repositories of zero or more function pointers. The list of pointers in a multicast delegate is called the invocation list. When a delegate hosts multiple function pointers, the functions are called on a first-in, first-out (FIFO) basis.

The delegate constructor is not overloaded and has a single parameter, which is the target method. For an instance method, use the object.method format. If static, use the class.method format. If the method and delegate are contained in the same class, neither the object nor class name is required. The following code initializes a delegate in a variety of ways:

 using System; namespace Donis.CSharpBook{     public delegate void DelegateClass();     public class Constructors{     public static void Main(){         DelegateClass del1=new             DelegateClass(Constructors.MethodA);         DelegateClass del2=new DelegateClass(MethodA);         ZClass obj=new ZClass();         DelegateClass del3=new DelegateClass(obj.MethodB);     }     public static void MethodA() {     }  }     public class ZClass {         public void MethodB() {         }     } } 

You can assign a function pointer directly to a delegate and omit the new operator, which is called delegate inference. Delegate inference infers a delegate signature from the function pointer, creates a new delegate, initializes the source delegate with the function pointer in the constructor, and assigns the source delegate to the target delegate. The source delegate and the target delegate should have compatible signatures. Although the code is more concise, the intent is not as obvious. Here is the previous code, changed for delegate inference:

 DelegateClass del1=Constructors.MethodA; DelegateClass del2=MethodA; ZClass obj=new ZClass(); DelegateClass del3=obj.MethodB; 

The signatures of delegates are not entirely rigid. Through contravariance and covariance, there is some flexibility. The signature of the function pointer does not have to match the delegate signature exactly.

Contravariance and Covariance

When are delegates compatible or similar? The parameters of the function pointer can be derivations of the parameters indicated in the delegate signature—this is called contravariance. Because the delegate parameters refine that of the function pointer, any input from the target method is acceptable to the delegate. Conversely, the return type of the delegate must be a derivation of the return type of the function pointer. The return of the function pointer can refine the return of the delegate. Therefore, any return from the delegate is compatible with that of the function pointer—this is called covariance. Contravariance and covariance expand the set of methods assignable to a delegate while maintaining type-safeness. Here is an example of both contravariance and covariance:

 using System; namespace Donis.CSharpBook{     delegate ZClass DelegateClass(BClass obj);     public class Starter{         public static void Main(){             DelegateClass del=MethodA;         }         public static YClass MethodA(AClass obj) {             return null;         }     }     public class ZClass {     }     public class YClass: ZClass {     }     public class AClass {     }     public class BClass: AClass {     } } 

In the preceding code, this is the signature of the delegate:

 delegate ZClass DelegateClass(BClass obj); 

This is the signature of the function pointer:

 public static YClass MethodA(AClass obj) 

The signature of the delegate and function pointer are not exactly the same. This is okay because the parameter of the delegate (BClass) refines the parameter of the function (AClass), which is contravariance. In addition, the return type of the function (YClass) refines the return type of the delegate (ZClass). This is covariance.

Invoking a Delegate

Delegates are invoked through the call operator or the Invoke method. The C# compiler translates the call operator into an invocation of the Invoke method, which has the same signature as the delegate and calls the function pointers contained in the delegate. The parameters of the delegate become the parameters and input to the function calls. If the delegate contains a function pointer, the return from that function becomes the return of the delegate. When multiple function pointers are stored in the delegate, the return of the last function becomes the return of the delegate.

Arrays of Delegates

Arrays of delegates can be the impetus of elegant solutions that would otherwise be unavailable. Creating an array of delegates is the same as creating an array of any type. Selecting from a set of tasks at run time is an example of when an array of delegates is essential to an elegant solution. The switch statement is one solution, in which each case of the switch statement is assigned to a task. Suppose that there are about three lines of code associated with each task. In the next revision of the application, 30 additional tasks are added, which results in an additional 90 lines of code. This solution provides linear growth and is not particularly scalable. With an array of delegates, regardless of the number of tasks, the solution is one line of code. Now and in the future, this solution remains one line of code. This solution is more scalable than the switch statement approach. In the following code, an array of delegates facilitates invoking a task from a menu of choices:

 using System; namespace Donis.CSharpBook{     public delegate void Task();     public class Starter{         public static void Main(){             // array of delegates             Task [] tasks={MethodA, MethodB, MethodC};             string resp;             do {                 Console.WriteLine("TaskA - 1");                 Console.WriteLine("TaskB - 2");                 Console.WriteLine("TaskC - 3");                 Console.WriteLine("Exit - x");                 resp=Console.ReadLine();                 if(resp.ToUpper()=="X") {                     break;                 }                 try {                     int choice=int.Parse(resp)-1;                     // as promised, one line of code to invoke                     // the correct method.                     tasks[choice]();                 }                 catch {                     Console.WriteLine("Invalid choice");                 }            } while(true);        }        public static void MethodA() {            Console.WriteLine("Doing TaskA");        }        public static void MethodB() {            Console.WriteLine("Doing TaskB");        }        public static void MethodC() {            Console.WriteLine("Doing TaskC");        }     } } 

System.MulticastDelegate Class

Delegates default to multicast delegates, which inherit from the System.MulticastDelegate class. A multicast delegate is similar to a basket. Multiple function pointers or delegates can be dropped into the basket. The list of delegates is stored in an invocation list, which can even include multiple instances of the same delegate. When you execute the contents of the basket, the function pointers of the delegates are called FIFO. Multicast delegates are useful for invoking a chain of functions.

Combining delegates Multiple delegates are combined using the Combine method, the plus operator (+), or the += assignment operator. Combine is a static method. In C#, the Combine method is called with two delegate parameters or an array of delegates. The method returns a reference to the combined delegate.

The following code combines two delegates separately wrapping functions MethodA and MethodB. When the Combine delegate is invoked, MethodA is run first and MethodB second. This is the order in which the function pointers are added to the multicast delegate. Both the Combine method and the += assignment operators are shown, with the Combine statement commented to avoid duplication.

 using System; namespace Donis.CSharpBook{  public delegate void DelegateClass();      public class Starter{          public static void Main(){              DelegateClass del=MethodA;              del+=MethodB;              // del=(DelegateClass) DelegateClass.Combine(              // new DelegateClass [] {MethodA, MethodB});              del();          }          public static void MethodA() {              Console.WriteLine("MethodA...");          }          public static void MethodB() {              Console.WriteLine("MethodB...");          }      } } 

Removing delegates To remove delegates from a multicast delegate, use the Remove method, the minus operator (-), or the -= assignment operator. Remove is a static method that accepts the source delegate as the first parameter and the delegate to remove as the second parameter. Be careful not to inadvertently remove all delegates from a multicast delegate, which causes a run-time error when invoked. The following code removes MethodB from a multicast delegate. When the delegate is invoked, MethodA is invoked, but not MethodB. The three methodologies are shown, with the Combine statement and operator minus commented to avoid duplication.

 using System; namespace Donis.CSharpBook{      public delegate void DelegateClass();      public class Starter{      public static void Main(){          DelegateClass del=MethodA;          del+=MethodB;          del+=MethodC;          del=del-MethodB;          // del=(DelegateClass) DelegateClass.Remove(del,             (DelegateClass) MethodB);          // del-=MethodB;          del();      }      public static void MethodA() {          Console.WriteLine("MethodA...");      }      public static void MethodB() {          Console.WriteLine("MethodB...");      }      public static void MethodC() {          Console.WriteLine("MethodC...");      }   } } 

Invocation List

Multicast delegates maintain an invocation list, which has an entry for each delegate of the multicast delegate. Entries are added to the invocation list in the same order in which delegates are added. GetInvocationList returns the invocation list as an array of delegates.

This is the syntax of the GetInvocationList method:

  • delegate [] GetInvocationList()

This code retrieves the list of delegates in a multicast delegate, in which each delegate is a wrapper of a function. The name of each function is displayed. The Delegate.Method property returns a MethodInfo type, which encapsulates the function abstracted by the delegate.

 using System; namespace Donis.CSharpBook{      public delegate void DelegateClass();      public class Starter{          public static void Main(){              DelegateClass del=(DelegateClass)              DelegateClass.Combine(new DelegateClass []                  { MethodA, MethodB, MethodA, MethodB } );              del();              foreach(DelegateClass item in                  del.GetInvocationList()) {              Console.WriteLine(item.Method.Name+                  " in invocation list.");          }      }      public static void MethodA() {          Console.WriteLine("MethodA...");      }      public static void MethodB() {          Console.WriteLine("MethodB...");      }    } } 

The delegate signature can contain reference parameters. When invoked, the reference is passed to the called functions. Reference parameters are a way for functions on an invocation list to share state. Value parameters are not shareable across the functions. Because the invocation list is called in sequence, beginning with the first delegate and function, each successive entry in the chain can view changes in the reference parameter. The next item in the chain can view those changes and possibly change the state again. In this way, when the multicast delegate is invoked, the state information is propagated along the invocation list. Furthermore, functions in invocation lists that have the same target object or class share the state of the object or class. The following code uses the reference parameter of a multicast delegate as a counter. The delegate has a value and reference parameter. The value parameter is lost between function calls in the invocation list. However, the reference parameter persists.

 using System; namespace Donis.CSharpBook{      public delegate void DelegateClass(int valCount,          ref int refCount);      public class Counter{          public static void Main(){              DelegateClass del=(DelegateClass) AddOne+                 (DelegateClass) AddTwo+ (DelegateClass) AddOne;              int valCount=0;              int refCount=0;              del(valCount, ref refCount);              Console.WriteLine("Value count = {0}",                  valCount); // 0              Console.WriteLine("Reference count = {0}",                  refCount); // 4          }          public static void AddOne(int valCount,                  ref int refCount){              ++valCount;              ++refCount;          }          public static void AddTwo(int valCount,                  ref int refCount) {                  valCount+=2;                  refCount+=2;              }          } } 

You can access the invocation list to execute each delegate and function therein directly. There are two reasons to invoke the invocation list directly. First, invoke the delegates explicitly to obtain the return of each delegate. When the invocation list is invoked implicitly, only the return of the last function is garnered. Second, invoke the invocation list directly in special circumstances (for example, to modify how exceptions are handled in a multicast delegate, which is described later).

The following code uses the invocation list to calculate a factorial. The Incrementer method increments a number, which is a reference parameter. The incremented value is returned from the method. Five delegates are created and initialized with the Increment method and combined into a multicast delegate. The foreach loop iterates the invocation list, multiplying the results of each function to calculate a factorial.

 using System; namespace Donis.CSharpBook{      public delegate int IncrementDelegate(          ref short refCount);      public class Factorial{          public static void Main(){              IncrementDelegate [] values=                  { Incrementer, Incrementer,                  Incrementer, Incrementer,                  Incrementer};              IncrementDelegate del=(IncrementDelegate)              IncrementDelegate.Combine(                  values);              long result=1;              short count=1;              foreach(IncrementDelegate number                      in del.GetInvocationList()) {                  result=result*number(ref count);              }              Console.WriteLine("{0} factorial is {1}",              del.GetInvocationList().Length,                  result);          }          public static int Incrementer(                  ref short refCount){              return refCount++;          }      } } 

Methods and properties Several members of delegate object, such as GetInvocationList, have already been introduced. Table 8-1 is a list of some of the public properties and methods of the MulticastDelegate type. Some of the members are inherited from System.Delegate.

Table 8-1: Delegate Members

Member

Description

BeginInvoke

Invokes a delegate asynchronously.

Combine

Combines delegates into a multicast delegate. This is a static method.

CreateDelegate

Defines a delegate at run time. This is a static method.

DynamicInvoke

Dynamically invokes a delegate that was created at run time.

EndInvoke

Requests the results of a delegate that was executed asynchronously.

Invoke

Executes a delegate, which calls all functions contained in the delegate.

Method

This is a property that returns the MethodInfo type of the last function in the invocation list. MethodInfo provides a description of a function.

Remove

Removes a delegate from a multicast delegate. This is a static method.

RemoveAll

Removes the invocation list of a delegate from the invocation list of another delegate. The result is returned. This is a static method.

Target

This is a property that returns the instance of the last function in the invocation list. For static functions, Target is null.

Generics and Delegates

A delegate can contain closed generic methods, whereas open generic methods cannot be added to a delegate. Some permutation of the generic method must be compatible with the signature of the delegate. Otherwise, the generic cannot be added to the invocation list. If the generic method contains actual parameters, the type is inferred. However, the return type cannot be inferred and must be specified.

In addition, the delegate itself can be generic and closed when the delegate is created. Functions added to a generic delegate need to match the signature of the closed generic delegate.

This is an example of a normal delegate initialized with a generic method:

 using System; namespace Donis.CSharpBook{      public delegate void DelegateClass(int data);      public class Starter{          public static void Main(){              DelegateClass del1=MethodA<int>;              del1(5);              DelegateClass del2=MethodA;              del1(10); // inferred          }          public static void MethodA <T>(T data) {              Console.WriteLine("MethodA ({0})", data);          }      } } 

This is an example of a generic delegate being initialized with a normal method:

 using System; namespace Donis.CSharpBook{      public delegate void DelegateClass<T>(T data);      public class Starter{          public static void Main(){              DelegateClass <string> del=MethodA;              del("data");          }          public static void MethodA(string data) {              Console.WriteLine("MethodA ({0})", data);          }      } } 

Asynchronous Invocation

Delegates are invoked synchronously via the Invoke method or call operator. Delegates can also be invoked asynchronously on a separate worker thread. A delegate performing a time-consuming task might improve application performance by running on a separate thread. This is especially true for Windows Forms applications, in which responsiveness of the user interface is a concern. The user interface could freeze for an extended period time while a synchronous delegate is performing an extended task. Other types of operations benefit from asynchronous delegates: network services, methods dependent on hardware devices, timed routines, and more. Synchronous solutions are simpler. However, this often falls short of the sophistication required for a professional application. Developers must be concerned with thread synchronization in asynchronous executions, which undoubtedly adds complexity. There is much more to writing robust multithread applications then simply spawning threads.

When a delegate is defined, the C# compiler adds several methods for invoking function pointers. The Invoke method is for synchronous invocation, while the BeginInvoke and EndInvoke methods are included for asynchronous invocation. The default for the call operator is synchronous execution.

BeginInvoke Method

BeginInvoke is callable on single-cast delegates alone. A single-cast delegate is a multicast delegate that contains a single function pointer. BeginInvoke adds the operation to a queue, where it is then serviced by a separate thread from a Common Language Runtime (CLR) managed thread pool. Therefore, functions on the invocation list run on a different thread from the caller of the delegate. The thread pool for asynchronous execution has a maximum of 25 threads per processor.

This is the syntax of the BeginInvoke method:

  • IAsyncResult BeginInvoke(arguments, AsyncCallback callback, object AsynState)

BeginInvoke begins with the parameters of the delegate signature. If the delegate signature names four arguments, those are the first four parameters of BeginInvoke. The next parameter is the completion routine. This routine is called back when the asynchronous call has completed. The final parameter is a state object. The completion routine and state objects are optional and either can be set to null. The return is an IAsyncResult object, which maintains the state of the object. IAsyncResult has four properties, which are listed in Table 8-2.

Table 8-2: IAsyncResult Properties

Property

Description

AsyncState

The state object that has information on the asynchronous operation.

AsyncWaitHandle

Use the WaitHandle to block until the asynchronous operation is completed.

CompleteSynchronously

Indicates that the operation completed synchronously.

IsCompleted

Indicates that the asynchronous operation has completed.

The following code highlights the IAsyncResult and delegate state object:

 using System; using System.Threading; namespace Donis.CSharpBook{      public delegate void DelegateClass();      public class Starter{          public static void Main(){              DelegateClass del=MethodA;              DelegateStateBag state=new DelegateStateBag();              IAsyncResult ar=del.BeginInvoke(Callback, state);              if(ar.IsCompleted==true) {                  Console.WriteLine("MethodA completed");              }              else {                  Console.WriteLine("MethodA not completed");              }              ar.AsyncWaitHandle.WaitOne();              // doing something else              Thread.Sleep(100);              lock(state) {                  Console.WriteLine("Back in Main");                  Console.WriteLine(state.message);              }          }          public static void Callback(IAsyncResult ar) {              DelegateStateBag state=                  (DelegateStateBag) ar.AsyncState;              lock(state) {                  Console.WriteLine("Callback running");                      ((DelegateStateBag) ar.AsyncState).message=                      "State object modified in callback.";              }          }          public static void MethodA() {              Console.WriteLine("MethodA running...");              Thread.Sleep(200);          }      }      class DelegateStateBag {          public string message;      } } 

This is the result of the application:

 MethodA not completed MethodA running... Callback running Back in Main State object modified in callback. 

Interestingly, the not completed message is received before MethodA has even started. This confirms that BeginInvoke does not directly invoke the function in the delegate. Rather, it adds a request to the thread pool queue, which is eventually handled. There is a Thread .Sleep statement near the end of the Main method. If removed, a race condition would exist between Main and the completion routine called Callback. What is the source of the race condition? Note the WaitHandle.WaitOne command in Main, which blocks Main until the asynchronous operation is complete. As the completion routine, Callback also waits for the asynchronous operation to finish. When the asynchronous operation finishes, the Main and Callback routines both resume, and the race begins. Callback needs to update the state object before Main displays the contents of the state object. The Thread.Sleep statement is a primitive way of removing the race condition and allowing the Callback routine to lock the shared state object first.

EndInvoke Method

EndInvoke provides that the result of an asynchronous operation is callable in the completion routine or the thread of the delegate caller. EndInvoke has the same signature as the delegate without the value parameters and has an additional final parameter, which is an IAsyncResult object. To obtain the signature of the EndInvoke method, remove the value parameters from the delegate signature and add an IAsyncResult parameter. The IAsyncResult parameter is the same IAsyncResult object that is returned from BeginInvoke. The return type of EndInvoke is identical to the return of the delegate.

This is the syntax of the EndInvoke method:

  • returntype EndInvoke(ref_out_arguments, IAsyncResult ar)

Calling EndInvoke before functions of the asynchronous delegate has finished executing blocks the caller until the operation is completed. For each BeginInvoke, there should be a complementing EndInvoke to confirm the results of the operation, detect an exception, and to allow the CLR to properly clean up the delegate. Calling the EndInvoke method more than once on the same delegate yields undefined results and should be avoided.

Here is sample code for the EndInvoke method:

 using System; using System.Threading; namespace Donis.CSharpBook{      public delegate int DelegateClass(out DateTime start,          out DateTime stop);      public class Starter{          public static void Main(){              DelegateClass del=MethodA;              DateTime start;              DateTime stop;              IAsyncResult ar=del.BeginInvoke(out start, out stop,                  null, null);              ar.AsyncWaitHandle.WaitOne();              // doing something else              int elapse=del.EndInvoke(out start, out stop, ar);              Console.WriteLine("Start time: {0}",              start.ToLongTimeString());              Console.WriteLine("Stop time: {0}",              stop.ToLongTimeString());              Console.WriteLine("Elapse time: {0} seconds",                  elapse);          }          public static int MethodA(out DateTime start,                  out DateTime stop) {              start=DateTime.Now;              Thread.Sleep(5000);              stop=DateTime.Now;              return (stop-start).Seconds;          }      } } 

Asynchronous Delegate Diagram

The preceding text explains how to invoke a delegate asynchronously and to obtain the results. Figure 8-1 diagrams the relationship and sequence of operations. This includes the delegate, the BeginInvoke method, the EndInvoke method, and the delegate completion routine.

image from book
Figure 8-1: Asynchronous processing of a delegate

Delegate Internals

Delegates may appear to be magical to developers because so much is hidden. The magician is the C# compiler. This section explains the magic of the compiler. Why should you care? Understanding how delegate are implemented internally should help in the how, where, and when to use delegates.

The C# compiler builds a class from the definition of a delegate. The class shares the name of the delegate and has four methods: a constructor, Invoke, BeginInvoke, and EndInvoke. Importantly, the delegate class is derived from System.MulticastDelegate. This is a typical definition of a delegate:

 public delegate int ADelegate(int arg1, ref int arg2); 

As viewed in ildasm, C# creates the ADelegate class from this delegate. (See Figure 8-2.)

image from book
Figure 8-2: C# manifested delegate class

Members of the delegate class, such as Invoke and BeginInvoke, were described earlier in this chapter. However, it is comforting to actually see them. It is not magic after all. There may be a slight surprise: The constructor of the delegate has two arguments. The code in this chapter shows the delegate constructor being called with a single parameter. The C# compiler helps us here: It interprets the single parameter as a target object and function pointer, which are the parameters of the two argument constructors. Also notice that the signatures of Invoke, BeginInvoke, and EndInvoke were defined as described earlier in this chapter.

Exceptions

What happens if an unhandled exception is raised in a multicast delegate? If the delegate is invoked inside a protected block of code, the exception is trapped. The invocation list is called until an exception occurs. Functions later in the invocation list do not execute. Let us assume that the invocation list has pointers to MethodA, MethodB, MethodC, and MethodD. They were also added to the delegate in that order. If an unhandled exception is raised in MethodB, MethodC and MethodD are not called. The situation might appear to be more complicated if the exception is raised in a delegate that is running asynchronously. In this circumstance, the exception is raised on a different thread. However, the run time will route the exception back to the calling thread, as evidenced in the following code. Therefore, the exception is handled as if the delegate were called synchronously.

 using System; using System.Threading; namespace Donis.CSharpBook{      public delegate void DelegateClass();      public class Starter{          public static void Main(){              Console.WriteLine("Running on primary thread");              try {                  DelegateClass del=MethodA;                  IAsyncResult ar=del.BeginInvoke(null, null);                  del.EndInvoke(ar);              }              catch(Exception except){                  Console.WriteLine("Running on primary thread");                  Console.WriteLine("Exception caught: "                      +except.Message);              }          }          public static void MethodA() {              if(Thread.CurrentThread.IsThreadPoolThread==true) {                  Console.WriteLine(                      "Running on a thread pool thread");              }              else {                  Console.WriteLine("Running on primary thread");              }              throw new Exception("failure");          }      } } 




Programming Microsoft Visual C# 2005(c) The Language
Microsoft Visual Basic 2005 BASICS
ISBN: 0619267208
EAN: 2147483647
Year: 2007
Pages: 161

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