Chapter 15. Delegates

What Is a Delegate?

A delegate can be thought of as an object that contains an ordered list of methods with the same signature and return type.

  • The list of methods is called the invocation list.

  • When a delegate is invoked, it calls each method in its invocation list.

Figure 15-1 represents a delegate with four methods in its invocation list.

A delegate as a list of methods

Figure 15-1. A delegate as a list of methods

A delegate with a single method is similar to a function pointer in C++. Unlike function pointers, however, delegates are object oriented and type-safe.

Methods in the Invocation List

Methods held by a delegate can be from any class or struct, as long as they match both the delegate's

  • Return type

  • Signature (including ref and out modifiers)

Methods in the invocation list can be either instance methods or static methods.

Declaring the Delegate Type

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.

  • The delegate type declaration, as with all type declarations, does not need to be declared inside a class.

Declaring the Delegate Type

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.

For example, the following code declares delegate type MyDel. The declaration specifies that delegates of this type will accept only methods that return no value and have a single int parameter. Figure 15-2 shows a representation of the delegate type on the left, and the delegate object on the right.

Declaring the Delegate Type
Delegate type and object

Figure 15-2. 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

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:

Creating the Delegate Object

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.

Creating the Delegate Object

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 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 15-3 shows the instantiations of the delegates. This code assumes that there is a class object called myInstObj, which has a method called MyM1 that 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.

Creating the Delegate Object
Instantiating the delegates

Figure 15-3. 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 15-3.

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

The following statements use the shortcut syntax, but again produce the results shown in Figure 15-3.

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 15-4 illustrates the code.

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

      ...
   delVar = SClass.OtherM2;   // Create and assign the new delegate object.
Assigning to a delegate variable

Figure 15-4. 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 15-5 illustrates the results of the preceding code. Notice that the operand delegates remain unchanged.

Combining delegates

Figure 15-5. 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—and it's perfectly fine to think of it that way. You can add a method, or another delegate, to a delegate by 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 15-6 shows the result.

MyDel delVar  = inst.MyM1;     // Create and initialize.
   delVar       += SCl.m3;        // Add a method.
   delVar       += X.Act;         // Add a method.
Result of adding methods to a delegate

Figure 15-6. Result of adding methods to a delegate

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 plus themethod listed on the right.

Removing Methods from a Delegate

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

delVar -= SCl.m3;             // Remove the method from the delegate.
Result of removing a method from a delegate

Figure 15-7. 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 without 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 end of the list and removes the first instance it finds of the matching method.

  • Attempting to delete a method that is not in the delegate 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 we'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). The invocation is illustrated in Figure 15-8.

MyDel delVar  = inst.MyM1;
   delVar       += SCl.m3;
   delVar       += X.Act;
      ...
   delVar( 55 );                              // Invoke the delegate.
      ...
When the delegate is invoked, it invokes each of the methods in its invocation list, with the same parameters with which it was called.

Figure 15-8. When the delegate is invoked, it invokes each of the methods in its invocation list, with the same parameters with which it was called.

A method can be in the invocation list more than once. If that is the case, 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 has methods.
           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 15-9 shows a graphical representation of the code.

Invoking Delegates with Return Values

This code produces the following output:

Value: 12
The return value of the last method executed is the value returned by the delegate.

Figure 15-9. 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 parameternot the initial value—is the one passed to the next method.

For example, the following code invokes a delegate with a reference parameter. Figure 15-10 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
The value of a reference parameter can change between calls.

Figure 15-10. 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 15-11 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.

Comparing a named method and an anonymous method

Figure 15-11. Comparing a named method and an anonymous method

Both sets of code in Figure 15-11 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-hand side of an assignment statement when combining delegates.

  • On the right-hand side of an assignment statement adding a delegate to an event. Chapter 16 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

Syntax of Anonymous Methods

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

Parameters

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

  • Number of parameters

  • Types 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();
                  };

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 parameter list, however, must omit the params keyword.

params Parameters

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 15-12.

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.

Scope of variables and parameters

Figure 15-12. 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 15-13 shows variable x defined outside the anonymous method. The code in the method, however, has access to x and can print its value.

Using an outer variable

Figure 15-13. Using an outer variable

Extension of 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 15-14 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.

Variable captured in an anonymous method

Figure 15-14. 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 allowed you to include short bits of inline code when creating or adding to delegates. 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 introduces 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:

  • Deleting the delegate keyword.

  • Placing 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

Note

Note The term lambda expression comes from the lambda calculus, which was developed in the 1920s and '30s 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, and 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 much cleaner and more understandable.

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);

   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));
   }

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., implicitly typed) unless the delegate has either ref or out parameters—in which case the types are required (i.e., 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 15-15 shows the syntax for lambda expressions.

The syntax for lambda expressions consists of the lambda operator with the parameter section on the left and the lambda body on the right.

Figure 15-15. 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
18.191.168.203