JetBrains Rider 2022.2 Help

Code Inspection: Implicitly captured closure

This inspection draws your attention to the fact that more closure values are being captured than is obviously visibly, which has an impact on the lifetime of these values.

Consider the following code:

using System; public class Class1 { private Action _someAction; public void Method() { var obj1 = new object(); var obj2 = new object(); _someAction += () => { Console.WriteLine(obj1); Console.WriteLine(obj2); }; // "Implicitly captured closure: obj2" _someAction += () => { Console.WriteLine(obj1); }; } }

In the first closure, we see that both obj1 and obj2 are being explicitly captured; we can see this just by looking at the code. For the second closure, we can see that obj1 is being explicitly captured, but JetBrains Rider is warning us that obj2 is being implicitly captured.

This is due to an implementation detail in the C# compiler. During compilation, closures are rewritten into classes with fields that hold the captured values, and methods that represent the closure itself. The C# compiler will only create one such private class per method, and if more than one closure is defined in a method, then this class will contain multiple methods, one for each closure, and it will also include all captured values from all closures.

If we look at the code that the compiler generates, it looks a little like this (some names have been cleaned up to ease reading):

public class Class1 { [CompilerGenerated] private sealed class <>c__DisplayClass1_0 { public object obj1; public object obj2; internal void <Method>b__0() { Console.WriteLine(obj1); Console.WriteLine(obj2); } internal void <Method>b__1() { Console.WriteLine(obj1); } } private Action _someAction; public void Method() { // Create the display class - just one class for both closures var dc = new Class1.<>c__DisplayClass1_0(); // Capture the closure values as fields on the display class dc.obj1 = new object(); dc.obj2 = new object(); // Add the display class methods as closure values _someAction += new Action(dc.<Method>b__0); _someAction += new Action(dc.<Method>b__1); } }

When the method runs, it creates the display class, which captures all values, for all closures. So even if a value isn't used in one of the closures, it will still be captured. This is the "implicit" capture that JetBrains Rider is highlighting.

The implication of this inspection is that the implicitly captured closure value will not be garbage collected until the closure itself is garbage collected. The lifetime of this value is now tied to the lifetime of a closure that does not explicitly use the value. If the closure is long lived, this might have a negative effect on your code, especially if the captured value is very large.

Note that while this is an implementation detail of the compiler, it is consistent across versions and implementations such as Microsoft (pre and post Roslyn) or Mono's compiler. The implementation must work as described in order to correctly handle multiple closures capturing a value type. For example, if multiple closures capture an int, then they must capture the same instance, which can only happen with a single shared private nested class. The side effect of this is that the lifetime of all captured values is now the maximum lifetime of any closure that captures any of the values.

Last modified: 08 March 2021