- What Is a Delegate?
- An Overview of Delegates
- Declaring the Delegate Type
- Creating the Delegate Object
- Assigning Delegates
- Combining Delegates
- Adding Methods to Delegates
- Removing Methods from a Delegate
- Invoking a Delegate
- Delegate Example
- Invoking Delegates with Return Values
- Invoking Delegates with Reference Parameters
- Anonymous Methods
- Lambda Expressions
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.
MyDel
. (Yes, a delegate type—not a delegate object. We'll get to this shortly.)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
;.MyDel
delegate object and initializes it to hold a reference to the PrintLow
method.MyDel
delegate object that holds a reference to the PrintHigh
method.Main
executes the del
delegate object, which executes whichever method (PrintLow
or PrintHigh
) it's holding.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
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.
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.
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.
ref
and out
modifiers)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.
The delegate type declaration differs from a method declaration in two ways. The delegate type declaration
delegate
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.
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:
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
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;
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.
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.
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.
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.
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.
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 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.
...
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.
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"); }
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
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 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
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 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
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 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.
Both sets of code in Figure 13-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
Parameter
Keyword list Statement block
↓ ↓ ↓
delegate ( Parameters ) { ImplementationCode }
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
};
...
}
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:
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:
params
type parameter.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)
{
...
};
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.
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 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.
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.
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.The code in the figure produces the following output:
Value of x: 5
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:
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);
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:
ref
or out
parameters—in which case the types are required (i.e., they're explicitly typed).Figure 13-16 shows the syntax for lambda expressions.
3.147.58.194