The Intuitive Guide to Understanding Closures in C#

Intro

Closures represent a programming technique that is widely used in pretty much every programming language nowadays. That’s why I believe every developer should have an excellent understanding of their behavior.

In this article, I will first start by presenting closures as a general concept. This intro discussion is meant to be language agnostic. However, it is one of the topics where a single showcase is worth thousands of words. That’s why I’ll drive the discussion by plenty of examples written in C#. I believe you should be able to grasp the key points even if you’re not very familiar with the language.

After the initial overview, I’ll dig into the implementation internals of closures in C#. You will directly explore the compiler-generated Intermediate Language (IL) code.

If that sounds daunting, don’t worry, I will also accompany the IL code with a very simplified custom representation, so the core concepts become apparent.

Understanding closures can save your program from very complex issues to troubleshoot. Some of those are unexpected memory leaks, exceptions for disposed resources, an explosion of created objects, etcetera.

Let’s dive in!

This article is influenced by the Closures material in the books C# in Depth by Jon Skeet and Effective C# by Bill Wagner.

What is a Closure Anyway?

Fundamentally, a Closure is a combination of two things – behavior and environment.

To explain what this means, let’s start with the following example:

static void Main(string[] args)
{
    var closure = CreateClosure();
    closure();
}

private static Action CreateClosure()
{
    int a = 5;
    void Closure() => Console.WriteLine(a);
    return Closure;
}

This program outputs the number “5”.

The CreateClosure method defines an inner function, called Closure (line 10), and returns that function (line 11). All the Closure function does is print the value of the variable a to the console.

The Main method calls the CreateClosure method (line 3) and executes the returned function (line 4).

The crucial thing to notice is that the Main method has no knowledge at all for the variable a. But still, the closure function, executed on line 4, somehow keeps track of it and prints its’ value to the console.

In some way, the function manages to preserve the variable a in its’ execution environment. In this way, a is accessible at some later point when you invoke the function.

In other words, the variable a is closed by (or bound in) the execution environment. That’s where the term Closure comes from.

Note that the described behavior is not C# specific at all. It’s just how closures work fundamentally.

There is plenty of materials online on closures and related terms like lexical scoping. It’s quite interesting to explore the evolution of this concept originating from the 1960s.

However, that’s not the goal of this article.

Here, I want to discuss how the C# language implements these semantics and precisely what the compiler does.

So, let’s get into that.

A More Complex Example

Let’s inspect the following code snippet:

class Program
{
    static void Main(string[] args)
    {
        new MyClass().MyMethod();
    }
}

public class MyClass
{
    private int _instanceVariable = 0;
    private static int _staticVariable = 0;
    
    public void MyMethod()
    {
        var localVariable = 0;
        
        void Closure()
        {
            Console.WriteLine($"{nameof(_instanceVariable)}: {_instanceVariable}");
            Console.WriteLine($"{nameof(_staticVariable)}: {_staticVariable}");
            Console.WriteLine($"{nameof(localVariable)}: {localVariable}");
        }
        
        _instanceVariable++;
        _staticVariable++;
        localVariable++;

        FunctionRunner.Run(Closure);
    }
}

public static class FunctionRunner
{
    public static void Run(Action callback) => callback();
}

What do you think the output of this little program is?

You may be surprised to see that it prints the following:

_instanceVariable: 1
_staticVariable: 1
localVariable: 1

Look at the inner function Closure, defined on line 18. It just prints the values of the variables _instanceVariable, _staticVariable, and localVariable.

From our previous example, we already know that closures capture their execution environment. But this example is more interesting.

You may have expected the output for all the variables to be “0” instead of “1”.

This would make sense.

At the point when the Closure function is defined, all of the variables have a value of 0. After that, they all get incremented. But even though the incrementation happens after the Closure function body, it still prints the incremented values when the function runs.

What does that mean?

Closures in C# do not capture the values of the variables but a reference to them. In that way, if any variable gets updated between the closure definition and its’ execution, the most recent values would be reflected.

How does that happen? Let’s discuss that next.

The Simple Representation

In this part, I will first represent a simplified version of what the compiler does in our case above.

I wrote this code for demonstration purposes. Of course, the compiler doesn’t care about human readability, so it would not care to emit code like that.

Still, in the next section, you’ll see that the actual IL is very similar conceptually to the fragment you see below:

public class MyClass
{
    private int _instanceVariable = 0;
    private static int _staticVariable = 0;

    private class ClosureContext
    {
        public MyClass myClass;
        public int localVariable;

        public void Closure()
        {
            Console.WriteLine($"{nameof(myClass._instanceVariable)}: {myClass._instanceVariable}");
            Console.WriteLine($"{nameof(_staticVariable)}: {_staticVariable}");
            Console.WriteLine($"{nameof(localVariable)}: {localVariable}");
        }
    }
    
    public void MyMethod()
    {
        var ctx = new ClosureContext();
        ctx.myClass = this;

        _instanceVariable++;
        _staticVariable++;
        ctx.localVariable++;

        FunctionRunner.Run(ctx.Closure);
    }
}

I’ve omitted the FunctionRunner class for simplicity.

Representation of “localVariable”

I will first concentrate only on the logic around modifying and printing localVariable as this is the most interesting use case from the example above.

In essence, the compiler creates an inner class (ClosureContext) and moves the Closure method in there. What’s more important, localVariable is now a public field in ClosureContext.

Notice how MyMethod creates an instance of the ClosureContext class (line 21) and uses it to access and increment localVariable. In this way, the same localVarialbe filed is referenced both by MyMethod and Closure.

This explains why when Closure is executed, it prints the latest value of localVariable.

Take a moment to comprehend this behavior representation. It’s not complicated at all once you wrap your head around it.

Representations of “_instanceVariable” and “_staticVariable”

If you now understand how the access to localVariable works, it should be quite easy to follow what happens with the other two variables.

_staticVariable is the easiest one. Declared as static means that any piece of code that belongs to MyClass (including nested classes) can access it directly. The Closure method in ClosureContext can just read it without any further transformations by the compiler.

_instanceVariable is just a little trickier. It’s an instance field, which means it can only be accessed through an object of type MyClass. That’s the reason the ClosureContext class has a public instance field MyClass (line 8). MyMethod just assigns the field with this (line 22).

I hope this all makes sense.

Now, it’s time to investigate the actual IL code!

The IL Code

For inspecting the IL code, I used dotPeek, but any type of decompiler would do the job. Of course, the IL is a lot more verbose and hard to read. However, armed with the understanding from the previous section and my comments, it should be quite easy to follow along.

The full IL code for MyClass is fairly long, so I decided to split and present it into two parts. First, the nested class (the equivalent of ClosureContext) and then the outer MyClass class.

Let’s start with the inner class.

“ClosureContext” Class

Below is a screenshot of the inner class that I called “ClosureContext” in my example from the previous section. Of course, the compiler wouldn’t give such a name for something automatically generated, but the idea stays the same.

If you ignore the low-level instructions and focus on the general logic following my comments, it should be straightforward for you to grasp the essence.

“MyClass” Class

Similarly to ClosureContext, I have provided many comments in the IL code for MyClass. Don’t forget this class contains ClosureContext as a nested class. It’s just not included in the screenshot for space optimization.

I hope you appreciate how similar the IL code is to the conceptual implementation. That is why I advise you to make sure you’re comfortable with the simplified version first and then dig deeper into the compiler-generated code in case you’re interested.

Summary

This article was about closures.

First, I explained what the term means from a conceptual standpoint.

Then, I described how closures are presented in C# and how access to bound variables work.

Finally, I explored the actual IL code generated by the compiler so that you could appreciate how similar this is to the conceptual overview.

I hope this was helpful. See you next time!

Resources

  1. C# in Depth: Fourth Edition
  2. Effective C#

Site Footer

Subscribe To My Newsletter

Email address