Lambda-calculus defined concept of closures in 1960s, but it was first time fully implemented in 1975 as a Scheme language feature. First time I heard of closure was at university, but didn’t think of it as a big deal until started to (more carefully) read JavaScript books. Than my closure attention shifted toward C#.
Let’s define terminology thru example. In following examples I’ll use lambda notation, although same works for just plain old anonymous delegates.
Output from example above is:
Idea of closure is that function (in our case anonymous method) is able to interact with environment within it was created. Note that we didn’t pass any parameters to anonymous method and still we are able to use outerAndCaptured variable !
From example above we have two outer variables. Outer variable is variable defined in scope where scope includes
an anonymous method. I think of outer as variable that could be captured by anonymous method. There goes another definition:
Captured variable is an outer variable that is later used in anonymous method defined within the same scope.
On the other side, inside if
block we defined another local variable. This variable is regular local variable because there
are no anonymous methods within its scope. Same goes for anonymousLocal
variable.
Don’t get fooled, this is more useful that it looks like!
First we must understand that captured variable is not a copy or value of outer variable. It is, by magic of compiler, same variable in outer and inner function:
Code above is printing first 3, than 2. Creating x delegate is not same as executing it; therefore first dumped value will be 3. Next console dump is proof that outer is changed inside delegate.
If you claim that you never used closure in C#, you are probably wrong, unless you skipped the best C# feature introduced in .NET 3.5 => LINQ.
You have hidden usage of closure in example below:
LINQ Where
is just extension method and x => x < max
is predicate made from lambda expression.
If we dial down .NET version to 2.0 and replace Where
extension method with FindAll
List method,
closure becomes more obvious:
Without closure, we would need all kind of predicates in order to pass different parameters!
Just because of closure we are not passing max
to predicate and we can have one and only one type of predicate:
Predicate<int>
regardless of the query. And this is a big deal!
Now, let’s talk about problems related to captured variables. Captured variable will live as long as any delegate instance referring to it. If you are not careful, this could lead to unpredicted results and memory leaks.
Resulting output will be:
If capturedVariable would be on the stack of DemonstrateLifeOfCapturedVariable()
it would be destroyed once method is finished.
This is not what we see in the result above. What actually happened here is that compiler created one extra object to hold
capturedVariable
on the heap. Delegate has reference to this object and prevent it from garbage
collection as long as delegate is alive.
There is one more surprise related to captured variables. When switching from C#4 to C#5 your code could start producing different results! Note that running example below in VS2013 using C#5 will still work correctly regardless of .NET version targeted. It is a feature of language, not .NET framework!
Result C#4:
Running code above in C#3 or C#4 will output 3 three times. All three actions are pointing to the same captured variable.
This is hardly expected by someone not familiar with closures and variable capturing.
Result C#5:
C#5 compiler will create separate variable for each iteration loop so output will be 1 2 3 as expected.
Hopefully, you have unit tests that will capture this or similar change as soon as you switch to new C# version.
C# closures are one of the things people use without deeper thinking. If you need to remember one thing from the text above, remember life cycle of captured variable. Memory leak is difficult to catch, the other bugs you’ll find easy.