Click here to Skip to main content
15,888,267 members
Articles / Programming Languages / C#
Article

Closures in C# Demystified

Rate me:
Please Sign up or sign in to vote.
4.92/5 (29 votes)
4 Sep 2023CPOL10 min read 20.7K   106   21   12
Tutorial on “Function closures” in C# language
This is tutorial text on “function closures” in C#. Some theory is explained, and several C# examples are shown.

1. Introduction

“Function closures” are tricky and sometimes difficult to understand. They are possible and present in C# language. Every ambitious C# developer should be familiar with function closures, either to use them or to competently read/debug other people's code. They offer fancy encapsulation in the form of a “captured variable” that is not so easy to comprehend. This tutorial tries to explain it and provide a sufficient number of examples. The intended audience is Intermediate C# developer and above.

While many might disagree, from the readability of the code point of view, I would argue that it is more readable to see classical encapsulation via a class than a “function closure” in the code. But of course, opinions differ, and many like to use the fancy staff.

2. Theoretical Background

Before some practical examples, some theoretical background is needed. In this article, we will not go for the full rigor in definitions, like one that can be found in a very good article [1]. We are more focused on explaining and comprehending the basic concepts, leaving a more formal approach to further reading or materials like references enclosed.

2.1. Simplified Definitions

Here are some plain-language explanations.

What are closures? Closures are created when you are inside a function/method in C# reference variable from the “above scope” and then you pass that function as a delegate around. That variable from the “above scope” is passed around with that function delegate.

How is the closure implemented? When the compiler notices that you are inside a function accessing the variable from the “above scope”, it creates a record in which it stores 1) the function in question; 2) the variable from the “above scope” (popularly called “captured variable”) and passes them around together.

Analogy model for thinking about implementation. One very good way to think about it and represent it in your mind is that the compiler creates a private class in which he encapsulates that variable from the “above scope” (so-called “captured variable”) and transforms a “function closure” into a method of that class and passes that private class around.

2.2. More Formal Definitions

Here are more formal definitions, still not as formal as [1]. You can skip this in the first reading.

A programming language with first-class functions. A programming language is said to have “First-class functions” if it treats functions as first-class citizens, meaning that functions can be assigned to variables, passed as arguments to another function, etc. C# is definitely such a language since delegates can be assigned to variables and passed around. Basically, it says that if you can somehow obtain a “pointer to a function” ( in C/C++ terminology) and pass it around, that is a special feature of that language, which we call “first-class function language”. In such languages, typically “function closure” concepts have sense and are possible. So, the concept of “function closure” has no sense in every programming language, just in some languages, and C# is one of them.

Free variable. That is a variable that is used locally but defined in the enclosing scope. That is what we sometimes call “variable from the above scope”.

Lexical scope. That is the definition area of an expression. For the variable, that is an area in which the variable is defined/created. It is often called “static scope” since it can be deduced from static program text.

What are closures? The “function closure” is the concept of implementing lexically scoped variable binding in a language with first-class functions. Practically, that means that “function closure” is storing a record (structure) of a function together with references/values of any free variables it has.

How is the closure implemented? Closures are typically implemented as a special data structure that contains 1) a pointer to the function code; 2) a representation of any “free variable” at the time of closure creation. The second part is sometimes referred to as the “function lexical environment”.

3. Example 1 – Local Function Syntax

Here is a practical example of C# “function closure” created, using local function syntax.

C#
public delegate void MyAction();

public class TestClass
{
    public static MyAction CreateClosureFunction()
    {
        //we created a scope for this variable             
        int i_capturedVariable = 0;

        //using local function syntax
        void ff()
        {
            //this is variable from the "above scope"
            //and it will be captured
            ++i_capturedVariable;
            Console.WriteLine("i_capturedVariable:" + i_capturedVariable);
        }

        return ff;
    }
}
static void Main(string[] args)
{
    MyAction? ClosureFunction1 = TestClass.CreateClosureFunction();

    //here scope is closed and variable
    //i_capturedVariable no longer exists

    Console.WriteLine("ClosureFunction1 invocation:");
    ClosureFunction1();
    ClosureFunction1();
    ClosureFunction1();
}

//==Result===========================================
/* 
ClosureFunction1 invocation:
i_capturedVariable:1
i_capturedVariable:2
i_capturedVariable:3
*/

The above code is an example of a “function closure” in C#.

Please look at variable i_capturedVariable. It is defined in “above scope” to the function scope where it is used. In the above terminology, that is a “free variable” and it will be bound to the function during assignment to the delegate ClosureFunction1. In the above terminology, it will become a “captured variable”. That variable is not in the function scope but is used inside the function, so when the function is passed around, it needs to be encapsulated with the function. The existence of such a variable is the main and only reason why “function closure” needs to be created. The main trick here is that at the moment in which the function is invoked/executed, the scope in which the variable i_capturedVariable is defined will no longer exist, so to make the function work, the compiler needs to encapsulate that variable with the function itself.

Please look at delegate ClosureFunction1. Function closures in C# are created only when the function is passed around via a delegate, which is analogous to the C/C++ “pointer to a function”.

Please a look at the assignment to the delegate ClosureFunction1. It looks like an ordinary assignment to the delegate, nothing in the code visibly indicates that the “function closure” is being created and assigned. All work is done by the compiler in the background. That is why it is sometimes not easy to recognize that “function closure” is being created. Only by decompiling of the C# code, one can see that the variable i_capturedVariable is being encapsulated and passed together with the function.

Please look at the execution result. What we see, is not only that “function closure” has access to the variable i_capturedVariable from the above scope, although that scope no longer exists, but also that it can use that variable to remember the state between invocations. That is why we say that the variable i_capturedVariable from the “above scope” is a “captured variable”.

Please look at the invocation/execution result again. The fact that now “function closure” represented by delegate ClosureFunction1 carries its own state encapsulated within itself, is a feature that is often a motivation for the creation and usage of “function closures”. That is a fancy way to encapsulate state with the function and is liked by many programmers.

4. Example 2 – Anonymous Function Syntax

Here is a practical example of C# “function closure” created, using anonymous function syntax.

C#
public delegate void MyAction();
static void Main(string[] args)
{       
    MyAction? ClosureFunction1 = null;

    {   
        //we created a scope for this variable             
        int i_capturedVariable = 0;

        //using anonymous function syntax
        ClosureFunction1 = delegate
        {
            //this is variable from the "above scope"
            //and it will be captured
            ++i_capturedVariable;
            Console.WriteLine("i_capturedVariable:" + i_capturedVariable);
        };
    };
    //here scope is closed and variable
    //i_capturedVariable no longer exists

    Console.WriteLine("ClosureFunction1 invocation:");
    ClosureFunction1();
    ClosureFunction1();
    ClosureFunction1();
}

//==Result===========================================
/* 
ClosureFunction1 invocation:
i_capturedVariable:1
i_capturedVariable:2
i_capturedVariable:3
*/

The above code is an example of a “function closure” in C#. Even though we this time used “anonymous function syntax”, all the comments made in Example 1 (paragraph 3) still apply and stay the same.

5. Example 3 – Lambda Expression Syntax

Here is a practical example of C# “function closure” created, using lambda expression syntax.

C#
public delegate void MyAction();
static void Main(string[] args)
{
    MyAction? ClosureFunction1 = null;

    {
        //we created a scope for this variable             
        int i_capturedVariable = 0;

        //using lambda expression syntax
        ClosureFunction1 = ()=>
        {
            //this is variable from the "above scope"
            //and it will be captured
            ++i_capturedVariable;
            Console.WriteLine("i_capturedVariable:" + i_capturedVariable);
        };
    };
    //here scope is closed and variable
    //i_capturedVariable no longer exists

    Console.WriteLine("ClosureFunction1 invocation:");
    ClosureFunction1();
    ClosureFunction1();
    ClosureFunction1();
}

//==Result===========================================
/* 
ClosureFunction1 invocation:
i_capturedVariable:1
i_capturedVariable:2
i_capturedVariable:3
*/

The above code is an example of a “function closure” in C#. Even though we this time used “lambda expression syntax”, all the comments made in Example 1 (paragraph 3) still apply and stay the same.

6. Example 4 – Same Captured Variable, but Not Shared

The question that is arising is: If we have two instances of the same “function closure”, do they reference the same “captured variable” or does each have its own instance? Here is the answer:

C#
public delegate void MyAction();

public class TestClass
{
    public static MyAction CreateClosureFunction()
    {
        //we created a scope for this variable             
        int i_capturedVariable = 0;

        //using local function syntax
        void ff()
        {
            //this is variable from the "above scope"
            //and it will be captured
            ++i_capturedVariable;
            Console.WriteLine("i_capturedVariable:" + i_capturedVariable);
        }

        return ff;
    }
}

static void Main(string[] args)
{
    MyAction? ClosureFunction1 = TestClass.CreateClosureFunction();
    MyAction? ClosureFunction2 = TestClass.CreateClosureFunction();

    //here scope is closed and variable
    //i_capturedVariable no longer exists

    Console.WriteLine("ClosureFunction1 invocation:");
    ClosureFunction1();
    ClosureFunction1();
    ClosureFunction1();
    Console.WriteLine("ClosureFunction2 invocation:");
    ClosureFunction2();
    ClosureFunction2();
    ClosureFunction2();
}

//==Result===========================================
/* 
ClosureFunction1 invocation:
i_capturedVariable:1
i_capturedVariable:2
i_capturedVariable:3
ClosureFunction2 invocation:
i_capturedVariable:1
i_capturedVariable:2
i_capturedVariable:3
*/

So, from the result of the execution, we see that each instance of the “function closure” has its own instance of the “captured variable”. Precisely speaking, that is true for this example, it is not necessary to always be like that. The key thing to notice is that every time TestClass.CreateClosureFunction() is executed, a new instance of i_capturedVariable is created and that is the reason why each function closure has its own instance of the captured variable.

7. Decompiling (Function Closure) Example 3 in C#

It is always interesting to see how C# compiler solves the problem with the compilation of “function closure”. I used JetBrains dotPeek to decompile above Example 3 (since it is quite simple) into option “Low-Lewel C#” code. Here is what Decompiler gave as a result:

C#
namespace Example3
{
  // Type Program with token 02000006
  internal class Program
  {
    // Method Main with token 06000006
    [/*Attribute with token 0C000017*/NullableContext(1)]
    private static void Main(/*Parameter with token 08000001*/string[] args)
    {
      Program.\u003C\u003Ec__DisplayClass1_0 cDisplayClass10 = 
                             new Program.\u003C\u003Ec__DisplayClass1_0();
      cDisplayClass10.i_capturedVariable = 0;
      Program.MyAction myAction = 
      new Program.MyAction((object) cDisplayClass10, __methodptr(\u003CMain\u003Eb__0));
      Console.WriteLine("ClosureFunction1 invocation:");
      myAction();
      myAction();
      myAction();
    }

    // Method .ctor with token 06000007
    public Program()
    {
      base.\u002Ector();
    }

    // Type MyAction with token 02000007
    public delegate void MyAction();

    // Type <>c__DisplayClass1_0 with token 02000008
    [/*Attribute with token 0C000018*/CompilerGenerated]
    private sealed class \u003C\u003Ec__DisplayClass1_0
    {
      // Field i_capturedVariable with token 04000004
      public int i_capturedVariable;

      // Method .ctor with token 0600000C
      public \u003C\u003Ec__DisplayClass1_0()
      {
        base.\u002Ector();
      }

      // Method <Main>b__0 with token 0600000D
      internal void \u003CMain\u003Eb__0()
      {
        ++this.i_capturedVariable;
        Console.WriteLine(string.Concat("i_capturedVariable:", 
                          this.i_capturedVariable.ToString()));
      }
    }
  }
}

Please notice that the compiler created private sealed class <>c__ DisplayClass1_0 to encapsulate i_capturedVariable and created method <Main>b__0() to represent lambda expression. Above in the Main method, it translated function capture invocation into class methods/attributes manipulation.

8. Example 5 – Same Captured Variable, Shared

Let us look again at the case when we have two instances of the same “function closure”, and they reference the same “captured variable”.

C#
public delegate void MyAction();
static void Main(string[] args)
{
    MyAction? ClosureFunction1 = null;
    MyAction? ClosureFunction2 = null;

    {
        //we created a scope for this variable             
        int i_capturedVariable = 0;

        //using lambda expression syntax
        ClosureFunction1 = () =>
        {
            //this is variable from the "above scope"
            //and it will be captured
            ++i_capturedVariable;
            Console.WriteLine("i_capturedVariable:" + i_capturedVariable);
        };

        ClosureFunction2 = () =>
        {
            //this is variable from the "above scope"
            //and it will be captured
            ++i_capturedVariable;
            Console.WriteLine("i_capturedVariable:" + i_capturedVariable);
        };
    };
    //here scope is closed and variable
    //i_capturedVariable no longer exists

    Console.WriteLine("ClosureFunction1 invocation:");
    ClosureFunction1();
    ClosureFunction1();
    ClosureFunction1();
    Console.WriteLine("ClosureFunction2 invocation:");
    ClosureFunction2();
    ClosureFunction2();
    ClosureFunction2();
}

//==Result===========================================
/* 
ClosureFunction1 invocation:
i_capturedVariable:1
i_capturedVariable:2
i_capturedVariable:3
ClosureFunction2 invocation:
i_capturedVariable:4
i_capturedVariable:5
i_capturedVariable:6
*/

From the execution result, we can see that they reference the same “captured variable”. If you look carefully into the code, you will see that it is one instance of i_capturedVariable that is being referenced by both function closures. After looking into it for a while, it will make sense. In the next example, we will show how to overcome that issue, if that is what we want.

9. Example 6 – Modifying Shared Captured Variable into Non-Shared

We will now modify the slightly above example so that each instance of function closure gets its own instance of the captured variable.

C#
public delegate void MyAction();
static void Main(string[] args)
{
    MyAction? ClosureFunction1 = null;
    MyAction? ClosureFunction2 = null;

    {
        //we created a scope for this variable             
        int i_capturedVariable = 0;

        //using lambda expression syntax
        ClosureFunction1 = () =>
        {
            //this is variable from the "above scope"
            //and it will be captured
            ++i_capturedVariable;
            Console.WriteLine("i_capturedVariable:" + i_capturedVariable);
        };
    }

    {
        //we created a scope for this variable             
        int i_capturedVariable = 0;

        ClosureFunction2 = () =>
        {
            //this is variable from the "above scope"
            //and it will be captured
            ++i_capturedVariable;
            Console.WriteLine("i_capturedVariable:" + i_capturedVariable);
        };
    };
    //here scope is closed and variable
    //i_capturedVariable no longer exists

    Console.WriteLine("ClosureFunction1 invocation:");
    ClosureFunction1();
    ClosureFunction1();
    ClosureFunction1();
    Console.WriteLine("ClosureFunction2 invocation:");
    ClosureFunction2();
    ClosureFunction2();
    ClosureFunction2();
}

//==Result===========================================
/* 
ClosureFunction1 invocation:
i_capturedVariable:1
i_capturedVariable:2
i_capturedVariable:3
ClosureFunction2 invocation:
i_capturedVariable:1
i_capturedVariable:2
i_capturedVariable:3
*/

So, from the result of the execution, we see that each instance of the “function closure” has its own instance of the “captured variable”. Now the code from Example 5 is better understood.

I made this modification and this example to emphasize to how to resolve the problem in Example 5, that you need to use different variables. Actually, Example 6 in concept is no different from Example 4, which is why Example 4 works the way it works. In that example, each time you use a different variable, only the machine sees it, but humans do not so easily. If you look into Example 4 and Example 6 for a while, you will see they are doing the same thing, just Example 6 is easier to read.

10. Function Closures and Multithreading

The question is how “function closures” behave in a multithreading environment. The answer is the same as other functions. If you look into Example 5, you will see that ClosureFunction1 and ClosureFunction2 share the same instance of the “captured variable”. As with any shared resource, if accessed from different threads, that can create the problem. The fact that shared resource is a fancy “captured variable” makes no difference.

One can easily imagine a case when two function closures share the same “captured variable” and are run on two different threads and are writing/reading to the shared resource being “captured variable” and as a consequence can run into all regular concurrency issues, like a race condition, etc. I will not create any example code here in order not to bloat the text.

11. Conclusion

Function closure is an interesting concept and technique and needs to be in the repertoire of every serious C# programmer. My personal feeling is it is a bit of “obscuring” code and I prefer to use class with class attribute encapsulation when possible. But it is widely accepted and present in C# code and one needs to understand it regardless of his/her personal preferences of using it.

12. References

History

  • 4th September, 2023: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Serbia Serbia
Mark Pelf is the pen name of just another Software Engineer from Belgrade, Serbia.
My Blog https://markpelf.com/

Comments and Discussions

 
QuestionSeek a license to promote your article Pin
Member 1617360831-Dec-23 3:25
Member 1617360831-Dec-23 3:25 
PraiseRe: Seek a license to promote your article Pin
Mark Pelf 2-Jan-24 3:38
mvaMark Pelf 2-Jan-24 3:38 
GeneralMy vote of 5 Pin
Ștefan-Mihai MOGA18-Oct-23 4:03
professionalȘtefan-Mihai MOGA18-Oct-23 4:03 
NewsAnother article of closures - this time in JavaScript Pin
Mark Pelf 4-Oct-23 22:03
mvaMark Pelf 4-Oct-23 22:03 
QuestionNice way to shoot oneself in the foot Pin
Peter Adam15-Sep-23 8:41
professionalPeter Adam15-Sep-23 8:41 
SuggestionOh, no, “there is no such thing as a free variable” Pin
Sergey Alexandrovich Kryukov10-Sep-23 18:32
mvaSergey Alexandrovich Kryukov10-Sep-23 18:32 
SuggestionWhere closures are useful Pin
Ralf Peine 20235-Sep-23 21:05
Ralf Peine 20235-Sep-23 21:05 
SuggestionExample 6 is wrong named Pin
Сергій Ярошко4-Sep-23 20:42
professionalСергій Ярошко4-Sep-23 20:42 
The example 6 looks like a far-fetched one. Two variabels i_capturedVariable are defined in the different blocks therefore they are different - not the "Same captured variable".
PraiseRe: Example 6 is wrong named Pin
Mark Pelf 4-Sep-23 20:49
mvaMark Pelf 4-Sep-23 20:49 
GeneralRe: Example 6 is wrong named Pin
Сергій Ярошко5-Sep-23 2:37
professionalСергій Ярошко5-Sep-23 2:37 
PraiseRe: Example 6 is wrong named Pin
Mark Pelf 5-Sep-23 4:21
mvaMark Pelf 5-Sep-23 4:21 
GeneralMy vote of 5 Pin
Сергій Ярошко4-Sep-23 20:33
professionalСергій Ярошко4-Sep-23 20:33 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.