Local Delegate Variables and Scope in C
In C#, local delegate variables are delegate instances, anonymous methods, or lambda expressions assigned to variables declared within a local method scope. This article explains what local delegate variables are, how they interact with surrounding local variables through a mechanism known as closures, and how they fundamentally alter the scope and lifetime of captured data from the stack to the managed heap.
What Are Local Delegate Variables?
A local delegate variable is a delegate instance—commonly utilizing
predefined types like Action or
Func—instantiated within the body of a method, property, or
local function. Rather than declaring a standalone named method
elsewhere in a class, developers use local delegates to encapsulate
executable logic directly where it is needed.
public void ProcessData()
{
// Local delegate variable
Func<int, int> square = x => x * x;
int result = square(5); // Returns 25
}Local delegates can be invoked immediately, passed as arguments to other methods (such as LINQ extensions), or returned for execution outside the containing method.
Variable Capture and Closures
The defining characteristic of local delegate variables is their ability to access variables declared in their enclosing scope. This behavior is called variable capture, and the combination of the delegate and its captured environment is known as a closure.
public Action CreateCounter()
{
int count = 0; // Local variable
// Delegate captures 'count'
Action increment = () =>
{
count++;
Console.WriteLine(count);
};
return increment;
}In standard C# execution, value types like int count
reside on the call stack and are deallocated as soon as the containing
method finishes executing. However, because the increment
delegate captures count, count must remain
accessible whenever the delegate is subsequently invoked.
How Local Delegates Affect Scope and Lifetime
When a local delegate captures an outer variable, it fundamentally changes how the C# compiler manages scope and memory:
1. Lifetime Extension Beyond Method Execution
Captured variables no longer vanish when the method exits. Under the hood, the C# compiler generates an internal display class on the managed heap. Both the captured variable and the delegate's implementation become members of this generated class. As a result, the variable is moved from the stack to the heap, and its lifetime is tied to the lifetime of the delegate instance rather than the stack frame of the method.
2. Shared State and Side Effects
Captured variables are evaluated by reference, not by value. If multiple delegates capture the same variable, or if the variable is modified after the delegate is defined, all references share that single, mutated state.
int multiplier = 2;
Func<int, int> multiply = x => x * multiplier;
multiplier = 3;
Console.WriteLine(multiply(10)); // Outputs 30, not 203. The Loop Variable Trap
A well-known consequence of variable capture occurs inside loops.
When capturing an index variable inside a for loop, each
delegate instance captures the same variable reference rather than a
point-in-time snapshot:
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
actions.Add(() => Console.WriteLine(i));
}
foreach (var action in actions)
{
action(); // Prints 3, 3, 3 instead of 0, 1, 2
}To isolate scope per iteration, copy the value to a new local variable declared inside the loop body before capturing it.
Mitigating Unintended Closures
Because closures allocate objects on the heap, careless use of local
delegates can introduce unexpected memory pressure or retain references
to large objects, preventing garbage collection. Modern C# provides the
static keyword for anonymous functions
(static () => { ... }) to enforce at compile-time that a
local delegate does not capture outer variables, preserving strict local
scoping and preventing accidental allocations.