6

I am trying to get my head around, what happens here ? What sort of code does the compiler produce?

public static void vc()
{
    var listActions = new List<Action>();

    foreach (int i in Enumerable.Range(1, 10))
    {
        listActions.Add(() => Console.WriteLine(i));
    }

    foreach (Action action in listActions)
    {
        action();
    }
}

static void Main(string[] args)
{ 
  vc();
}

output: 10 10 .. 10

According to this, a new instance of ActionHelper would be created for every iteration. So in that case, I would assume it should print 1..10. Can someone give me some pseudo code of what the compiler is doing here ?

Thanks.

Community
  • 1
  • 1
Everything Matters
  • 2,672
  • 4
  • 25
  • 42

3 Answers3

10

In this line

 listActions.Add(() => Console.WriteLine(i));

the variable i, is captured, or if you wish, created a pointer to the memory location of that variable. That means that every delegate got a pointer to that memory location. After this loop execution:

foreach (int i in Enumerable.Range(1, 10))
{
    listActions.Add(() => Console.WriteLine(i));
}

for obvious reasons i is 10, so the memory content that all pointers present in Action(s) are pointing, becomes 10.

In other words, i is captured.

By the way, should note, that according to Eric Lippert, this "strange" behaviour would be resolved in C# 5.0.

So in the C# 5.0 your program would print as expected:

1,2,3,4,5...10

EDIT:

Can not find Eric Lippert's post on subject, but here is another one:

Closure in a Loop Revisited

Tigran
  • 61,654
  • 8
  • 86
  • 123
  • 1
    I linked it in my answer, but since this one will likely end up at the top of the votes list: http://blogs.msdn.com/b/ericlippert/archive/2009/11/12/closing-over-the-loop-variable-considered-harmful.aspx – Joel Coehoorn Oct 12 '12 at 14:58
  • C# 5.0 was released on August 2012 and it's included in Visual Studio 2012, so technically it's already "fixed". – Paolo Moretti Oct 12 '12 at 15:01
  • @PaoloMoretti: or better wait for service pack 1 ? :) – Tigran Oct 12 '12 at 15:03
9

What sort of code does the compiler produce ?

It sounds like you expect this:

foreach (int temp in Enumerable.Range(1, 10))
{
    int i = temp;
    listActions.Add(() => Console.WriteLine(i));
}

That uses a different varaible for each iteration, and so the value of the variable at the time the lambda is created will be captured. In fact, you could use this exact code and get the result you want.

But what the compiler actually does is something closer to this:

int i;
foreach (i in Enumerable.Range(1, 10))
{
    listActions.Add(() => Console.WriteLine(i));
}

This makes it clear that you are capturing the same variable on each iteration. When you later go and actually execute that code, they all refer to the same value that has already be incremented up to 10.

This is not a bug in the compiler or runtime... it was a conscious decision on the part of the language design team. However, due to confusion about how this works, they have reversed that decision and decided to risk a breaking change to make it work more like you expect for C# 5.

Joel Coehoorn
  • 399,467
  • 113
  • 570
  • 794
  • I think for new programmers one other common misconception is not that the foreach generates different variables, but that lambdas close over values rather than variables. – Servy Oct 12 '12 at 15:23
  • @Servy - for reference types, it's effectively the same thing in most circumstances. – Joel Coehoorn Oct 12 '12 at 15:50
  • Well, no, really, it's not. If you never change the value of the variable to reference another object (just like if you never change a value type to be another value) the it doesn't matter if you close over the variable or the value. Unless the variable is read only (or at least not changed between the creation of the closure and when it's invoked) there's a difference. Additionally, this is dealing with a value type, not a reference type. – Servy Oct 12 '12 at 15:53
0

In essence what is happening is that the compiler is declaring your int i outside of the loop and thus is not creating a new value of it for each action, just referencing the same one each time. By the time you execute the code i is 10 and so it prints 10 a lot.

Chris
  • 27,210
  • 6
  • 71
  • 92