Closures and Captured Variables
A lambda captures the variable itself, not a copy of its value, which explains the loop surprise and the lifetime capture creates.
int total = 0; Action add = () => total++; // add shares total; it does not copy itA lambda can use a variable declared outside it. Those are called outer variables, and a lambda that uses one is a closure.
The detail that matters: the lambda captures the variable, not a copy of its value. Change the variable after creating the lambda and the lambda sees the new value. Change it inside the lambda and code outside sees that too. It is one piece of storage with two names for it.
How it works
The compiler moves the captured variable into a hidden class and rewrites every use of it, inside the lambda and outside, to read and write one field of one object. Nothing is copied, which is why the sharing happens. It also means the variable is not collected while any delegate holding it is still alive, so a captured variable can outlive the method that declared it.
The loop surprise
A for loop declares its counter once. Every lambda created in the body captures that one variable, so after the loop they all report its final value. A foreach loop gets a fresh variable each iteration, which has been true since C# 5, so each lambda there captures its own. Fix the for case by copying the counter into a variable declared inside the body and capturing that.
Two more things to know
A lambda that uses a field or method of the surrounding object captures this, keeping that whole object alive. And static on a lambda, available since C# 9, turns accidental capture into a compile error.
Example
List<Func<int>> fromFor = new();
for (int i = 0; i < 3; i++)
{
// All three lambdas capture the same `i`, not its value at this moment.
fromFor.Add(() => i);
}
List<Func<int>> fromForeach = new();
int[] values = [0, 1, 2];
foreach (int n in values)
{
// A foreach variable is new on each iteration, so each lambda has its own.
fromForeach.Add(() => n);
}
// The for loop ended with i equal to 3, and every lambda reports that.
Console.WriteLine(string.Join(", ", fromFor.Select(f => f())));
Console.WriteLine(string.Join(", ", fromForeach.Select(f => f())));
// Output:
// 3, 3, 3
// 0, 1, 2When to use it
- Build one action per item in a loop and have each remember its own item.
- Keep a running total that several lambdas read and update.
- Capture a configured threshold once and reuse it inside a filter.
- Work out why every generated button in a list does the same thing.
More examples
Fixing the for-loop capture
The one-line fix for the most common closure bug. You need it any time you build delegates in a for loop: a list of actions, a set of callbacks, one handler per generated row.
List<Func<int>> broken = new();
List<Func<int>> fixedUp = new();
for (int i = 0; i < 3; i++)
{
broken.Add(() => i); // captures the one loop counter
int copy = i; // a brand new variable on every iteration
fixedUp.Add(() => copy); // captures this iteration's copy
}
Console.WriteLine(string.Join(",", broken.Select(f => f())));
Console.WriteLine(string.Join(",", fixedUp.Select(f => f())));
// Output:
// 3,3,3
// 0,1,2Two lambdas sharing one captured variable
Capture is shared, not per lambda: both of these refer to the same storage as count itself. This is how a pair of lambdas can hold state between calls without a class, and also how such state gets modified from somewhere you did not expect.
int count = 0;
Action increment = () => count++;
Func<int> read = () => count;
increment();
increment();
Console.WriteLine(read()); // the lambda sees the current value
Console.WriteLine(count); // and so does the original variable
// Output:
// 2
// 2Refusing to capture, with static
Worth doing for any lambda stored in a long-lived field. Without static, a lambda that quietly used an instance member would capture this and keep the whole enclosing object alive for as long as the delegate exists.
static class Discount
{
// `static` on the lambda guarantees it captures nothing, so it cannot
// accidentally hold on to surrounding state.
private static readonly Func<decimal, decimal> _tenPercentOff =
static amount => amount * 0.9m;
public static decimal Apply(decimal amount) => _tenPercentOff(amount);
}
// Apply(100m) returns 90.0m
Discussion