Question

C# foreach Closure Behavior Explained: Why Loop Variables Were Reused

csharpforeachlambdascopeanonymous-methods

Question

In C#, foreach loop variables have historically interacted in a surprising way with anonymous methods and lambda expressions.

For example:

foreach (var s in strings)
{
    query = query.Where(i => i.Prop == s);
}

This can lead to the modified-closure problem, where every Where clause ends up using the final value of s instead of the value from each individual iteration.

A common explanation is that the compiler effectively treated the loop more like this:

string s;
while (enumerator.MoveNext())
{
    s = enumerator.Current;
    // ...
}

rather than this:

while (enumerator.MoveNext())
{
    string s = enumerator.Current;
    // ...
}

At the same time, the loop variable in a foreach is not accessible outside the loop:

foreach (string s in strings)
{
}

var finalString = s; // Error: s is out of scope here

So the question is: why was foreach designed this way? Was there a practical reason to reuse the same loop variable across iterations, or was this mainly an early design choice made before closures, anonymous methods, and lambdas became common in C#?

Short Answer

By the end of this page, you will understand how closures capture variables in C#, why foreach loop variables historically caused bugs with lambdas, what changed in newer C# versions, and how to write safe loop code when creating delegates or deferred queries.

Concept

In this question, the real topic is closure capture in loops, especially in C# foreach statements.

A closure happens when a lambda or anonymous method uses a variable from an outer scope. Instead of copying the variable's value at that moment, the lambda usually captures the variable itself.

That detail matters a lot.

If the same variable is reused across loop iterations, then every lambda created inside the loop refers to that one shared variable. By the time the lambdas run, the variable may have been updated several times, often ending with the last value from the loop.

Why this matters

This shows up in real code when you:

  • build LINQ queries inside loops
  • register event handlers
  • create async callbacks
  • store lambdas in collections for later use

If you expect each lambda to remember a different loop value, but they all share one captured variable, your program behaves incorrectly.

The historical foreach issue in C#

Older versions of C# treated the foreach iteration variable as if one variable existed for the whole loop, and that variable received a new value on each iteration. Because closures capture variables, not snapshots of values, lambdas inside the loop all captured the same variable.

That is why code like this was dangerous:

var actions = new List<Action>();

foreach (var s in new[] { "a", "b", "c" })
{
    actions.Add(() => Console.WriteLine(s));
}

foreach (var action in actions)
{
    action();
}

In older C# behavior, this could print:

c
c
c

instead of:

a
b
c

Was it intentional?

Historically, this behavior was mainly a result of the language design at the time. Early C# did not yet have the later closure-heavy programming style that became common with LINQ and lambdas. Once closures became widespread, this design became a well-known source of bugs.

What changed?

In newer C# versions, the behavior of foreach was changed so that the iteration variable acts like a fresh variable for each iteration when captured by a closure. This fixed the common bug for foreach.

However, this did not make all loop capture problems disappear. For example, for loops still require care because the loop variable is typically one variable whose value changes.

Key idea

  • Closures capture variables, not just values.
  • If one variable is reused, all closures share it.
  • If a fresh variable exists per iteration, each closure gets its own captured variable.

That is the core concept behind the original question.

Mental Model

Think of a closure like attaching a note to a box, not to the value inside the box.

  • The variable is the box.
  • The current value is whatever is currently inside the box.
  • A lambda remembers the box, not a photo of its contents.

If a loop keeps reusing the same box and just swaps out the contents:

  • first iteration: box contains "a"
  • second iteration: same box now contains "b"
  • third iteration: same box now contains "c"

then every lambda that captured that box sees whatever is in it later, often "c".

If instead each iteration gets a new box:

  • iteration 1 gets box A with "a"
  • iteration 2 gets box B with "b"
  • iteration 3 gets box C with "c"

then each lambda remembers a different box, so each one prints the expected value.

This mental model explains both the bug and the fix.

Take Quiz

Syntax and Examples

Basic closure capture

string name = "Alice";
Func<string> getName = () => name;

name = "Bob";
Console.WriteLine(getName());

Output:

Bob

The lambda captures the variable name, not the original value "Alice".

Safe capture inside a loop

A common way to avoid problems is to create a local copy inside the loop:

var actions = new List<Action>();

foreach (var s in new[] { "a", "b", "c" })
{
    var current = s;
    actions.Add(() => Console.WriteLine(current));
}

foreach (var action in actions)
{
    action();
}

Output:

a
b
c

Here, current is a separate variable for each iteration.

Safer foreach behavior in modern C#

In modern C#, foreach capture was changed to behave the way most developers expect:

var actions = new List<Action>();

foreach (var s in new[] { "a", "b", "c" })
{
    actions.Add(() => Console.WriteLine(s));
}

This now works as expected in modern C# compilers.

But for still needs care

var actions = new List<Action>();

for (int i = 0; i < 3; i++)
{
    actions.Add(() => Console.WriteLine(i));
}

foreach (var action in actions)
{
    action();
}

This often prints:

3
3
3

To fix it:

for (int i = 0; i < 3; i++)
{
    int copy = i;
    actions.Add(() => Console.WriteLine(copy));
}

Step by Step Execution

Consider this example:

var funcs = new List<Func<string>>();

foreach (var word in new[] { "red", "green", "blue" })
{
    var copy = word;
    funcs.Add(() => copy);
}

foreach (var func in funcs)
{
    Console.WriteLine(func());
}

Step-by-step

  1. funcs is created as an empty list.
  2. The foreach loop starts with word = "red".
  3. copy is assigned "red".
  4. A lambda () => copy is added to the list.
    • This lambda captures the variable copy.
  5. Next iteration: word = "green".
  6. A new copy variable is created for this iteration and gets "green".
  7. Another lambda captures this new copy.
  8. Next iteration: word = "blue".
  9. Another new copy variable is created and gets "blue".
  10. A third lambda captures that variable.
  11. The second loop runs each stored lambda.
  12. The lambdas return "red", "green", and "blue".

Why it works

Each lambda closes over a different variable.

Contrast with the risky pattern

var funcs = new List<Func<int>>();

for (int i = 0; i < 3; i++)
{
    funcs.Add(() => i);
}

Here, all lambdas capture the same i. After the loop ends, i is 3, so every lambda returns 3.

Real World Use Cases

Closures inside loops appear in many practical situations.

Building dynamic LINQ filters

foreach (var category in categories)
{
    var currentCategory = category;
    query = query.Where(p => p.Category == currentCategory);
}

Used in:

  • search pages
  • admin dashboards
  • API filtering

Registering event handlers

foreach (var button in buttons)
{
    var currentButton = button;
    currentButton.Click += (sender, args) =>
    {
        Console.WriteLine(currentButton.Text);
    };
}

Used in desktop apps, UI frameworks, and component systems.

Creating background jobs or callbacks

foreach (var file in files)
{
    var currentFile = file;
    tasks.Add(Task.Run(() => ProcessFile(currentFile)));
}

Used in:

  • batch processing
  • file import tools
  • parallel work queues

Test generation

foreach (var input in testInputs)
{
    var currentInput = input;
    testCases.Add(() => Validate(currentInput));
}

Useful when generating reusable test delegates.

Take Quiz

Real Codebase Usage

In real codebases, developers rarely think about closure rules in isolation. They appear as part of larger patterns.

Pattern: local copy before deferred execution

When code will run later, developers often create a local copy.

foreach (var id in ids)
{
    var currentId = id;
    workers.Add(() => LoadUser(currentId));
}

This is common when work is deferred, queued, or asynchronous.

Pattern: query composition

LINQ queries are often built step by step.

foreach (var term in terms)
{
    var currentTerm = term;
    query = query.Where(x => x.Name.Contains(currentTerm));
}

This pattern matters because LINQ often uses deferred execution. The query may not run until much later.

Pattern: guard against loop capture in for

Even though foreach was improved, many teams still use a local copy habit consistently because it is safe and clear.

for (int i = 0; i < items.Count; i++)
{
    int index = i;
    callbacks.Add(() => Console.WriteLine(items[index]));
}

Pattern: validation and error reporting

foreach (var field in fields)
{
    var currentField = field;
    rules.Add(value => ValidateField(currentField, value));
}

This appears in:

  • form validation
  • rules engines
  • configuration systems

Team practice

In mature codebases, developers often:

  • avoid depending on subtle compiler behavior
  • make loop captures explicit
  • review deferred-execution code carefully
  • use descriptive local names like currentItem or capturedValue

Common Mistakes

Mistake 1: assuming a lambda stores the current value automatically

Broken example:

var actions = new List<Action>();

for (int i = 0; i < 3; i++)
{
    actions.Add(() => Console.WriteLine(i));
}

Why it fails:

  • all lambdas capture the same i
  • by execution time, i has become 3

Fix:

for (int i = 0; i < 3; i++)
{
    int copy = i;
    actions.Add(() => Console.WriteLine(copy));
}

Mistake 2: forgetting deferred execution in LINQ

Broken example:

foreach (var term in terms)
{
    query = query.Where(x => x.Name.Contains(term));
}

In older foreach behavior, this could capture the same variable repeatedly.

Safer version:

foreach (var term in terms)
{
    var currentTerm = term;
    query = query.Where(x => x.Name.Contains(currentTerm));
}

Mistake 3: assuming foreach and for behave the same way today

They do not behave the same in this context.

  • modern foreach is safer for capture
  • for still commonly captures one changing variable

Mistake 4: blaming async when the real issue is closure capture

Code like this may look like an async problem:

for (int i = 0; i < 3; i++)
{
    Task.Run(() => Console.WriteLine(i));
}

But the root problem is usually closure capture, not Task.Run itself.

Mistake 5: not knowing your language version

If you work on older C# code or read old blog posts, behavior may differ from current compilers. Always verify whether the example depends on historical foreach semantics.

Comparisons

ConceptWhat gets capturedRisk levelNotes
foreach in older C#One reused iteration variableHighCaused the classic modified-closure bug
foreach in modern C#A fresh iteration variable per iterationLowerMatches developer expectation
for loop variableUsually one changing variableHighStill easy to capture incorrectly
Local copy inside loopSeparate variable per iterationLowMost explicit and reliable

Closure vs value copy

ApproachBehavior
Capture variable in lambdaLambda sees the variable's later value
Copy to local variable firstLambda sees that iteration's copied value

foreach vs for

Featureforeachfor
Best for iterating collectionsYesSometimes
Gives index directlyNoYes
Historically tricky with closuresYes, in older C#Yes
Needs local copy for safetySometimes, for clarity or old codeOften

The main lesson is not that foreach is bad. It is that closures depend on variable lifetime and scope.

Take Quiz

Cheat Sheet

Quick rules

  • A closure captures a variable, not just its value.
  • If that variable changes later, the lambda sees the updated value.
  • Older C# foreach behavior reused the same iteration variable.
  • Modern C# foreach creates safer capture behavior.
  • for loop variables still commonly need a local copy.

Safe pattern

foreach (var item in items)
{
    var current = item;
    actions.Add(() => Use(current));
}

Safe for pattern

for (int i = 0; i < items.Count; i++)
{
    int index = i;
    actions.Add(() => Use(items[index]));
}

Risky pattern

for (int i = 0; i < 3; i++)
{
    actions.Add(() => Console.WriteLine(i));
}

Remember

  • Deferred execution makes closure bugs more visible.
  • LINQ queries may run later, not where they are written.
  • If unsure, create a local copy inside the loop.

FAQ

Why did old foreach loops cause closure bugs in C#?

Because lambdas captured the loop variable itself, and older foreach behavior reused that variable across iterations.

Was this a compiler bug or language design?

It was language/compiler behavior based on the original design, not just a random bug.

Has C# fixed foreach closure capture?

Yes. Modern C# changed foreach capture semantics so each iteration behaves as if it has its own variable.

Do for loops still have this problem?

Yes. for loop variables are still commonly captured as one changing variable, so a local copy is often needed.

What is the modified closure pitfall?

It is the bug where several lambdas unexpectedly share one captured variable whose value changes later.

Is creating a local copy still a good idea?

Yes. It makes intent explicit and keeps code safe across loop styles and older codebases.

Does LINQ make this problem worse?

LINQ does not create the bug, but deferred execution often makes the bug easier to notice because the query runs later.

How can I avoid closure bugs in loops?

Create a new local variable inside the loop and capture that variable instead of the changing loop variable.

Take Quiz

Mini Project

Description

Build a small C# program that demonstrates closure capture in loops and then fixes it. This project helps you see the bug directly instead of only reading about it. You will create delegates in a loop, run them later, compare incorrect behavior with correct behavior, and practice the safe local-copy pattern used in real code.

Goal

Create a console app that shows the difference between unsafe loop capture and safe per-iteration capture.

Requirements

  1. Create a list of actions using a for loop that captures the loop variable incorrectly.
  2. Execute the actions and display the unexpected output.
  3. Create a second list of actions using a local copy inside the loop.
  4. Execute the corrected actions and display the expected output.
  5. Add a short message in the program explaining which version is safe and why.
Take Quiz

Keep learning

Related questions

View more