C# DISCUSSION

Why do my C# lambdas created in a for loop all see the final value of the loop variable?

Started by farhanaziz C# closurescaptured variablesfor loop lambdaTask.Runforeach semantics
5 replies 248 views 6 participants
Latest activity · 30 Sep 2026

Why do my C# lambdas created in a for loop all see the final value of the loop variable?

farhanaziz C# Forum
#1

I create one handler per channel in a loop: for (int i = 0; i < 4; i++) { actions.Add(() => Console.WriteLine(i)); }. Invoking the four actions afterwards prints 4 four times instead of 0, 1, 2, 3. The same happens when I start tasks in the loop with Task.Run(() => ReadChannel(i)): sometimes a channel is read twice, and ReadChannel(4) throws an out-of-range exception.

Oddly, the equivalent foreach loop prints the expected values. Why do the two loops behave differently, and what is the correct pattern?

Community replies 5

Re: Why do my C# lambdas created in a for loop all see the final value of the loop variable?

#2

A lambda captures variables, not values. The compiler moves the captured variable i into a hidden heap-allocated object, and both the loop and every lambda use that one field. A for loop has a single i for the whole loop: it is declared once in the initialiser and incremented on each pass. All four lambdas share it, and by the time you invoke them the loop has finished and i is 4, the value that made i < 4 false.

With Task.Run the lambdas run while the loop is still counting, so each one sees whatever i happens to be at that moment: duplicates, skipped values, and 4.

Re: Why do my C# lambdas created in a for loop all see the final value of the loop variable?

#3

The fix is to give each iteration its own variable: for (int i = 0; i < 4; i++) { int channel = i; actions.Add(() => Console.WriteLine(channel)); }. A variable declared inside the loop body is a fresh variable on every iteration, so each lambda captures a different channel and the output is 0, 1, 2, 3.

You can also avoid the shared variable by generating the work from a sequence: var tasks = Enumerable.Range(0, 4).Select(n => Task.Run(() => ReadChannel(n))).ToArray();. Here n is a lambda parameter, separate for each element, and ToArray() forces the query to run so the tasks actually start.

Re: Why do my C# lambdas created in a for loop all see the final value of the loop variable?

#4

foreach behaves differently because of a deliberate language change in C# 5. Before that version the foreach iteration variable was also a single shared variable and had exactly this bug. Since C# 5 the variable is logically declared inside the loop body, so each iteration gets a new one and closures capture distinct values.

The for loop was left alone, because its variable is declared once by the programmer and modified by the programmer's own increment expression, and the body may legitimately change it too. So foreach is safe on any modern compiler, while for, and while with a variable declared outside, need the local copy.

Re: Why do my C# lambdas created in a for loop all see the final value of the loop variable?

#5

Capture by variable is a feature in other places. A lambda can update an outer local: int total = 0; list.ForEach(x => total += x); works because total lives in the closure object and the enclosing method sees the change.

It has consequences, though. Captured locals live as long as any delegate that references the closure, which can keep a large buffer alive behind a forgotten event handler. And when several tasks modify one captured variable, it is shared mutable state with no synchronisation; use Interlocked.Increment(ref count) or a lock. Since C# 9 a lambda can be marked static, and the compiler then rejects any accidental capture.

Re: Why do my C# lambdas created in a for loop all see the final value of the loop variable?

#6

A related trap is deferred execution in LINQ. A query runs when it is enumerated, not when it is defined: int limit = 10; var q = values.Where(v => v > limit); limit = 100; and then enumerating q filters with 100, because the lambda reads the captured variable at enumeration time. Call .ToList() at the point where you want the snapshot.

To see what happens in your loop, open the compiled assembly in a decompiler such as ILSpy. You will find a compiler-generated class with a field i, created once before the for loop in the broken version and once per iteration after the fix.

TEP COMMUNITY