Proposal: Lambda Capture Lists
See original GitHub issue(Note: this proposal was briefly discussed in #98, the C# design notes for Jan 21, 2015. It has not been updated further based on the discussion that’s already occurred on that thread.)
Background
Today, lambdas automatically capture any referenced state into a closure object. These captures happen by reference, in that the “local” variable that’s closed over is compiled as a field onto the “display class,” with an instance of that class used both by the containing method and by the lambda defined in it.
// Original code
public static void ContainingMethod()
{
int i = 42;
Action a = () => {
Console.WriteLine(i);
};
}
// Approximate compiled equivalent
public static void ContainingMethod()
{
var class2 = new <>c__DisplayClass1();
class2.i = 42;
Action action = new Action(class2.<ContainingMethod>b__0);
}
private sealed class <>c__DisplayClass1
{
public int i;
public void <ContainingMethod>b__0()
{
Console.WriteLine(this.i);
}
}
The ability to write such concise code and have the compiler generate all of the necessary boilerplate is a huge productivity win.
Problem
While this is a productivity win, it also hides some key aspects of how the mechanism works, in particular how the data makes its way into the lambda and where that data is stored, namely in an allocated object.
Solution: Explicitly Specifying What Gets Captured
When C++11 introduced lambda support, it explicitly disabled the implicit automatic capturing of any referenced state. Instead, developers are forced to state their intentions for what they want captured by using a “capture list.” C# today behaves as if all captured state is captured by reference, and we would retain that by default, but we could also allow developers (optionally) to use capture lists to be more explicit about what they want captured. Using a capture list, the previously explored ContainingMethod example could be written:
public static void ContainingMethod()
{
int i = 42;
Action a = [i]() => {
Console.WriteLine(i);
};
}
This states that the lambda captures the ‘i’ variable and nothing else. As such, this code is the exact equivalent of the previous example and will result in exactly the same code being generated by the compiler. However, now that we’ve specified a capture list, any attempt by the method to use a value not included in the capture list will be met with an error. This verification helps the developer not only better understand what state is being used, it also helps to enable compiler-verification that no allocations are involved in a closure. If a closure is instead written with an empty capture list, the developer can be assured that the lambda won’t capture any state, won’t require a display class, and thus won’t result in an allocation (in most situations, the compiler will then also be able to statically cache the delegate, so that the delegate will only be allocated once for the program rather than once per method call):
public static void ContainingMethod()
{
int i = 42;
Action a = []() => {
Console.WriteLine(i); // Error: can’t access non-captured ‘i'
};
}
Additional Support: Capturing By Value
Today if a developer wants the equivalent of capturing by value instead of capturing by reference, they must first make a copy outside of the closure and reference that copy, e.g
public static void ContainingMethod()
{
int i = 42; // variable to be captured by value
...
int iCopy = i;
Action a = () => {
Console.WriteLine(iCopy);
};
}
In this example, since the lambda closes over iCopy rather than i, it’s effectively capturing a copy by reference, and thus has the same semantics as if capturing i by value. This, however, is verbose and error prone, in that a developer must ensure that iCopy is only used inside the lambda and not elsewhere, and in particular not inside of another lambda that might close over the same value. Instead, we could support assignment inside of a capture list:
public static void ContainingMethod()
{
int i = 42; // variable to be captured by value
...
Action a = [int iCopy = i]() => {
Console.WriteLine(iCopy);
};
}
Now, only iCopy and not i can be used inside of the lambda, and iCopy is not available outside of the scope of the lambda.
// Compiled equivalent
public static void ContainingMethod()
{
int i = 42;
var class2 = new <>c__DisplayClass1();
class2.iCopy = i;
Action action = new Action(class2.<ContainingMethod>b__0);
}
private sealed class <>c__DisplayClass1
{
public int iCopy;
public void <ContainingMethod>b__0()
{
Console.WriteLine(this.iCopy);
}
}
With the developer explicitly specifying what to capture and how to capture it, the effects of the lambda capture are made much clearer for both the developer writing the code and someone else reading the code, improving the ability to catch errors in code reviews.
Alternate Approach
Instead of or in addition to support for lambda capture lists, we should consider adding support for placing attributes on lambdas. This would allow for a wide-range of features, but in particular would allow for static analysis tools and diagnostics to separately implement features like what lambda capture lists are trying to enable.
For example, if the “[]” support for specifying an empty capture list is unavailable but a developer was able to apply attributes to lambdas, they could create a “NoClosure” attribute and an associated Roslyn diagnostic that would flag cases where a lambda annotated with [NoClosure] actually captured something:
public static void ContainingMethod()
{
int i = 42;
Action a = [NoClosure]() => { // diagnostic error: lambda isn't allowed to capture
Console.WriteLine(i);
};
}
Issue Analytics
- State:
- Created 9 years ago
- Reactions:30
- Comments:29 (8 by maintainers)
Top GitHub Comments
I would also like to know why this idea was dismissed. It’s not just an academic language consideration: the lack of explicit capture in C# creates real-world bugs that the suggested alternatives cannot fix. Consider this program, a distilled example of a problem that my company faced yesterday:
Did you spot the bug? As the precautionary
GC.Collect
might suggest, the 1GB array isn’t freed. This makes no sense until you disassemble the compiler-generated display classes and discover that both lambdas are merged into the same display class. This class holds a reference to bothlargeObject
andsmallObject
, and thus our lightweight and long-lived deferred action owns an enormous chunk of memory that it never accesses. In the original code the two lambdas were much further apart and even in different scopes, making the problem far harder to spot.The fix that I applied to our codebase was creating a local copy of
largeObject
just above the creation of the first lambda, causing its capture to be moved into the display class for just that scope. (In the example above I’d need to add a dummy scope around the copy and lambda to replicate this.) The other option is to explicitly create my own display class equivalent. Both require writing copious comments to prevent someone from simplifying the code and re-introducing the bug later, and neither meet the goals of C#'s lambda support. The fact that I had to use a third-party disassembly tool to work out what was happening is pretty poor too.“Lambdas that capture nothing, or only
this
” won’t help here. De-optimising the way in which the compiler generates display classes would, at the cost of runtime performance across the board. The best solution is allowing C+±style explicit lambda capture as suggested in this proposal.It seems that you forgot the
foreach
scope hack that was added because people just couldn’t get how things work or just wished they work differently. And the problem still persists forfor
loops and can’t be solved by hacking thefor
loop variable scope. Value capture solves this cleanly.That’s a dubious request. C++ may have its (perceived) shortcomings but it also did a lot of things right and this is one of them. Copying a feature from C++ doesn’t mean that C# suddenly transforms in C++.