This chapter examines three innovative C# features: delegates, events, and lambda expressions. A delegate provides a way to encapsulate a method. An event is a notification that some action has occurred. Delegates and events are related because an event is built upon a delegate. Both expand the set of programming tasks to which C# can be applied. The lambda expression is a relatively new syntactic feature that offers a streamlined, yet powerful way to define what is, essentially, a unit of executable code. Lambda expressions are often used when working with delegates and events because a delegate can refer to a lambda expression. (Lambda expressions are also very important to LINQ, which is described in Chapter 19.) Also examined are anonymous methods, covariance, contravariance, and method group conversions.
Let’s begin by defining the term delegate. In straightforward language, a delegate is an object that can refer to a method. Therefore, when you create a delegate, you are creating an object that can hold a reference to a method. Furthermore, the method can be called through this reference. In other words, a delegate can invoke the method to which it refers. As you will see, this is a very powerful concept.
It is important to understand that the same delegate can be used to call different methods during the runtime of a program by simply changing the method to which the delegate refers. Thus, the method that will be invoked by a delegate is not determined at compile time, but rather at runtime. This is the principal advantage of a delegate.
NOTE If you are familiar with C/C++, then it will help to know that a delegate in C# is similar to a function pointer in C/C++.
A delegate type is declared using the keyword delegate. The general form of a delegate declaration is shown here:
delegate ret-type name (parameter-list);
Here, ret-type is the type of value returned by the methods that the delegate will be calling. The name of the delegate is specified by name. The parameters required by the methods called through the delegate are specified in the parameter-list. Once created, a delegate instance can refer to and call methods whose return type and parameter list match those specified by the delegate declaration.
A key point to understand is that a delegate can be used to call any method that agrees with its signature and return type. Furthermore, the method can be either an instance method associated with an object or a static method associated with a class. All that matters is that the return type and signature of the method agree with those of the delegate.
To see delegates in action, let’s begin with the simple example shown here:
// A simple delegate example.
using System;
// Declare a delegate type.
delegate string StrMod(string str);
class DelegateTest {
// Replaces spaces with hyphens.
static string ReplaceSpaces(string s) {
Console.WriteLine("Replacing spaces with hyphens.");
return s.Replace(' ', '-');
}
// Remove spaces.
static string RemoveSpaces(string s) {
string temp = "";
int i;
Console.WriteLine("Removing spaces.");
for(i=0; i < s.Length; i++)
if(s[i] != ' ') temp += s[i];
return temp;
}
// Reverse a string.
static string Reverse(string s) {
string temp = "";
int i, j;
Console.WriteLine("Reversing string.");
for(j=0, i=s.Length-1; i >= 0; i--, j++)
temp += s[i];
return temp;
}
static void Main() {
// Construct a delegate.
StrMod strOp = new StrMod(ReplaceSpaces);
string str;
// Call methods through the delegate.
str = strOp("This is a test.");
Console.WriteLine("Resulting string: " + str);
Console.WriteLine();
strOp = new StrMod(RemoveSpaces);
str = strOp("This is a test.");
Console.WriteLine("Resulting string: " + str);
Console.WriteLine();
strOp = new StrMod(Reverse);
str = strOp("This is a test.");
Console.WriteLine("Resulting string: " + str);
}
}
The output from the program is shown here:
Replacing spaces with hyphens.
Resulting string: This-is-a-test.
Removing spaces.
Resulting string: Thisisatest.
Reversing string.
Resulting string: .tset a si sihT
Let’s examine this program closely. The program declares a delegate type called StrMod, shown here:
delegate string StrMod(string str);
Notice that StrMod takes one string parameter and returns a string.
Next, in DelegateTest, three static methods are declared, each with a single parameter of type string and a return type of string. Thus, they match the StrMod delegate. These methods perform some type of string modification. Notice that ReplaceSpaces( ) uses one of string’s methods, called Replace( ), to replace spaces with hyphens.
In Main( ), a StrMod reference called strOp is created and assigned a reference to ReplaceSpaces( ). Pay close attention to this line:
StrMod strOp = new StrMod(ReplaceSpaces);
Notice how the method ReplaceSpaces( ) is passed as a parameter. Only its name is used; no parameters are specified. This can be generalized. When instantiating a delegate, you specify only the name of the method to which you want the delegate to refer. Of course, the method’s signature must match that of the delegate’s declaration. If it doesn’t, a compile-time error will result.
Next, ReplaceSpaces( ) is called through the delegate instance strOp, as shown here:
str = strOp("This is a test.");
Because strOp refers to ReplaceSpaces( ), ReplaceSpaces( ) is invoked.
Next, strOp is assigned a reference to RemoveSpaces( ), and then strOp is called again. This time, RemoveSpaces( ) is invoked.
Finally, strOp is assigned a reference to Reverse( ) and strOp is called. This results in Reverse( ) being called.
The key point of the example is that the invocation of strOp results in a call to the method referred to by strOp at the time at which the invocation occurs. Thus, the method to call is resolved at runtime, not compile time.
Since version 2.0, C# has included an option that significantly simplifies the syntax that assigns a method to a delegate. This feature is called method group conversion, and it allows you to simply assign the name of a method to a delegate, without using new or explicitly invoking the delegate’s constructor.
For example, here is the Main( ) method of the preceding program rewritten to use method group conversions:
static void Main() {
// Construct a delegate using method group conversion.
StrMod strOp = ReplaceSpaces; // use method group conversion
string str;
// Call methods through the delegate.
str = strOp("This is a test.");
Console.WriteLine("Resulting string: " + str);
Console.WriteLine();
strOp = RemoveSpaces; // use method group conversion
str = strOp("This is a test.");
Console.WriteLine("Resulting string: " + str);
Console.WriteLine();
strOp = Reverse; // use method group conversion
str = strOp("This is a test.");
Console.WriteLine("Resulting string: " + str);
}
Pay special attention to the way that strOp is created and assigned the method ReplaceSpaces( ) in this line:
StrMod strOp = ReplaceSpaces; // use method group conversion
The name of the method is assigned directly to strOp. C# automatically provides a conversion from the method to the delegate type. This syntax can be generalized to any situation in which a method is assigned to (or converted to) a delegate type.
Because the method group conversion syntax is simpler than the old approach, it is used throughout the remainder of this book.
Although the preceding example used static methods, a delegate can also refer to instance methods. It must do so, however, through an object reference. For example, here is a rewrite of the previous example, which encapsulates the string operations inside a class called StringOps. Notice that the method group conversion syntax can also be applied in this situation.
// Delegates can refer to instance methods, too.
using System;
// Declare a delegate type.
delegate string StrMod(string str);
class StringOps {
// Replaces spaces with hyphens.
public string ReplaceSpaces(string s) {
Console.WriteLine("Replacing spaces with hyphens.");
return s.Replace(' ', '-');
}
// Remove spaces.
public string RemoveSpaces(string s) {
string temp = "";
int i;
Console.WriteLine("Removing spaces.");
for(i=0; i < s.Length; i++)
if(s[i] != ' ') temp += s[i];
return temp;
}
// Reverse a string.
public string Reverse(string s) {
string temp = "";
int i, j;
Console.WriteLine("Reversing string.");
for(j=0, i=s.Length-1; i >= 0; i--, j++)
temp += s[i];
return temp;
}
}
class DelegateTest {
static void Main() {
StringOps so = new StringOps(); // create an instance of StringOps
// Initialize a delegate.
StrMod strOp = so.ReplaceSpaces;
string str;
// Call methods through delegates.
str = strOp("This is a test.");
Console.WriteLine("Resulting string: " + str);
Console.WriteLine();
strOp = so.RemoveSpaces;
str = strOp("This is a test.");
Console.WriteLine("Resulting string: " + str);
Console.WriteLine();
strOp = so.Reverse;
str = strOp("This is a test.");
Console.WriteLine("Resulting string: " + str);
}
}
This program produces the same output as the first, but in this case, the delegate refers to methods on an instance of StringOps.
One of the most exciting features of a delegate is its support for multicasting. In simple terms, multicasting is the ability to create an invocation list, or chain, of methods that will be automatically called when a delegate is invoked. Such a chain is very easy to create. Simply instantiate a delegate, and then use the + or += operator to add methods to the chain. To remove a method, use – or – =. If the delegate returns a value, then the value returned by the last method in the list becomes the return value of the entire delegate invocation. Thus, a delegate that makes use of multicasting will often have a void return type.
Here is an example of multicasting. Notice that it reworks the preceding examples by changing the string manipulation method’s return type to void and using a ref parameter to return the altered string to the caller. This makes the methods more appropriate for multicasting.
// Demonstrate multicasting.
using System;
// Declare a delegate type.
delegate void StrMod(ref string str);
class MultiCastDemo {
// Replaces spaces with hyphens.
static void ReplaceSpaces(ref string s) {
Console.WriteLine("Replacing spaces with hyphens.");
s = s.Replace(' ', '-');
}
// Remove spaces.
static void RemoveSpaces(ref string s) {
string temp = "";
int i;
Console.WriteLine("Removing spaces.");
for(i=0; i < s.Length; i++)
if(s[i] != ' ') temp += s[i];
s = temp;
}
// Reverse a string.
static void Reverse(ref string s) {
string temp = "";
int i, j;
Console.WriteLine("Reversing string.");
for(j=0, i=s.Length-1; i >= 0; i--, j++)
temp += s[i];
s = temp;
}
static void Main() {
// Construct delegates.
StrMod strOp;
StrMod replaceSp = ReplaceSpaces;
StrMod removeSp = RemoveSpaces;
StrMod reverseStr = Reverse;
string str = "This is a test";
// Set up multicast.
strOp = replaceSp;
strOp += reverseStr;
// Call multicast.
strOp(ref str);
Console.WriteLine("Resulting string: " + str);
Console.WriteLine();
// Remove replace and add remove.
strOp -= replaceSp;
strOp += removeSp;
str = "This is a test."; // reset string
// Call multicast.
strOp(ref str);
Console.WriteLine("Resulting string: " + str);
Console.WriteLine();
}
}
Here is the output:
Replacing spaces with hyphens.
Reversing string.
Resulting string: tset-a-si-sihT
Reversing string.
Removing spaces.
Resulting string: .tsetasisihT
In Main( ), four delegate instances are created. One, strOp, is null. The other three refer to specific string modification methods. Next, a multicast is created that calls RemoveSpaces( ) and Reverse( ). This is accomplished via the following lines:
strOp = replaceSp;
strOp += reverseStr;
First, strOp is assigned replaceSp. Next, using +=, reverseStr is added. When strOp is invoked, both methods are invoked, replacing spaces with hyphens and reversing the string, as the output illustrates.
Next, replaceSp is removed from the chain, using this line:
strOp -= replaceSp;
and removeSP is added using this line:
strOp += removeSp;
Then, strOp is again invoked. This time, spaces are removed and the string is reversed.
Delegate chains are a powerful mechanism because they allow you to define a set of methods that can be executed as a unit. This can increase the structure of some types of code. Also, as you will soon see, delegate chains have a special value to events.
There are two features that add flexibility to delegates: covariance and contravariance. Normally, the method that you pass to a delegate must have the same return type and signature as the delegate. However, covariance and contravariance relax this rule slightly, as it pertains to derived types. Covariance enables a method to be assigned to a delegate when the method’s return type is a class derived from the class specified by the return type of the delegate. Contravariance enables a method to be assigned to a delegate when a method’s parameter type is a base class of the class specified by the delegate’s declaration.
Here is an example that illustrates both covariance and contravariance:
// Demonstrate covariance and contravariance.
using System;
class X {
public int Val;
}
// Y is derived from X.
class Y : X { }
// This delegate returns X and takes a Y argument.
delegate X ChangeIt(Y obj);
class CoContraVariance {
// This method returns X and has an X parameter.
static X IncrA(X obj) {
X temp = new X();
temp.Val = obj.Val + 1;
return temp;
}
// This method returns Y and has a Y parameter.
static Y IncrB(Y obj) {
Y temp = new Y();
temp.Val = obj.Val + 1;
return temp;
}
static void Main() {
Y Yob = new Y();
// In this case, the parameter to IncrA
// is X and the parameter to ChangeIt is Y.
// Because of contravariance, the following
// line is OK.
ChangeIt change = IncrA;
X Xob = change(Yob);
Console.WriteLine("Xob: " + Xob.Val);
// In the next case, the return type of
// IncrB is Y and the return type of
// ChangeIt is X. Because of covariance,
// the following line is OK.
change = IncrB;
Yob = (Y) change(Yob);
Console.WriteLine("Yob: " + Yob.Val);
}
}
The output from the program is shown here:
Xob: 1
Yob: 1
In the program, notice that class Y is derived from class X. Next, notice that the delegate ChangeIt is declared like this:
delegate X ChangeIt(Y obj);
ChangeIt returns X and has a Y parameter. Next, notice that the methods IncrA( ) and IncrB( ) are declared as shown here:
static X IncrA(X obj)
static Y IncrB(Y obj)
The IncrA( ) method has an X parameter and returns X. The IncrB( ) method has a Y parameter and returns Y. Given covariance and contravariance, either of these methods can be passed to ChangeIt, as the program illustrates.
Therefore, this line
ChangeIt change = IncrA;
uses contravariance to enable IncrA( ) to be passed to the delegate because IncrA( ) has an X parameter, but the delegate has a Y parameter. This works because, with contravariance, if the parameter type of the method passed to a delegate is a base class of the parameter type used by the delegate, then the method and the delegate are compatible.
The next line is also legal, but this time it is because of covariance:
change = IncrB;
In this case, the return type of IncrB( ) is Y, but the return type of ChangeIt is X. However, because the return type of the method is a class derived from the return type of the delegate, the two are compatible.
All delegates are classes that are implicitly derived from System.Delegate. You don’t normally need to use its members directly, and this book makes no explicit use of System.Delegate. However, its members may be useful in certain specialized situations.
Although the preceding examples show the “how” behind delegates, they don’t really illustrate the “why.” In general, delegates are useful for two main reasons. First, as shown later in this chapter, delegates support events. Second, delegates give your program a way to execute methods at runtime without having to know precisely what those methods are at compile time. This ability is quite useful when you want to create a framework that allows components to be plugged in. For example, imagine a drawing program (a bit like the standard Windows Paint accessory). Using a delegate, you could allow the user to plug in special color filters or image analyzers. Furthermore, the user could create a sequence of these filters or analyzers. Such a scheme could be easily handled using a delegate.
You will often find that the method referred to by a delegate is used only for that purpose. In other words, the only reason for the method is so it can be invoked via a delegate. The method is never called on its own. In such a case, you can avoid the need to create a separate method by using an anonymous function. An anonymous function is, essentially, an unnamed block of code that is passed to a delegate constructor. One advantage to using an anonymous function is simplicity. There is no need to declare a separate method whose only purpose is to be passed to a delegate.
Beginning with version 3.0, C# defines two types of anonymous functions: anonymous methods and lambda expressions. The anonymous method was added by C# 2.0. The lambda expression was added by C# 3.0. In general, the lambda expression improves on the concept of the anonymous method and is now the preferred approach to creating an anonymous function. However, anonymous methods are still used in legacy C# code. Therefore, it is important to know how they work. Furthermore, anonymous methods are the precursor to lambda expressions and a clear understanding of anonymous methods makes it easier to understand aspects of the lambda expression. Also, there is a narrow set of cases in which an anonymous method can be used, but a lambda expression cannot. Therefore, both anonymous methods and lambda expressions are described in this chapter.
An anonymous method is one way to create an unnamed block of code that is associated with a specific delegate instance. An anonymous method is created by following the keyword delegate with a block of code. To see how this is done, let’s begin with a simple example. The following program uses an anonymous method that counts from 0 to 5.
// Demonstrate an anonymous method.
using System;
// Declare a delegate type.
delegate void CountIt();
class AnonMethDemo {
static void Main() {
// Here, the code for counting is passed
// as an anonymous method.
CountIt count = delegate {
// This is the block of code passed to the delegate.
for(int i=0; i <= 5; i++)
Console.WriteLine(i);
}; // notice the semicolon
count();
}
}
This program first declares a delegate type called CountIt that has no parameters and returns void. Inside Main( ), a CountIt instance called count is created, and it is passed the block of code that follows the delegate keyword. This block of code is the anonymous method that will be executed when count is called. Notice that the block of code is followed by a semicolon, which terminates the declaration statement. The output from the program is shown here:
0
1
2
3
4
5
It is possible to pass one or more arguments to an anonymous method. To do so, follow the delegate keyword with a parenthesized parameter list. Then, pass the argument(s) to the delegate instance when it is called. For example, here is the preceding program rewritten so that the ending value for the count is passed:
// Demonstrate an anonymous method that takes an argument.
using System;
// Notice that CountIt now has a parameter.
delegate void CountIt(int end);
class AnonMethDemo2 {
static void Main() {
// Here, the ending value for the count
// is passed to the anonymous method.
CountIt count = delegate (int end) {
for(int i=0; i <= end; i++)
Console.WriteLine(i);
};
count(3);
Console.WriteLine();
count(5);
}
}
In this version, CountIt now takes an integer argument. Notice how the parameter list is specified after the delegate keyword when the anonymous method is created. The code inside the anonymous method has access to the parameter end in just the same way it would if a named method were being created. The output from this program is shown next:
0
1
2
3
0
1
2
3
4
5
An anonymous method can return a value. The value is returned by use of the return statement, which works the same in an anonymous method as it does in a named method. As you would expect, the type of the return value must be compatible with the return type specified by the delegate. For example, here the code that performs the count also computes the summation of the count and returns the result:
// Demonstrate an anonymous method that returns a value.
using System;
// This delegate returns a value.
delegate int CountIt(int end);
class AnonMethDemo3 {
static void Main() {
int result;
// Here, the ending value for the count
// is passed to the anonymous method.
// A summation of the count is returned.
CountIt count = delegate (int end) {
int sum = 0;
for(int i=0; i <= end; i++) {
Console.WriteLine(i);
sum += i;
}
return sum; // return a value from an anonymous method
};
result = count(3);
Console.WriteLine("Summation of 3 is " + result);
Console.WriteLine();
result = count(5);
Console.WriteLine("Summation of 5 is " + result);
}
}
In this version, the value of sum is returned by the code block that is associated with the count delegate instance. Notice that the return statement is used in an anonymous method in just the same way that it is used in a named method. The output is shown here:
0
1
2
3
Summation of 3 is 6
0
1
2
3
4
5
Summation of 5 is 15
A local variable or parameter whose scope includes an anonymous method is called an outer variable. An anonymous method has access to and can use these outer variables. When an outer variable is used by an anonymous method, that variable is said to be captured. A captured variable will stay in existence at least until the delegate that captured it is subject to garbage collection. Thus, even though a local variable will normally cease to exist when its block is exited, if that local variable is being used by an anonymous method, then that variable will stay in existence at least until the delegate referring to that method is destroyed.
The capturing of a local variable can lead to unexpected results. For example, consider this version of the counting program. As in the previous version, the summation of the count is computed. However, in this version, a CountIt object is constructed and returned by a static method called Counter( ). This object uses the variable sum, which is declared in the enclosing scope provided by Counter( ), rather than in the anonymous method, itself. Thus, sum is captured by the anonymous method. Inside Main( ), Counter( ) is called to obtain a CountIt object. Thus, sum will not be destroyed until the program finishes.
// Demonstrate a captured variable.
using System;
// This delegate returns int and takes an int argument.
delegate int CountIt(int end);
class VarCapture {
static CountIt Counter() {
int sum = 0;
// Here, a summation of the count is stored
// in the captured variable sum.
CountIt ctObj = delegate (int end) {
for(int i=0; i <= end; i++) {
Console.WriteLine(i);
sum += i;
}
return sum;
};
return ctObj;
}
static void Main() {
// Get a counter.
CountIt count = Counter();
int result;
result = count(3);
Console.WriteLine("Summation of 3 is " + result);
Console.WriteLine();
result = count(5);
Console.WriteLine("Summation of 5 is " + result);
}
}
The output is shown here. Pay special attention to the summation value.
0
1
2
3
Summation of 3 is 6
0
1
2
3
4
5
Summation of 5 is 21
As you can see, the count still proceeds normally. However, notice the summation value for 5. It shows 21 instead of 15! The reason for this is that sum is captured by ctObj when it is created by the Counter( ) method. This means it remains in existence until count is subject to garbage collection at the end of the program. Thus, its value is not destroyed when Counter( ) returns or with each call to the anonymous method when count is called in Main( ).
Although captured variables can result in rather counterintuitive situations, such as the one just shown, it makes sense if you think about it a bit. The key point is that when an anonymous method captures a variable, that variable cannot go out of existence until the delegate that captures it is no longer being used. If this were not the case, then the captured variable could be undefined when it is needed by the delegate.
Although anonymous methods are still part of C#, they have been largely superceded by a better approach: the lambda expression. It is not an overstatement to say that the lambda expression is one of the most important features added to C# since its original 1.0 release. Based on a distinctive syntactic element, the lambda expression provides a powerful alternative to the anonymous method. Although a principal use of lambda expressions is found when working with LINQ (see Chapter 19), they are also applicable to (and commonly used with) delegates and events. This use of lambda expressions is described here.
A lambda expression is the second way that an anonymous function can be created. (The other type of anonymous function is the anonymous method, described in the preceding section.) Thus, a lambda expression can be assigned to a delegate. Because a lambda expression is more streamlined than the equivalent anonymous method, lambda expressions are now the recommended approach in almost all cases.
All lambda expressions use the lambda operator, which is =>. This operator divides a lambda expression into two parts. On the left the input parameter (or parameters) is specified. On the right is the lambda body. The => operator is sometimes verbalized as “goes to” or “becomes.”
C# supports two types of lambda expressions, and it is the lambda body that determines what type is being created. If the lambda body consists of a single expression, then an expression lambda is being created. In this case, the body is free-standing—it is not enclosed between braces. If the lambda body consists of a block of statements enclosed by braces, then a statement lambda is being created. A statement lambda can contain multiple statements and include such things as loops, method calls, and if statements. The following sections describe both kinds of lambdas.
In an expression lambda, the expression on the right side of the => acts on the parameter (or parameters) specified by the left side. The result of the expression becomes the result of the lambda operator and is returned.
Here is the general form of an expression lambda that takes only one parameter:
param => expr
When more than one parameter is required, then the following form is used:
(param-list) => expr
Therefore, when two or more parameters are needed, they must be enclosed by parentheses. If no parameters are needed, then empty parentheses must be used.
Here is a simple expression lambda:
count => count + 2
Here count is the parameter that is acted on by the expression count + 2. Thus, the result is the value of count increased by two. Here is another example:
n => n % 2 == 0
In this case, this expression returns true if n is even and false if it is odd.
To use a lambda expression involves two steps. First, declare a delegate type that is compatible with the lambda expression. Second, declare an instance of the delegate, assigning to it the lambda expression. Once this has been done, the lambda expression can be executed by calling the delegate instance. The result of the lambda expression becomes the return value.
The following program shows how to put the two expression lambdas just shown into action. It declares two delegate types. The first, called Incr, takes an int argument and returns an int result. The second, called IsEven, takes an int argument and returns a bool result. It then assigns the lambda expressions to instances of those delegates. Finally, it executes the lambda expressions through the delegate instances.
// Use two simple lambda expressions.
using System;
// Declare a delegate that takes an int argument
// and returns an int result.
delegate int Incr(int v);
// Declare a delegate that takes an int argument
// and returns a bool result.
delegate bool IsEven(int v);
class SimpleLambdaDemo {
static void Main() {
// Create an Incr delegate instance that refers to
// a lambda expression that increases its parameter by 2.
Incr incr = count => count + 2;
// Now, use the incr lambda expression.
Console.WriteLine("Use incr lambda expression: ");
int x = -10;
while(x <= 0) {
Console.Write(x + " ");
x = incr(x); // increase x by 2
}
Console.WriteLine("\n");
// Create an IsEven delegate instance that refers to
// a lambda expression that returns true if its parameter
// is even and false otherwise.
IsEven isEven = n => n % 2 == 0;
// Now, use the isEven lambda expression.
Console.WriteLine("Use isEven lambda expression: ");
for(int i=1; i <= 10; i++)
if(isEven(i)) Console.WriteLine(i + " is even.");
}
}
The output is shown here:
Use incr lambda expression:
-10 -8 -6 -4 -2 0
Use isEven lambda expression:
2 is even.
4 is even.
6 is even.
8 is even.
10 is even.
In the program, pay special attention to these declarations:
Incr incr = count => count + 2;
IsEven isEven = n => n % 2 == 0;
The first assigns to incr a lambda expression that returns the result of increasing the value passed to count by 2. This expression can be assigned to an Incr delegate because it is compatible with Incr’s declaration. The argument used in the call to incr is passed to count. The result is returned. The second declaration assigns to isEven an expression that returns true if the argument is even and false otherwise. Thus, it is compatible with the IsEven delegate declaration.
At this point, you might be wondering how the compiler knows the type of the data used in a lambda expression. For example, in the lambda expression assigned to incr, how does the compiler know that count is an int? The answer is that the compiler infers the type of the parameter and the expression’s result type from the delegate type. Thus, the lambda parameters and return value must be compatible with the parameter type(s) and return type of the delegate.
Although type inference is quite useful, in some cases, you might need to explicitly specify the type of a lambda parameter. To do so, simply include the type name. For example, here is another way to declare the incr delegate instance:
Incr incr = (int count) => count + 2;
Notice now that count is explicitly declared as an int. Also notice the use of parentheses. They are now necessary. (Parentheses can be omitted only when exactly one parameter is specified and no type specifier is used.)
Although the preceding two lambda expressions each used one parameter, lambda expressions can use any number, including zero. When using more than one parameter you must enclose them within parentheses. Here is an example that uses a lambda expression to determine if a value is within a specified range:
(low, high, val) => val >= low && val <= high;
Here is a delegate type that is compatible with this lambda expression:
delegate bool InRange(int lower, int upper, int v);
Thus, you could create an InRange delegate instance like this:
InRange rangeOK = (low, high, val) => val >= low && val <= high;
After doing so, the lambda expression can be executed as shown here:
if(rangeOK(1, 5, 3)) Console.WriteLine("3 is within 1 to 5.");
One other point: Lambda expressions can use outer variables in the same way as anonymous methods, and they are captured in the same way.
As mentioned, there are two basic flavors of the lambda expression. The first is the expression lambda, which was discussed in the preceding section. As explained, the body of an expression lambda consists solely of a single expression. The second type of lambda expression is the statement lambda. A statement lambda expands the types of operations that can be handled within a lambda expression because it allows the body of lambda to contain multiple statements. For example, using a statement lambda you can use loops, if statements, declare variables, and so on. A statement lambda is easy to create. Simply enclose the body within braces. Aside from allowing multiple statements, it works much like the expression lambdas just discussed.
Here is an example that uses a statement lambda to compute and return the factorial of an int value:
// Demonstrate a statement lambda.
using System;
// IntOp takes one int argument and returns an int result.
delegate int IntOp(int end);
class StatementLambdaDemo {
static void Main() {
// A statement lambda that returns the factorial
// of the value it is passed.
IntOp fact = n => {
int r = 1;
for(int i=1; i < = n; i++)
r = i * r;
return r;
};
Console.WriteLine("The factorial of 3 is " + fact(3));
Console.WriteLine("The factorial of 5 is " + fact(5));
}
}
The output is shown here:
The factorial of 3 is 6
The factorial of 5 is 120
In the program, notice that the statement lambda declares a variable called r, uses a for loop, and has a return statement. These are legal inside a statement lambda. In essence, a statement lambda closely parallels an anonymous method. Therefore, many anonymous methods will be converted to statement lambdas when updating legacy code. One other point: When a return statement occurs within a lambda expression, it simply causes a return from the lambda. It does not cause the enclosing method to return.
Before concluding, it is worthwhile to see another example that shows the statement lambda in action. The following program reworks the first delegate example in this chapter so it uses statement lambdas (rather than standalone methods) to accomplish various string modifications:
// The first delegate example rewritten to use
// statement lambdas.
using System;
// Declare a delegate type.
delegate string StrMod(string s);
class UseStatementLambdas {
static void Main() {
// Create delegates that refer to lambda expressions
// that perform various string modifications.
// Replaces spaces with hyphens.
StrMod ReplaceSpaces =s => {
Console.WriteLine("Replacing spaces with hyphens.");
return s.Replace(' ', '-');
};
// Remove spaces.
StrMod RemoveSpaces =s => {
string temp = "";
int i;
Console.WriteLine("Removing spaces.");
for(i=0; i < s.Length; i++)
if(s[i] != ' ') temp += s[i];
return temp;
};
// Reverse a string.
StrMod Reverse =s => {
string temp = "";
int i, j;
Console.WriteLine("Reversing string.");
for(j=0, i=s.Length-1; i >= 0; i--, j++)
temp += s[i];
return temp;
};
string str;
// Call methods through the delegate.
StrMod strOp = ReplaceSpaces;
str = strOp("This is a test.");
Console.WriteLine("Resulting string: " + str);
Console.WriteLine();
strOp = RemoveSpaces;
str = strOp("This is a test.");
Console.WriteLine("Resulting string: " + str);
Console.WriteLine();
strOp = Reverse;
str = strOp("This is a test.");
Console.WriteLine("Resulting string: " + str);
}
}
The output, which is the same as the original version, is shown here:
Replacing spaces with hyphens.
Resulting string: This-is-a-test.
Removing spaces.
Resulting string: Thisisatest.
Reversing string.
Resulting string: .tset a si sihT
Another important C# feature is built upon the foundation of delegates: the event. An event is, essentially, an automatic notification that some action has occurred. Events work like this: An object that has an interest in an event registers an event handler for that event. When the event occurs, all registered handlers are called. Event handlers are represented by delegates.
Events are members of a class and are declared using the event keyword. Its most commonly used form is shown here:
event event-delegate event-name;
Here, event-delegate is the name of the delegate used to support the event, and event-name is the name of the specific event object being declared.
Let’s begin with a very simple example:
// A very simple event demonstration.
using System;
// Declare a delegate type for an event.
delegate void MyEventHandler();
// Declare a class that contains an event.
class MyEvent {
public event MyEventHandler SomeEvent;
// This is called to raise the event.
public void OnSomeEvent() {
if(SomeEvent != null)
SomeEvent();
}
}
class EventDemo {
// An event handler.
static void Handler() {
Console.WriteLine("Event occurred");
}
static void Main() {
MyEvent evt = new MyEvent();
// Add Handler() to the event list.
evt.SomeEvent += Handler;
// Raise the event.
evt.OnSomeEvent();
}
}
This program displays the following output:
Event occurred
Although simple, this program contains all the elements essential to proper event handling. Let’s look at it carefully. The program begins by declaring a delegate type for the event handler, as shown here:
delegate void MyEventHandler();
All events are activated through a delegate. Thus, the event delegate type defines the return type and signature for the event. In this case, there are no parameters, but event parameters are allowed.
Next, an event class, called MyEvent, is created. Inside the class, an event called SomeEvent is declared, using this line:
public event MyEventHandler SomeEvent;
Notice the syntax. The keyword event tells the compiler that an event is being declared.
Also declared inside MyEvent is the method OnSomeEvent( ), which is the method a program will call to raise (or “fire”) an event. (That is, this is the method called when the event occurs.) It calls an event handler through the SomeEvent delegate, as shown here:
if(SomeEvent != null)
SomeEvent();
Notice that a handler is called if and only if SomeEvent is not null. Since other parts of your program must register an interest in an event in order to receive event notifications, it is possible that OnSomeEvent( ) could be called before any event handler has been registered. To prevent calling on a null reference, the event delegate must be tested to ensure that it is not null.
Inside EventDemo, an event handler called Handler( ) is created. In this simple example, the event handler simply displays a message, but other handlers could perform more meaningful actions. In Main( ), a MyEvent object is created, and Handler( ) is registered as a handler for this event, by adding it as shown here:
MyEvent evt = new MyEvent();
// Add Handler() to the event list.
evt.SomeEvent += Handler;
Notice that the handler is added using the += operator. Events support only += and – =. In this case, Handler( ) is a static method, but event handlers can also be instance methods.
Finally, the event is raised as shown here:
// Raise the event.
evt.OnSomeEvent();
Calling OnSomeEvent( ) causes all registered event handlers to be called. In this case, there is only one registered handler, but there could be more, as the next section explains.
Like delegates, events can be multicast. This enables multiple objects to respond to an event notification. Here is an event multicast example:
// An event multicast demonstration.
using System;
// Declare a delegate type for an event.
delegate void MyEventHandler();
// Declare a class that contains an event.
class MyEvent {
public event MyEventHandler SomeEvent;
// This is called to raise the event.
public void OnSomeEvent() {
if(SomeEvent != null)
SomeEvent();
}
}
class X {
public void Xhandler() {
Console.WriteLine("Event received by X object");
}
}
class Y {
public void Yhandler() {
Console.WriteLine("Event received by Y object");
}
}
class EventDemo2 {
static void Handler() {
Console.WriteLine("Event received by EventDemo");
}
static void Main() {
MyEvent evt = new MyEvent();
X xOb = new X();
Y yOb = new Y();
// Add handlers to the event list.
evt.SomeEvent += Handler;
evt.SomeEvent += xOb.Xhandler;
evt.SomeEvent += yOb.Yhandler;
// Raise the event.
evt.OnSomeEvent();
Console.WriteLine();
// Remove a handler.
evt.SomeEvent -= xOb.Xhandler;
evt.OnSomeEvent();
}
}
The output from the program is shown here:
Event received by EventDemo
Event received by X object
Event received by Y object
Event received by EventDemo
Event received by Y object
This example creates two additional classes, called X and Y, which also define event handlers compatible with MyEventHandler. Thus, these handlers can also become part of the event chain. Notice that the handlers in X and Y are not static. This means that objects of each must be created, and the handler linked to each instance must be added to the event chain. The differences between instance and static handlers are examined in the next section.
Although both instance methods and static methods can be used as event handlers, they do differ in one important way. When a static method is used as a handler, an event notification applies to the class. When an instance method is used as an event handler, events are sent to specific object instances. Thus, each object of a class that wants to receive an event notification must register individually. In practice, most event handlers are instance methods, but, of course, this is subject to the specific application. Let’s look at an example of each.
The following program creates a class called X that defines an instance method as an event handler. This means that each X object must register individually to receive events. To demonstrate this fact, the program multicasts an event to three objects of type X.
/* Individual objects receive notifications when instance
event handlers are used. */
using System;
// Declare a delegate type for an event.
delegate void MyEventHandler();
// Declare a class that contains an event.
class MyEvent {
public event MyEventHandler SomeEvent;
// This is called to raise the event.
public void OnSomeEvent() {
if(SomeEvent != null)
SomeEvent();
}
}
class X {
int id;
public X(int x) { id = x; }
// This is an instance method that will be used as an event handler.
public void Xhandler() {
Console.WriteLine("Event received by object " + id);
}
}
class EventDemo3 {
static void Main() {
MyEvent evt = new MyEvent();
X o1 = new X(1);
X o2 = new X(2);
X o3 = new X(3);
evt.SomeEvent += o1.Xhandler;
evt.SomeEvent += o2.Xhandler;
evt.SomeEvent += o3.Xhandler;
// Raise the event.
evt.OnSomeEvent();
}
}
The output from this program is shown here:
Event received by object 1
Event received by object 2
Event received by object 3
As the output shows, each object registers its interest in an event separately, and each receives a separate notification.
Alternatively, when a static method is used as an event handler, events are handled independently of any object, as the following program shows:
/* A class receives the notification when
a static method is used as an event handler. */
using System;
// Declare a delegate type for an event.
delegate void MyEventHandler();
// Declare a class that contains an event.
class MyEvent {
public event MyEventHandler SomeEvent;
// This is called to raise the event.
public void OnSomeEvent() {
if(SomeEvent != null)
SomeEvent();
}
}
class X {
/* This is a static method that will be used as
an event handler. */
public static void Xhandler() {
Console.WriteLine("Event received by class.");
}
}
class EventDemo4 {
static void Main() {
MyEvent evt = new MyEvent();
evt.SomeEvent += X.Xhandler;
// Raise the event.
evt.OnSomeEvent();
}
}
The output from this program is shown here:
Event received by class.
In the program, notice that no object of type X is ever created. However, since Xhandler( ) is a static method of X, it can be attached to SomeEvent and executed when OnSomeEvent( ) is called.
The form of event used in the preceding examples created events that automatically manage the event handler invocation list, including the adding and subtracting of event handlers to and from the list. Thus, you did not need to implement any of the list management functionality yourself. Because they manage the details for you, these types of events are by far the most commonly used. It is possible, however, to provide the event handler list operations yourself, perhaps to implement some type of specialized event storage mechanism.
To take control of the event handler list, you will use an expanded form of the event statement, which allows the use of event accessors. The accessors give you control over how the event handler list is implemented. This form is shown here:
event event-delegate event-name {
add {
// code to add an event to the chain
}
remove {
// code to remove an event from the chain
}
}
This form includes the two event accessors add and remove. The add accessor is called when an event handler is added to the event chain, by using +=. The remove accessor is called when an event handler is removed from the chain, by using – =.
When add or remove is called, it receives the handler to add or remove as a parameter. As with other types of accessors, this parameter is called value. By implementing add and remove, you can define a custom event-handler storage scheme. For example, you could use an array, a stack, or a queue to store the handlers.
Here is an example that uses the accessor form of event. It uses an array to hold the event handlers. Because the array is only three elements long, only three event handlers can be held in the chain at any one time.
// Create a custom means of managing the event invocation list.
using System;
// Declare a delegate type for an event.
delegate void MyEventHandler();
// Declare a class that holds up to 3 events.
class MyEvent {
MyEventHandler[] evnt = new MyEventHandler[3];
public event MyEventHandler SomeEvent {
// Add an event to the list.
add {
int i;
for(i=0; i < 3; i++)
if(evnt[i] == null) {
evnt[i] = value;
break;
}
if (i == 3) Console.WriteLine("Event list full.");
}
// Remove an event from the list.
remove {
int i;
for(i=0; i < 3; i++)
if(evnt[i] == value) {
evnt[i] = null;
break;
}
if (i == 3) Console.WriteLine("Event handler not found.");
}
}
// This is called to raise the events.
public void OnSomeEvent() {
for(int i=0; i < 3; i++)
if(evnt[i] != null) evnt[i]();
}
}
// Create some classes that use MyEventHandler.
class W {
public void Whandler() {
Console.WriteLine("Event received by W object");
}
}
class X {
public void Xhandler() {
Console.WriteLine("Event received by X object");
}
}
class Y {
public void Yhandler() {
Console.WriteLine("Event received by Y object");
}
}
class Z {
public void Zhandler() {
Console.WriteLine("Event received by Z object");
}
}
class EventDemo5 {
static void Main() {
MyEvent evt = new MyEvent();
W wOb = new W();
X xOb = new X();
Y yOb = new Y();
Z zOb = new Z();
// Add handlers to the event list.
Console.WriteLine("Adding events.");
evt.SomeEvent += wOb.Whandler;
evt.SomeEvent += xOb.Xhandler;
evt.SomeEvent += yOb.Yhandler;
// Can't store this one -- full.
evt.SomeEvent += zOb.Zhandler;
Console.WriteLine();
// Raise the events.
evt.OnSomeEvent();
Console.WriteLine();
// Remove a handler.
Console.WriteLine("Remove xOb.Xhandler.");
evt.SomeEvent -= xOb.Xhandler;
evt.OnSomeEvent();
Console.WriteLine();
// Try to remove it again.
Console.WriteLine("Try to remove xOb.Xhandler again.");
evt.SomeEvent -= xOb.Xhandler;
evt.OnSomeEvent();
Console.WriteLine();
// Now, add Zhandler.
Console.WriteLine("Add zOb.Zhandler.");
evt.SomeEvent += zOb.Zhandler;
evt.OnSomeEvent();
}
}
The output from the program is shown here:
Adding events.
Event list full.
Event received by W object
Event received by X object
Event received by Y object
Remove xOb.Xhandler.
Event received by W object
Event received by Y object
Try to remove xOb.Xhandler again.
Event handler not found.
Event received by W object
Event received by Y object
Add zOb.Zhandler.
Event received by W object
Event received by Z object
Event received by Y object
Let’s examine this program closely. First, an event handler delegate called MyEventHandler is defined. Next, the class MyEvent is declared. It begins by defining a three-element array of event handlers called evnt, as shown here:
MyEventHandler[] evnt = new MyEventHandler[3];
This array will be used to store the event handlers that are added to the event chain. The elements in evnt are initialized to null by default.
Next, the event SomeEvent is declared. It uses the accessor form of the event statement, as shown here:
public event MyEventHandler SomeEvent {
// Add an event to the list.
add {
int i;
for(i=0; i < 3; i++)
if(evnt[i] == null) {
evnt[i] = value;
break;
}
if (i == 3) Console.WriteLine("Event queue full.");
}
// Remove an event from the list.
remove {
int i;
for(i=0; i < 3; i++)
if(evnt[i] == value) {
evnt[i] = null;
break;
}
if (i == 3) Console.WriteLine("Event handler not found.");
}
}
When an event handler is added, add is called and a reference to the handler (contained in value) is put into the first unused (that is, null) element of evnt. If no element is free, then an error is reported. (Of course, throwing an exception when the list is full would be a better approach for real-world code.) Since evnt is only three elements long, only three event handlers can be stored. When an event handler is removed, remove is called and the evnt array is searched for the reference to the handler passed in value. If it is found, its element in the array is assigned null, thus removing the handler from the list.
When an event is raised, OnSomeEvent( ) is called. It cycles through the evnt array, calling each event handler in turn.
As the preceding example shows, it is relatively easy to implement a custom event-handler storage mechanism if one is needed. For most applications, though, the default storage provided by the non-accessor form of event is better. The accessor-based form of event can be useful in certain specialized situations, however. For example, if you have a program in which event handlers need to be executed in order of their priority and not in the order in which they are added to the chain, then you could use a priority queue to store the handlers.
NOTE In multithreaded applications, you will usually need to synchronize access to the event accessors. For information on multithreaded programming, see Chapter 23.
Events can be specified in interfaces. Implementing classes must supply the event. Events can also be specified as abstract. A derived class must implement the event. Accessor-based events cannot, however, be abstract. An event can be specified as sealed. Finally, an event can be virtual, which means that it can be overridden in a derived class.
Anonymous methods and lambda expressions are especially useful when working with events because often the event handler is not called by any code other than the event handling mechanism. As a result, there is usually no reason to create a standalone method. Thus, the use of lambda expressions or anonymous methods can significantly streamline event handling code.
Since lambda expressions are now the preferred approach, we will start there. Here is an example that uses a lambda expression as an event handler:
// Use a lambda expression as an event handler.
using System;
// Declare a delegate type for an event.
delegate void MyEventHandler(int n);
// Declare a class that contains an event.
class MyEvent {
public event MyEventHandler SomeEvent;
// This is called to raise the event.
public void OnSomeEvent(int n) {
if(SomeEvent != null)
SomeEvent(n);
}
}
class LambdaEventDemo {
static void Main() {
MyEvent evt = new MyEvent();
// Use a lambda expression as an event handler.
evt.SomeEvent += (n) =>
Console.WriteLine("Event received. Value is " + n);
// Raise the event twice.
evt.OnSomeEvent(1);
evt.OnSomeEvent(2);
}
}
Event received. Value is 1
Event received. Value is 2
In the program, pay special attention to the way the lambda expression is used as an event handler, as shown here:
evt.SomeEvent += (n) =>
Console.WriteLine("Event received. Value is " + n);
The syntax for using a lambda expression event handler is the same as that for using a lambda expression with any other type of delegate.
Although lambda expressions are now the preferred way to construct an anonymous function, you can still use an anonymous method as an event handler if you so choose. For example, here is the event handler from the previous example rewritten to use an anonymous method:
// Use an anonymous method as an event handler.
evt.SomeEvent += delegate(int n) {
Console.WriteLine("Event received. Value is" + n);
};
As you can see, the syntax for using an anonymous event handler is the same as that for any anonymous method.
C# allows you to write any type of event you desire. However, for component compatibility with the .NET Framework, you will need to follow the guidelines Microsoft has established for this purpose. At the core of these guidelines is the requirement that event handlers have two parameters. The first is a reference to the object that generated the event. The second is a parameter of type EventArgs that contains any other information required by the handler. Thus, .NET-compatible event handlers will have this general form:
void handler (object sender, EventArgs e) {
// ...
}
Typically, the sender parameter is passed this by the calling code. The EventArgs parameter contains additional information and can be ignored if it is not needed.
The EventArgs class itself does not contain fields that you use to pass additional data to a handler. Instead, EventArgs is used as a base class from which you will derive a class that contains the necessary fields. EventArgs does include one static field called Empty, which is an EventArgs object that contains no data.
Here is an example that creates a .NET-compatible event:
// A .NET-compatible event.
using System;
// Derive a class from EventArgs.
class MyEventArgs : EventArgs {
public int EventNum;
}
// Declare a delegate type for an event.
delegate void MyEventHandler(object sender, MyEventArgs e);
// Declare a class that contains an event.
class MyEvent {
static int count = 0;
public event MyEventHandler SomeEvent;
// This raises SomeEvent.
public void OnSomeEvent() {
MyEventArgs arg = new MyEventArgs();
if(SomeEvent != null) {
arg.EventNum = count++;
SomeEvent(this, arg);
}
}
}
class X {
public void Handler(object sender, MyEventArgs e) {
Console.WriteLine("Event " + e.EventNum +
" received by an X object.");
Console.WriteLine("Source is " + sender);
Console.WriteLine();
}
}
class Y {
public void Handler(object sender, MyEventArgs e) {
Console.WriteLine("Event " + e.EventNum +
" received by a Y object.");
Console.WriteLine("Source is " + sender);
Console.WriteLine();
}
}
class EventDemo6 {
static void Main() {
X ob1 = new X();
Y ob2 = new Y();
MyEvent evt = new MyEvent();
// Add Handler() to the event list.
evt.SomeEvent += ob1.Handler;
evt.SomeEvent += ob2.Handler;
// Raise the event.
evt.OnSomeEvent();
evt.OnSomeEvent();
}
}
Event 0 received by an X object.
Source is MyEvent
Event 0 received by a Y object.
Source is MyEvent
Event 1 received by an X object.
Source is MyEvent
Event 1 received by a Y object.
Source is MyEvent
In this example, MyEventArgs is derived from EventArgs. MyEventArgs adds just one field of its own: EventNum. The event handler delegate MyEventHandler now takes the two parameters required by the .NET Framework. As explained, the first is an object reference to the generator of the event. The second is a reference to EventArgs or a class derived from EventArgs. The event handler in the X and Y classes, Handler( ), also has the same types of parameters.
Inside MyEvent, a MyEventHandler called SomeEvent is declared. In the OnSomeEvent( ) method, SomeEvent is called with the first argument being this, and the second argument being a MyEventArgs instance. Thus, the proper arguments are passed to MyEventHandler to fulfill the requirements for .NET compatibility.
The previous program declared its own event delegate. However, there is no need to do this because the .NET Framework provides a built-in generic delegate called EventHandler<TEventArgs>. (See Chapter 18 for a discussion of generic types.) Here, the type of TEventArgs specifies the type of the argument passed to the EventArgs parameter of the event. For example, in the preceding program, SomeEvent in MyEvent could have been declared like this:
public event EventHandler<MyEventArgs> SomeEvent;
In general, it is better to use this approach rather than defining your own delegate.
For many events, the EventArgs parameter is unused. To help facilitate the creation of code in these situations, the .NET Framework includes a non-generic delegate called EventHandler, which can be used to declare event handlers in which no extra information is needed. Here is an example that uses EventHandler:
// Use the built-in EventHandler delegate.
using System;
// Declare a class that contains an event.
class MyEvent {
public event EventHandler SomeEvent; // uses EventHandler delegate
// This is called to raise SomeEvent.
public void OnSomeEvent() {
if(SomeEvent != null)
SomeEvent(this, EventArgs.Empty);
}
}
class EventDemo7 {
static void Handler(object sender, EventArgs e) {
Console.WriteLine("Event occurred");
Console.WriteLine("Source is " + sender);
}
static void Main() {
MyEvent evt = new MyEvent();
// Add Handler() to the event list.
evt.SomeEvent += Handler;
// Raise the event.
evt.OnSomeEvent();
}
}
In this case, the EventArgs parameter is unused and is passed the placeholder object EventArgs.Empty. The output is shown here:
Event occurred
Source is MyEvent
Events are frequently used in message-based environments such as Windows. In such an environment, a program simply waits until it receives a message, and then it takes the appropriate action. Such an architecture is well suited for C#-style event handling because it is possible to create event handlers for various messages and then simply invoke a handler when a message is received. For example, the left-button mouse click message could be tied to an event called LButtonClick. When a left-button click is received, a method called OnLButtonClick( ) can be called, and all registered handlers will be notified.
Although developing a Windows program that demonstrates this approach is beyond the scope of this chapter, it is possible to give an idea of how such an approach would work. The following program creates an event handler that processes keystrokes. The event is called KeyPress, and each time a key is pressed, the event is raised by calling OnKeyPress( ). Notice that .NET-compatible events are created and that lambda expressions provide the event handlers.
// A keypress event example.
using System;
// Derive a custom EventArgs class that holds the key.
class KeyEventArgs : EventArgs {
public char ch;
}
// Declare a keypress event class.
class KeyEvent {
public event EventHandler <KeyEventArgs> KeyPress;
// This is called when a key is pressed.
public void OnKeyPress(char key) {
KeyEventArgs k = new KeyEventArgs();
if(KeyPress != null) {
k.ch = key;
KeyPress(this, k);
}
}
}
// Demonstrate KeyEvent.
class KeyEventDemo {
static void Main() {
KeyEvent kevt = new KeyEvent();
ConsoleKeyInfo key;
int count = 0;
// Use a lambda expression to display the keypress.
kevt.KeyPress += (sender, e) =>
Console.WriteLine(" Received keystroke: " + e.ch);
// Use a lambda expression to count keypresses.
kevt.KeyPress += (sender, e) =>
count++; // count is an outer variable
Console.WriteLine("Enter some characters. " +
"Enter a period to stop.");
do {
key = Console.ReadKey();
kevt.OnKeyPress(key.KeyChar);
} while(key.KeyChar != '.');
Console.WriteLine(count + " keys pressed.");
}
}
Here is a sample run:
Enter some characters. Enter a period to stop.
t Received keystroke: t
e Received keystroke: e
s Received keystroke: s
t Received keystroke: t
. Received keystroke: .
5 keys pressed.
The program begins by deriving a class from EventArgs called KeyEventArgs, which is used to pass a keystroke to an event handler. Next, a delegate called KeyHandler defines the event handler for keystroke events. The class KeyEvent encapsulates the keypress event. It defines the event KeyPress.
In Main( ), a KeyEvent object called kevt is created. Next, an event handler based on a lambda expression is added to kvet.KeyPress that displays each key as it is entered, as shown here:
kevt.KeyPress += (sender, e) =>
Console.WriteLine(" Received keystroke: " + e.ch);
Next, another lambda expression–based handler is added to kvet.KeyPress by the following code. It counts the number of keypresses.
kevt.KeyPress += (sender, e) =>
count++; // count is an outer variable
Notice that count is a local variable declared in Main( ) that is initialized to zero.
Next, a loop is started that calls kevt.OnKeyPress( ) when a key is pressed. This causes the registered event handlers to be notified. When the loop ends, the number of keypresses is displayed. Although quite simple, this example illustrates the essence of event handling. The same basic approach will be used for other event handling situations. Of course, in some cases, anonymous event handlers will not be appropriate and named methods will need to be employed.