C# closure
last modified October 5, 2026
A closure is code that uses a variable from an enclosing scope. In C#, a lambda expression, anonymous method, or local function becomes a closure when it captures such a variable.
The compiler stores a captured local variable in a generated object so that the code and the variable can continue to work together after the enclosing method returns. The closure captures the variable itself, not a copy of its value.
var limit = 0; Predicate<int> greaterThan = value => value > limit;
The greaterThan delegate captures limit. Because the
variable is captured, changing limit later changes what the
predicate tests.
C# closure example
The following method returns a function that retains access to its captured
counter after CreateCounter has returned.
var counter = CreateCounter();
Console.WriteLine(counter());
Console.WriteLine(counter());
Console.WriteLine(counter());
Func<int> CreateCounter()
{
int count = 0;
return () => ++count;
}
The returned delegate and the captured count variable share the same
closure. The variable remains alive as long as the delegate can reach it, so
successive calls continue from the previous value.
$ dotnet run 1 2 3
C# closure and changing a captured variable
A closure observes the current value of a captured variable. It does not freeze the value from the moment the delegate is created.
var limit = 0; Func<int, bool> greaterThan = value => value > limit; Console.WriteLine(greaterThan(5)); limit = 10; Console.WriteLine(greaterThan(5));
Both calls use the same captured limit. The first call returns
True; after the assignment, the second call returns
False.
$ dotnet run True False
C# closure with LINQ
Closures are useful with LINQ because a query can use a value defined outside
its predicate. The following query is evaluated when the foreach
loop enumerates it, not when the query is declared.
using System.Linq;
var values = new[] { -1, -2, 0, 1, 5, 3 };
var limit = 0;
var result = values.Where(value => value > limit);
limit = 2;
foreach (var value in result)
{
Console.WriteLine(value);
}
The predicate captures limit. Since LINQ to Objects uses deferred
execution, the enumeration sees the updated value of 2 and prints
only values greater than two.
$ dotnet run 5 3
C# closure and loop variables
Since C# 5, the iteration variable of a foreach loop is a fresh
variable for every iteration, so a lambda can capture it directly. Each
delegate then keeps its own value.
var actions = new List<Action>();
foreach (var number in new[] { 1, 2, 3 })
{
actions.Add(() => Console.WriteLine(number));
}
foreach (var action in actions)
{
action();
}
The foreach iteration variable is per-iteration, so the three
delegates print 1, 2, and 3.
$ dotnet run 1 2 3
C# closure and a for loop variable
A for loop is different: it declares its variable once, outside
the body, and every iteration shares that single variable. A closure that
captures it observes the final value, so copy the variable into a local inside
the body when each delegate needs its own value.
var actions = new List<Action>();
for (var i = 0; i < 3; i++)
{
var captured = i;
actions.Add(() => Console.WriteLine(captured));
}
foreach (var action in actions)
{
action();
}
Because each iteration copies i into its own captured
variable, the delegates print 0, 1, and
2. Without the local, all three would share i and
print 3.
$ dotnet run 0 1 2
C# closures and threads
Capturing a variable extends its lifetime, but it does not make access to that variable thread-safe. If multiple threads or tasks read and write the same captured variable, protect the access with synchronization or use a suitable thread-safe type.
using System.Linq;
using System.Threading.Tasks;
int count = 0;
var gate = new object();
var tasks = Enumerable.Range(0, 2).Select(_ => Task.Run(() =>
{
for (var i = 0; i < 1_000; i++)
{
lock (gate)
{
count++;
}
}
})).ToArray();
await Task.WhenAll(tasks);
Console.WriteLine(count);
Here, count is captured by the task delegates. The
lock statement makes each increment atomic with respect to the
other task, so the output is deterministically 2000. Without
synchronization, concurrent updates to a captured variable can be lost.
$ dotnet run 2000
Source
Lambda expressions - capture of outer variables
In this article we have worked with closures in C# and examined variable capture, lifetime, deferred execution, and synchronization.
Author
List all C# tutorials.