Modern C# · Lesson 28

Closures capture variables, not frozen values

Predict a delegate after the state it references changes.

How it works

A lambda can refer to variables declared outside its own body. That captured state is a closure. The add delegate refers to adjustment, so its first call adds two and its later call adds five after adjustment changes. Creating the delegate did not freeze the original integer value as an independent constant.

This behavior can be useful for a policy that intentionally follows current state, but it can also create surprising delayed work. A callback may execute long after the code that constructed it. To understand its result, ask which variable it captured, who can change that variable, and when the callback actually runs.

The loop demonstrates a deliberate snapshot. A new local variable named snapshot is created for each loop-body execution and initialized from i. Each stored delegate captures that iteration's local. Invoking the delegates after the loop therefore produces zero, one, two instead of all delegates observing the loop variable's final value.

Do not try to fix capture problems by invoking callbacks sooner unless the application's timing contract really permits it. The robust fix is to decide whether shared live state or a per-operation value is intended, then express that choice explicitly. A name such as snapshot helps readers recognize the ownership decision.

Practice by removing the local snapshot and capturing i directly in the for loop. Predict the resulting sequence before running it. Restore the correct behavior by passing a value to a helper that returns a delegate, and explain why a separate helper parameter creates the needed per-call captured state.

Finally, consider the lifetime of captured objects. A long-lived delegate can keep referenced data reachable even after the constructing method returns. Capturing a small value may be harmless, while accidentally retaining a large object graph can matter. Keep callback state focused and test delayed execution separately from immediate execution.

A working example

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;

int adjustment = 2;
Func<int, int> add = value => value + adjustment;
Console.WriteLine(add(10));
adjustment = 5;
Console.WriteLine(add(10));
var actions = new List<Func<int>>();
for (int i = 0; i < 3; i++) {
    int snapshot = i;
    actions.Add(() => snapshot);
}
Console.WriteLine(string.Join(",", actions.Select(action => action())));

Expected output

12
15
0,1,2
Try this
Create a local snapshot inside each iteration when each delegate needs its own value.