9

Why does the following code print 11 twice?

int i = 10;
Action fn1 = () => Console.WriteLine(i);
i = 11;
Action fn2 = () => Console.WriteLine(i);
fn1();
fn2();

Output 11 11

According to the answers in this post - How to tell a lambda function to capture a copy instead of a reference in C#? - a lambda is translated into a class with a copy of the captured variable. If that is the case shouldn't my example have printed 10 & 11?

Now, when a lambda captures by reference how does it affect the life time of the variable captured. For instance, assume that the above piece of code was in a function & the Actions' scope was global to the variable like this:

class Test
{
  Action _fn1;
  Action _fn2;

  void setActions()
  {
    int i = 10;
    _fn1 = () => Console.WriteLine(i);
    i = 11;
    _fn2 = () => Console.WriteLine(i);
  }

  static void Main()
  {
    setActions();
    _fn1();
    _fn2();
  }
}

In this scenario wouldn't the variable i have gone out of scope when the actions are invoked? So, are the actions left with a dangling pointer sort of reference?

Social Developer
  • 405
  • 5
  • 16

3 Answers3

9

If that is the case shouldn't my example have printed 10 & 11?

No, because you've only got a single variable - fn1 captures the variable, not its current value. So a method like this:

static void Foo()
{
    int i = 10;
    Action fn1 = () => Console.WriteLine(i);
    i = 11;
    Action fn2 = () => Console.WriteLine(i);
    fn1();
    fn2(); 
}

is translated like this:

class Test
{
    class MainVariableHolder
    {
        public int i;
        public void fn1() => Console.WriteLine(i);
        public void fn2() => Console.WriteLine(i);
    }

    void Foo()
    {
        var holder = new MainVariableHolder();
        holder.i = 10;
        Action fn1 = holder.fn1;
        holder.i = 11;
        Action fn2 = holder.fn2;
        fn1();
        fn2();
    }
}

This answers your second point too: the variable is "hoisted" into a class, so its lifetime is effectively extended as long as the delegate is alive.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • Thanks for the answer. But, why would the two lambdas be represented by a single class? I thought they would each have a separate class. Could you point me to an evidence to this? I attempted looking at the IL code via ILSpy but it doesn't show the classes generated for the lambda. – Social Developer Jun 12 '17 at 09:57
  • 1
    @Sumith: It couldn't use two classes, as then you'd end up with two variables - if `fn1` modifies `i`, that needs to be visible in `fn2`. As for how you verify it: use ildasm instead of ilspy, or (if you can) disable "optimizations" in ilspy (so that it does less work to reconstruct the original C#) – Jon Skeet Jun 12 '17 at 09:59
  • 1
    @Sumith: If you go into View / Options and untick "Decompile lambdas" etc, you'll see code like I've shown. – Jon Skeet Jun 12 '17 at 10:02
  • 1
    Would this be an example of closures as also described in this [article](http://csharpindepth.com/Articles/Chapter5/Closures.aspx) – Scrobi Jun 12 '17 at 10:18
  • 1
    @Scrobi: Yes, it is. – Jon Skeet Jun 12 '17 at 10:22
  • I wish it worked more like the C++ lambdas where you can clearly specify whether to capture by value or capture by reference. (By the by I am just yet to read "The Beauty of Closures" shared by @Scrobi) – Social Developer Jun 12 '17 at 10:29
0

Indeed a class is generated that has as it's fields the captured variables, not their values. So, when the lamba would be executed the runtime would check for the current value of i, which is 11, since you have updated this, before you call the lambda. This is why you see this. In order to validate this argument, you could rearrange the ordering of your statements as below:

int i = 10;
Action fn1 = () => Console.WriteLine(i);
fn1();
i = 11;
Action fn2 = () => Console.WriteLine(i);
fn2();
Christos
  • 53,228
  • 8
  • 76
  • 108
-1

It prints twice because of deferred execution, not because of the way it captures variables.

When the execution is deferred, it will print the latest value of i, since i is what is captured, not it's value.

Linnau
  • 39
  • 1
  • 4
  • 2
    " since i is what is captured, not it's value." - in other way, it *is* because of the way it captures variables. (The point is it captures the variable, not the value) – Jon Skeet Jun 12 '17 at 09:57