C H A P T E R  13

Delegates

What Is a Delegate?

You can think of a delegate as an object that holds one or more methods. Normally, of course, you wouldn’t think of “executing” an object, but a delegate is different from a typical object. You can execute a delegate, and when you do so, it executes the method or methods that it “holds.”

In this chapter I'll explain the syntax and semantics of creating and using delegates. In later chapters you'll see how you can use delegates to pass executable code from one method to another—and why that's a useful thing.

We'll start with the example code on the next page. Don't worry if everything isn't completely clear at this point, because I'll explain the details of delegates throughout the rest of the chapter.

  • The code starts with the declaration of a delegate type called MyDel. (Yes, a delegate typenot a delegate object. We'll get to this shortly.)
  • Class Program declares three methods: PrintLow, PrintHigh, and Main. The delegate object we will create shortly will hold either the PrintLow or PrintHigh method—but which one will be used won't be determined until run time.
  • Main declares a local variable called del, which will hold a reference to a delegate object of type MyDel. This doesn't create the object—it just creates the variable that will hold a reference to the delegate object, which will be created and assigned to it several lines below.
  • Main creates an object of the .NET class Random, which is a random-number-generator class. The program then calls the object's Next method, with 99 as its input parameter. This returns a random integer between 0 and 99, and stores that value in local variable randomValue;.
  • The next line checks whether the random value returned and stored is less than 50. (Notice that we're using the ternary conditional operator here to return one or the other of the delegate objects.)
    • If the value is less than 50, it creates a MyDel delegate object and initializes it to hold a reference to the PrintLow method.
    • Otherwise, it creates a MyDel delegate object that holds a reference to the PrintHigh method.
  • Finally, Main executes the del delegate object, which executes whichever method (PrintLow or PrintHigh) it's holding.

Image Note If you're coming from a C++ background, the fastest way for you to understand delegates is to think of them as type-safe, object-oriented C++ function pointers on steroids.

   delegate void MyDel(int value);   // Declare delegate TYPE.

   class Program
   {
      void PrintLow( int value )
      {
         Console.WriteLine( "{0} - Low Value", value );
      }

      void PrintHigh( int value )
      {
         Console.WriteLine( "{0} - High Value", value );
      }

      static void Main( )
      {
         Program program = new Program();

         MyDel   del;            // Declare delegate variable.

         // Create random-integer-generator object and get a random
         // number between 0 and 99.
         Random  rand    = new Random();
         int randomValue = rand.Next( 99 );

         // Create a delegate object that contains either PrintLow or
         // PrintHigh, and assign the object to the del variable.
         del = randomValue < 50
                  ? new MyDel( program.PrintLow  )
                  : new MyDel( program.PrintHigh );

         del( randomValue );    // Execute the delegate.
      }
   }

Because we're using the random-number generator, the program will produce different values on different runs. One run of the program produced the following output:


28 - Low Value

An Overview of Delegates

Now let's go into the details. A delegate is a user-defined type, just as a class is a user-defined type. But whereas a class represents a collection of data and methods, a delegate holds one or more methods and a set of predefined operations.

You use a delegate by performing the following steps. I'll go through each of these steps in detail in the following sections.

  1. Declare a delegate type. A delegate declaration looks like a method declaration, except that it doesn't have an implementation block.
  2. Declare a delegate variable of the delegate type.
  3. Create an object of the delegate type and assign it to the delegate variable. The new delegate object includes a reference to a method that must have the same signature and return type as the delegate type defined in the first step.
  4. You can optionally add additional methods into the delegate object. These methods must have the same signature and return type as the delegate type defined in the first step.
  5. Throughout your code you can then invoke the delegate, just as it if it were a method. When you invoke the delegate, each of the methods it contains is executed.

In looking at the previous steps, you might have noticed that they're similar to the steps in creating and using a class. Figure 13-1 compares the processes of creating and using classes and delegates.

Image

Figure 13-1. A delegate is a user-defined reference type, like a class.

You can think of a delegate as an object that contains an ordered list of methods with the same signature and return type, as illustrated in Figure 13-2.

  • The list of methods is called the invocation list.
  • Methods held by a delegate can be from any class or struct, as long as they match both of the following:
    • The delegate's return type
    • The delegate's signature (including ref and out modifiers)
  • Methods in the invocation list can be either instance methods or static methods.
  • When a delegate is invoked, each method in its invocation list is executed.
Image

Figure 13-2. A delegate as a list of methods

Declaring the Delegate Type

As I stated in the previous section, delegates are types, just as classes are types. And as with classes, a delegate type must be declared before you can use it to create variables and objects of the type. The following example code declares a delegate type:

        Keyword      Delegate type name
             ↓                      ↓
   delegate void MyDel( int x );
             ↑        ↑
                     Return type     Signature

The declaration of a delegate type looks much like the declaration of a method, in that it has both a return type and a signature. The return type and signature specify the form of the methods that the delegate will accept.

The preceding declaration specifies that delegate objects of type MyDel will only accept methods that have a single int parameter and that have no return value. Figure 13-3 shows a representation of the delegate type on the left and the delegate object on the right.

Image

Figure 13-3. Delegate type and object

The delegate type declaration differs from a method declaration in two ways. The delegate type declaration

  • Is prefaced with the keyword delegate
  • Does not have a method body

Image Note  Even though the delegate type declaration looks like a method declaration, it doesn't need to be declared inside a class because it's a type declaration.

Creating the Delegate Object

A delegate is a reference type and therefore has both a reference and an object. After a delegate type is declared, you can declare variables and create objects of the type. The following code shows the declaration of a variable of a delegate type:

   Delegate type     Variable
               ↓              ↓
      MyDel  delVar;

There are two ways you can create a delegate object. The first is to use an object-creation expression with the new operator, as shown in the following code. The operand of the new operator consists of the following:

  • The delegate type name.
  • A set of parentheses containing the name of a method to use as the first member in the invocation list. The method can be either an instance method or a static method.
                                            Instance method
                                                       ↓             
delVar = new MyDel( myInstObj.MyM1 );       // Create delegate and save ref.
dVar   = new MyDel( SClass.OtherM2 );       // Create delegate and save ref.
                                                       ↑
                      Static method

You can also use the shortcut syntax, which consists of just the method specifier, as shown in the following code. This code and the preceding code are semantically equivalent. Using the shortcut syntax works because there is an implicit conversion between a method name and a compatible delegate type.

   delVar = myInstObj.MyM1;          // Create delegate and save reference.
   dVar   = SClass.OtherM2;          // Create delegate and save reference.

For example, the following code creates two delegate objects: one with an instance method and the other with a static method. Figure 13-4 shows the instantiations of the delegates. This code assumes that there is an object called myInstObj, which is an instance of a class that has defined a method called MyM1, which returns no value and takes an int as a parameter. It also assumes that there is a class called SClass, which has a static method OtherM2 with a return type and signature matching those of delegate MyDel.

   delegate void MyDel(int x);               // Declare delegate type.
   MyDel delVar, dVar;                       // Create two delegate variables.
                                                   Instance method
                            ↓     
   delVar = new MyDel( myInstObj.MyM1 );     // Create delegate and save ref.
   dVar   = new MyDel( SClass.OtherM2 );     // Create delegate and save ref.
                                                              ↑
                                                      Static method
Image

Figure 13-4. Instantiating the delegates

Besides allocating the memory for the delegate, creating a delegate object also places the first method in the delegate's invocation list.

You can also create the variable and instantiate the object in the same statement, using the initializer syntax. For example, the following statements also produce the same configuration shown in Figure 13-4:

   MyDel delVar = new MyDel( myInstObj.MyM1 );
   MyDel dVar   = new MyDel( SClass.OtherM2 );

The following statements use the shortcut syntax, but again produces the results shown in Figure 13-4:

   MyDel delVar = myInstObj.MyM1;
   MyDel dVar   = SClass.OtherM2;

Assigning Delegates

Because delegates are reference types, you can change the reference contained in a delegate variable by assigning to it. The old delegate object will be disposed of by the garbage collector (GC) when it gets around to it.

For example, the following code sets and then changes the value of delVar. Figure 13-5 illustrates the code.

   MyDel delVar;
   delVar = myInstObj.MyM1;   // Create and assign the delegate object.

      ...
   delVar = SClass.OtherM2;   // Create and assign the new delegate object.
Image

Figure 13-5. Assigning to a delegate variable

Combining Delegates

All the delegates you've seen so far have had only a single method in their invocation lists. Delegates can be “combined” by using the addition operator. The result of the operation is the creation of a new delegate, with an invocation list that is the concatenation of copies of the invocation lists of the two operand delegates.

For example, the following code creates three delegates. The third delegate is created from the combination of the first two.

   MyDel delA = myInstObj.MyM1;
   MyDel delB = SClass.OtherM2;
   
   MyDel delC = delA + delB;                  // Has combined invocation list

Although the term combining delegates might give the impression that the operand delegates are modified, they are not changed at all. In fact, delegates are immutable. After a delegate object is created, it cannot be changed.

Figure 13-6 illustrates the results of the preceding code. Notice that the operand delegates remain unchanged.

Image

Figure 13-6. Combining delegates

Adding Methods to Delegates

Although you saw in the previous section that delegates are, in reality, immutable, C# provides syntax for making it appear that you can add a method to a delegate, using the += operator.

For example, the following code “adds” two methods to the invocation list of the delegate. The methods are added to the bottom of the invocation list. Figure 13-7 shows the result.

   MyDel delVar  = inst.MyM1;     // Create and initialize.
   delVar       += SCl.m3;        // Add a method.
   delVar       += X.Act;         // Add a method.
Image

Figure 13-7. Result of “adding” methods to a delegate. In reality, because delegates are immutable, the resulting delegate with three methods in its invocation list is an entirely new delegate pointed at by the variable.

What is actually happening, of course, is that when the += operator is used, a new delegate is created, with an invocation list that is the combination of the delegate on the left and the method listed on the right. This new delegate is then assigned to the delVar variable.

You can add a method to a delegate more than once. Each time you add it, it creates a new element in the invocation list.

Removing Methods from a Delegate

You can also remove a method from a delegate, using the -= operator. The following line of code shows the use of the operator. Figure 13-8 shows the result of this code when applied to the delegate illustrated in Figure 13-7.

   delVar -= SCl.m3;             // Remove the method from the delegate.
Image

Figure 13-8. Result of removing a method from a delegate

As with adding a method to a delegate, the resulting delegate is actually a new delegate. The new delegate is a copy of the old delegate—but its invocation list no longer contains the reference to the method that was removed.

The following are some things to remember when removing methods:

  • If there are multiple entries for a method in the invocation list, the -= operator starts searching at the bottom of the list and removes the first instance of the matching method it finds.
  • Attempting to delete a method that is not in the invocation list has no effect.
  • Attempting to invoke an empty delegate throws an exception. You can check whether a delegate's invocation list is empty by comparing the delegate to null. If the invocation list is empty, the delegate is null.

Invoking a Delegate

You invoke a delegate by calling it, as if it were simply a method. The parameters used to invoke the delegate are used to invoke each of the methods on the invocation list (unless one of the parameters is an output parameter, which I'll cover shortly).

For example, the delegate delVar, as shown in the following code, takes a single integer input value. Invoking the delegate with a parameter causes it to invoke each of the members in its invocation list with the same parameter value (55, in this case). Figure 13-9 illustrates the invocation.

   MyDel delVar  = inst.MyM1;
   delVar       += SCl.m3;
   delVar       += X.Act;
      ...
   delVar( 55 );                              // Invoke the delegate.
      ...
Image

Figure 13-9. When the delegate is invoked, it executes each of the methods in its invocation list, with the same parameters with which it was called.

If a method is in the invocation list more than once, then when the delegate is invoked, the method will be called each time it is encountered in the list.

Delegate Example

The following code defines and uses a delegate with no parameters and no return value. Note the following about the code:

  • Class Test defines two print functions.
  • Method Main creates an instance of the delegate and then adds three more methods.
  • The program then invokes the delegate, which calls its methods. Before invoking the delegate, however, it checks to make sure it's not null.
   // Define a delegate type with no return value and no parameters.
   delegate void PrintFunction();

   class Test
   {
       public void Print1()
       { Console.WriteLine("Print1 -- instance"); }

       public static void Print2()
       { Console.WriteLine("Print2 -- static"); }
   }

   class Program
   {
       static void Main()
       {
           Test t = new Test();    // Create a test class instance.
           PrintFunction pf;       // Create a null delegate.

           pf = t.Print1;          // Instantiate and initialize the delegate.

           // Add three more methods to the delegate.
           pf += Test.Print2;
           pf += t.Print1;
           pf += Test.Print2;
           // The delegate now contains four methods.

           if( null != pf )           // Make sure the delegate isn't null.
              pf();                   // Invoke the delegate.
           else
              Console.WriteLine("Delegate is empty");
       }
   }

This code produces the following output:


Print1 -- instance
Print2 -- static
Print1 -- instance
Print2 -- static

Invoking Delegates with Return Values

If a delegate has a return value and more than one method in its invocation list, the following occurs:

  • The value returned by the last method in the invocation list is the value returned from the delegate invocation.
  • The return values from all the other methods in the invocation list are ignored.

For example, the following code declares a delegate that returns an int value. Main creates an object of the delegate and adds two additional methods. It then calls the delegate in the WriteLine statement and prints its return value. Figure 13-10 shows a graphical representation of the code.

   delegate int MyDel( );                 // Declare delegate with return value.
   class MyClass {
      int IntValue = 5;
      public int Add2() { IntValue += 2; return IntValue;}
      public int Add3() { IntValue += 3; return IntValue;}
   }
   
   class Program {
      static void Main( ) {
         MyClass mc = new MyClass();
         MyDel mDel = mc.Add2;          // Create and initialize the delegate.
         mDel += mc.Add3;               // Add a method.
         mDel += mc.Add2;               // Add a method.
         Console.WriteLine("Value: {0}", mDel() );
      }
                                                                            ↑
   }                       Invoke the delegate and use the return value.

This code produces the following output:


Value: 12
Image

Figure 13-10. The return value of the last method executed is the value returned by the delegate.

Invoking Delegates with Reference Parameters

If a delegate has a reference parameter, the value of the parameter can change upon return from one or more of the methods in the invocation list.

  • When calling the next method in the invocation list, the new value of the parameter—not the initial value—is the one passed to the next method.

For example, the following code invokes a delegate with a reference parameter. Figure 13-11 illustrates the code.

   delegate void MyDel( ref int X );
   
   class MyClass
   {
      public void Add2(ref int x) { x += 2; }
      public void Add3(ref int x) { x += 3; }
      static void Main()
      {
         MyClass mc = new MyClass();
   
         MyDel mDel = mc.Add2;
         mDel += mc.Add3;
         mDel += mc.Add2;
   
         int x = 5;
         mDel(ref x);
   
         Console.WriteLine("Value: {0}", x);
      }
   }

This code produces the following output:


Value: 12
Image

Figure 13-11. The value of a reference parameter can change between calls.

Anonymous Methods

So far, you've seen that you can use either static methods or instance methods to instantiate a delegate. In either case, the method itself can be called explicitly from other parts of the code and, of course, must be a member of some class or struct.

What if, however, the method is used only one time—to instantiate the delegate? In that case, other than the syntactic requirement for creating the delegate, there is no real need for a separate, named method. Anonymous methods allow you to dispense with the separate, named method.

  • An anonymous method is a method that is declared inline, at the point of instantiating a delegate.

For example, Figure 13-12 shows two versions of the same class. The version on the left declares and uses a method named Add20. The version on the right uses an anonymous method instead. The nonshaded code of both versions is identical.

Image

Figure 13-12. Comparing a named method and an anonymous method

Both sets of code in Figure 13-12 produce the following output:


25
26

Using Anonymous Methods

You can use an anonymous method in the following places:

  • As an initializer expression when declaring a delegate variable.
  • On the right side of an assignment statement when combining delegates.
  • On the right side of an assignment statement adding a delegate to an event. Chapter 14 covers events.

Syntax of Anonymous Methods

The syntax of an anonymous method expression includes the following components:

  • The type keyword delegate
  • The parameter list, which can be omitted if the statement block doesn't use any parameters
  • The statement block, which contains the code of the anonymous method
                                Parameter
        Keyword               list                         Statement block
             ↓                     ↓                                          ↓                     
   delegate ( Parameters )  { ImplementationCode }
Return Type

An anonymous method does not explicitly declare a return type. The behavior of the implementation code itself, however, must match the delegate's return type by returning a value of that type. If the delegate has a return type of void, then the anonymous method code cannot return a value.

For example, in the following code, the delegate's return type is int. The implementation code of the anonymous method must therefore return an int on all pathways through the code.

              Return type of delegate type
                           ↓
   delegate int OtherDel(int InParam);
   
   static void Main()
   {
       OtherDel del = delegate(int x)
                   {
                       return x + 20 ;                   // Returns an int
                   };
          ...
   }
Parameters

Except in the case of array parameters, the parameter list of an anonymous method must match that of the delegate with respect to the following three characteristics:

  • Number of parameters
  • Types and positions of the parameters
  • Modifiers

You can simplify the parameter list of an anonymous method by leaving the parentheses empty or omitting them altogether, but only if both of the following are true:

  • The delegate's parameter list does not contain any out parameters.
  • The anonymous method does not use any parameters.

For example, the following code declares a delegate that does not have any out parameters and an anonymous method that does not use any parameters. Since both conditions are met, you can omit the parameter list from the anonymous method.

   delegate void SomeDel ( int X );                 // Declare the delegate type.   
   SomeDel SDel = delegate                          // Parameter list omitted
                  {
                     PrintMessage();
                     Cleanup();
                  };
The params Parameters

If the delegate declaration's parameter list contains a params parameter, then the params keyword is omitted from the parameter list of the anonymous method. For example, in the following code:

  • The delegate type declaration specifies the last parameter as a params type parameter.
  • The anonymous method's parameter list, however, must omit the params keyword.
              params keyword used in delegate type declaration
                                                                    ↓
   delegate void SomeDel( int X, params int[] Y);
                                        params keyword omitted in matching anonymous method
                                                                      ↓
   SomeDel mDel = delegate (int X, int[] Y)
            {
               ...
            };

Scope of Variables and Parameters

The scopes of parameters and local variables declared inside an anonymous method are limited to the body of the implementation code, as illustrated in Figure 13-13.

For example, the following anonymous method defines parameter y and local variable z. After the close of the body of the anonymous method, y and z are no longer in scope. The last line of the code would produce a compile error.

Image

Figure 13-13. Scope of variables and parameters

Outer Variables

Unlike the named methods of a delegate, anonymous methods have access to the local variables and environment of the scope surrounding them.

  • Variables from the surrounding scope are called outer variables.
  • An outer variable used in the implementation code of an anonymous method is said to be captured by the method.

For example, the code in Figure 13-14 shows variable x defined outside the anonymous method. The code in the method, however, has access to x and can print its value.

Image

Figure 13-14. Using an outer variable

Extension of a Captured Variable's Lifetime

A captured outer variable remains alive as long as its capturing method is part of the delegate, even if the variable would have normally gone out of scope.

For example, the code in Figure 13-15 illustrates the extension of a captured variable's lifetime.

  • Local variable x is declared and initialized inside a block.
  • Delegate mDel is then instantiated, using an anonymous method that captures outer variable x.
  • When the block is closed, x goes out of scope.
  • If the WriteLine statement following the close of the block were to be uncommented, it would cause a compile error, because it references x, which is now out of scope.
  • The anonymous method inside delegate mDel, however, maintains x in its environment and prints its value when mDel is invoked.
Image

Figure 13-15. Variable captured in an anonymous method

The code in the figure produces the following output:


Value of x: 5

Lambda Expressions

C# 2.0 introduced anonymous methods, which we've just looked at. The syntax for anonymous methods, however, is somewhat verbose and requires information that the compiler itself already knows. Rather than requiring you to include this redundant information, C# 3.0 introduced lambda expressions, which pare down the syntax of anonymous methods. You'll probably want to use lambda expressions instead of anonymous methods. In fact, if lambda expressions had been introduced first, there never would have been anonymous methods.

In the anonymous method syntax, the delegate keyword is redundant because the compiler can already see that you're assigning the method to a delegate. You can easily transform an anonymous method into a lambda expression by doing the following:

  • Delete the delegate keyword.
  • Place the lambda operator, =>, between the parameter list and the body of the anonymous method. The lambda operator is read as “goes to.”

The following code shows this transformation. The first line shows an anonymous method being assigned to variable del. The second line shows the same anonymous method, after having been transformed into a lambda expression, being assigned to variable le1.

   MyDel del = delegate(int x)    { return x + 1; } ;     // Anonymous method
   MyDel le1 =         (int x) => { return x + 1; } ;     // Lambda expression

Image Note  The term lambda expression comes from the lambda calculus, which was developed in the 1920s and 1930s by mathematician Alonzo Church and others. The lambda calculus is a system for representing functions and uses the Greek letter lambda () to represent a nameless function. More recently, functional programming languages such as Lisp and its dialects use the term to represent expressions that can be used to directly describe the definition of a function, rather than using a name for it.

This simple transformation is less verbose and looks cleaner, but it only saves you six characters. There's more, however, that the compiler can infer, allowing you to simplify the lambda expression further, as shown in the following code.

  • From the delegate's declaration, the compiler also knows the types of the delegate's parameters, so the lambda expression allows you to leave out the parameter types, as shown in the assignment to le2.
    • Parameters listed with their types are called explicitly typed.
    • Those listed without their types are called implicitly typed.
  • If there's only a single, implicitly typed parameter, you can leave off the parentheses surrounding it, as shown in the assignment to le3.
  • Finally, lambda expressions allow the body of the expression to be either a statement block or an expression. If the statement block contains a single return statement, you can replace the statement block with just the expression that follows the return keyword, as shown in the assignment to le4.
   MyDel del = delegate(int x)    { return x + 1; } ;     // Anonymous method
   MyDel le1 =         (int x) => { return x + 1; } ;     // Lambda expression
   MyDel le2 =             (x) => { return x + 1; } ;     // Lambda expression
   MyDel le3 =              x  => { return x + 1; } ;     // Lambda expression
   MyDel le4 =              x  =>          x + 1    ;     // Lambda expression

The final form of the lambda expression has about one-fourth the characters of the original anonymous method and is cleaner and easier to understand.

The following code shows the full transformation. The first line of Main shows an anonymous method being assigned to variable del. The second line shows the same anonymous method, after having been transformed into a lambda expression, being assigned to variable le1.

   delegate double MyDel(int par);

   class Program
   {   
      static void Main()
      {
         MyDel del = delegate(int x)    { return x + 1; } ;  // Anonymous method
   
         MyDel le1 =         (int x) => { return x + 1; } ;  // Lambda expression
         MyDel le2 =             (x) => { return x + 1; } ;
         MyDel le3 =              x  => { return x + 1; } ;
         MyDel le4 =              x  =>          x + 1    ;
   
         Console.WriteLine("{0}", del (12));
         Console.WriteLine("{0}", le1 (12));  Console.WriteLine("{0}", le2 (12));
         Console.WriteLine("{0}", le3 (12));  Console.WriteLine("{0}", le4 (12));
      }
   }

This code produces the following output:


13
13
13
13
13

Some important points about lambda expression parameter lists are the following:

  • The parameters in the parameter list of a lambda expression must match that of the delegate in number, type, and position.
  • The parameters in the parameter list of an expression do not have to include the type (i.e., they're implicitly typed) unless the delegate has either ref or out parameters—in which case the types are required (i.e., they're explicitly typed).
  • If there is only a single parameter and it is implicitly typed, the surrounding parentheses can be omitted. Otherwise, they are required.
  • If there are no parameters, you must use an empty set of parentheses.

Figure 13-16 shows the syntax for lambda expressions.

Image

Figure 13-16. The syntax for lambda expressions consists of the lambda operator, with the parameter section on the left and the lambda body on the right.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.58.194