Delegates, lambdas and closures
Func, Action and method groups, what a lambda captures, and the for-loop capture that still catches people out.
After this lesson you can
- Store a method or a lambda in a Func or Action and call it later
- Explain that a closure captures a variable, not its value at the time
- Use a captured variable on purpose to keep state between calls
A delegate is a type-safe reference to a method: an object you can store, pass around and invoke. You rarely declare your own delegate types any more; the generic ones cover almost everything:
Func<T1, ..., TResult>: takes arguments, returns a value.Action<T1, ...>: takes arguments, returns nothing.Predicate<T>: an older delegate type with the same shape asFunc<T, bool>, used byList.FindAlland friends. The two are distinct types, so one cannot be assigned to the other.
A delegate can point at a named method (a method group) or at a lambda, an inline anonymous function:
public static class Solution{ static int Twice(int x) => x * 2; public static string Describe() { Func<int, int> f = Twice; // method group Func<int, int> g = x => x + 1; // lambda Predicate<string> empty = string.IsNullOrEmpty; var lines = new List<string>(); Action<string> log = null; log += s => lines.Add("A:" + s); log += s => lines.Add("B:" + s); log("hi"); Func<int, int> composed = x => g(f(x)); return $"f(5)={f(5)} g(5)={g(5)} composed(5)={composed(5)} empty(\"\")={empty("")} | {string.Join(",", lines)}"; }}log += ... shows that delegates are multicast: += builds an
invocation list, and invoking the delegate calls every entry in order.
Events, in the next lesson, are built on exactly this.
Closures capture variables
A lambda may use local variables of the method that created it. The compiler moves such a variable into a hidden object shared by the method and the lambda, so the variable lives as long as the lambda does, even after the method has returned. That is a closure.
public static class Solution{ static Func<int> MakeCounter() { int count = 0; // captured: lives on after MakeCounter returns return () => ++count; } public static string Describe() { var a = MakeCounter(); var b = MakeCounter(); a(); a(); int fromA = a(); int fromB = b(); var forActions = new List<Func<int>>(); for (int i = 0; i < 3; i++) forActions.Add(() => i); var foreachActions = new List<Func<int>>(); foreach (var n in new[] { 0, 1, 2 }) foreachActions.Add(() => n); return $"a={fromA} b={fromB} | for: {string.Join(",", forActions.Select(f => f()))} | " + $"foreach: {string.Join(",", foreachActions.Select(f => f()))}"; }}Each call to MakeCounter creates a new count, so a and b count
independently: a reached 3 while b is at 1. This is how you give a
function private state without writing a class.
The second half shows the classic trap. A for loop declares one
variable i for the whole loop. All three lambdas captured that same
variable, and by the time they run the loop has left it at 3. A
foreach loop is different: since C# 5 it declares a fresh iteration
variable each time round, so each lambda sees its own value.
Common mistakes
- Capturing a
forloop variable. Shown above: every lambda sees the final value. Copy it first (int copy = i;) and capture the copy. - Expecting a captured value to be frozen. A closure reads the variable when it runs, so a later assignment to that variable changes what the lambda returns.
- Creating the shared state inside the lambda. State that must survive between calls (a cache, a counter) has to be declared outside the lambda and captured; declared inside, it is created fresh on every call. The exercise below is about exactly this.
What interviewers ask
- "What is the difference between
Funcand ?" returns a value (its last type argument), returns .
Exercise · Pro
A memoizer that remembers between calls
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade