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.
public delegate void MyAction();
public class TestClass
{
public static MyAction CreateClosureFunction()
{
int i_capturedVariable = 0;
void ff()
{
++i_capturedVariable;
Console.WriteLine("i_capturedVariable:" + i_capturedVariable);
}
return ff;
}
}
static void Main(string[] args)
{
MyAction? ClosureFunction1 = TestClass.CreateClosureFunction();
Console.WriteLine("ClosureFunction1 invocation:");
ClosureFunction1();
ClosureFunction1();
ClosureFunction1();
}
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.
public delegate void MyAction();
static void Main(string[] args)
{
MyAction? ClosureFunction1 = null;
{
int i_capturedVariable = 0;
ClosureFunction1 = delegate
{
++i_capturedVariable;
Console.WriteLine("i_capturedVariable:" + i_capturedVariable);
};
};
Console.WriteLine("ClosureFunction1 invocation:");
ClosureFunction1();
ClosureFunction1();
ClosureFunction1();
}
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.
public delegate void MyAction();
static void Main(string[] args)
{
MyAction? ClosureFunction1 = null;
{
int i_capturedVariable = 0;
ClosureFunction1 = ()=>
{
++i_capturedVariable;
Console.WriteLine("i_capturedVariable:" + i_capturedVariable);
};
};
Console.WriteLine("ClosureFunction1 invocation:");
ClosureFunction1();
ClosureFunction1();
ClosureFunction1();
}
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:
public delegate void MyAction();
public class TestClass
{
public static MyAction CreateClosureFunction()
{
int i_capturedVariable = 0;
void ff()
{
++i_capturedVariable;
Console.WriteLine("i_capturedVariable:" + i_capturedVariable);
}
return ff;
}
}
static void Main(string[] args)
{
MyAction? ClosureFunction1 = TestClass.CreateClosureFunction();
MyAction? ClosureFunction2 = TestClass.CreateClosureFunction();
Console.WriteLine("ClosureFunction1 invocation:");
ClosureFunction1();
ClosureFunction1();
ClosureFunction1();
Console.WriteLine("ClosureFunction2 invocation:");
ClosureFunction2();
ClosureFunction2();
ClosureFunction2();
}
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:
namespace Example3
{
internal class Program
{
[NullableContext(1)]
private static void Main(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();
}
public Program()
{
base.\u002Ector();
}
public delegate void MyAction();
[CompilerGenerated]
private sealed class \u003C\u003Ec__DisplayClass1_0
{
public int i_capturedVariable;
public \u003C\u003Ec__DisplayClass1_0()
{
base.\u002Ector();
}
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”.
public delegate void MyAction();
static void Main(string[] args)
{
MyAction? ClosureFunction1 = null;
MyAction? ClosureFunction2 = null;
{
int i_capturedVariable = 0;
ClosureFunction1 = () =>
{
++i_capturedVariable;
Console.WriteLine("i_capturedVariable:" + i_capturedVariable);
};
ClosureFunction2 = () =>
{
++i_capturedVariable;
Console.WriteLine("i_capturedVariable:" + i_capturedVariable);
};
};
Console.WriteLine("ClosureFunction1 invocation:");
ClosureFunction1();
ClosureFunction1();
ClosureFunction1();
Console.WriteLine("ClosureFunction2 invocation:");
ClosureFunction2();
ClosureFunction2();
ClosureFunction2();
}
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.
public delegate void MyAction();
static void Main(string[] args)
{
MyAction? ClosureFunction1 = null;
MyAction? ClosureFunction2 = null;
{
int i_capturedVariable = 0;
ClosureFunction1 = () =>
{
++i_capturedVariable;
Console.WriteLine("i_capturedVariable:" + i_capturedVariable);
};
}
{
int i_capturedVariable = 0;
ClosureFunction2 = () =>
{
++i_capturedVariable;
Console.WriteLine("i_capturedVariable:" + i_capturedVariable);
};
};
Console.WriteLine("ClosureFunction1 invocation:");
ClosureFunction1();
ClosureFunction1();
ClosureFunction1();
Console.WriteLine("ClosureFunction2 invocation:");
ClosureFunction2();
ClosureFunction2();
ClosureFunction2();
}
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