Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Pro CSharp 2008 And The .NET 3.5 Platform [eng]

.pdf
Скачиваний:
78
Добавлен:
16.08.2013
Размер:
22.5 Mб
Скачать

342CHAPTER 11 DELEGATES, EVENTS, AND LAMBDAS

The address of the method on which it makes calls

The arguments (if any) of this method

The return value (if any) of this method

Note Unlike C(++) function pointers, .NET delegates can point to either static or instance methods.

Once a delegate has been created and provided the necessary information, it may dynamically invoke the method(s) it points to at runtime. As you will see, every delegate in the .NET Framework (including your custom delegates) is automatically endowed with the ability to call its methods synchronously or asynchronously. This fact greatly simplifies programming tasks, given that we can call a method on a secondary thread of execution without manually creating and managing a Thread object.

Note We will examine the asynchronous behavior of delegate types during our investigation of the System.Threading namespace in Chapter 18.

Defining a Delegate in C#

When you want to create a delegate in C#, you make use of the delegate keyword. The name of your delegate can be whatever you desire. However, you must define the delegate to match the signature of the method it will point to. For example, assume you wish to build a delegate named BinaryOp that can point to any method that returns an integer and takes two integers as input parameters:

//This delegate can point to any method,

//taking two integers and returning an integer. public delegate int BinaryOp(int x, int y);

When the C# compiler processes delegate types, it automatically generates a sealed class deriving from System.MulticastDelegate. This class (in conjunction with its base class, System.Delegate) provides the necessary infrastructure for the delegate to hold onto a list of methods to be invoked at a later time. For example, if you examine the BinaryOp delegate using ildasm.exe, you would find the class shown in Figure 11-1.

As you can see, the compiler-generated BinaryOp class defines three public methods. Invoke() is perhaps the core method, as it is used to invoke each method maintained by the delegate type in a synchronous manner, meaning the caller must wait for the call to complete before continuing on its way. Strangely enough, the synchronous Invoke() method need not be called explicitly from your C# code. As you will see in just a bit, Invoke() is called behind the scenes when you make use of the appropriate C# syntax.

BeginInvoke() and EndInvoke() provide the ability to call the current method asynchronously on a separate thread of execution. If you have a background in multithreading, you are aware that one of the most common reasons developers create secondary threads of execution is to invoke methods that require time to complete. Although the .NET base class libraries provide an entire namespace devoted to multithreaded programming (System.Threading), delegates provide this functionality out of the box.

CHAPTER 11 DELEGATES, EVENTS, AND LAMBDAS

343

Figure 11-1. The C# delegate keyword represents a sealed class deriving from

System.MulticastDelegate.

Now, how exactly does the compiler know how to define the Invoke(), BeginInvoke(), and EndInvoke() methods? To understand the process, here is the crux of the compiler-generated BinaryOp class type (bold marks the items specified by the defined delegate type):

sealed class BinaryOp : System.MulticastDelegate

{

public BinaryOp(object target, uint functionAddress); public int Invoke(int x, int y);

public IAsyncResult BeginInvoke(int x, int y, AsyncCallback cb, object state);

public int EndInvoke(IAsyncResult result);

}

First, notice that the parameters and return value defined for the Invoke() method exactly match the definition of the BinaryOp delegate. The initial parameters to BeginInvoke() members (two integers in our case) are also based on the BinaryOp delegate; however, BeginInvoke() will always provide two final parameters (of type AsyncCallback and object) that are used to facilitate asynchronous method invocations. Finally, the return value of EndInvoke() is identical to the original delegate declaration and will always take as a sole parameter an object implementing the

IAsyncResult interface.

Let’s see another example. Assume you have defined a delegate type that can point to any method returning a string and receiving three System.Boolean input parameters:

public delegate string MyDelegate(bool a, bool b, bool c);

This time, the compiler-generated class breaks down as follows:

sealed class MyDelegate : System.MulticastDelegate

{

public MyDelegate(object target, uint functionAddress); public string Invoke(bool a, bool b, bool c);

public IAsyncResult BeginInvoke(bool a, bool b, bool c, AsyncCallback cb, object state);

public string EndInvoke(IAsyncResult result);

}

344 CHAPTER 11 DELEGATES, EVENTS, AND LAMBDAS

Delegates can also “point to” methods that contain any number of out or ref parameters (as well as array parameters marked with the params keyword). For example, assume the following delegate type:

public delegate string MyOtherDelegate(out bool a, ref bool b, int c);

The signatures of the Invoke() and BeginInvoke() methods look as you would expect; however, check out the EndInvoke() method, which now includes the set of all out/ref arguments defined by the delegate type:

sealed class MyOtherDelegate : System.MulticastDelegate

{

public MyOtherDelegate (object target, uint functionAddress); public string Invoke(out bool a, ref bool b, int c);

public IAsyncResult BeginInvoke(out bool a, ref bool b, int c, AsyncCallback cb, object state);

public string EndInvoke(out bool a, ref bool b, IAsyncResult result);

}

To summarize, a C# delegate definition results in a sealed class with three compiler-generated methods whose parameter and return types are based on the delegate’s declaration. The following pseudo-code approximates the basic pattern:

// This is only pseudo-code!

public sealed class DelegateName : System.MulticastDelegate

{

public DelegateName (object target, uint functionAddress);

public delegateReturnValue Invoke(allDelegateInputRefAndOutParams);

public IAsyncResult BeginInvoke(allDelegateInputRefAndOutParams, AsyncCallback cb, object state);

public delegateReturnValue EndInvoke(allDelegateRefAndOutParams, IAsyncResult result);

}

The System.MulticastDelegate and

System.Delegate Base Classes

So, when you build a type using the C# delegate keyword, you indirectly declare a class type that derives from System.MulticastDelegate. This class provides descendents with access to a list that contains the addresses of the methods maintained by the delegate type, as well as several additional methods (and a few overloaded operators) to interact with the invocation list. Here are some select members of System.MulticastDelegate:

public abstract class MulticastDelegate : Delegate

{

// Returns the list of methods "pointed to."

public sealed override Delegate[] GetInvocationList();

// Overloaded operators.

public static bool operator ==(MulticastDelegate d1, MulticastDelegate d2); public static bool operator !=(MulticastDelegate d1, MulticastDelegate d2);

CHAPTER 11 DELEGATES, EVENTS, AND LAMBDAS

345

// Used internally to manage the list of methods maintained by the delegate. private IntPtr _invocationCount;

private object _invocationList;

}

System.MulticastDelegate obtains additional functionality from its parent class, System.Delegate. Here is a partial snapshot of the class definition:

public abstract class Delegate : ICloneable, ISerializable

{

// Methods to interact with the list of functions.

public static Delegate Combine(params Delegate[] delegates); public static Delegate Combine(Delegate a, Delegate b);

public static Delegate Remove(Delegate source, Delegate value); public static Delegate RemoveAll(Delegate source, Delegate value);

// Overloaded operators.

public static bool operator ==(Delegate d1, Delegate d2); public static bool operator !=( Delegate d1, Delegate d2);

// Properties that expose the delegate target. public MethodInfo Method { get; }

public object Target { get; }

}

Now, understand that you can never directly derive from these base classes in your code (it is a compiler error to do so). Nevertheless, when you use the delegate keyword, you have indirectly created a class that “is-a” MulticastDelegate. Table 11-1 documents the core members commonplace to all delegate types.

Table 11-1. Select Members of System.MultcastDelegate/System.Delegate

Inherited Member

Meaning in Life

Method

This property returns a System.Reflection.MethodInfo type that

 

represents details of a static method maintained by the delegate.

Target

If the method to be called is defined at the object level (rather than a

 

static method), Target returns an object that represents the method

 

maintained by the delegate. If the value returned from Target

 

equals null, the method to be called is a static member.

Combine()

This static method adds a method to the list maintained by the

 

delegate. In C#, you trigger this method using the overloaded +=

 

operator as a shorthand notation.

GetInvocationList()

This method returns an array of System.Delegate types, each

 

representing a particular method that may be invoked.

Remove()

These static methods remove a method (or all methods) from the

RemoveAll()

delegate’s invocation list. In C#, the Remove() method can be called

 

indirectly using the overloaded -= operator.

 

 

The Simplest Possible Delegate Example

Delegates can tend to cause a great deal of confusion when encountered for the first time. Thus, to get the ball rolling, let’s take a look at a very simple Console Application program (named

346CHAPTER 11 DELEGATES, EVENTS, AND LAMBDAS

SimpleDelegate) that makes use of the BinaryOp delegate type you’ve seen previously. Here is the complete code, with analysis to follow:

namespace SimpleDelegate

{

//This delegate can point to any method,

//taking two integers and returning an integer.

public delegate int BinaryOp(int x, int y);

//This class contains methods BinaryOp will

//point to.

public class SimpleMath

{

public static int Add(int x, int y) { return x + y; }

public static int Subtract(int x, int y) { return x - y; }

}

class Program

{

static void Main(string[] args)

{

Console.WriteLine("***** Simple Delegate Example *****\n");

//Create a BinaryOp object that

//"points to" SimpleMath.Add().

BinaryOp b = new BinaryOp(SimpleMath.Add);

// Invoke Add() method indirectly using delegate object.

Console.WriteLine("10 + 10 is {0}", b(10, 10)); Console.ReadLine();

}

}

}

Again, notice the format of the BinaryOp delegate, which can point to any method taking two integers and returning an integer (the actual name of the method pointed to is irrelevant). Here, we have created a class named SimpleMath, which defines two static methods that (surprise, surprise) match the pattern defined by the BinaryOp delegate.

When you want to insert the target method to a given delegate, simply pass in the name of the method to the delegate’s constructor. At this point, you are able to invoke the member pointed to using a syntax that looks like a direct function invocation:

// Invoke() is really called here!

Console.WriteLine("10 + 10 is {0}", b(10, 10));

Under the hood, the runtime actually calls the compiler-generated Invoke() method. You can verify this fact for yourself if you open your assembly in ildasm.exe and investigate the CIL code within the Main() method:

.method private hidebysig static void Main(string[] args) cil managed

{

...

callvirt instance int32 SimpleDelegate.BinaryOp::Invoke(int32, int32)

}

CHAPTER 11 DELEGATES, EVENTS, AND LAMBDAS

347

Although C# does not require you to explicitly call Invoke() within your code base, you are free to do so. Thus, the following code statement is permissible:

Console.WriteLine("10 + 10 is {0}", b.Invoke(10, 10));

Recall that .NET delegates are type safe. Therefore, if you attempt to pass a delegate a method that does not “match the pattern,” you receive a compile-time error. To illustrate, assume the SimpleMath class now defines an additional method named SquareNumber(), which takes a single integer as input:

public class SimpleMath

{

...

public static int SquareNumber(int a) { return a * a; }

}

Given that the BinaryOp delegate can only point to methods that take two integers and return an integer, the following code is illegal and will not compile:

// Error! Method does not match delegate pattern!

BinaryOp b2 = new BinaryOp(SimpleMath.SquareNumber);

Investigating a Delegate Object

Let’s spice up the current example by creating a static method (named DisplayDelegateInfo()) within the Program type. This method will print out names of the methods maintained by the incoming delegate type as well as the name of the class defining the method. To do so, we will iterate over the System.Delegate array returned by GetInvocationList(), invoking each object’s Target and Method properties:

static void DisplayDelegateInfo(Delegate delObj)

{

//Print the names of each member in the

//delegate's invocation list.

foreach (Delegate d in delObj.GetInvocationList())

{

Console.WriteLine("Method Name: {0}", d.Method); Console.WriteLine("Type Name: {0}", d.Target);

}

}

Assuming you have updated your Main() method to actually call this new helper method, you would find the output shown in Figure 11-2.

Figure 11-2. Examining a delegate’s invocation list

348 CHAPTER 11 DELEGATES, EVENTS, AND LAMBDAS

Notice that the name of the type (SimpleMath) is currently not displayed by the Target property. The reason has to do with the fact that our BinaryOp delegate is pointing to a static method and therefore there is no object to reference! However, if we update the Add() and Subtract() methods to be nonstatic (simply by deleting the static keywords), we could create an instance of the SimpleMath type and specify the methods to invoke using the object reference:

static void Main(string[] args)

{

Console.WriteLine("***** Simple Delegate Example *****\n");

//.NET delegates can also point to instance methods as well.

SimpleMath m = new SimpleMath(); BinaryOp b = new BinaryOp(m.Add);

//Show information about this object.

DisplayDelegateInfo(b);

Console.WriteLine("10 + 10 is {0}", b(10, 10)); Console.ReadLine();

}

In this case, we would find the output shown in Figure 11-3.

Figure 11-3. Examining a delegate’s invocation list (once again)

Source Code The SimpleDelegate project is located under the Chapter 11 subdirectory.

Retrofitting the Car Type with Delegates

Clearly, the previous SimpleDelegate example was intended to be purely illustrative in nature, given that there would be no reason to build a delegate simply to add two numbers. To provide a more realistic use of delegate types, let’s retrofit the Car type created in Chapter 9 to send notifications using .NET delegates rather than a custom callback interface. Beyond no longer implementing IEngineNotification, here are the basic steps we will need to take:

Define new delegate types that will send notifications to the caller.

Declare a member variable of these delegate types in the Car class.

Create helper functions on the Car that allow the caller to set the methods maintained by the delegate member variables.

Update the Accelerate() method to invoke the delegate’s invocation list under the correct circumstances.

CHAPTER 11 DELEGATES, EVENTS, AND LAMBDAS

349

To begin, create a new Console Application project named CarDelegate and insert your previous Car and Radio definitions from the CallbackInterface example of Chapter 9 (you may wish to change the namespace containing these types to the current project name or import the CallbackInterface namespace as an alternative). Consider the following updates to the Car class, which address the first three points:

public class Car

{

// Define the delegate types.

public delegate void AboutToBlow(string msg); public delegate void Exploded (string msg);

//Define member variables of each delegate type. private AboutToBlow almostDeadList;

private Exploded explodedList;

//Add members to the invocation lists using helper methods. public void OnAboutToBlow(AboutToBlow clientMethod)

{ almostDeadList = clientMethod; }

public void OnExploded(Exploded clientMethod) { explodedList = clientMethod; }

...

}

Notice in this example that we define the delegate types directly within the scope of the Car type. As you explore the base class libraries, you will find it is quite common to define a delegate within the scope of the type it naturally works with. On a related note, given that the compiler transforms a delegate into a full class definition, what we have actually done is create two nested classes (AboutToBlow and Exploded) within the Car class.

Next, note that we declare two private member variables (one for each delegate type) and two helper functions (OnAboutToBlow() and OnExploded()) that allow the client to add a method to the delegate’s invocation list. In concept, these methods are similar to the Advise() and Unadvise() methods we created during the CallbackInterface example. Of course, in this case, the incoming parameter is a client-allocated delegate object rather than a class implementing a custom interface.

Note Strictly speaking, we could have defined our delegate member variables as public, therefore avoiding the need to create additional registration methods. However, by defining the members as private, we are enforcing encapsulation services and providing a more type-safe solution. You’ll revisit the risk of public delegate member variables later in this chapter when examining the C# event keyword.

At this point, we need to update the Accelerate() method to invoke each delegate, rather than iterate over an ArrayList of client-side sinks (as we did in the CallbackInterface example). Here is the update:

public void Accelerate(int delta)

{

// If the car is dead, fire Exploded event. if (carIsDead)

{

if (explodedList != null) explodedList("Sorry, this car is dead...");

}

else

350 CHAPTER 11 DELEGATES, EVENTS, AND LAMBDAS

{

currSpeed += delta;

// Almost dead?

if (10 == maxSpeed - currSpeed && almostDeadList != null)

{

almostDeadList("Careful buddy! Gonna blow!");

}

// Still OK!

if (currSpeed >= maxSpeed) carIsDead = true;

else

Console.WriteLine("->CurrSpeed = {0}", currSpeed);

}

}

Notice that before we invoke the methods maintained by the almostDeadList and explodedList member variables, we are checking them against a null value. The reason is that it will be the job of the caller to allocate these objects by calling the OnAboutToBlow() and OnExploded() helper methods. If the caller does not call these methods, and we attempt to invoke the delegate’s invocation list,

we will trigger a NullReferenceException and bomb at runtime (which would obviously be a bad thing!). Now that we have the delegate infrastructure in place, observe the updates to the Program class:

class Program

{

static void Main(string[] args)

{

Console.WriteLine("***** Delegates as event enablers *****\n");

// Make a car as usual.

Car c1 = new Car("SlugBug", 100, 10);

//Register event handlers with Car type. c1.OnAboutToBlow(new Car.AboutToBlow(CarAboutToBlow)); c1.OnExploded(new Car.Exploded(CarExploded));

//Speed up (this will trigger the events).

Console.WriteLine("***** Speeding up *****");

for (int i = 0; i < 6; i++) c1.Accelerate(20);

Console.ReadLine();

}

// The Car will call these methods.

public static void CarAboutToBlow(string msg) { Console.WriteLine(msg); }

public static void CarExploded(string msg) { Console.WriteLine(msg); }

}

The only major point to be made here is the fact that the caller is the entity that assigns the delegate member variables via the helper registration methods. Also, because the AboutToBlow and Exploded delegates are nested within the Car class, we must allocate them using their full name

CHAPTER 11 DELEGATES, EVENTS, AND LAMBDAS

351

(e.g., Car.AboutToBlow). Like any delegate constructor, we pass in the name of the method to add to the invocation list, which in this case are two static members on the Program class (if you wanted to wrap these methods in a new class, it would look very similar to the CarEventSink type of the

CallbackInterface example).

Enabling Multicasting

Recall that .NET delegates have the intrinsic ability to multicast. In other words, a delegate object can maintain a list of methods to call, rather than a single method. When you wish to add multiple methods to a delegate object, you simply make use of the overloaded += operator, rather than a direct assignment. To enable multicasting on the Car type, we could update the OnAboutToBlow() and OnExploded() methods as follows:

public class Car

{

// Add member to the invocation lists.

public void OnAboutToBlow(AboutToBlow clientMethod) { almostDeadList += clientMethod; }

public void OnExploded(Exploded clientMethod) { explodedList += clientMethod; }

...

}

With this, the caller can now register multiple targets for the same callback:

class Program

{

static void Main(string[] args)

{

Console.WriteLine("***** Delegates as event enablers *****\n");

Car c1 = new Car("SlugBug", 100, 10);

// Register multiple event handlers! c1.OnAboutToBlow(new Car.AboutToBlow(CarAboutToBlow)); c1.OnAboutToBlow(new Car.AboutToBlow(CarIsAlmostDoomed));

c1.OnExploded(new Car.Exploded(CarExploded));

...

}

// Car will call these.

public static void CarAboutToBlow(string msg) { Console.WriteLine(msg); }

public static void CarIsAlmostDoomed(string msg)

{Console.WriteLine("Critical Message from Car: {0}", msg); } public static void CarExploded(string msg)

{Console.WriteLine(msg); }

}

In terms of CIL code, the += operator resolves to a call to the static Delegate.Combine() method (in fact, you could call Delegate.Combine() directly, but the += operator offers a simpler alternative). Ponder the following CIL implementation of OnAboutToBlow():

.method public hidebysig instance void OnAboutToBlow

(class CarDelegate.Car/AboutToBlow clientMethod) cil managed

{

.maxstack 8