Skip to main content
๐Ÿ“œ WAYPOINT LESSON

Closures and captured variables

โญโญโญ advancedโณ 15 min read๐Ÿ“ Lesson 172 of 265

Display classes, capture lifetime, the loop-variable fix, and closure allocation costs.

Closures and captured variables

When a lambda captures a local, the compiler hoists that local into a compiler-generated display class; both the method and the lambda share one instance. Consequences that decide bugs:

var actions = new List<Action>();
for (int i = 0; i < 3; i++)
    actions.Add(() => Console.Write(i));
// C# 5+: prints 012 โ€” foreach loops capture per-iteration
// but `for` loop variables are ONE variable shared by all iterations

Since C# 5, foreach iteration variables are captured per-iteration (the historical footgun is fixed there); a for loop's index is still a single variable โ€” every closure sees its final value unless you copy it into a local inside the loop.

Captured variables live as long as the delegate: a lambda holding a large object keeps the whole object reachable โ€” this is the managed memory leak shape in event-handler code (Module 14). Capture deliberately:

// capture a snapshot, not the variable:
for (int i = 0; i < 3; i++)
{
    var copy = i;
    actions.Add(() => Use(copy));   // 0, 1, 2
}

And prefer capturing small, long-lived things (a service, a formatter) over loop locals. Each capture that differs per iteration is one display class allocation โ€” for hot paths, restructure so one closure serves many calls (parameterize instead of capturing).

โšก Now practice

Ready to Code
Delegate drillsMethod groups vs lambdas, capture analysis, delegate composition, and a mini rule engine.
3 challenges ยท ยท ~45 min