5

I'm reading Jon Skeet's C# in Depth.

On page 156 he has an example, Listing 5.13 "Capturing multiple variable instantiations with multiple delegates".

List<ThreadStart> list = new List<ThreadStart>();

for(int index=0; index < 5; index++;)
{
    int counter = index*10;
    list.Add(delegate
          {
              Console.WriteLine(counter);
              counter++;
          }
        );
}

foreach(ThreadStart t in list)
{
    t();
}

list[0]();
list[0]();
list[0]();

list[1]();

In the explanation after this listing, he says "each of the delegate instances has captured a different variable in this case."

I understand this well enough because I understand that each time you close over a variable the compiler generates IL that encapsulates it in a new class made specifically to allow that variable to be captured (essentially making it a reference type so that the value it is referring to doesn't get destroyed with the stack frame of the currently executing scope).

But then he talks about what would have happened had we captured index directly instead of creating the counter variable - "all the delegates would have shared the same variable".

This I don't understand. Isn't index in the same scope as counter? Why would the compiler not also create a new instance of index for each delegate?


Note: I think I figured it out as I typed this question, but I will leave the question up here for posterity. I think the answer is that index is actually in a different scope as counter. Index is essentially declared "outside" the for loop...it is the same variable every time.

Taking a look at the IL generated for a for loop, it proves the variables are declared outside the loop (length and i were variables declared in the for loop declaration).

.locals init (
    [0] int32 length,
    [1] int32 i,
    [2] bool CS$4$0000
)

IL_0000: nop
IL_0001: ldc.i4.s 10
IL_0003: stloc.0
IL_0004: ldc.i4.0
IL_0005: stloc.1
IL_0006: br.s IL_001b
// loop start (head: IL_001b)
    IL_0008: nop
    IL_0009: ldloca.s i
    IL_000b: call instance string [mscorlib]System.Int32::ToString()
    IL_0010: call void [mscorlib]System.Console::WriteLine(string)
    IL_0015: nop
    IL_0016: nop
    IL_0017: ldloc.1
    IL_0018: ldc.i4.1
    IL_0019: add
    IL_001a: stloc.1

    IL_001b: ldloc.1
    IL_001c: ldloc.0
    IL_001d: clt
    IL_001f: stloc.2
    IL_0020: ldloc.2
    IL_0021: brtrue.s IL_0008
// end loop

One thing I think the book might have done better regarding this subject is really explain what the compiler is doing, because all this "magic" makes sense if you understand that the compiler is wrapping the closed over variable in a new class.

Please correct any misconceptions or misunderstandings I might have. Also, feel free to elaborate on and/or add to my explanation.

richard
  • 12,263
  • 23
  • 95
  • 151
  • 1
    Capture does NOT ever create a new variable. It is the declaration of the counter variable that creates a new variable. No, they are not of the same scope and I believe that is what Jon is trying to show. Each loop counter goes out of scope, which in this case is a good thing. But index can't go out of scope, else you have nothing to increment. – Aron Nov 21 '13 at 10:47
  • Aron. I see what you mean. The question kind of answered itself...the new instance is created same as in every other situation. I really just was confused at first because I thought the `counter` and `index` were in the same scope until I looked at the IL. – richard Nov 21 '13 at 11:40
  • 2
    @RichardDesLonde: In addition to looking at the IL, consider looking at the language specification, which clearly states in section 3.7 "*The scope of a local variable declared in a for-initializer of a for statement is the for-initializer, the for-condition, the for-iterator, and the contained statement of the for statement.*" Clearly the scope of the variable declared inside the contained statement is the contained statement itself. And therefore the scopes are different; nested, yes, but different. – Eric Lippert Nov 21 '13 at 16:04
  • Thanks Eric. I think I need to bookmark the specification so I can quickly reference it. – richard Nov 21 '13 at 20:57
  • 1
    @RichardDesLonde: My recommendation is that you both download the Word document form, and get a copy of the annotated print specification "The C# Programming Language". There's a link in the sidebar of my blog, http://ericlippert.com. The annotated edition has interesting commentaries from me, Jon Skeet, Bill Wagner and many more. – Eric Lippert Nov 22 '13 at 23:38
  • Oh! That's a great idea. I would really like to read those commentaries. Thanks for the great suggestion! – richard Nov 23 '13 at 02:49

2 Answers2

1

It sounds like you've worked out the answer - you don't get a new index instance each time round the loop. If you consider the ways you're allowed to modify the index value inside the loop - eg. you can increment it if you want to skip items, set it back to zero in some cases, or anything else you like - it should be clear that you've only got one instance of index, not a new one for every iteration.

On the other hand, a new counter is created on every iteration - if you made a change to it at the bottom of that loop, it would have no effect on the counter variable that the next iteration uses.

foreach loops used to reuse their loop variable, in the same way as for loops, and this was a common gotcha for people - see Is there a reason for C#'s reuse of the variable in a foreach?

Eric Lippert explains that they've changed foreach in C# 5 to get a new variable every time, and also that they're leaving for as-is.

Community
  • 1
  • 1
Neil Vass
  • 5,251
  • 2
  • 22
  • 25
0

As far as I understood closures. It is the anonymous delegate which triggers the creation of a class for the variables involved in the delegates code block. Consider the following code:

class SomeClass
{
    public int counter;

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

//... 

List<ThreadStart> list = new List<ThreadStart>();

for (int index = 0; index < 5; index++)
{
    var instance  = new SomeClass { counter = index * 10 };
    list.Add(instance.DoSomething);
}

foreach (ThreadStart t in list)
{
    t();
}

This code does exactly the same as in the original example. The variable instance is defined inside of the for loop, so its scope ends at every iteration, however it is not freed by the garbage collector since it is referenced by list. This is the reason why a class is created in case of anonymous delegates. You cannot do it otherwise.

Daniel Leiszen
  • 1,827
  • 20
  • 39