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

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

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

372 CHAPTER 11 DELEGATES, EVENTS, AND LAMBDAS

// Register event handlers as anonymous methods. c1.AboutToBlow += delegate

{

aboutToBlowCounter++;

Console.WriteLine("Eek! Going too fast!");

};

c1.AboutToBlow += delegate(string msg)

{

aboutToBlowCounter++;

Console.WriteLine("Critical Message from Car: {0}", msg);

};

...

Console.WriteLine("AboutToBlow event was fired {0} times.", aboutToBlowCounter);

Console.ReadLine();

}

Once you run this updated Main() method, you will find the final Console.WriteLine() reports the AboutToBlow event was fired twice.

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

Understanding Method Group Conversions

Another delegate-and-event-centric feature of C# is termed method group conversion. This feature allows you to register the “simple” name of an event handler (in fact, you may have noticed that this syntax was actually used earlier in this chapter during the GenericDelegate example). To illustrate, let’s revisit the SimpleMath type examined earlier in this chapter, which is now updated with a new event named ComputationFinished:

public class SimpleMath

{

//Not bothering to create a System.EventArgs

//derived type here.

public delegate void MathMessage(string msg); public event MathMessage ComputationFinished;

public int Add(int x, int y)

{

ComputationFinished("Adding complete."); return x + y;

}

public int Subtract(int x, int y)

{

ComputationFinished("Subtracting complete."); return x - y;

}

}

CHAPTER 11 DELEGATES, EVENTS, AND LAMBDAS

373

If we are not using anonymous method syntax, you know that the way we would handle the

ComputationComplete event is as follows:

class Program

{

static void Main(string[] args)

{

SimpleMath m = new SimpleMath(); m.ComputationFinished +=

new SimpleMath.MathMessage(ComputationFinishedHandler); Console.WriteLine("10 + 10 is {0}", m.Add(10, 10)); Console.ReadLine();

}

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

}

However, we can register the event handler with a specific event like this (the remainder of the code is identical):

m.ComputationFinished += ComputationFinishedHandler;

Notice that we are not directly allocating the associated delegate object, but rather simply specifying a method that matches the delegate’s expected signature (a method returning void and taking a single System.String in this case). Understand that the C# compiler is still ensuring type safety. Thus, if the ComputationFinishedHandler() method did not take a System.String and return void, we would be issued a compiler error.

It is also possible to explicitly convert an event handler into an instance of the delegate it relates to. This can be helpful if you need to obtain the underlying delegate to interact with the inherited members of System.MulticastDelegate. For example:

//Event handlers to be converted into

//their underlying delegate.

SimpleMath.MathMessage mmDelegate = (SimpleMath.MathMessage)ComputationFinishedHandler;

Console.WriteLine(mmDelegate.Method);

If you executed this code, the final Console.WriteLine() prints out the signature of

ComputationFinishedHandler, as shown in Figure 11-8.

Figure 11-8. You can extract a delegate from the related event handler.

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

374 CHAPTER 11 DELEGATES, EVENTS, AND LAMBDAS

The C# 2008 Lambda Operator

To conclude our look at the .NET event architecture, we will close with an examination of C# 2008 lambda expressions. As explained earlier in this chapter, C# supports the ability to handle events “inline” by assigning a block of code statements directly to an event (using anonymous methods), rather than building a stand-alone method to be called by the underlying delegate. Lambda expressions are nothing more than a more concise way to author anonymous methods and ultimately simplify how we work with the .NET delegate type.

Note C# 2008 also allows you to represent lambda expressions as an in-memory object using expression trees. This can be very useful for third parties who are building software that needs to extend the functionality of existing lambdas, as well as when programming with Language Integrated Query (LINQ). Consult the .NET Framework 3.5 SDK documentation.

To set the stage for our examination of lambda expressions, create a new Console Application named SimpleLambdaExpressions. Now, consider the FindAll() method of the generic List<T> type. This method is expecting a generic delegate of type System.Predicate<T>, used to wrap any method returning a Boolean and taking a specified T as the only input parameter. Add a method (named TraditionalDelegateSyntax()) within your Program type that interacts with the System. Predicate<T> type to discover the even numbers in a List<T> of integers:

class Program

{

static void Main(string[] args)

{

Console.WriteLine("***** Fun with Lambdas *****\n");

TraditionalDelegateSyntax();

Console.ReadLine();

}

static void TraditionalDelegateSyntax()

{

//Make a list of integers.

List<int> list = new List<int>(); list.AddRange(new int[] { 20, 1, 4, 8, 9, 44 });

//Call FindAll() using traditional delegate syntax.

Predicate<int> callback = new Predicate<int>(IsEvenNumber); List<int> evenNumbers = list.FindAll(callback);

Console.WriteLine("Here are your even numbers:"); foreach (int evenNumber in evenNumbers)

{

Console.Write("{0}\t", evenNumber);

}

Console.WriteLine();

}

// Target for the Predicate<> delegate. static bool IsEvenNumber(int i)

{

// Is it an even number?

CHAPTER 11 DELEGATES, EVENTS, AND LAMBDAS

375

return (i % 2) == 0;

}

}

Here, we have a method (IsEvenNumber()) that is in charge of testing the incoming integer parameter to see whether it is even or odd via the C# modulo operator, %. If you execute your application, you will find the numbers 20, 4, 8, and 44 print out to the console.

While this traditional approach to working with delegates works as expected, the IsEvenNumber() method, however, is only invoked under very limited circumstances; specifically, when we call FindAll(), which leaves us with the baggage of a full method definition. If we were to instead use an anonymous method, our code would clean up considerably. Consider the following new method of the Program type:

static void AnonymousMethodSyntax()

{

//Make a list of integers.

List<int> list = new List<int>(); list.AddRange(new int[] { 20, 1, 4, 8, 9, 44 });

//Now, use an anonymous method.

List<int> evenNumbers = list.FindAll(delegate(int i) { return (i % 2) == 0; } );

Console.WriteLine("Here are your even numbers:"); foreach (int evenNumber in evenNumbers)

{

Console.Write("{0}\t", evenNumber);

}

Console.WriteLine();

}

In this case, rather than directly creating a Predicate<T> delegate type, and then authoring a stand-alone method, we are able to inline a method anonymously. While this is a step in the right direction, we are still required to use the delegate keyword (or a strongly typed Predicate<T>), and must ensure that the parameter list is a dead-on match. As well, as you may agree, the syntax used to define an anonymous method can be viewed as being a bit hard on the eyes, which is even more apparent here:

List<int> evenNumbers = list.FindAll( delegate(int i)

{

return (i % 2) == 0;

}

);

Lambda expressions can be used to simplify our call to FindAll() even more. When we make use of this new syntax, there is no trace of the underlying delegate whatsoever. Consider the following update to our code base:

static void LambdaExpressionSyntax()

{

//Make a list of integers.

List<int> list = new List<int>(); list.AddRange(new int[] { 20, 1, 4, 8, 9, 44 });

//Now, use a C# 2008 lambda expression.

List<int> evenNumbers = list.FindAll(i => (i % 2) == 0);

376 CHAPTER 11 DELEGATES, EVENTS, AND LAMBDAS

Console.WriteLine("Here are your even numbers:"); foreach (int evenNumber in evenNumbers)

{

Console.Write("{0}\t", evenNumber);

}

Console.WriteLine();

}

In this case, notice the rather strange statement of code passed into the FindAll() method, which is in fact a lambda expression. Notice that in this iteration of the example, there is no trace whatsoever of the Predicate<T> delegate (or the delegate keyword for that matter). All we have specified is the lambda expression: i => (i % 2) == 0.

Before we break this syntax down, at this level simply understand that lambda expressions can be used anywhere you would have used an anonymous method or a strongly typed delegate (typically with far fewer keystrokes). Under the hood, the C# compiler translates our expression into a standard anonymous method making use of the Predicate<T> delegate type (which can be verified using ildasm.exe or reflector.exe). Specifically, the following code statement:

// This lambda expression...

List<int> evenNumbers = list.FindAll(i => (i % 2) == 0);

is compiled into the following approximate C# code:

// ...becomes this anonymous method.

List<int> evenNumbers = list.FindAll(delegate (int i)

{

return (i % 2) == 0; });

Dissecting a Lambda Expression

A lambda expression is written by first defining a parameter list, followed by the => token (C#’s token for the lambda operator found in the lambda calculus), followed by a set of statements (or a single statement) that will process these arguments. From a very high level, a lambda expression can be understood as follows:

ArgumentsToProcess => StatementsToProcessThem

Within our LambdaExpressionSyntax() method, things break down like so:

//"i" is our parameter list.

//"(i % 2) == 0" is our statement set to process "i".

List<int> evenNumbers = list.FindAll(i => (i % 2) == 0);

The parameters of a lambda expression can be explicitly or implicitly typed. Currently, the underlying data type representing the i parameter (an integer) is determined implicitly. The compiler is able to figure out that i is an integer based on the context of the overall lambda expression and the underlying delegate. However, it is also possible to explicitly define the type of each parameter in the expression, by wrapping the data type and variable name in a pair of parentheses as follows:

// Now, explicitly state what the parameter type.

List<int> evenNumbers = list.FindAll((int i) => (i % 2) == 0);

As you have seen, if a lambda expression has a single, implicitly typed parameter, the parentheses may be omitted from the parameter list. If you wish to be consistent regarding your use of lambda parameters, you are free to always wrap the parameter list within parentheses, leaving us with this expression:

CHAPTER 11 DELEGATES, EVENTS, AND LAMBDAS

377

List<int> evenNumbers = list.FindAll((i) => (i % 2) == 0);

Finally, notice that currently our expression has not been wrapped in parentheses (we have of course wrapped the modulo statement to ensure it is executed first before the test for equality). Lambda expressions do allow for the statement to be wrapped as follows:

// Now, wrap the expression as well.

List<int> evenNumbers = list.FindAll((i) => ((i % 2) == 0));

Now that you have seen the various ways to build a lambda expression, how can we read this lambda statement in human-friendly terms? Leaving the raw mathematics behind, the following explanation fits the bill:

//My list of parameters (in this case a single integer named i)

//will be processed by the expression (i % 2) == 0.

List<int> evenNumbers = list.FindAll((i) => ((i % 2) == 0));

Processing Arguments Within Multiple Statements

Our first lambda expression was a single statement that ultimately evaluated to a Boolean. However, as you know full well, many delegate targets must perform a number of code statements. For this reason, C# 2008 allows you to build lambda expressions using multiple statement blocks. When your expression must process the parameters using multiple lines of code, you can do so by denoting a scope for these statements using the expected curly brackets. Consider the following example update to our LambdaExpressionSyntax() method:

static void LambdaExpressionSyntax()

{

//Make a list of integers.

List<int> list = new List<int>(); list.AddRange(new int[] { 20, 1, 4, 8, 9, 44 });

//Now process each argument within a group of

//code statements.

List<int> evenNumbers = list.FindAll((i) =>

{

Console.WriteLine("value of i is currently: {0}", i); bool isEven = ((i % 2) == 0);

return isEven; });

Console.WriteLine("Here are your even numbers:"); foreach (int evenNumber in evenNumbers)

{

Console.Write("{0}\t", evenNumber);

}

Console.WriteLine();

}

In this case, our parameter list (again, a single integer named i) is being processed by a set of code statements. Beyond the calls to Console.WriteLine(), our modulo statement has been broken into two code statements for increased readability. Assuming each of these three methods are called from within Main():

static void Main(string[] args)

{

Console.WriteLine("***** Fun with Lambdas *****\n");

TraditionalDelegateSyntax();

378 CHAPTER 11 DELEGATES, EVENTS, AND LAMBDAS

AnonymousMethodSyntax();

Console.WriteLine();

LambdaExpressionSyntax();

Console.ReadLine();

}

we will find the output in Figure 11-9.

Figure 11-9. The output of your first lambda expression

Source Code The SimpleLambdaExpressions project can be found under the Chapter 11 subdirectory.

Retrofitting the CarDelegate Example Using Lambda

Expressions

Given that the whole reason for lambda expressions is to provide a clean, concise manner to define an anonymous method (and therefore indirectly a manner to simplify working with delegates), let’s retrofit the CarDelegate project we created earlier in this chapter. Here is a simplified version of that project’s Program class, which makes use of “normal” delegate syntax to respond to each callback:

class Program

{

static void Main(string[] args)

{

Console.WriteLine("***** More Fun with Lambdas *****\n");

// Make a car as usual.

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

// Normal delegate syntax.

c1.OnAboutToBlow(new Car.AboutToBlow(CarAboutToBlow)); c1.OnExploded(new Car.Exploded(CarExploded));

// Speed up (this will generate the events). Console.WriteLine("\n***** Speeding up *****");

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

CHAPTER 11 DELEGATES, EVENTS, AND LAMBDAS

379

Console.ReadLine();

}

// Delegate targets.

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

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

}

Here again is the Main() method now making use of anonymous methods:

static void Main(string[] args)

{

Console.WriteLine("***** More Fun with Lambdas *****\n");

// Make a car as usual.

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

// Now use anonymous methods.

c1.OnAboutToBlow(delegate(string msg) { Console.WriteLine(msg); }); c1.OnExploded(delegate(string msg) { Console.WriteLine(msg); });

// Speed up (this will generate the events.) Console.WriteLine("\n***** Speeding up *****");

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

Console.ReadLine();

}

And finally, here is the Main() method now using lambda expressions:

static void Main(string[] args)

{

Console.WriteLine("***** More Fun with Lambdas *****\n");

// Make a car as usual.

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

// Now with lambdas!

c1.OnAboutToBlow(msg => { Console.WriteLine(msg); }); c1.OnExploded(msg => { Console.WriteLine(msg); });

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

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

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

Console.ReadLine();

}

Source Code The CarDelegateWithLambdas project can be found under the Chapter 11 subdirectory.

380 CHAPTER 11 DELEGATES, EVENTS, AND LAMBDAS

Lambda Expressions with Multiple (or Zero) Parameters

Each of the lambda expressions you have seen here processed a single parameter. This is not a requirement, however, as a lambda expression may process multiple arguments or provide no arguments whatsoever. To illustrate the first scenario, create a final Console Application named LambdaExpressionsMultipleParams. Next, assume the following incarnation of the SimpleMath type:

public class SimpleMath

{

public delegate void MathMessage(string msg, int result); private MathMessage mmDelegate;

public void SetMathHandler(MathMessage target) {mmDelegate = target; }

public void Add(int x, int y)

{

if (mmDelegate != null)

mmDelegate.Invoke("Adding has completed!", x + y);

}

}

Notice that the MathMessage delegate is expecting two parameters. To represent them as a lambda expression, our Main() method might be written as follows:

static void Main(string[] args)

{

//Register w/ delegate as a lambda expression.

SimpleMath m = new SimpleMath(); m.SetMathHandler((msg, result) =>

{Console.WriteLine("Message: {0}, Result: {1}", msg, result);});

//This will execute the lambda expression.

m.Add(10, 10); Console.ReadLine();

}

Here, we are leveraging type inference, as our two parameters have not been strongly typed for simplicity. However, we could call SetMathHandler() as follows:

m.SetMathHandler((string msg, int result) => {Console.WriteLine("Message: {0}, Result: {1}", msg, result);});

Finally, if you are using a lambda expression to interact with a delegate taking no parameters at all, you may do so by supplying a pair of empty parentheses as the parameter. Thus, assuming you have defined the following delegate type:

public delegate string VerySimpleDelegate();

you could handle the result of the invocation as follows:

// Prints "Enjoy your string!" to the console.

VerySimpleDelegate d = new VerySimpleDelegate( () => {return "Enjoy your string!";} ); Console.WriteLine(d.Invoke());

So hopefully at this point you can see the overall role of lambda expressions and understand how they provide a “functional manner” to work with anonymous methods and delegate types. Although the new lambda operator (=>) might take a bit to get used to, always remember a lambda expression can be broken down to the following simple equation:

ArgumentsToProcess => StatementsToProcessThem

CHAPTER 11 DELEGATES, EVENTS, AND LAMBDAS

381

It is worth pointing out that the LINQ programming model also makes substantial use of lambda expressions to help simplify your coding efforts. You will examine LINQ beginning in Chapter 14.

Source Code The LambdaExpressionsMultipleParams project can be found under the Chapter 11 subdirectory.

Summary

In this chapter, you have examined a number of ways in which multiple objects can partake in a bidirectional conversation. First, you examined the C# delegate keyword, which is used to indirectly construct a class derived from System.MulticastDelegate. As you have seen, a delegate is simply an object that maintains a list of methods to call when told to do so. These invocations may be made synchronously (using the Invoke() method) or asynchronously (via the BeginInvoke() and EndInvoke() methods). Again, the asynchronous nature of .NET delegate types will be examined in Chapter 18.

You then examined the C# event keyword which, when used in conjunction with a delegate type, can simplify the process of sending your event notifications to awaiting callers. As shown via the resulting CIL, the .NET event model maps to hidden calls on the System.Delegate/System. MulticastDelegate types. In this light, the C# event keyword is purely optional in that it simply saves you some typing time.

This chapter also examined a C# language feature termed anonymous methods. Using this syntactic construct, you are able to directly associate a block of code statements to a given event. As you have seen, anonymous methods are free to ignore the parameters sent by the event and have access to the “outer variables” of the defining method. You also examined a simplified way to register events using method group conversion.

Finally, we wrapped things up by examining the C# 2008 lambda operator, =>. As shown, this syntax is a great shorthand notation for authoring anonymous methods, where a stack of arguments can be passed into a group of statements for processing.