A delegate can be thought of as an object that contains an ordered list of methods with the same signature and return type.
Figure 15-1 represents a delegate with four methods in its invocation list.
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.
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.
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.
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
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:
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.
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
.
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;
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.
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.
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.
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.
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.
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
.
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. ...
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.
The following code defines and uses a delegate with no parameters and no return value. Note the following about the code:
// 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
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.
This code produces the following output:
Value: 12
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 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
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.
Both sets of code in Figure 15-11 produce the following output:
25 26
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.
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
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.
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(); };
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.
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.
Unlike the named methods of a delegate, anonymous methods have access to the local variables and environment of the scope surrounding them.
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.
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.
The code in the figure produces the following output:
Value of x: 5
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 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.
18.191.168.203