CHAPTER 5
Much of the user interface work you’ll do is event based, and C# supports this through type members called events. For events to work, you need some infrastructure to specify methods that can be called, which surfaces through delegates and lambdas. This chapter will explain how delegates, events, and lambdas work in C#.
Delegates have a few capabilities in C#: referencing methods, dispatching multiple methods, asynchronous execution, and event typing. This can be confusing because many other language features serve only a single purpose. Differentiating and comparing all of these capabilities of C# delegates adds complexity that you might not be familiar with. This discussion is going to cut the feature list somewhat to hopefully illuminate delegates and make them less complex as you move forward with practical implementation. In particular, I’ll focus on delegates as method references and event types.
Note: I’ll avoid deep discussion of delegate multi-cast and asynchronous execution because they’re rarely used and largely replaced by other language features. For example, events support multi-cast dispatch and C# 5.0 introduces a capability referred to as async.
Let’s first examine the role of a delegate as a reference to a method. To do this, the delegate specifies the signature of a method that it can reference, like this:
public delegate double Add(double num1, double num2); |
You might notice that a delegate looks like an abstract method, except it has the delegate type definition keyword. A delegate definition is a reference type, just like a class, struct, or interface. The previous delegate definition is for a delegate type named Add that takes two double arguments and returns a result of type double. Just like other types, delegate accessibility can only be public or internal and is internal by default.
There are esoteric uses of delegates that I won’t get into, but I do want to focus on the most practical and common way to use delegates: as event types.
Events are type members that allow a type to notify other types of things that have happened. A very common example is a user interface with a button. You’ll want to write code that does something when a user clicks that button. Here are the pieces you need to make that happen:
As you can see, delegates have a lot of moving parts. In particular, pay attention to #6. Delegates prevent you, or anyone, or anything from assigning an arbitrary method to an event. Here’s an example that defines a delegate and a class with an event of that delegate type.
using System; public class ClickEventArgs : EventArgs { public string Name { get; set; } } public delegate void ClickHandler(object sender, ClickEventArgs e); public class CalculatorButton { public event ClickHandler Clicked; } |
An event can be a member of a class, struct, or interface. If it is an interface member, it means that classes or structs that implement that interface must also have the event in their definitions. An event has the event modifier and adheres to the same accessibility rules as other type members like methods and properties.
If a delegate serves the purpose you need, you can use it. In fact, the FCL includes many reusable types, including reusable delegate types that you can use without needing to create your own. There’s even a .NET type named EventHandler that nearly matches the signature of ClickHandler, where the sender is typically the source of the event, and EventArgs is a base class you can derive from to create your own custom type for sharing information with methods and event calls when fired. The previous code, with ClickEventArgs, derives from EventArgs, a type that comes with the .NET Framework. The following example simulates an event to demonstrate how to write a method that handles that event in code.
using System; public class CalculatorButton { public event ClickHandler Clicked; public void SimulateClick() { if (Clicked != null) { ClickEventArgs args = new ClickEventArgs(); args.Name = "Add"; Clicked(this, args); } } } public class Program { public static void Main() { Program prg = new Program(); CalculatorButton calcBtn = new CalculatorButton(); calcBtn.Clicked += new ClickHandler(prg.CalculatorBtnClicked); calcBtn.Clicked += prg.CalculatorBtnClicked; calcBtn.SimulateClick(); Console.ReadKey(); } public void CalculatorBtnClicked(object sender, ClickEventArgs e) { Console.WriteLine( $"Caller is a CalculatorButton: {sender is CalculatorButton} and is named {e.Name}"); } } |
Again, this example has a lot of moving parts, but they follow the list at the start of this section about defining and using events. First, notice that CalculateButton has a new method, SimulateClick. Since we simplified the code by avoiding the UI, we have to fake a user clicking a button. That said, SimulateClick demonstrates the proper way to fire an event in your own code. Before firing an event, make sure that a user has assigned methods to the event by checking for null. Whenever no methods are assigned, the event will be null. SimulateClick sets up the ClickEventArgs parameter. In this case, it’s only a Name property, but you would provide any relevant information available for the EventArgs type you were using and what information a method that received this event might need. Next, fire the event by calling it like a method. This causes the event to call each method assigned to it, one by one, in the order they were assigned. The first parameter is the this keyword, which indicates that the instance of the containing type, CalculatorButton, gets passed to the methods. The second is the ClickEventArgs variable that was previously instantiated and had its Name property set.
The Main method shows how to assign methods to events. Notice the += operator is used twice to assign two methods to the CalculateButton instance, calcBtn, and Clicked event. The CalculatorBtnClicked method is an instance method, so the prg instance provides access to that method during assignment.
The first assignment creates a new instance of the ClickHandler delegate. Delegates are types and you can instantiate them. You instantiate delegates with a method, which becomes the method the delegate refers to. Remember how I explained that delegates are references to methods? In this case, the new instance of the ClickHandler delegate refers to the CalculatorBtnClicked method. The second assignment shows a newer and simpler syntax for accomplishing the same task as the first; it just uses the method name. This is called delegate inference and means that since the method assigned to the event has the same signature of the event’s delegate, the C# compiler will take care of instantiating the delegate with that method behind the scenes for you.
Finally, calling SimulateClick on the CalculatorButton instance, calcBtn, fires the event as explained previously. Regardless of whether the program assigns the same method to the event twice, firing the event causes all assigned delegates to fire, which calls the methods assigned to each delegate to execute. Therefore, the CalculatorBtnClicked method will execute two times.
You might wonder why I had to define SimulateClick inside of CalculatorButton instead of just firing the event from Main. The reason is because external code can’t fire an event. An event can only be fired from inside of its containing type.
Instead of assigning named methods, you can assign code blocks directly to events.
A lambda is a nameless method. Sometimes you have a block of code that serves one specific purpose and you don’t need to define it as a method. Methods are nice because they let you modularize your code and have something to refer to with delegates and call from multiple places. But a lot of times you just need to run some code for a specific operation. Lambdas are quick and simple ways to assign and execute a block of code.
Note: A lambda is also a very sophisticated language feature that lets you translate between parse trees and code. This is a core feature of Language Integrated Query (LINQ), which I’ll discuss more in Chapter 7. For daily practical use, working with lambdas as parse trees is rare.
Just like methods, lambdas can have parameters, a body, and can return values. The following code listing is an example of a lambda.
using System; public class Program { public static void Main() { Action hello = () => Console.WriteLine("Hello!"); hello(); Console.ReadKey(); } } |
Action is a reusable delegate in the .NET Framework, and hello is variable of the Action delegate type. The lambda starts with an empty parameter list, meaning no parameters. The => operator separates the parameter list and body and is referred to as either “such that” or “goes to”. Next, you see the body, which is a single statement. Since hello is a delegate, you can call it just like a method and it will execute the lambda, which prints “Hello!” to the console.
With a single statement, you don’t need to use curly braces on the body, but you do with multiple statements, as in the following example.
using System; public class Program { public static void Main() { Predicate<string> validator = word => { int count = word.Length; return count > 3; }; ValidateInput(validator); ValidateInput(word => { int count = word.Length; return count > 3; }); Console.ReadKey(); } public static void ValidateInput(Predicate<string> validator) { string input = "Hello"; bool isValid = validator(input); Console.WriteLine($"Is Valid: {isValid}"); } } |
The previous example assigns a lambda to the .NET Framework’s Predicate delegate, which is designed to return a bool. The lambda has a single parameter, word, of type string. Since the lamba has more than one statement, it requires curly braces. The example shows how to pass the lambda both as a variable and as an entire lambda.
Predicate is a generic delegate. The type parameter is set to <string>, meaning that the lambda parameter is type string. You’ll learn more about generics in Chapter 6.
The ValidateInput method passes a string to validator and assigns the results to the isValid variable. It’s just like a method call, except there isn’t a method, just code; it is quick to write and limited in scope.
Another way to use lambdas is with events. The following example shows a different way to write an event handler method for the Clicked event in the previous CalculatorButton example.
using System; public class Program { public static void Main() { CalculatorButton calcBtn = new CalculatorButton(); calcBtn.Clicked += (object sender, ClickEventArgs e) => { Console.WriteLine( $"Caller is a CalculatorButton: {sender is CalculatorButton} and is named {e.Name}"); Console.WriteLine(message); }; calcBtn.SimulateClick(); Console.ReadKey(); } } |
The first thing you might notice in this example is that the delegate assignment to the Clicked event is now a lambda. If you have two or more parameters, they must be enclosed in parentheses as a comma-separated list. If the body of the lambda includes two or more lines, they must be terminated with semicolons and enclosed in braces. Notice that the signature of the lambda matches the ClickEventHandler defined previously.
In addition to the Action and Predicate<T> delegates you’ve seen in previous examples, the FCL has a set of delegates named Func<T> that you can reuse at will. Here’s an example that rewrites the previous example, using Func<T, TResult> instead of Predicate<T>.
|
using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; class Program { public static void Main() { Func<string, bool> validator = word => { int count = word.Length; return count > 3; }; ValidateInput(validator); ValidateInput(word => { int count = word.Length; return count > 3; }); Console.ReadKey(); } public static void ValidateInput(Func<string, bool> validator) { string input = "Hello"; bool isValid = validator(input); Console.WriteLine($"Is Valid: {isValid}"); } } |
This is nearly identical to the previous program, except it uses Func<string, bool> instead of Predicate<string>. As mentioned previously, both Func<T, TResult> and Predicate<T> are generic delegates. The type specifications in angle brackets are plug-ins for types applied to parameters and return types. The following listing shows the Predicate<T> delegate as defined in the FCL.
public delegate bool Predicate<T>(T obj); |
It refers to a method that returns a bool, but accepts a parameter of type T. So, Predicate<string> means that the parameter to the method referred to is a string. Similarly, here’s the FCL definition of Func<T, TResult>.
public delegate TResult Func<T, TResult>(T arg); |
This shows that Func<T, TResult> accepts a parameter of type T and returns a value of type TResult. In Code Listing 81, Func<string, bool> refers to a method with a parameter of type string that returns type bool.
For convenience, the FCL offers 18 overloads of the Func delegate, allowing between 0 and 16 input parameters and 1 return parameter type. This covers many scenarios, and you can reuse the provided delegates from the FCL to go a long way. You can create your own delegates only when you need to.
While not necessarily lambdas, expression-bodied members give you some shorthand syntax for properties and methods. The following listing provides an example.
using System; class Program { public static string Today => DateTime.Now.ToShortDateString(); public static void Log(string message) => Console.WriteLine(message); public static void Main() { Log($"{Today} is a good day."); Console.ReadKey(); } } |
The Program class defines the Today property and Log method as expression-bodied members. Main shows how these members are used like normal methods and properties.
You learned about delegates, events, and lambdas. A delegate is a reference to a method. You can pass a delegate around in code or assign it to an event. The method a delegate refers to must have the same signature of the delegate. An event is a type member, defined with a delegate type. You can assign as many delegates to an event as you need and each one executes when the event fires. You can only fire an event from within the type the event is defined in. Instead of methods, you can use lambdas when you don’t need to define a separate method. You can refer to a lambda with a delegate, pass a lambda as a parameter, or assign a lambda to an event. Expression-bodied members let you write properties and methods with a shorthand syntax.