question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

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:closed
  • Created 9 years ago
  • Reactions:30
  • Comments:29 (8 by maintainers)

github_iconTop GitHub Comments

13reactions
Artfunkelcommented, Aug 1, 2017

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:

class Program
{
	static void Main(string[] args)
	{
		MyMethod(new byte[1024 * 1024 * 1024], string.Empty);

		GC.Collect();
		System.Diagnostics.Debugger.Break();
	}

	static List<Action> m_deferredActions = new List<Action>();

	static void WorkWithLargeObject(object largeObject, int iteration) { }
	static void WorkWithSmallObject(object smallObject) { }

	static void MyMethod(object largeObject, object smallObject)
	{
		Parallel.For(0, 10, (i) => WorkWithLargeObject(largeObject, i));

		m_deferredActions.Add(() => WorkWithSmallObject(smallObject));
	}
}

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 both largeObject and smallObject, 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.

3reactions
mikedncommented, Feb 4, 2015

Developers have been using implicit capture in C# lambdas successfully for many years now

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 for for loops and can’t be solved by hacking the for loop variable scope. Value capture solves this cleanly.

Please don’t turn this language into C++.

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++.

Read more comments on GitHub >

github_iconTop Results From Across the Web

C++ | Capture *this in lambda expression: Timeline of change
How the current object (*this) can be captured in a lambda expression has gone through some changes since C++11.
Read more >
C++23: How lambdas are going to change?
A small proposal that doesn't change how existing code behaves but ... Whatever is in the capture list, it's not visible in the...
Read more >
Deducing Lambda Capture Types - c++
I've recently found that capturing a const object by value in a lambda, implies that the variable inside the labmda's body (i.e. the...
Read more >
Lambda syntax should be more liberal in what it accepts
To capture "everything needed by this lambda, and nothing else," simply add a single = to your capture-list (to capture by value), or...
Read more >
std-proposals: Allow maybe_unused for lambda captures?
Hello, Currently, the lambda capture is defined as: capture: simple-capture init-capture simple-capture: identifier & identifier
Read more >

github_iconTop Related Medium Post

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found