.NET
closures
programming
software development
C#

What are 'closures' in .NET?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Closures are the mechanism that lets a lambda or local function keep using variables from the surrounding scope after that scope would normally be gone. In day-to-day C# code, this matters whenever you pass behavior around with LINQ, callbacks, asynchronous work, or factory methods.

What a Closure Really Captures

In C#, a closure captures variables, not just the values those variables held at one instant. The compiler rewrites the code so the captured variables live inside a generated helper object, sometimes called a display class. The lambda then reads and writes fields on that helper object.

That behavior explains why closures are useful and why they sometimes surprise people.

Here is a simple example:

csharp
1using System;
2
3Func<int> CreateCounter()
4{
5    int count = 0;
6    return () =>
7    {
8        count++;
9        return count;
10    };
11}
12
13var counter = CreateCounter();
14Console.WriteLine(counter()); // 1
15Console.WriteLine(counter()); // 2
16Console.WriteLine(counter()); // 3

Even though CreateCounter has already returned, the lambda still has access to count. That preserved state is the closure.

Why Closures Are Useful

Closures are a lightweight way to attach state to behavior without creating a dedicated class every time. They are especially convenient for small pieces of business logic.

You will see them often in LINQ:

csharp
1using System;
2using System.Linq;
3
4int minimum = 10;
5int[] numbers = [4, 10, 12, 20];
6
7var filtered = numbers.Where(n => n >= minimum);
8
9Console.WriteLine(string.Join(", ", filtered)); // 10, 12, 20

The predicate closes over minimum. If minimum changes before enumeration happens, the query sees the new value because it captured the variable itself:

csharp
minimum = 15;
Console.WriteLine(string.Join(", ", filtered)); // 20

That interaction between closures and deferred execution is important. A LINQ query may not evaluate when you write it, only when you enumerate it.

The Classic Loop-Capture Bug

The most famous closure mistake is capturing a loop variable and expecting each lambda to get a different copy.

csharp
1using System;
2using System.Collections.Generic;
3
4var actions = new List<Action>();
5
6for (int i = 0; i < 3; i++)
7{
8    actions.Add(() => Console.WriteLine(i));
9}
10
11foreach (var action in actions)
12{
13    action();
14}

This prints 3 three times, not 0, 1, and 2. All lambdas captured the same i, and by the time they run the loop has finished.

The fix is to create a new local variable inside the loop:

csharp
1using System;
2using System.Collections.Generic;
3
4var actions = new List<Action>();
5
6for (int i = 0; i < 3; i++)
7{
8    int copy = i;
9    actions.Add(() => Console.WriteLine(copy));
10}
11
12foreach (var action in actions)
13{
14    action();
15}

Now each lambda closes over a different variable.

Closures with Async Code

Closures are also common in asynchronous code because lambdas are frequently passed into tasks, timers, and event handlers. The same rules apply: if the variable changes before the callback runs, the callback sees the changed variable.

csharp
1using System;
2using System.Threading.Tasks;
3
4int retries = 0;
5
6Func<Task> operation = async () =>
7{
8    retries++;
9    await Task.Delay(10);
10    Console.WriteLine($"Attempt {retries}");
11};
12
13await operation();
14await operation();

This is often desirable, but it becomes risky if multiple callbacks mutate shared captured state concurrently. In that case, you may need locking or a different design.

When a Closure Is Better Than a Class

Use a closure when the state is small, short-lived, and tightly coupled to one piece of behavior. If the state grows, needs multiple methods, or deserves a clear name in the domain model, a dedicated type is usually easier to maintain.

Closures are a tool for reducing noise, not for hiding important structure. If future readers must reverse-engineer what several captured variables mean, the closure has already become too clever.

Common Pitfalls

  • Forgetting that closures capture variables by reference-like behavior, not a frozen snapshot.
  • Combining closures with deferred LINQ execution and then wondering why later variable changes affect the query.
  • Capturing loop variables without creating a per-iteration copy.
  • Holding large objects in captured variables longer than intended, which can extend their lifetime and memory use.
  • Mutating captured state from multiple asynchronous callbacks without synchronization.

Summary

  • A closure lets a lambda or local function keep using surrounding variables after the outer method returns.
  • In C#, the compiler implements this by moving captured variables into a generated helper object.
  • Closures are useful for counters, callbacks, LINQ filters, and small stateful functions.
  • The most common bug is capturing the loop variable instead of a per-iteration copy.
  • If the captured state becomes large or central to the design, a named type is often clearer.

Course illustration
Course illustration

All Rights Reserved.