1. What is a closure?
In programming, a closure (closure) is a function that captures variables from an outer context. Put simply, if a lambda expression or anonymous method uses variables declared outside its body, that function becomes a closure. It "remembers" what the values of those variables were at the moment of creation.
Real-life analogy:
Imagine you wrote a secret recipe on a piece of paper and tucked it into an envelope. Even if the original paper gets lost or can't be accessed directly (the variable becomes unavailable), whoever has that envelope (the lambda) still has access to the recipe.
Simple example:
int x = 42;
Func<int> getX = () => x;
Console.WriteLine(getX()); // 42
Here getX is a closure because it uses the variable x declared outside itself.
2. Why variable capture matters
In C# closures are used literally everywhere:
- In collections and LINQ queries
- To pass parameters to events or asynchronous methods
- When creating event handlers inside loops
- To keep "context" between different calls
Without closures many common C# practices would be impossible or extremely inconvenient.
Practical scenario
Let's say we build a reminder app: the user sets a series of reminders, and at some point (in a minute, an hour, a week...) it should show the appropriate message. It's easy to pass a lambda as a handler that "remembered" what to remind about. That's variable capture in action — classic use.
3. How C# implements variable capture
Under the hood C# does a neat trick: when you have a lambda expression that uses outer variables, the compiler automatically creates a helper class — a display class. All "captured" variables become fields of that class.
Schematically it looks like this:
Outer variable ──► DisplayClass
▲
│
Closure (lambda)
Illustration in code
Here's what happens "under the hood":
int x = 5;
Func<int> f = () => x;
// Here the compiler does roughly this:
class DisplayClass
{
public int x;
public int Lambda() => x;
}
DisplayClass display = new DisplayClass();
display.x = 5;
Func<int> f = display.Lambda;
This explains why the closure continues to see the current value of the variable even after leaving the block where it was declared.
4. Is the variable value "frozen" or does it change?
In C# variables are captured by reference, not by value. That means if a lambda uses a variable and that variable is changed elsewhere — the lambda will see the new value.
Example:
int x = 10;
Func<int> getX = () => x;
x = 20;
Console.WriteLine(getX()); // 20, not 10!
Students often expect that getX() will always return 10 because the variable was "captured". But in reality the lambda reads the variable, which still exists and can be changed.
When is the value actually fixed?
If a variable is declared in a loop with a new scope per iteration, for example using foreach, and a new variable is created each iteration — the lambda will "remember" the current value.
5. Examples: closure in a loop — a common trap
Common mistake
We want to create an array of delegates, each of which prints its own number from the loop:
Action[] actions = new Action[5];
for (int i = 0; i < 5; i++)
{
actions[i] = () => Console.WriteLine(i);
}
foreach (var action in actions)
action();
What will the program print?
5
5
5
5
5
Whoa! Why not 0,1,2,3,4?
Reason:
The lambda captures the same variable i, which keeps changing as the loop runs. When you later call the delegates, i is already 5.
How to do it correctly?
You need to create a separate variable inside the loop body:
Action[] actions = new Action[5];
for (int i = 0; i < 5; i++)
{
int index = i; // New variable for each iteration!
actions[i] = () => Console.WriteLine(index);
}
foreach (var action in actions)
action();
Now the program will print:
0
1
2
3
4
How is this related to the display class?
In the first version all delegates "hook" to the same field — that's why the result is the same. In the second case a new local variable is created for each iteration, so each delegate gets its own DisplayClass with a unique value.
6. Practical scenarios for using variable capture
Example 1: Event handling with "context"
Suppose in our small app there's a list of tasks, and each has a handler attached to a "complete" button. We need the lambda inside the handler to remember which task to process:
foreach (var task in tasks)
{
button.Click += (sender, e) => CompleteTask(task);
}
Here the variable task is captured on each iteration. It's important to ensure it's declared correctly inside the loop so you don't fall into the trap from the example above.
Example 2: Asynchronous operations
Closures are often used to pass parameters into async logic — for example, saving a variable into a local "slot" when starting an async task:
for (int i = 0; i < 3; i++)
{
int index = i; // Mandatory!
Task.Run(() => Console.WriteLine($"Task #{index}"));
}
Without the local variable all tasks will print the same number, which is usually not what we wanted.
Example 3: LINQ queries
LINQ over collections often uses closures to filter or transform elements with respect to variables from an outer scope. For example:
string prefix = "Task";
var filtered = tasks.Where(t => t.Name.StartsWith(prefix));
Here the lambda in Where remembered the value of prefix and calls the StartsWith method.
7. Peculiarities, limitations and common mistakes when working with closures
Mistake #1: using one loop variable for all delegates.
If in a loop all delegates reference the same variable, the result will be surprising. You should create a new local variable inside the loop for each delegate to avoid the shared reference.
Mistake #2: closures over variables outside the method.
If a closure captures a class field or a variable declared outside the current method, it will hold a reference to that variable. This can lead to memory leaks because the garbage collector can't free the object while closures still reference it.
Mistake #3: long-lived delegates with closures.
If a delegate with a closure is kept for a long time (for example in a static field), the variables it references also stay in memory longer than expected. This often causes hidden memory leaks and performance problems.
GO TO FULL VERSION