Removing Methods from a Delegate
Invoking Delegates with Return Values
Invoking Delegates with Reference Parameters
A delegate is a user-defined type, like a class. But whereas a class represents a collection of data, a delegate keeps track of one or more methods. You use a delegate by doing the following. We'll go through each of these steps in detail in the following sections.
In looking at the previous steps, you might have noticed that they're similar to the steps in creating and using a class. Figure 15-1 compares the processes of creating and using classes and delegates.
Figure 15-1. A delegate is a user-defined reference type, like a class.
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.
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 15-2.
ref
and out
modifiers)Figure 15-2. A delegate as a list of methods
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 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
. This declaration specifies that delegates of this type will only accept methods that have a single int
parameter and that have no return value. Figure 15-3 shows a representation of the delegate type on the left and the delegate object on the right.
Figure 15-3. Delegate type and object
The delegate type declaration differs from a method declaration in two ways. The delegate type declaration
delegate
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:
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-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
returning no value and taking 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
.
Figure 15-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 15-4:
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-4:
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-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.
Figure 15-5. Assigning to a delegate variable
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-6 illustrates the results of the preceding code. Notice that the operand delegates remain unchanged.
Figure 15-6. Combining 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 15-7 shows the result.
MyDel delVar = inst.MyM1; // Create and initialize.
delVar += SCl.m3; // Add a method.
delVar += X.Act; // Add a method.
Figure 15-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 plus the method listed on the right. This new delegate is then assigned to the delVar
variable.
You can also remove a method from a delegate, using the -=
operator. The following code shows the use of the operator. Figure 15-8 shows the result of this code when applied to the delegate illustrated in Figure 15-7.
delVar -= SCl.m3; // Remove the method from the delegate.
Figure 15-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:
-=
operator starts searching at the bottom of the list and removes the first instance of the matching method it finds.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). Figure 15-9 illustrates the invocation.
MyDel delVar = inst.MyM1;
delVar += SCl.m3;
delVar += X.Act;
...
delVar( 55 ); // Invoke the delegate.
...
Figure 15-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.
A method can be in the invocation list more than once. If it's in the list more than once, 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:
Test
defines two print functions.Main
creates an instance of the delegate and then adds three more methods.null
. // Define a delegate type with no return value and no parameters.
delegate void PrintFunction();
class Test
{
public void Print1()
{ Console.WriteLine("Print1 -- instance"); }
class Program
{ 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
If a delegate has a return value and more than one method in its invocation list, the following occurs:
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-10 shows a graphical representation of the code.
This code produces the following output:
Value: 12
Figure 15-10. The return value of the last method executed is the value returned by the delegate.
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.
For example, the following code invokes a delegate with a reference parameter. Figure 15-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
Figure 15-11. The value of a reference parameter can change between calls.
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.
For example, Figure 15-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.
Figure 15-12. Comparing a named method and an anonymous method
Both sets of code in Figure 15-12 produce the following output:
25
26
You can use an anonymous method in the following places:
The syntax of an anonymous method expression includes the following components:
delegate
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:
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:
out
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, this happens:
params
type parameter.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-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.
Figure 15-13. Scope of variables and parameters
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-14 shows variable x
defined outside the anonymous method. The code in the method, however, has access to x
and can print its value.
Figure 15-14. Using an outer variable
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-15 illustrates the extension of a captured variable's lifetime.
x
is declared and initialized inside a block.mDel
is then instantiated, using an anonymous method that captures outer variable x
.x
goes out of scope.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.mDel
, however, maintains x
in its environment and prints its value when mDel
is invoked.Figure 15-15. Variable captured in an anonymous method
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 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:
delegate
keyword.=>
, 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 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.
le2
.
le3
.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);
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:
ref
or out
parameters—in which case the types are required (that is, explicitly typed).Figure 15-16 shows the syntax for lambda expressions.
Figure 15-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.
3.142.114.19