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

C# ПІДРУЧНИКИ / c# / Apress - Accelerated C# 2005

.pdf
Скачиваний:
85
Добавлен:
12.02.2016
Размер:
2.09 Mб
Скачать

234C H A P T E R 1 0 D E L E G AT E S, A N O N Y M O U S F U N C T I O N S, A N D E V E N T S

instance method or a static method. Of course, this example is rather contrived, but it gives a clear indication of the basic usage of delegates within C#.

In all of the cases in the previous code, a single action takes place when the delegate is called. It is possible to chain delegates together so that multiple actions take place.

Delegate Chaining

Delegate chaining allows you to create a linked list of delegates such that when the delegate at the head of the list is called, all of the delegates in the chain are called. The System.Delegate class provides a few static methods to manage lists of delegates. To create delegate lists, you rely on the following methods declared inside of the System.Delegate type:

public class Delegate : ICloneable, ISerializable

{

public static Delegate Combine( Delegate[] );

public static Delegate Combine( Delegate first, Delegate second );

}

Notice that the Combine methods take the delegates to combine and return another Delegate. The Delegate returned is a new instance of a MulticastDelegate. That is because Delegate instances are treated as immutable. For example, the caller of Combine() may wish to create a delegate list but leave the original delegate instances in the same state they were in. The only way to do that is to treat delegate instances as immutable when creating delegate chains.

Notice that the first version of Combine() listed previously takes an array of delegates to form the constituents of the new delegate list, and the second form takes just a pair of delegates. However, in both cases, any one of the Delegate instances could itself already be a delegate chain. So, you can see that some fairly complex nesting can take place here.

To remove delegates from a list, you rely upon the following two static methods on System.Delegate:

public class Delegate : IClonable, ISerializable

{

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

}

As with the Combine methods, the Remove and RemoveAll methods return a new Delegate instance created from the previous two. The Remove method removes the last occurrence of value in the source delegate list, whereas RemoveAll() removes all occurrences of the value delegate list from the source delegate list. Notice that I said that the value parameter may represent a delegate list rather than just a single delegate. Again, these methods have the ability to meet any complex delegate list management needs.

Let’s look at a modified form of the code example in the last section to see how you can combine the delegates:

using System;

public delegate double ProcessResults( double x, double y );

public class Processor

{

public Processor( double factor ) { this.factor = factor;

}

C H A P T E R 1 0 D E L E G AT E S, A N O N Y M O U S F U N C T I O N S, A N D E V E N T S

235

public double Compute( double x, double y ) { double result = (x+y)*factor;

Console.WriteLine( "InstanceResults: {0}", result ); return result;

}

public static double StaticCompute( double x, double y ) {

double result = (x+y)*0.5;

Console.WriteLine( "StaticResult: {0}", result ); return result;

}

private double factor;

}

public class EntryPoint

{

static void Main() {

Processor proc1 = new Processor( 0.75 ); Processor proc2 = new Processor( 0.83 );

ProcessResults[] delegates = new ProcessResults[] { new ProcessResults( proc1.Compute ),

new ProcessResults( proc2.Compute ),

new ProcessResults( Processor.StaticCompute )

};

ProcessResults chained = (ProcessResults) Delegate.Combine( delegates );

double combined = chained( 4, 5 );

Console.WriteLine( "Output: {0}", combined );

}

}

Notice that instead of calling all of the delegates, this example chains them together and then calls them by calling through the head of the chain. This example features some major differences from the previous example, though. First of all, the resultant double that comes out of the chained invocation is the result of the last delegate called, which, in this case, is the delegate pointing to the static method StaticCompute. The return values from the other delegates in the chain are simply lost. Also, if any of the delegates throws an exception, processing of the delegate chain will terminate and the CLR will begin to search for the next exception-handling frame on the stack. Be aware that if you declare delegates that take parameters by reference, then each delegate that uses the reference parameter will see the changes made by the previous delegate in the chain. This could be

a desired effect, or it could be a surprise, depending on what your intentions are. Finally, notice that before invoking the delegate chain, you must cast the delegate back into the explicit delegate type. This is necessary for the compiler to know how to invoke the delegate. The type returned from the Combine and Remove methods is of type System.Delegate, which doesn’t have enough type information for the compiler to figure out how to invoke it.

Iterating Through Delegate Chains

Sometimes you need to call a chain of delegates, but you need to harvest the return values from each invocation, or you may need to specify the ordering of the calls in the chain. For these times,

236C H A P T E R 1 0 D E L E G AT E S, A N O N Y M O U S F U N C T I O N S, A N D E V E N T S

the System.Delegate type, from which all delegates derive, offers the GetInvocationList method to acquire an array of delegates where each element in the array corresponds to a delegate in the invocation list. Once you obtain this array, you can call the delegates in any order you please, and you can process the return value from each delegate appropriately. You could also put an exception frame around each entry in the list so that an exception in one delegate invocation will not abort the remaining invocations. This modified version of the previous example shows how to call each delegate in the chain explicitly:

using System;

public delegate double ProcessResults( double x, double y );

public class Processor

{

public Processor( double factor ) { this.factor = factor;

}

public double Compute( double x, double y ) { double result = (x+y)*factor;

Console.WriteLine( "InstanceResults: {0}", result ); return result;

}

public static double StaticCompute( double x, double y ) {

double result = (x+y)*0.5;

Console.WriteLine( "StaticResult: {0}", result ); return result;

}

private double factor;

}

public class EntryPoint

{

static void Main() {

Processor proc1 = new Processor( 0.75 ); Processor proc2 = new Processor( 0.83 );

ProcessResults[] delegates = new ProcessResults[] { new ProcessResults( proc1.Compute ),

new ProcessResults( proc2.Compute ),

new ProcessResults( Processor.StaticCompute )

};

ProcessResults chained = (ProcessResults) Delegate.Combine( delegates ); Delegate[] chain = chained.GetInvocationList();

double accumulator = 0;

for( int i = 0; i < chain.Length; ++i ) { ProcessResults current = (ProcessResults) chain[i]; accumulator += current( 4, 5 );

}

Console.WriteLine( "Output: {0}", accumulator );

}

}

C H A P T E R 1 0 D E L E G AT E S, A N O N Y M O U S F U N C T I O N S, A N D E V E N T S

237

Unbound (Open Instance) Delegates

All of the delegate examples so far show how to wire up a delegate to a static method on a specific type or to an instance method on a specific instance. This abstraction provides excellent decoupling, but the delegate doesn’t really imitate or represent a pointer to a method per se, since it is bound to a method on a specific instance. What if you want to have a delegate represent an instance method, and then you want to invoke that same delegate on a collection of instances?

For this task, you need to use an open instance delegate. When you call a method on an instance, a hidden parameter at the beginning of the parameter list is known as this, which represents the current instance.1 When you wire up a closed instance delegate to an instance method on an object instance, the delegate passes the object instance as the this reference when it calls the instance method. With open instance delegates, the delegate defers this action to the one who invokes the delegate. Thus, you can provide the object instance to call on at delegate invocation time.

Let’s look at an example of what this would look like. Imagine a collection of Employee types, and the company has decided to give everyone a 10% raise at the end of the year. All of the Employee objects are contained in a collection type, and now you need to iterate over each employee, applying the raise by calling the Employee.ApplyRaiseOf method:

using System;

using System.Reflection;

using System.Collections.Generic;

delegate void ApplyRaiseDelegate( Employee emp, Decimal percent );

public class Employee

{

private Decimal salary;

public Employee( Decimal salary ) { this.salary = salary;

}

public Decimal Salary { get {

return salary;

}

}

public void ApplyRaiseOf( Decimal percent ) { salary *= (1 + percent);

}

}

public class EntryPoint

{

static void Main() {

List<Employee> employees = new List<Employee>();

employees.Add( new Employee(40000) ); employees.Add( new Employee(65000) ); employees.Add( new Employee(95000) );

1. Reference Chapter 4 for more details on this in regard to reference and value types.

238 C H A P T E R 1 0 D E L E G AT E S, A N O N Y M O U S F U N C T I O N S, A N D E V E N T S

// Create open instance delegate MethodInfo mi =

typeof(Employee).GetMethod( "ApplyRaiseOf", BindingFlags.Public | BindingFlags.Instance );

ApplyRaiseDelegate applyRaise = (ApplyRaiseDelegate ) Delegate.CreateDelegate( typeof(ApplyRaiseDelegate),

mi );

// Apply raise.

foreach( Employee e in employees ) { applyRaise( e, (Decimal) 0.10 );

// Send new salary to console. Console.WriteLine( e.Salary );

}

}

}

First, notice that the declaration of the delegate has an Employee type declared at the beginning of the parameter list. This is how you expose the hidden instance pointer so that you can bind it later. Had you used this delegate to represent a closed instance delegate, the Employee parameter would have been omitted. Unfortunately, the C# language doesn’t have any special syntax for creating open instance delegates. Therefore, you must use one of the more generalized Delegate.CreateDelegate() overloads to create the delegate instance as shown, and before you can do that, you must use reflection to obtain the MethodInfo instance representing the method to bind to.

The key point to notice here is that nowhere during the instantiation of the delegate do you provide a specific object instance. You won’t provide that until the point of delegate invocation. The foreach loop shows how you invoke the delegate and provide the instance to call upon at the same time. Even though the ApplyRaiseOf method that the delegate is wired to takes only one parameter, the delegate invocation requires two parameters, so that you can provide the instance on which to make the call.

The previous example shows how to create and invoke an open instance delegate; however, the delegate could still be more general and more useful in a broad sense. In that example, you declared the delegate such that it knew it was going to be calling a method on a type of Employee. Thus, at invocation time, you could have placed the call only on an instance of Employee or a type derived from Employee. You can use a generic delegate to declare the delegate such that the type on which it is called is unspecified at declaration time.2 Such a delegate is potentially much more useful. It allows you to state the following: “I want to represent a method that matches this signature supported by an as-of-yet unspecified type.” Only at the point of instantiation of the delegate are you required to provide the concrete type that will be called. Examine the following modifications to the previous example:

delegate void ApplyRaiseDelegate<T>( T instance, Decimal percent );

public class EntryPoint

{

static void Main() {

List<Employee> employees = new List<Employee>();

2. I cover generics in Chapter 11.

C H A P T E R 1 0 D E L E G AT E S, A N O N Y M O U S F U N C T I O N S, A N D E V E N T S

239

employees.Add( new Employee(40000) ); employees.Add( new Employee(65000) ); employees.Add( new Employee(95000) );

// Create open instance delegate MethodInfo mi =

typeof(Employee).GetMethod( "ApplyRaiseOf", BindingFlags.Public | BindingFlags.Instance );

ApplyRaiseDelegate<Employee> applyRaise = (ApplyRaiseDelegate<Employee> ) Delegate.CreateDelegate(

typeof(ApplyRaiseDelegate<Employee>), mi );

// Apply raise.

foreach( Employee e in employees ) { applyRaise( e, (Decimal) 0.10 );

// Send new salary to console. Console.WriteLine( e.Salary );

}

}

}

Now, the delegate is much more generic. You can imagine that this delegate could be useful in some circumstances. For example, imagine an imaging program that supports applying filters to various objects on the canvas. Suppose you need a delegate to represent a generic filter type that, when applied, is provided a percentage value to indicate how much of an effect it should have on the object. Using generic, open instance delegates, you could represent such a general notion.

Events

In many cases, when you use delegates as a callback mechanism, you may just want to notify someone that some event happened, such as a button press in a UI. Suppose that you’re designing a media player application. Somewhere in the UI is a Play button. In a well-designed system, the UI and the control logic are separated by a well-defined abstraction, commonly implemented using

a form of the Bridge pattern. The abstraction facilitates slapping on an alternate UI later, or even better, since UI operations are normally platform-specific, it facilitates porting the application to another platform. For example, the Bridge pattern works well in situations where you want to decouple your control logic from the UI.

Note The purpose of the Bridge pattern, as defined in Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides (Boston, MA: Addison-Professional, 1995), is to decouple an abstraction from an implementation so that the two can vary independently.

By using the Bridge pattern, you can facilitate the scenario where changes that occur in the core system don’t force changes in the UI, and most importantly, where changes in the UI don’t force changes in the core system. One common way of implementing this pattern is by creating well-defined interfaces into the core system that the UI then uses to communicate with it, and vice versa. Delegates are an excellent mechanism to use to help define such an interface. With a delegate, you can begin to say things as abstract as, “When the user wants to play, I want you to call registered methods passing

240C H A P T E R 1 0 D E L E G AT E S, A N O N Y M O U S F U N C T I O N S, A N D E V E N T S

any information germane to the action.” The beauty here is that the core system doesn’t care how the user indicates to the UI that he wants the player to start playing media. It could be a button press, or there could be some sort of brain-wave detection device that recognizes what the user is thinking. To the core system, it doesn’t matter, and you can change and interchange both independently without breaking the other. Both sides adhere to the same agreed-upon interface contract, which in this case is a specifically formed delegate and a means to register that delegate with the event-generating entity.3

This pattern of usage, also known as publish/subscribe, is so common, even outside the realm of UI development, that the .NET runtime designers were so generous as to define a formalized built-in event mechanism. When you declare an event within a class underneath the covers, the compiler implements some hidden methods that allow you to register and unregister delegates that get called when a specific event is raised. In essence, an event is a shortcut that saves you the time of having to write the register and unregister methods that manage a delegate chain yourself. Let’s take a look at a simple event sample based on the previous discussion:

using System;

// Arguments passed from UI when play event occurs. public class PlayEventArgs : EventArgs

{

public PlayEventArgs( string filename ) { this.filename = filename;

}

private string filename; public string Filename {

get { return filename; }

}

}

public class PlayerUI

{

// define event for play notifications.

public event EventHandler<PlayEventArgs> PlayEvent;

public void UserPressedPlay() { OnPlay();

}

protected virtual void OnPlay() { // fire the event.

EventHandler<PlayEventArgs> localHandler = PlayEvent;

if( localHandler != null ) { localHandler( this,

new PlayEventArgs("somefile.wav") );

}

}

}

public class CorePlayer

{

public CorePlayer() { ui = new PlayerUI();

3. In Chapter 5, I cover the topic of contracts and interfaces in detail.

Note

C H A P T E R 1 0 D E L E G AT E S, A N O N Y M O U S F U N C T I O N S, A N D E V E N T S

241

// Register our event handler. ui.PlayEvent += this.PlaySomething;

}

private void PlaySomething( object source, PlayEventArgs args ) {

// Play the file.

}

private PlayerUI ui;

}

public class EntryPoint

{

static void Main() {

CorePlayer player = new CorePlayer();

}

}

Even though the syntax of this simple event may look complicated, the overall idea is that you’re creating a well-defined interface through which to notify interested parties that the user wants to play a file. That well-defined interface is encapsulated inside the PlayEventArgs class. Events put certain rules upon how you use delegates. The delegate must not return anything, and it must accept two arguments. The first argument is an object reference representing the party generating the event. The second argument must be a type derived from System.EventArgs. Your EventArgs derived class is where you define any event-specific arguments.

Note In .NET 1.1, you had to explicitly define the delegate type behind the event. In .NET 2.0, you can use the new generic EventHandler<T> class to shield you from this mundane chore.

Notice that I’ve declared the event using the generic EventHandler<T> class. When registering handlers using the += operator, as a shortcut you can provide only the reference to the method to call, and the compiler will create the EventHandler<T> instance for you. You optionally could follow the += operator with a new expression creating a new instance of EventHandler<T>, but if the compiler provides the shortcut shown, why type more syntax that makes the code harder to read?

Notice the way that the event is defined within the PlayerUI class using the event keyword. The event keyword is first followed by the defined event delegate, which is then followed by the name of the event—in this case, PlayEvent. The PlayEvent identifier means two entirely different things depending on what side of the decoupling fence you’re on. From the perspective of the event gener- ator—in this case, PlayerUI—the PlayEvent event is used just like a delegate. You can see this usage inside the OnPlay method. Typically, a method such as OnPlay is called in response to a UI button press. It notifies all of the registered listeners by calling through the PlayEvent event (delegate).

The popular idiom when raising events is to raise the event within a protected virtual method named On<event>, where <event> is replaced with the name of the event—in this case, OnPlay. This way, derived classes can easily modify the actions taken when the event needs to be raised. In C#, you must test the event for null before calling it; otherwise, the result could be a NullReferenceException. The OnPlay method makes

a local copy of the event before testing it for null. This avoids the race condition where the event is set to null from another thread after the null check passes and before the event is raised.

242 C H A P T E R 1 0 D E L E G AT E S, A N O N Y M O U S F U N C T I O N S, A N D E V E N T S

From the event consumer side of the fence, the PlayEvent identifier is used completely differently, as you can see in the CorePlayer constructor. The C# language has overloaded the += and -= operators for events to provide a compact notation for registering and unregistering event listeners. In the CorePlayer constructor, you could have also registered the event as follows:

ui.PlayEvent += this.PlaySomething;

That’s the basic structure of events. As I alluded to earlier, .NET events are a shortcut to creating delegates and the interfaces with which to register those delegates. As proof of this, you can examine the IL generated from compiling the previous example. Under the covers, the compiler has generated two methods, add_OnPlay() and remove_OnPlay(), which get called when you use the overloaded += and -= operators. These methods manage the addition and removal of delegates from the event delegate chain. In fact, the C# compiler doesn’t allow you to call these methods explicitly, so you must use the operators.

The event mechanism defines two hidden function members, or accessors, which is similar to the way properties define hidden accessors. You may be wondering if there is some way to control the body of those function members as you can with properties. The answer is yes, and the syntax is similar to that of properties. I’ve modified the PlayerUI class to show the way to handle event add and remove operations explicitly:

public class PlayerUI

{

// define event for play notifications. private EventHandler<PlayEventArgs> playEvent;

public event EventHandler<PlayEventArgs> PlayEvent { add {

playEvent = (EventHandler<PlayEventArgs>) Delegate.Combine( playEvent, value );

}

remove {

playEvent = (EventHandler<PlayEventArgs>) Delegate.Remove( playEvent, value );

}

}

public void UserPressedPlay() { OnPlay();

}

protected virtual void OnPlay() { // fire the event.

EventHandler<PlayEventArgs> localHandler = playEvent;

if( localHandler != null ) { localHandler( this,

new PlayEventArgs("somefile.wav") );

}

}

}

Inside the add and remove sections of the event declaration, the delegate being added or removed is referenced through the value keyword, which is identical to the way property setters work. This example uses Delegate.Combine() and Delegate.Remove() to manage an internal delegate chain named playEvent. This example is a bit contrived because the default event mechanism does essentially the same thing, but I show it here for the sake of example.

C H A P T E R 1 0 D E L E G AT E S, A N O N Y M O U S F U N C T I O N S, A N D E V E N T S

243

Note You would want to define custom event accessors explicitly if you needed to define some sort of custom event storage mechanism, or if you needed to perform any other sort of custom processing when events are registered or unregistered.

One final comment regarding design patterns is in order. As described, you can see that events are ideal for implementing a publish/subscribe design pattern, where many listeners are registering for notification (publication) of an event. Similarly, you can use .NET events to implement a form of the Observer pattern, where various entities register to receive notifications that some other entity has changed. These are only two design patterns that events facilitate.

Anonymous Methods

Many times, you may find yourself creating a delegate for a callback that does something very simple. Imagine that you’re implementing a simple engine that processes an array of integers. Let’s say that you design the system flexibly, so that when the processor works on the array of integers, it uses an algorithm that you supply at the point of invocation. This pattern of usage is called the Strategy pattern. In this pattern, you can choose to use a different computation strategy by providing

a mechanism to specify the algorithm to use at run time. A delegate is the perfect tool for implementing such a system. Let’s see what an example would look like:

using System;

public delegate int ProcStrategy( int x );

public class Processor

{

private ProcStrategy strategy; public ProcStrategy Strategy {

set {

strategy = value;

}

}

public int[] Process( int[] array ) { int[] result = new int[ array.Length ];

for( int i = 0; i < array.Length; ++i ) { result[i] = strategy( array[i] );

}

return result;

}

}

public class EntryPoint

{

private static int MultiplyBy2( int x ) { return x*2;

}

private static int MultiplyBy4( int x ) { return x*4;

}

Соседние файлы в папке c#