Logic
This is a common gotcha of delegates, it easy actually, when you say:
item.Invoke();
The delegate will run, and that delegate is:
Console.WriteLine(iCnt);
And what's the value of iCnt
at that moment? well it is 10
because you incremented it until it reached that value. So it prints 10
.
And since you will call 10 of those delegates, and they all print 10
, you get 10
10 times.
You could make it use the value of iCnt
on the iteration where the delegate was created using another variable:
while (iCnt < 10)
{
var value = iCnt;
lstOfDelegate.Add(delegate { Console.WriteLine(value); });
iCnt++;
}
Now, that variable never changes value, and the delegates should work as expected.
Behind the scenes
The implementation behind the scenes is using a hidden anonymous class, that has iCnt
as a field. Then { Console.WriteLine(iCnt); }
is created as an anonymous method of that class. On runtime, a delegate is created pointing to an instance of the anonymous class, and with a pointer to the anonymous method.
I created the code for an equivalent full program (below) and compiled it using Roslyn to see the .IL it generated.
C# code:
using System;
using System.Collections.Generic;
public class Program
{
public delegate void DoSomething();
public static void Main(string[] args)
{
List<DoSomething> lstOfDelegate = new List<DoSomething>();
int iCnt = 0;
while (iCnt < 10)
{
lstOfDelegate.Add(delegate { Console.WriteLine(iCnt); });
iCnt++;
}
foreach (var item in lstOfDelegate)
{
item.Invoke();
}
Console.ReadLine();
}
}
What follows is a reinterpretation of the IL (this code won't work but shows the way the compiler writes the code, added comments):
public class Program
{
/* This is the delegate class, it uses runtime code, not IL */
public class DoSomething : System.MulticastDelegate
{
public DoSomething(object object, int method) { /*...*/ }
public override void Invoke() { /*...*/ }
public override void BeginInvoke() { /*...*/ }
Public override void EndInvoke() { /*...*/ }
}
/* This is the hidden anonymous class,
the system refers to it as '<>c__DisplayClass1_0'.
notice it contains iCnt. */
[CompilerGenerated()]
private class '<>c__DisplayClass1_0'
{
/* iCnt was moved to here by the compiler. */
public int iCnt;
public '<>c__DisplayClass1_0'() { /* Default constructor */ }
/* This is the method the delegate invokes.*/
internal '<Main>b__0'()
{
Console.WriteLine(this.iCnt);
}
}
public static void Main(string[] args)
{
'<>c__DisplayClass1_0' var0; // A reference to the anonymous class
List<DoSomething> var1; // lstOfDelegate
int var2; // temp variable for the increment
bool var3; // temp variable for the while conditional
List<DoSomething>.Enumerator var4; // enumerator, used by foreach
DoSomething var5; // temp variable for the delegate
// Instantiate the anonymous class
// As you can see, there is only one instance,
// so there is only one iCnt
var0 = new '<>c__DisplayClass1_0'();
// List<DoSomething> lstOfDelegate = new List<DoSomething>();
var1 = new List<DoSomething>();
// int iCnt = 0;
var0.iCnt = 0;
goto IL_003b; // while (iCnt < 10) {
IL_0016:
// lstOfDelegate.Add(delegate { Console.WriteLine(iCnt); });
var1.add(new DoSomething(var0, funcPtr('<>c__DisplayClass1_0'.'<Main>b__0')));
// iCnt++;
var2 = var0.iCnt;
var0.iCnt = var2 + 1;
IL_003b:
var3 = var0.iCnt < 10;
if (var3) goto IL_0016; // }
var4 = var1.GetEnumerator();
goto IL_0067; // foreach (var item in lstOfDelegate) {
try {
IL_0054:
var5 = var4.Current;
var5.Invoke();
IL_0067:
if (var4.MoveNext()) goto IL_0054;
} finally {
var4.Dispose();
}
Console.ReadLine();
}
public Program() { /* Default constructor */ }
}
Notes:
- As you can see all the delegates added to the list are actually the same.
- When adding a new variable in the loop, it changes the semantics to having a new variable each iteration. And then the compiler generates code to instantiate a new
'<>c__DisplayClass1_0'
on each iteration, instead of once at the start of Main
.
- The implementation of the methods of DoSomething is not in IL.
- You will notice the loops are reversed, that's up to the compiler.
funcPtr
is representing the OpCode ldftn
, it gets an int that the runtime uses as pointer to a method. There is no direct equivalent in C#.
- The try-finally block in .NET is declarative, I added it on the position where it belongs. In .NET entering a try block is no actual instruction.
- You can also see the structure of a foreach loop in the code.
- The generated code uses full name qualification always, I removed it for readability.
Hopefully reading the code above clears any doubts. Just in case, I have added the IL generated by Roslyn below:
.class private auto ansi '<Module>'
{
} // end of class <Module>
.class public auto ansi beforefieldinit Program
extends [mscorlib]System.Object
{
// Nested Types
.class nested public auto ansi sealed DoSomething
extends [mscorlib]System.MulticastDelegate
{
// Methods
.method public hidebysig specialname rtspecialname
instance void .ctor (
object 'object',
native int 'method'
) runtime managed
{
} // end of method DoSomething::.ctor
.method public hidebysig newslot virtual
instance void Invoke () runtime managed
{
} // end of method DoSomething::Invoke
.method public hidebysig newslot virtual
instance class [mscorlib]System.IAsyncResult BeginInvoke (
class [mscorlib]System.AsyncCallback callback,
object 'object'
) runtime managed
{
} // end of method DoSomething::BeginInvoke
.method public hidebysig newslot virtual
instance void EndInvoke (
class [mscorlib]System.IAsyncResult result
) runtime managed
{
} // end of method DoSomething::EndInvoke
} // end of class DoSomething
.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
.field public int32 iCnt
// Methods
.method public hidebysig specialname rtspecialname
instance void .ctor () cil managed
{
// Method begins at RVA 0x20f4
// Code size 8 (0x8)
.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
.method assembly hidebysig
instance void '<Main>b__0' () cil managed
{
// Method begins at RVA 0x20fd
// Code size 14 (0xe)
.maxstack 8
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldfld int32 Program/'<>c__DisplayClass1_0'::iCnt
IL_0007: call void [mscorlib]System.Console::WriteLine(int32)
IL_000c: nop
IL_000d: ret
} // end of method '<>c__DisplayClass1_0'::'<Main>b__0'
} // end of class <>c__DisplayClass1_0
// Methods
.method public hidebysig static
void Main (
string[] args
) cil managed
{
// Method begins at RVA 0x2050
// Code size 136 (0x88)
.maxstack 3
.locals init (
[0] class Program/'<>c__DisplayClass1_0',
[1] class [mscorlib]System.Collections.Generic.List`1<class Program/DoSomething>,
[2] int32,
[3] bool,
[4] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class Program/DoSomething>,
[5] class Program/DoSomething
)
IL_0000: newobj instance void Program/'<>c__DisplayClass1_0'::.ctor()
IL_0005: stloc.0
IL_0006: nop
IL_0007: newobj instance void class [mscorlib]System.Collections.Generic.List`1<class Program/DoSomething>::.ctor()
IL_000c: stloc.1
IL_000d: ldloc.0
IL_000e: ldc.i4.0
IL_000f: stfld int32 Program/'<>c__DisplayClass1_0'::iCnt
IL_0014: br.s IL_003b
IL_0016: nop
IL_0017: ldloc.1
IL_0018: ldloc.0
IL_0019: ldftn instance void Program/'<>c__DisplayClass1_0'::'<Main>b__0'()
IL_001f: newobj instance void Program/DoSomething::.ctor(object, native int)
IL_0024: callvirt instance void class [mscorlib]System.Collections.Generic.List`1<class Program/DoSomething>::Add(!0)
IL_0029: nop
IL_002a: ldloc.0
IL_002b: ldfld int32 Program/'<>c__DisplayClass1_0'::iCnt
IL_0030: stloc.2
IL_0031: ldloc.0
IL_0032: ldloc.2
IL_0033: ldc.i4.1
IL_0034: add
IL_0035: stfld int32 Program/'<>c__DisplayClass1_0'::iCnt
IL_003a: nop
IL_003b: ldloc.0
IL_003c: ldfld int32 Program/'<>c__DisplayClass1_0'::iCnt
IL_0041: ldc.i4.s 10
IL_0043: clt
IL_0045: stloc.3
IL_0046: ldloc.3
IL_0047: brtrue.s IL_0016
IL_0049: nop
IL_004a: ldloc.1
IL_004b: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> class [mscorlib]System.Collections.Generic.List`1<class Program/DoSomething>::GetEnumerator()
IL_0050: stloc.s 4
IL_0052: br.s IL_0067
IL_0054: ldloca.s 4
IL_0056: call instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class Program/DoSomething>::get_Current()
IL_005b: stloc.s 5
IL_005d: nop
IL_005e: ldloc.s 5
IL_0060: callvirt instance void Program/DoSomething::Invoke()
IL_0065: nop
IL_0066: nop
IL_0067: ldloca.s 4
IL_0069: call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class Program/DoSomething>::MoveNext()
IL_006e: brtrue.s IL_0054
IL_0070: leave.s IL_0081
IL_0072: ldloca.s 4
IL_0074: constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<class Program/DoSomething>
IL_007a: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_007f: nop
IL_0080: endfinally
IL_0081: call string [mscorlib]System.Console::ReadLine()
IL_0086: pop
IL_0087: ret
Try IL_0052-IL_0072 Finally IL_0072-IL_0081
} // end of method Program::Main
.method public hidebysig specialname rtspecialname
instance void .ctor () cil managed
{
// Method begins at RVA 0x20f4
// Code size 8 (0x8)
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: nop
IL_0007: ret
} // end of method Program::.ctor
} // end of class Program