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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With