Question
C# foreach Closure Behavior Explained: Why Loop Variables Were Reused
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[] { , , })
{
actions.Add(() => Console.WriteLine(s));
}
( action actions)
{
action();
}
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.
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 behavior in modern C#
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
funcsis created as an empty list.- The
foreachloop starts withword = "red". copyis assigned"red".- A lambda
() => copyis added to the list.- This lambda captures the variable
copy.
- This lambda captures the variable
- Next iteration:
word = "green". - A new
copyvariable is created for this iteration and gets"green". - Another lambda captures this new .
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
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
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,
ihas become3
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:
Comparisons
| Concept | What gets captured | Risk level | Notes |
|---|---|---|---|
foreach in older C# | One reused iteration variable | High | Caused the classic modified-closure bug |
foreach in modern C# | A fresh iteration variable per iteration | Lower | Matches developer expectation |
for loop variable | Usually one changing variable | High | Still easy to capture incorrectly |
| Local copy inside loop | Separate variable per iteration | Low | Most explicit and reliable |
Closure vs value copy
| Approach |
|---|
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#
foreachbehavior reused the same iteration variable. - Modern C#
foreachcreates safer capture behavior. forloop 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.
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
- Create a list of actions using a
forloop that captures the loop variable incorrectly. - Execute the actions and display the unexpected output.
- Create a second list of actions using a local copy inside the loop.
- Execute the corrected actions and display the expected output.
- Add a short message in the program explaining which version is safe and why.
Keep learning
Related questions
AddTransient vs AddScoped vs AddSingleton in ASP.NET Core Dependency Injection
Learn the differences between AddTransient, AddScoped, and AddSingleton in ASP.NET Core DI with examples and practical usage.
C# Type Checking Explained: typeof vs GetType() vs is
Learn when to use typeof, GetType(), and is in C#. Understand exact type checks, inheritance, and safe type testing clearly.
C# Version Numbers Explained: C# vs .NET Framework and Why “C# 3.5” Is Incorrect
Learn the correct C# version numbers, how they map to .NET releases, and why terms like C# 3.5 are inaccurate and confusing.