Local variables declared outside a lambda expression (including parameters of the containing method) are called the outer variables of that lambda. (The this reference, though technically not a variable, is also considered to be an outer variable.) When a lambda body uses an outer variable, the variable is said to be captured (or, equivalently, closed over) by the lambda. In Listing 13.20, we use an outer variable to count how many times BubbleSort() performs a comparison. Output 13.2 shows the results of this listing.
Note that comparisonCount appears outside the lambda expression and is incremented inside it. After calling the BubbleSort() method, comparisonCount is printed out to the console.
Normally, the lifetime of a local variable is tied to its scope; when control leaves the scope, the storage location associated with the variable is no longer valid. But a delegate created from a lambda that captures an outer variable might have a longer (or shorter) lifetime than the local variable normally would, and the delegate must be able to safely access the outer variable every time the delegate is invoked. Therefore, the lifetime of a captured variable is extended: It is guaranteed to live at least as long as the longest-lived delegate object capturing it. (And it may live even longer than that—precisely how the compiler generates the code that ensures outer variable lifetimes are extended is an implementation detail and subject to change.)
The C# compiler takes care of generating CIL code that shares comparisonCount between the anonymous method and the method that declares it.
To avoid any unintended consequences with captured data from outside the anonymous function, C# 9.0 enables a static modifier on the declaration. This causes the compiler to verify there is no closure over data captured from outside the anonymous function declaration. (It has nothing to do with whether the function is instance or static.) To observe the behavior, see Listing 13.21.
In general, consider using static local function declarations by default, and only switching to the less restrictive version when needed, thereby increasing the intentionality of the closure.
The CIL code generated by the C# compiler for anonymous functions that capture outer variables is more complex than the code for a simple anonymous function that captures nothing. Listing 13.22 shows the C# equivalent of the CIL code used to implement outer variables for the code in Listing 13.20.
Notice that the captured local variable is never “passed” or “copied” anywhere. Rather, the captured local variable (comparisonCount) is a single variable whose lifetime the compiler has extended by implementing it as an instance field rather than as a local variable. All usages of the local variable are rewritten to be usages of the field.
The generated class, __LocalsDisplayClass, is a data structure (a class in C#) that contains an expression and the variables (public fields in C#) necessary to evaluate the expression.
What do you think the output of Listing 13.23 should be?
Most people expect that the output will be that shown in Output 13.3, and in C# 5.0 it is. In previous versions of C#, however, the output is that shown in Output 13.4.
A lambda expression captures a variable and always uses the latest value of the variable; it does not capture and preserve the value that the variable had when the delegate was created. This is normally what you want—after all, the whole point of capturing comparisonCount in Listing 13.20 was to ensure that its latest value would be used when it was incremented. Loop variables are no different; when you capture a loop variable, every delegate captures the same loop variable. When the loop variable changes, every delegate that captured this loop variable sees the change. The C# 4.0 behavior is therefore justified—but is almost never what the author of the code wants.
In C# 5.0, the C# language was changed so that the loop variable of a foreach loop is now considered to be a fresh variable every time the loop iterates; therefore, each delegate creation captures a different variable, rather than all iterations sharing the same variable. This change was not applied to the for loop, however: If you write similar code using a for loop, any loop variable declared in the header of the for statement will be considered a single outer variable when captured. If you need to write code that works the same in both C# 5.0 and previous C# versions, use the pattern shown in Listing 13.24.
Now there is clearly one fresh variable per loop iteration; each delegate is, in turn, closed over a different variable.