66

I've seen countless posts on how variable capture pulls in variables for the creation of the closure, however they all seem to stop short of specific details and call the whole thing "compiler magic".

I'm looking for a clear-cut explanation of:

  1. How local variables are actually captured.
  2. The difference (if any) between capturing value types vs. reference types.
  3. And whether there is any boxing occurring with respect to value types.

My preference would be for an answer in terms of values and pointers (closer to the heart of what happens internally), though I will accept a clear answer involving values and references as well.

DuckMaestro
  • 15,232
  • 11
  • 67
  • 85
  • 2
    Did you read the documentation? – David Heffernan Mar 25 '11 at 21:30
  • What makes you think pointers are involved? Bear in mind that this is done at the level of C# itself - it's not done by the CLR. – Jon Skeet Mar 25 '11 at 21:37
  • Under the hood references are pointers. I'm looking for that kind of under the hood explanation only if it makes things clearer to understand. – DuckMaestro Mar 25 '11 at 21:40
  • 1
    Under the hood references are pointers on some of the current implementations and even in them there is no guarantee that they will be kept that way. A .Net interpreter or compiler to FPGA could do otherwise and still run all valid non-unsafe code without having any notion of pointers. – Julien Roncaglia Mar 25 '11 at 21:44
  • 3
    @DuckMaestro: VirtualBlackFox is exactly right. The implementation of pointers is irrelevant to the guarantees provided by the C# language specification. It's definitely worth trying to stay at an appropriate level of thinking when understanding features - and closures can definitely be understood without thinking about what exactly the virtual machine (or whatever) is doing. – Jon Skeet Mar 25 '11 at 21:53
  • Thanks to both. Makes sense. Will update title to have less emphasis on pointers. – DuckMaestro Mar 25 '11 at 21:56

1 Answers1

87
  1. Is tricky. Will come onto it in a minute.
  2. There's no difference - in both cases, it's the variable itself which is captured.
  3. Nope, no boxing occurs.

It's probably easiest to demonstrate how the capturing works via an example...

Here's some code using a lambda expression which captures a single variable:

using System;

class Test
{
    static void Main()
    {
        Action action = CreateShowAndIncrementAction();
        action();
        action();
    }

    static Action CreateShowAndIncrementAction()
    {
        Random rng = new Random();
        int counter = rng.Next(10);
        Console.WriteLine("Initial value for counter: {0}", counter);
        return () =>
        {
            Console.WriteLine(counter);
            counter++;
        };
    }
}

Now here's what the compiler's doing for you - except that it would use "unspeakable" names which couldn't really occur in C#.

using System;

class Test
{
    static void Main()
    {
        Action action = CreateShowAndIncrementAction();
        action();
        action();
    }

    static Action CreateShowAndIncrementAction()
    {
        ActionHelper helper = new ActionHelper();        
        Random rng = new Random();
        helper.counter = rng.Next(10);
        Console.WriteLine("Initial value for counter: {0}", helper.counter);

        // Converts method group to a delegate, whose target will be a
        // reference to the instance of ActionHelper
        return helper.DoAction;
    }

    class ActionHelper
    {
        // Just for simplicity, make it public. I don't know if the
        // C# compiler really does.
        public int counter;

        public void DoAction()
        {
            Console.WriteLine(counter);
            counter++;
        }
    }
}

If you capture variables declared in a loop, you'd end up with a new instance of ActionHelper for each iteration of the loop - so you'd effectively capture different "instances" of the variables.

It gets more complicated when you capture variables from different scopes... let me know if you really want that sort of level of detail, or you could just write some code, decompile it in Reflector and follow it through :)

Note how:

  • There's no boxing involved
  • There are no pointers involved, or any other unsafe code

EDIT: Here's an example of two delegates sharing a variable. One delegate shows the current value of counter, the other increments it:

using System;

class Program
{
    static void Main(string[] args)
    {
        var tuple = CreateShowAndIncrementActions();
        var show = tuple.Item1;
        var increment = tuple.Item2;

        show(); // Prints 0
        show(); // Still prints 0
        increment();
        show(); // Now prints 1
    }

    static Tuple<Action, Action> CreateShowAndIncrementActions()
    {
        int counter = 0;
        Action show = () => { Console.WriteLine(counter); };
        Action increment = () => { counter++; };
        return Tuple.Create(show, increment);
    }
}

... and the expansion:

using System;

class Program
{
    static void Main(string[] args)
    {
        var tuple = CreateShowAndIncrementActions();
        var show = tuple.Item1;
        var increment = tuple.Item2;

        show(); // Prints 0
        show(); // Still prints 0
        increment();
        show(); // Now prints 1
    }

    static Tuple<Action, Action> CreateShowAndIncrementActions()
    {
        ActionHelper helper = new ActionHelper();
        helper.counter = 0;
        Action show = helper.Show;
        Action increment = helper.Increment;
        return Tuple.Create(show, increment);
    }

    class ActionHelper
    {
        public int counter;

        public void Show()
        {
            Console.WriteLine(counter);
        }

        public void Increment()
        {
            counter++;
        }
    }
}
Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 3
    @Jon You say that the capture is of variables rather than values. This means, I guess, that if two lambdas, declared in the same method, refer to the same variable, then they both capture the same variable. This if one of the lambdas modifies that variable, then the other one sees the modified value held by that variable. Or am I wide of the mark? – David Heffernan Mar 25 '11 at 21:41
  • 2
    @David: Yes, that's exactly right. In that case, there would be two instance methods in the generated class, and both delegates would reference the same target instance. – Jon Skeet Mar 25 '11 at 21:42
  • 1
    @Jon Thanks. I don't actually know any C# and most of my time is spent with Delphi. The Delphi equivalent behaves the same way. It tends not to arise in most use cases, but I think most people's naive expectation is that values are captured. You can get an awful long way on that particular misunderstanding. – David Heffernan Mar 25 '11 at 21:46
  • 2
    @David: Yes you can. Particularly as that's how anonymous classes work in Java :( – Jon Skeet Mar 25 '11 at 21:51
  • @Jon There must be some cool example of someone creating two lambdas which share captured variables and mutually modifying them in a way to achieve something useful...... – David Heffernan Mar 25 '11 at 21:52
  • @Jon. I'm not sure I follow David's comment-question and your answer. If the captured variable is a value type, and you have two lambdas, won't those lambdas (by the example code above) take two separate copies of the value-type variable? such that if the first lambda later modifies that value, that the second lambda still has the value at the time of the copying? – DuckMaestro Mar 25 '11 at 21:54
  • @DuckMaestro: No, they won't. A single instance of the helper class will be created, and the two lambdas will both use that one instance to create delegates. Note how there isn't a `counter` variable in the `CreateShowAndIncrementAction` method in the expanded version - that variable *only* exists in the helper class. If two references refer to the same instance of that class, then a change via one will be seen by the other. There's no copying involved. – Jon Skeet Mar 25 '11 at 21:57
  • Example of David's scenario coming up... and now there. – Jon Skeet Mar 25 '11 at 22:00
  • @Jon. Thank you! That makes perfect sense. – DuckMaestro Mar 25 '11 at 22:04
  • @Duck The key is to think of it as *variable* capture and **not** as *value* capture. I think once you have that clear, then it becomes easy to reason with. – David Heffernan Mar 25 '11 at 22:10
  • Aye. In hindsight that now makes a lot more sense. Thanks. – DuckMaestro Mar 25 '11 at 22:43
  • What if the variable that was captured has gone out of scope? For example, the lambda is returned from a function. – Brent Aug 23 '13 at 05:52
  • @Brent: The variable still exists and can be used - the C# compiler effectively hoists it to another type, and makes sure that both the "immediate" code in the method and the code in the lambda expression use the same variable. – Jon Skeet Aug 23 '13 at 05:59
  • @DavidHeffernan - (3 years later ...) See the example in section 3.1 of _Structure and Interpretation of Computer Programs_ [here](http://mitpress.mit.edu/sicp/full-text/book/book-Z-H-20.html#%_sec_3.1) where a bank account is implemented with two lambdas (withdraw and deposit) that share a captured account balance. – davidbak Jun 28 '14 at 20:00
  • 1
    @JonSkeet, what happens when one of the captured variables is a class field, and one is not? Does the anonymous method point to that instance's field? Is this instance somehow passed to the generated class? And another question - is it possible, that the compiler doesn't know what anonymous methods share some captured variable? – Bart Juriewicz Jun 28 '16 at 20:47
  • 1
    @Bart: In that case, it's not the field that is captured, but the 'this' reference. If that's all that's captured, the compiler can just generate an instance method. – Jon Skeet Jun 28 '16 at 20:49
  • @JonSkeet: What do you mean the variable itself is captured. If it is a reference type do yo mean a copy of the reference is made and it is the same object just 2 reference to the same object? – jdawiz Aug 06 '19 at 16:21
  • 1
    @jdawiz: No, precisely the opposite - the variable continues to act like a variable; the value is *not* just copied. If you change the variable value (e.g. to refer to a different object) outside the closure, you can see the change inside the closure and vice versa. – Jon Skeet Aug 06 '19 at 16:33