Outer Variables

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.

Listing 13.20: Using an Outer Variable in a Lambda Expression
1. public class Program
2. {
3.     public static void Main()
4.     {
5.         int[] items = new int[5];
6.         int comparisonCount = 0;
7.  
8.         for (int i = 0; i < items.Length; i++)
9.         {
10.             Console.Write("Enter an integer:");
11.             string? text = Console.ReadLine();
12.             if (!int.TryParse(text, out items[i]))
13.             {
14.                 Console.WriteLine($"'{text}' is not a valid integer.");
15.                 return;
16.             }
17.         }
18.  
19.         DelegateSample.BubbleSort(items,
20.             (int first, int second) =>
21.             {
22.                 comparisonCount++;
23.                 return first < second;
24.             }
25.         );
26.  
27.         for (int i = 0; i < items.Length; i++)
28.         {
29.             Console.WriteLine(items[i]);
30.         }
31.  
32.         Console.WriteLine("Items were compared {0} times.",
33.             comparisonCount);
34.     }
35. }
Output 13.2
Enter an integer:5
Enter an integer:1
Enter an integer:4
Enter an integer:2
Enter an integer:3
5
4
3
2
1
Items were compared 10 times.

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.

Static Anonymous Functions

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.

Listing 13.21: Declaring a Static Anonymous Function
1. int comparisonCount = 0;
2.
3. DelegateSample.BubbleSort(items,
4.     static (int first, int second) =>
5.     {
6.         // Error CS8820: A static anonymous function
7.         // cannot contain a reference to comparisonCount.
8.         comparisonCount++;
9.         return first < second;
10.     }
11. );
12.  
13. for (int i = 0; i < items.Length; i++)
14. {
15.     Console.WriteLine(items[i]);
16. }
17.  
18. Console.WriteLine("Items were compared {0} times.",
19.     comparisonCount);
20.     }
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.

AdVanced Topic
Outer Variable CIL Implementation

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.

Listing 13.22: C# Equivalent of CIL Code Generated by Compiler for Outer Variables
1. public class Program
2. {
3.     // ...
4.     private sealed class __LocalsDisplayClass_00000001
5.     {
6.         public int comparisonCount;
7.         public bool __AnonymousMethod_00000000(
8.             int first, int second)
9.         {
10.             comparisonCount++;
11.             return first < second;
12.         }
13.     }
14.  
15.     public static void Main()
16.     {
17.         __LocalsDisplayClass_00000001 locals = new();
18.         locals.comparisonCount = 0;
19.         int[] items = new int[5];
20.  
21.         for (int i = 0; i < items.Length; i++)
22.         {
23.             Console.Write("Enter an integer: ");
24.             string? text = Console.ReadLine();
25.             if (!int.TryParse(text, out items[i]))
26.             {
27.                 Console.WriteLine($"'{text}' is not a valid integer.");
28.                 return;
29.             }
30.         }
31.  
32.         DelegateSample.BubbleSort
33.             (items, locals.__AnonymousMethod_00000000);
34.         for (int i = 0; i < items.Length; i++)
35.         {
36.             Console.WriteLine(items[i]);
37.         }
38.  
39.         Console.WriteLine("Items were compared {0} times.",
40.             locals.comparisonCount);
41.     }
42. }

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.

AdVanced Topic
Accidentally Capturing Loop Variables

What do you think the output of Listing 13.23 should be?

Listing 13.23: Capturing Loop Variables in C# 5.0
1. public class CaptureLoop
2. {
3.     public static void Main()
4.     {
5.         var items = new string[] { "Moe""Larry""Curly" };
6.         var actions = new List<Action>();
7.         foreach(string item in items)
8.         {
9.             actions.Add(() => { Console.WriteLine(item); });
10.         }
11.         foreach(Action action in actions)
12.         {
13.             action();
14.         }
15.     }
16. }

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.

Output 13.3: C# 5.0 Output
Moe
Larry
Curly
Output 13.4: C# 4.0 Output
Curly
Curly
Curly

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.

Listing 13.24: Loop Variable Capture Workaround before C# 5.0
1. public class DoNotCaptureLoop
2. {
3.     public static void Main()
4.     {
5.         var items = new string[] { "Moe""Larry""Curly" };
6.         var actions = new List<Action>();
7.         foreach(string item in items)
8.         {
9.             string _item = item;
10.             actions.Add(
11.                 () => { Console.WriteLine(_item); });
12.         }
13.         foreach(Action action in actions)
14.         {
15.             action();
16.         }
17.     }
18. }

Now there is clearly one fresh variable per loop iteration; each delegate is, in turn, closed over a different variable.

Guidelines
AVOID capturing loop variables in anonymous functions.

{{ snackbarMessage }}
;