Closure is a very important concept related to anonymous functions and lambdas. They allow a programmer to use local variables of the parent method inside a body of the inline function and then execute it at any time. The question is, how the C# compiler saves these local variables and how are they restored later - let’s check it.
I’m pretty sure that a lot of programmers know the example below - it greatly shows the trap related to inline functions using parent’s local variables. We have a simple method CreateDelegates
which creates a list of Action objects, and then populates it with n
lambda methods, where each of them just writes a value of i
variable. Our Main
method is also simple and just calls every lambda created earlier.
|
|
Somebody who sees this example the first time can think: “every lambda takes the value of i
variable exactly at the time it’s created, so the program should in the result write numbers from 0 to n”. I remember my surprise a few years ago when after running similar code I’ve seen this result:
10
10
10
10
10
10
10
10
10
10
To understand what happened here, we have to know how closures feature works in .NET. I’ve decompiled this program using dotPeek to see what has been generated by the compiler (if you want to do it yourself, remember to enable “Show Compiler-generated Code” option):
|
|
We can clearly see that there are some new elements here. The most important one is a new class <>c__DisplayClass1_0
which contains our closure. It has one field i
which is our iterator variable from the for
loop in Main
method. There is also a default constructor which basically calls object constructor and <CreateDelegates>b__0()
method where code of our lambda is stored.
The second change can be spotted in our Main
method. Just before the loop, the compiler creates a new instance of <>c__DisplayClass1_0
class, which will hold closure for our lambdas. for
loop also has been modified - we don’t use a local variable as iterator anymore because it has been captured by our closure. This exactly explains why the invocation of every delegate will give us a 10
as a result - this is the last value after the end of for
loop which is shared between all lambdas.
This effect can be easily fixed by creating a copy of iterator variable, such like this:
|
|
Now, the compiler will generate separate display class instances for every lambda, which means that there will be 10 copies of iterator variables and each of them will contain a valid value.
|
|
The program will give us the correct and desired result:
0
1
2
3
4
5
6
7
8
9
Using anonymous functions and lambdas without knowledge about display classes can be very dangerous. Let’s take our previous example and add something, which will modify iterator variable i
inside lambda:
|
|
Somebody can think: “every lambda has its own copy of i
variable, so incrementation won’t have any effect - Console.WriteLine
will still write the old value”. That’s sadly not true:
10
11
12
13
14
15
16
17
18
19
Remember that every lambda shares i
variable contained by the display class? Every invocation will make an incrementation and the new value will be used in the next lambda call.
Let’s make it more dangerous. Imagine that we want to display elements of the array populated with numbers from 0 to 9. After each Console.WriteLine
, the array will be set to null:
|
|
Again, without knowledge about how closures work in .NET, somebody will think that array is a separate reference for each lambda we created earlier. In fact, we will get the exception at the second call:
System.NullReferenceException: 'Object reference not set to an instance of an object.'
CS$<>8__locals1.array was null.
Fortunately, today’s tools are smart and detect a lot of potential bugs made by programmers even before compilation. In Visual Studio, using an i
variable as in the first example will generate tooltip with the warning Captured variable is modified in the outer scope
. Never ignore this - as we saw in the previous examples, bugs can be hidden at first glance and break your app after a few calls of the same lambda.
Anonymous functions and lambdas are very powerful features in C# - they allow us to write our code in a cleaner and simpler way. Additionally, closures ensure that every local variable of the parent method will be saved and safe to use at any time. Use them carefully - never ignore warnings and always think if some local variables will be shared.