Delegates, lambdas and events

C# · Practical · 12 min

Lambdas are everywhere in modern C#: LINQ, ASP.NET Core minimal APIs, callbacks and configuration.

A delegate is a type-safe reference to a method. You rarely declare your own; the built-in generic delegates cover most needs:

  • Func<T1, ..., TResult> takes arguments and returns a value: Func<int, int> square.
  • Action<T1, ...> returns nothing.
  • Predicate<T> is an older Func<T, bool>.

A lambda expression creates a delegate inline: x => x * x, (a, b) => a + b, or a block x => { var y = x * 2; return y; }. Since C# 10, lambdas get a natural type, so var add = (int a, int b) => a + b; works, and they can have default parameter values (C# 12).

Lambdas capture variables from the enclosing scope (a closure). The variable itself is captured, not its value at that time, so later changes are visible. Use static x => ... to forbid captures when you want to avoid accidental allocations.

Method groups convert to delegates too: names.ForEach(Console.WriteLine);.

An event is a delegate field that outsiders can only subscribe to (+=) and unsubscribe from (-=); only the owning class can raise it. The convention is event EventHandler<TArgs>? Changed; raised with Changed?.Invoke(this, args). Forgetting to unsubscribe from long-lived publishers keeps subscribers alive, a classic memory leak.

Example

Func<int, int> square = x => x * x;
Action<string> shout = s => Console.WriteLine(s.ToUpper());
var add = (int a, int b = 1) => a + b;

Console.WriteLine(square(5));     // 25
shout("hi");                      // HI
Console.WriteLine(add(2));        // 3

int counter = 0;
Action inc = () => counter++;     // captures the variable
inc(); inc();
Console.WriteLine(counter);       // 2

static int Apply(Func<int, int> f, int v) => f(v);
Console.WriteLine(Apply(square, 3)); // 9

var t = new Thermostat();
t.Changed += (_, temp) => Console.WriteLine($"now {temp}");
t.Set(21);                        // now 21

class Thermostat
{
    public event EventHandler<int>? Changed;
    public void Set(int value) => Changed?.Invoke(this, value);
}

Remember

  • Func returns a value, Action does not
  • Lambdas capture variables, not snapshots of their values
  • Events allow outside code to subscribe with += but not raise
  • Unsubscribe from long-lived events to avoid leaks

Watch out for

  • Capturing a loop variable of a for loop in lambdas and seeing its final value in all of them.

Check yourself

  1. int x = 1; Func<int> f = () => x; x = 5; What does f() return?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-expressions

Delegates, lambdas and events · C# · Topolearn