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

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

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

244 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

private static void PrintArray( int[] array ) { for( int i = 0; i < array.Length; ++i ) { Console.Write( "{0}", array[i] );

if( i != array.Length-1 ) { Console.Write( ", " );

}

}

Console.Write( "\n" );

}

static void Main() {

// Create an array of integers. int[] integers = new int[] {

1, 2, 3, 4

};

Processor proc = new Processor();

proc.Strategy = new ProcStrategy( EntryPoint.MultiplyBy2 ); PrintArray( proc.Process(integers) );

proc.Strategy = new ProcStrategy( EntryPoint.MultiplyBy4 ); PrintArray( proc.Process(integers) );

}

}

Conceptually, the idea sounds really easy. However, in practice, you must do a few complicated things to make this work. First, you have to define a delegate type to represent the strategy method. In the previous example, that’s the ProcStrategy delegate type. Then, you have to write the various strategy methods themselves. After that, the delegates are created and bound to those methods and registered with the processor. In essence, these actions feel disjointed in their flow. It would feel much more natural to be able to define the delegate method in a less verbose way. Many times, the infrastructure required with using delegates makes the code hard to follow, since the pieces of the mechanism are sprinkled around various different places in the code.

Anonymous methods provide an easier and compact way to define simple delegates such as these. Anonymous methods are new to C# 2005, and in short, they allow you to define the method body of the delegate at the point where you instantiate the delegate. Let’s look at how you can modify the previous example to use anonymous methods. The following is the updated portion of the example:

public class EntryPoint

{

private static void PrintArray( int[] array ) { for( int i = 0; i < array.Length; ++i ) { Console.Write( "{0}", array[i] );

if( i != array.Length-1 ) { Console.Write( ", " );

}

}

Console.Write( "\n" );

}

static void Main() {

// Create an array of integers. int[] integers = new int[] {

1, 2, 3, 4

};

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

245

Processor proc = new Processor(); proc.Strategy = delegate(int x) {

return x*2;

};

PrintArray( proc.Process(integers) );

proc.Strategy = delegate(int x) { return x*4;

};

PrintArray( proc.Process(integers) );

proc.Strategy = delegate { return 0;

};

PrintArray( proc.Process(integers) );

}

}

Notice that the two methods, MultiplyBy2 and MultiplyBy4, are gone. Instead, a delegate is created using a special syntax for anonymous methods at the point where it is assigned to the Processor.Strategy property. You can see that the syntax is almost as if you took the delegate declaration and the method you wired the delegate to and mashed them together into one. Basically, anywhere that you can pass a delegate instance as a parameter, you can pass an anonymous method instead.

When you pass an anonymous method in a parameter list that accepts a delegate, or when you assign a delegate type from an anonymous method, you must be concerned with anonymous method type conversion. Behind the scenes, your anonymous method is turned into a regular delegate that is treated just like any other delegate instance.

When you assign an anonymous method to a delegate instance storage location, a number of rules must apply. First, the parameter types of the delegate must be compatible with those of the anonymous method. In the previous example’s first two delegate usages, I showed you the long way to declare an anonymous method. Some of you may have noticed the different syntax in the third usage in the previous example. I left out the parameter list because the body of the method doesn’t even use it. Yet, I was still able to set the Strategy property based upon this anonymous method, so clearly, some type conversion has occurred. Basically, if the anonymous method has no parameter list, then it is convertible to a delegate type that has a parameter list, as long as the list doesn’t include any out or ref parameters. If there are out parameters, the anonymous method is forced to list them in its parameter list at the point of declaration.

Second, if the anonymous method does list any parameters in its declaration, it must list the same count of parameters as the delegate type, and each one of those types must be implicitly convertible to the types in the delegate declaration. Finally, the return type returned from the anonymous method must be implicitly convertible to the declared return type of the delegate type it is being assigned to. Since the anonymous method declaration syntax doesn’t explicitly state what the return type is, the compiler must examine each return statement within the anonymous method and make sure it returns a type that matches the convertibility rules.

So far, anonymous methods have saved a small amount of typing and made the code more readable. But let’s look at the scoping rules involved with anonymous methods. With C#, you already know that curly braces define units of nested scope. The braces delimiting anonymous methods are no different. Take a look at the following modifications to the previous example:

using System;

public delegate int ProcStrategy( int x );

246 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

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 Factor

{

public Factor( int fact ) { this.fact = fact;

}

private int fact;

public ProcStrategy Multiplier { get {

// This is an anonymous method. return delegate(int x) {

return x*fact;

};

}

}

public ProcStrategy Adder { get {

// This is an anonymous method. return delegate(int x) {

return x+fact;

};

}

}

}

public class EntryPoint

{

private static void PrintArray( int[] array ) { for( int i = 0; i < array.Length; ++i ) { Console.Write( "{0}", array[i] );

if( i != array.Length-1 ) { Console.Write( ", " );

}

}

Console.Write( "\n" );

}

static void Main() {

// Create an array of integers.

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

247

int[] integers = new int[] { 1, 2, 3, 4

};

Factor factor = new Factor( 2 ); Processor proc = new Processor(); proc.Strategy = factor.Multiplier; PrintArray( proc.Process(integers) );

proc.Strategy = factor.Adder; factor = null;

PrintArray( proc.Process(integers) );

}

}

In particular, pay close attention to the Factor class in the previous example. I have made the Processor more flexible so that I can apply the factor differently, using either multiplication or addition. Notice that the anonymous methods in the Factor class are using a variable that is accessible within the scope they are defined—namely, the factor instance field. You can do this because the regular scoping rules apply to even the block of the anonymous method. There’s something tricky going on here, though. See where I set the factor instance variable in Main() to null? Notice that

I did it before the delegate obtained from the Factor.Adder property is used. That’s fine, because the Adder property returns a delegate instance, even though I decided to declare the delegate as an anonymous method rather than the original way. But what about that Factor.fact instance field? If I set the factor variable to null in Main(), then the GC can collect the factor object even before the delegate, which uses the field, is done with it, right? Could this actually be a volatile race condition if the GC collects the Factor.fact instance before the delegate is finished with it? The answer is no, because the delegate has captured the variable.

Within anonymous method declarations, any variables defined outside the scope of the anonymous method but accessible to the anonymous method’s scope, including the this reference, are considered outer variables. And whenever an anonymous method body references one of these variables, it is said that the anonymous method has captured the variable. Thus, the Factor.fact field in the previous example will continue to live as it is still referenced in active delegates.

The ability of anonymous method bodies to access variables within their containing definition scope is enormously useful. Imagine how much more difficult it would have been to achieve the same mechanism as the previous example using regular delegates. You would have to create

a mechanism, external to the delegate, to maintain the factor that you want the delegate to use. One solution when using standard delegates is to introduce another level of indirection in the form of

a class, as is so often done when solving problems like these. However, I’m sure you’ll agree that anonymous methods can save a fair amount of work, not to mention they can increase the brevity and readability of your code significantly.

Beware the Captured Variable Surprise

When a variable is captured by an instance of an anonymous method, you have to be careful of the implications that can have. Keep in mind that a captured variable’s representation lives on the heap somewhere, and the variable in a delegate instance is merely a reference to that data. Therefore, it’s entirely possible that two delegate instances created from an anonymous method can hold references to the same variable. Let me show an example of what I’m talking about:

using System;

public delegate void PrintAndIncrement();

248 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

public class EntryPoint

{

public static PrintAndIncrement[] CreateDelegates() { PrintAndIncrement[] delegates = new PrintAndIncrement[3]; int someVariable = 0;

int anotherVariable = 1;

for( int i = 0; i < 3; ++i ) { delegates[i] = delegate {

Console.WriteLine( someVariable++ );

};

}

return delegates;

}

static void Main() {

PrintAndIncrement[] delegates = CreateDelegates(); for( int i = 0; i < 3; ++i ) {

delegates[i]();

}

}

}

The anonymous method inside the CreateDelegates method captures someVariable, which is a local variable in the CreateDelegates method scope. However, since three instances of the anonymous method are put into the array, three anonymous method instances have now captured the same instance of the same variable. Therefore, when the previous code is run, the result looks like this:

0

1

2

As each delegate is called, it prints and increments the same variable. Now, consider what effect a small change in the CreateDelegates method can have. If you move the someVariable declaration into the loop that creates the delegate array, a fresh instance of the local variable is instantiated every time you go through the loop. Notice the following change to the CreateDelegates method:

public static PrintAndIncrement[] CreateDelegates() { PrintAndIncrement[] delegates = new PrintAndIncrement[3]; for( int i = 0; i < 3; ++i ) {

int someVariable = 0; delegates[i] = delegate {

Console.WriteLine( someVariable++ );

};

}

return delegates;

}

This time, the output is as follows:

0

0

0

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

249

This is why you need to be careful when you use variable capture in anonymous delegates. In the first case, the three delegates all captured the same variable. In the second case, they all captured separate instances of the variable, because each iteration of the for loop creates a new (separate) variable on the stack. Although you should keep this powerful feature handy in your bag of tricks, you must know what you’re doing so you don’t end up shooting yourself in the foot.

Savvy readers may be wondering how the previous code can possibly work without blowing up, since the captured variables are value types that live on the stack by default. Remember that value types are created on the stack unless they happen to be declared as a field in a reference type that is created on the heap, which includes the case when they are boxed. However, someVariable is a local variable, so under normal circumstances, it is created on the stack. But, these are not normal circumstances. Clearly, it’s not possible for an instance of an anonymous method to capture a local variable on the stack and expect it to be there later when it needs to reference it. It must live on the heap. Local value type variables that are captured must have different lifetime rules than other noncaptured local value type variables. Therefore, the compiler does quite a bit of magic under the covers when it encounters local value type captured variables.

When the compiler encounters a captured value type variable, it silently creates a class behind the scenes. Where the code initializes the local variable, the compiler generates IL code that creates an instance of this transparent class and initializes the field, which, in this case, represents someVariable. You can verify this with the first example if you open the compiled code in ILDASM. I included the dummy variable anotherVariable so you could see the difference in how the IL treats them. Since anotherVariable is not captured, it is created on the stack, as you’d expect. The following code contains a portion of the IL for the CreateDelegates() call after compiling the example with debugging symbols turned on:

// Code

size

85 (0x55)

.maxstack 5

 

.locals

init ([0] class PrintAndIncrement[] delegates,

[1]int32 anotherVariable,

[2]int32 i,

[3]class PrintAndIncrement '<>9__CachedAnonymousMethodDelegate1',

[4]class EntryPoint/'<>c__DisplayClass2' '<>8__locals3',

[5]class PrintAndIncrement[] CS$1$0000,

[6]bool CS$4$0001)

IL_0000:

ldnull

 

IL_0001:

stloc.3

 

IL_0002:

newobj

instance void EntryPoint/'<>c__DisplayClass2'::.ctor()

IL_0007:

stloc.s

'<>8__locals3'

IL_0009:

nop

 

IL_000a:

ldc.i4.3

 

IL_000b:

newarr

PrintAndIncrement

IL_0010:

stloc.0

 

IL_0011:

ldloc.s

'<>8__locals3'

IL_0013:

ldc.i4.0

 

IL_0014:

stfld

int32 EntryPoint/'<>c__DisplayClass2'::someVariable

IL_0019:

ldc.i4.1

 

IL_001a:

stloc.1

 

IL_001b:

ldloc.1

 

IL_001c:

call

void [mscorlib]System.Console::WriteLine(int32)

Note the two variables’ usages. In line IL_0002, a new instance of the hidden class is created. In this case, the compiler named the class <>c__DisplayClass2. That class contains a public instance field named someVariable, and it gets assigned in IL_0014. The compiler has transparently inserted the proverbial extra level of indirection in the form of a class to solve this sticky wicket of local value types captured by anonymous methods. Also, note the fact that anotherVariable is treated just like a normal stack-based variable, as can be shown by the fact that it is declared in the local variables portion of the method.

250 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

Anonymous Methods As Delegate Parameter Binders

Anonymous methods, coupled with variable capture, can provide a convenient means of implementing parameter binding on delegates. Parameter binding is a technique where you want to call a delegate, typically with more than one parameter, in a way where one or more of the parameters are fixed while the others can vary per delegate invocation. For example, if you have a delegate that takes two parameters, and you’d like to convert it into a delegate that takes one parameter where the other parameter is fixed, you could use parameter binding to accomplish this feat. Those of you C++ programmers who are familiar with the STL or the Boost Library may be familiar with parameter binders. Let me show an example of what I’m talking about:

using System;

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

public class Bind2nd

{

public delegate int BoundDelegate( int x );

public Bind2nd( Operation del, int arg2 ) { this.del = del;

this.arg2 = arg2;

}

public BoundDelegate Binder { get {

return delegate( int arg1 ) { return del( arg1, arg2 );

};

}

}

private Operation del; private int arg2;

}

public class EntryPoint

{

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

}

static void Main() {

Bind2nd binder = new Bind2nd(

new Operation(EntryPoint.Add), 4 );

Console.WriteLine( binder.Binder(2) );

}

}

In the previous example, the delegate of type Operation with two parameters, which calls back into the static EntryPoint.Add method, is converted into a delegate that only takes one parameter. The second parameter is fixed using the Bind2nd class. Basically, the instance field Bind2nd.arg2 is set to the value that you want the second parameter fixed to. Then, the Bind2nd.Binder property returns a new delegate in the form of an anonymous method instance, which captures the instance field and applies it along with the first parameter that is applied at the point of invocation.

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

251

Readers familiar with the C++ STL are probably exclaiming that this example would be infinitely more useful if Bind2nd was generic so it could support a generic two-parameter delegate, much like the binder in STL does. This would be nice indeed; however, some language barriers make it a bit tricky. Let’s start with an attempt to make the delegate type generic in the Bind2nd class. You could try the following:

// WILL NOT COMPILE !!!

public class Bind2nd< DelegateType >

{

public delegate int BoundDelegate( int x );

public Bind2nd( DelegateType del, int arg2 ) { this.del = del;

this.arg2 = arg2;

}

public BoundDelegate Binder { get {

return delegate( int arg1 ) {

return del( arg1, arg2 ); // OUCH!

};

}

}

private DelegateType del; private int arg2;

}

This is a noble attempt, but unfortunately, it fails miserably because the compiler gets confused inside the anonymous method body and complains that an instance field is being used like a method. The compiler’s correct. That’s exactly what you want to do, even though the compiler cannot make heads or tails of it. What is a programmer to do?

Another attempt involves generic constraints. Using constraints, you can say that even though the type is generic, it must derive from a certain base class or implement a specific interface. Fair enough! Let’s just help the compiler out and tell it that DelegateType will derive from System.Delegate, as follows:

// STILL WILL NOT COMPILE !!!

public class Bind2nd< DelegateType > where DelegateType : Delegate

{

public delegate int BoundDelegate( int x );

public Bind2nd( DelegateType del, int arg2 ) { this.del = del;

this.arg2 = arg2;

}

public BoundDelegate Binder { get {

return delegate( int arg1 ) {

return del( arg1, arg2 ); // OUCH!

};

}

}

252 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

private DelegateType del; private int arg2;

}

Alas, we’re stuck again! This time the compiler says that a constraint of type Delegate is not allowed. It turns out that the solution lies with using generic delegates coupled with the Delegate. CreateDelegate static method to get the job done. The following is a solution to the problem:

using System;

using System.Reflection;

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

public class Bind2nd<Arg1Type, Arg2Type, ReturnType>

{

public delegate ReturnType UnboundDelegate<UBArg1Type, UBArg2Type>( UBArg1Type arg1,

UBArg2Type arg2 );

public delegate ReturnType BoundDelegate<BArg1Type>( BArg1Type x );

public Bind2nd( Delegate del, Arg2Type arg2 ) { // Get the types from the delegate.

object target = del.Target; MethodInfo targetMethod = del.Method;

Type targetType = targetMethod.ReflectedType;

if( target == null ) { // Static method

this.del = (UnboundDelegate<Arg1Type, Arg2Type>) Delegate.CreateDelegate(

typeof(UnboundDelegate<Arg1Type, Arg2Type>), targetType,

targetMethod.Name );

}else {

//Instance method

this.del = (UnboundDelegate<Arg1Type, Arg2Type>) Delegate.CreateDelegate(

typeof(UnboundDelegate<Arg1Type, Arg2Type>), target,

targetMethod.Name );

}

this.arg2 = arg2;

}

public BoundDelegate<Arg1Type> Binder { get {

return delegate( Arg1Type arg1 ) { return del( arg1, arg2 );

};

}

}

private UnboundDelegate<Arg1Type, Arg2Type> del; private Arg2Type arg2;

}

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

253

public class EntryPoint

{

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

}

static void Main() {

Bind2nd<int,int,int> binder = new Bind2nd<int,int,int>( new Operation(EntryPoint.Add),

4 );

Console.WriteLine( binder.Binder(2) );

}

}

The trickery behind this solution lies in two places. First, in order for the anonymous method to be able to use the del field as a method, the compiler must know that it is a delegate. Also, it cannot simply be of type System.Delegate. In order to call through to a delegate using the method call syntax, it must be a concrete delegate type. That’s where the generic delegate UnboundDelegate comes in. For good measure, I’ve also introduced the BoundDelegate type, which is a generic delegate that only takes one parameter—the unbound parameter. The second trick is in the constructor. Unfortunately, the C# language doesn’t allow a shortcut to convert a delegate of one type to another type, even if both delegate types support the exact same method signature. Therefore, you have to crack the target type and method information out of the original delegate in order to build an UnboundDelegate instance via the call to Delegate.CreateDelegate(). Armed with these two tricks, you now have a working generic binder.

Strategy Pattern

Delegates offer up a handy mechanism to implement the Strategy pattern. In a nutshell, the Strategy pattern allows you to dynamically swap computational algorithms based upon the runtime situation. For example, consider the common case of sorting a group of items. Let’s suppose that you want the sort to occur as quickly as possible. However, due to system circumstances, more temporary memory is required in order to achieve this speed. This works great for collections of reasonably manageable size, but if the collection grows to be huge, it’s possible that the amount of memory needed to perform the quick sort could exceed the system resource capacity. For those cases, you can provide a sort algorithm that is much slower but uses far fewer resources. The Strategy pattern allows you to swap out these algorithms at run time depending on the conditions. This example, although a tad contrived, illustrates the purpose of the Strategy pattern perfectly.

Typically, you implement the Strategy pattern using interfaces. You declare an interface that all implementations of the strategy implement. Then, the consumer of the algorithm doesn’t have to care which concrete implementation of the strategy it is using. Figure 10-1 features a diagram that describes this typical usage.

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