I was browsing through some C# examples and came upon this:
using System;
using System.Collections.Generic;
namespace ConsoleApp1
{
class Program
{
delegate void Printer();
static void Main()
{
List<Printer> printers = new List<Printer>();
for (int i = 0; i < 10; i++)
{
printers.Add(delegate { var d = i; Console.WriteLine(d); });
}
foreach (var printer in printers)
{
printer();
}
Console.ReadLine();
}
}
}
I expected this to output 0
through 9
, since an int
is a value type and d
should be set to whatever i
is at that time.
However, this outputs 10
ten times.
Why is this? Is the int not a reference instead inside the delegate?
Note: I am not trying to solve a problem here, just understand how this works, in a way that is re-applicable.
Edit: Example of my confusion
int i = 9;
int d = 8;
d = i;
i++;
Console.WriteLine(d);
This would show that i
is passed as a value, and not a reference. I expected the same inside the closure, but was surprised.
Thanks for the comments, I understand more now, the code in the delegate isn't executed till after and it uses i
that exists outside of it in a generic class made by the compiler?
In javascript this same kind of code outputs 1-9, which is what I expected in C#. https://jsfiddle.net/L21xLaq0/2/.
I think that most answers are good and comments are good but i would suggest looking into decompiled code transformed into C#:
private static void Main()
{
List<Program.Printer> printers = new List<Program.Printer>();
int i;
int j;
for (i = 0; i < 10; i = j + 1)
{
printers.Add(delegate
{
int d = i;
Console.WriteLine(d);
});
j = i;
}
foreach (Program.Printer printer in printers)
{
printer();
}
Console.ReadLine();
}
It's how dnSpy read my code from IL Instructions. On the first glance there are 2 things that you must know about delegate that you added:
It's also worth looking at IL code of class that is auto generated to represent delegate. It will reveal completely what is being done under the hood:
.class nested private auto ansi sealed beforefieldinit '<>c__DisplayClass1_0'
extends [mscorlib]System.Object
{
.custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
01 00 00 00
)
// Fields
// Token: 0x04000005 RID: 5
.field public int32 i
// Methods
// Token: 0x06000007 RID: 7 RVA: 0x000020F4 File Offset: 0x000002F4
.method public hidebysig specialname rtspecialname
instance void .ctor () cil managed
{
// Header Size: 1 byte
// Code Size: 8 (0x8) bytes
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: nop
IL_0007: ret
} // end of method '<>c__DisplayClass1_0'::.ctor
// Token: 0x06000008 RID: 8 RVA: 0x00002100 File Offset: 0x00000300
.method assembly hidebysig
instance void '<Main>b__0' () cil managed
{
// Header Size: 12 bytes
// Code Size: 16 (0x10) bytes
// LocalVarSig Token: 0x11000002 RID: 2
.maxstack 1
.locals init (
[0] int32 d
)
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldfld int32 ConsoleApp1.Program/'<>c__DisplayClass1_0'::i
IL_0007: stloc.0
IL_0008: ldloc.0
IL_0009: call void [mscorlib]System.Console::WriteLine(int32)
IL_000E: nop
IL_000F: ret
} // end of method '<>c__DisplayClass1_0'::'<Main>b__0'
} // end of class <>c__DisplayClass1_0
Code is very long but it is worth noting that this class has public int field inside.
.field public int32 i
It's getting interesting at this point :P.
You can also see a constructor that does nothing. There is no assignment or whatever else when object is created. Nothing special excluding creating Object is done.
When you print your variable you are accessing public field inside your delegate which is i.
ldfld int32 ConsoleApp1.Program/'<>c__DisplayClass1_0'::i
Now you should scratch your head and do not know what is going on anymore because we did not assign i inside this private class. But this i field is public and it is being modified inside Program's main method.
.method private hidebysig static
void Main () cil managed
{
// Header Size: 12 bytes
// Code Size: 136 (0x88) bytes
// LocalVarSig Token: 0x11000001 RID: 1
.maxstack 3
.entrypoint
.locals init (
[0] class [mscorlib]System.Collections.Generic.List`1<class ConsoleApp1.Program/Printer> printers,
[1] class ConsoleApp1.Program/'<>c__DisplayClass1_0' 'CS$<>8__locals0', //There is only one variable of your class that has method that is going to be invoked. You do not have 10 unique methods.
[2] int32,
[3] bool,
[4] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class ConsoleApp1.Program/Printer>,
[5] class ConsoleApp1.Program/Printer printer
)
IL_0007: newobj instance void ConsoleApp1.Program/'<>c__DisplayClass1_0'::.ctor() //your class that is going to be used by delegate is created here
IL_000C: stloc.1 //and stored in local variable at index 1
/*(...)*/
IL_000E: ldc.i4.0 //we are putting 0 on stack
IL_000F: stfld int32 ConsoleApp1.Program/'<>c__DisplayClass1_0'::i //and assign 0 to variable i which is inside this private class.
// loop start (head: IL_003B)
/*(...)*/
IL_0019: ldftn instance void ConsoleApp1.Program/'<>c__DisplayClass1_0'::'<Main>b__0'() //It push pointer to the main function of our private nested class on the stack.
IL_001F: newobj instance void ConsoleApp1.Program/Printer::.ctor(object, native int) //We create new delegate which will be pointing on our local DisplayClass_1_0
IL_0024: callvirt instance void class [mscorlib]System.Collections.Generic.List`1<class ConsoleApp1.Program/Printer>::Add(!0) //We are adding delegate
/* (...) */
IL_002C: ldfld int32 ConsoleApp1.Program/'<>c__DisplayClass1_0'::i //loads i from our local private class into stack
IL_0031: stloc.2 //and put it into local variable 2
IL_0033: ldloc.2 //puts local variable at index 2 on the stack
IL_0034: ldc.i4.1 // nputs 1 onto stack
IL_0035: add //1 and local varaible 2 are being add and value is pushed on the evaluation stack
IL_0036: stfld int32 ConsoleApp1.Program/'<>c__DisplayClass1_0'::i //we are replacing i in our instance of our private class with value that is result of addition one line before.
//This is very weird way of adding 1 to i... Very weird. Maybe there is a reason behind that
/* (...) */
// end loop
/* (...) */
.try
{
/* (...) */
// loop start (head: IL_0067)
/* (...) */
IL_0056: call instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class ConsoleApp1.Program/Printer>::get_Current() //gets element from list at current iterator position
/* (...) */
IL_0060: callvirt instance void ConsoleApp1.Program/Printer::Invoke() //Invokes your delegate.
/* (...) */
// end loop
IL_0070: leave.s IL_0081
} // end .try
finally
{
/* (...) */
} // end handler
IL_0081: call string [mscorlib]System.Console::ReadLine()
/* (...) */
} // end of method Program::Main
Code is commented by me. But in short.
edit: @evk was faster :P.