CodeGym /Courses /C# SELF /Closures

Closures

C# SELF
Level 51, Lesson 3
Available

1. Introduction

In programming a closure is not a way to lock a door in JavaScript, but a mechanism where a lambda expression or anonymous method captures variables from the surrounding context and "remembers" them even after the block where they were declared has finished. Simply put, a closure is a function that remembered the conditions in which it was born and keeps those values like a small suitcase with personal items (variables).

Closure is a function together with the environment (scope) that existed at the moment of its creation.

Simple example of a closure

Let's look at it in practice:


Func<int> MakeCounter()
{
    int count = 0;
    return () =>
    {
        count++;
        return count;
    };
}

Call it like this:


var counter = MakeCounter();
Console.WriteLine(counter()); // 1
Console.WriteLine(counter()); // 2
Console.WriteLine(counter()); // 3

How does it work?

  • The variable count is declared inside the method MakeCounter.
  • The lambda () => { ... } is returned out and now lives outside the method.
  • But! It remembers the variable count, even though the method MakeCounter has long finished.

This is a closure: the lambda "closed over" (captured) the variable count from the surrounding context.

What exactly does the lambda "capture"?

  • local variables from the surrounding method (scope),
  • method parameters,
  • variables in blocks (for, foreach, etc.).

Important: variables are captured not by value, but by reference! If we change a variable inside a closure, it will change "outside" as well. In fact the C# compiler creates a special helper class for these variables — but it's enough to remember this conceptually to use closures confidently.

2. Closure and lexical scope

Let's refine the "function factory" example with a closure:


Func<int, int> PowerFactory(int power)
{
    return x =>
    {
        int result = 1;
        for (int i = 0; i < power; i++)
            result *= x;
        return result;
    };
}

Usage:


var square = PowerFactory(2);   // x^2
var cube = PowerFactory(3);     // x^3

Console.WriteLine(square(5)); // 25
Console.WriteLine(cube(2));   // 8

The functions square and cube are created with different values of the variable power, and each remembers its own. For every call to PowerFactory a "separate backpack" of captured values is created.

3. Mutation of captured variables

Sometimes the question arises: what happens if you create multiple lambdas in a loop that capture the loop variable? This is where it's easy to step on a rake (i.e., run into a trap).

Example: closure in a loop


var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
    actions.Add(() => Console.WriteLine(i));
}
foreach (var action in actions)
    action(); // ???

What do you expect? 0, 1, 2? In reality it will be:

3
3
3

Why so? All lambdas "refer" to the same variable i. By the end of the loop i already equals 3, and that's the value all our Actions will see.

Fixed version

To make each lambda capture its "own" value, create a new variable inside the loop body:


var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
    int copy = i;
    actions.Add(() => Console.WriteLine(copy));
}
foreach (var action in actions)
    action(); // 0 1 2

Now copy is a new variable on each iteration, and the closure captures that one.

4. Applying closures to real tasks

Data processing and callbacks

When you do something asynchronous or defer execution (event handlers, filtering, task scheduling) — a closure allows you to "pack" logic together with parameters. For example:


void ProcessList(List<int> list, int threshold)
{
    var filtered = list.Where(x => x > threshold);
    foreach (var item in filtered)
        Console.WriteLine(item);
}

Here the lambda inside Where captures the variable threshold.

Creating "factories" of functions

Pass a parameter — get a function with that parameter "baked in". This trick is convenient for configuring filters, sort comparers, UI reactions, etc.

Managing state

Sometimes you need to keep a bit of state without a separate class:


Func<string, string> CreateGreeting()
{
    string prefix = "Hello";
    return name =>
    {
        return $"{prefix}, {name}!";
    };
}

5. Useful nuances

Under the hood: how closure works in C#

Everything captured by a lambda is turned by the compiler into a helper class: variables become fields, and the lambda becomes a method. That's why the state of variables "lives" between calls.

Each "factory" spawns a small object. That's normal — .NET manages such objects efficiently and allocates them only when really needed.

Reminder: don't capture "big" objects unnecessarily

If you close over a large object (for example, a UI form), it won't be freed while the lambda is alive. A classic cause of leaks is subscribing to events with a lambda that captured a "heavy" context and forgetting to unsubscribe (+=/-=).

Closures and collection lifetime — example with LINQ

Closures make LINQ flexible: filters remember their parameters.


List<string> colors = new List<string> { "Red", "Green", "Blue", "Yellow" };
string startsWith = "B";
var filtered = colors.Where(c => c.StartsWith(startsWith));
foreach (var color in filtered)
    Console.WriteLine(color); // "Blue"

If you then change startsWith, the result changes too:


startsWith = "R";
foreach (var color in filtered)
    Console.WriteLine(color); // "Red"

This happens because the closure references the same variable startsWith, and the method StartsWith checks the current value each time.

6. Common mistakes when working with closures

Error #1: capturing a variable that changes before use.
A classic situation in loops: a lambda inside a closure "looks" at the same loop variable which by the time of invocation already has a different value. As a result the function works with data you didn't expect. Fix it by introducing a separate variable inside the loop.

Error #2: capturing too large a context.
A closure drags the whole object instead of a specific field/value. This creates unnecessary dependencies and makes code harder. Capture only what's needed.

Error #3: holding heavy resources and memory leaks.
Subscribing to an event with a lambda that captured a "heavy" object and not unsubscribing — the object won't be freed. Watch subscription lifetimes and use explicit unsubscribe (-=).

Error #4: loss of code manageability.
Excessive use of closures makes it harder to understand the sources of data and their changes, especially when the closure is declared far from the call site. Keep logic close and don't overuse them.

2
Task
C# SELF, level 51, lesson 3
Locked
Creating a simple function with built-in data (closure)
Creating a simple function with built-in data (closure)
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION