Delegates, Events and Lambdas in C#
Codevity Admin · 10/25/2026
Codevity Admin · 10/25/2026
Delegates, events and lambdas are tightly connected, and interviewers like to ask about all three together.
A delegate is a type-safe reference to a method. It defines a signature, and any method with that signature can be assigned to it. Delegates let you pass behaviour around as a value, which is the basis of callbacks, events and LINQ.
Operation add = (a, b) => a + b;
Operation multiply = (a, b) => a * b;
Console.WriteLine(add(2, 3)); // 5
Console.WriteLine(multiply(2, 3)); // 6
public delegate int Operation(int a, int b);A delegate can hold several methods (a multicast delegate). Calling it runs them in order.
You rarely declare custom delegates, because the framework provides generic ones:
| Delegate | Signature | Typical use |
|---|---|---|
Func<T, TResult> | Takes arguments, returns a value (the last type argument) | Transformations: Func<int, int> |
Action<T> | Takes arguments, returns nothing (void) | Side effects: logging, callbacks |
Predicate<T> | Takes one argument, returns bool | Tests: List<T>.FindAll, RemoveAll |
Func<int, int> square = x => x * x;
Action<string> log = message => Console.WriteLine($"LOG: {message}");
Predicate<int> isEven = n => n % 2 == 0;
Console.WriteLine(square(5)); // 25
log("started"); // LOG: started
Console.WriteLine(isEven(4)); // TrueA lambda is a short anonymous method (x => x * x). A lambda can use variables from the surrounding scope: it captures them. The captured variable itself is shared, not a copy of its value at that moment.
That causes a classic trap:
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
actions.Add(() => Console.Write(i + " "));
}
foreach (var action in actions) action(); // 3 3 3
Console.WriteLine();
actions.Clear();
for (int i = 0; i < 3; i++)
{
int copy = i; // a new variable for each iteration
actions.Add(() => Console.Write(copy + " "));
}
foreach (var action in actions) action(); // 0 1 2
Console.WriteLine();In the first loop there is a single i, and by the time the actions run it equals 3. In the second, each iteration has its own copy. (A foreach loop variable is created fresh for every iteration, so it does not have this problem.)
An event is a delegate field with restricted access. Outside the class that declares it, other code can only subscribe (+=) or unsubscribe (-=). It cannot invoke the event or replace all the handlers, so the publisher stays in control.
var button = new Button();
button.Clicked += (sender, e) => Console.WriteLine("Clicked!");
button.Click(); // Clicked!
public class Button
{
public event EventHandler? Clicked;
public void Click() => Clicked?.Invoke(this, EventArgs.Empty);
}The standard pattern is EventHandler or EventHandler<TEventArgs>, with the sender and an event-args object. ?.Invoke avoids a NullReferenceException when nobody has subscribed.
| Delegate field | Event | |
|---|---|---|
| Invoked from outside the class | Yes | No |
Can be overwritten with = from outside | Yes | No: only += and -= |
| Typical purpose | Callback parameter | Notification to any number of listeners |
A publisher holds a reference to every subscriber's handler. If a short-lived object subscribes to the event of a long-lived object (a static class, a singleton, a service) and never unsubscribes, the long-lived publisher keeps the subscriber alive. The garbage collector cannot free it. This is the classic "memory leak in a managed language".
Fix it by unsubscribing () when the subscriber is done, usually in , or by using weak events.
-=DisposeFunc<int, int, int>