C# delegates, events, and lambdas
Pass behaviour as a value and let objects notify subscribers when something happens.
On this page
C# methods and classes.
- Pass behaviour as a value and let objects notify subscribers when something happens.
- Predict the sample output and explain the state changes.
- Complete the practice task and check the boundary cases.
The concept
A delegate is a type that represents a method with a given signature. Func<TArg, TResult> and Action<TArg> are generic delegates for functions that return a value and for functions that do not. A lambda expression, such as x => x * x, creates one in place. Functions passed as arguments make code flexible: a method can take a Func that decides how to compute something.
An event is a delegate that outside code can subscribe to with += and unsubscribe from with -=, but cannot invoke or reset. The class that declares it raises the event with ?.Invoke, which does nothing if there are no subscribers. This is the observer pattern built into the language. The publisher does not know who is listening.
Code example
using System;
class Button {
public event Action<string> Clicked;
public void Click(string user) {
Clicked?.Invoke(user);
}
}
class Program {
static void Main() {
Func<int, int> square = x => x * x;
Func<int, int> addOne = x => x + 1;
Func<int, int> squareThenAddOne = x => addOne(square(x));
Console.WriteLine(square(5) + " " + squareThenAddOne(5));
var button = new Button();
int clicks = 0;
Action<string> counter = user => clicks++;
Action<string> greeter = user => Console.WriteLine("hello " + user);
button.Clicked += counter;
button.Clicked += greeter;
button.Click("Ada");
button.Clicked -= greeter;
button.Click("Bo");
Console.WriteLine("clicks: " + clicks);
}
}25 26 hello Ada clicks: 2
Follow the code
Open the walkthrough to inspect the example one step at a time.
Show guided walkthrough
using System; class Button { public event Action<string> Clicked; public void Click(string user) { Clicked?.Invoke(user); }} class Program { static void Main() { Func<int, int> square = x => x * x; Func<int, int> addOne = x => x + 1; Func<int, int> squareThenAddOne = x => addOne(square(x)); Console.WriteLine(square(5) + " " + squareThenAddOne(5)); var button = new Button(); int clicks = 0; Action<string> counter = user => clicks++; Action<string> greeter = user => Console.WriteLine("hello " + user); button.Clicked += counter; button.Clicked += greeter; button.Click("Ada"); button.Clicked -= greeter; button.Click("Bo"); Console.WriteLine("clicks: " + clicks); }}Follow the example
Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.
View the complete dry-run table
| Step / line | What happens | State after step | Output so far |
|---|---|---|---|
| A lambda stored in a delegate variable. | — | — | |
| Delegates can be combined by calling one inside another. | — | 25 26 | |
| Subscribe a handler to the event. | — | 25 26 | |
| Both handlers run: the counter increments clicks and the greeter prints. | clicks = 1 | 25 26 hello Ada | |
| Unsubscribe the greeter. | clicks = 1 | 25 26 hello Ada | |
| Only the counter runs now, so nothing is printed. | clicks = 2 | 25 26 hello Ada | |
| Two clicks were counted. | clicks = 2 | 25 26 hello Ada clicks: 2 |
Behind the scenes
How C# processes this code
C# compiles to code for the .NET runtime, which invokes the program entry point.
Cost and common mistakes
Raising an event costs O(S) for S subscribers.
- Raising an event with no subscribers throws a NullReferenceException unless you use ?.Invoke.
- A subscriber that is never removed stays reachable from the publisher and cannot be garbage collected. Unsubscribe when you are done.
- A lambda captures variables, not values. Changing a captured variable later changes what the lambda sees.
Try it yourself
Why is the event keyword used instead of a public delegate field?
Reveal answer and reasoning
With a plain public delegate, outside code could invoke it or replace all subscribers with =. An event only allows += and -=, so the publisher stays in control.