1

this below example print only 10 for ten times. could you please tell me whats going in background.

 public delegate void DoSomething();

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();
}

accroding to me it should print 0 To 9, but it print 10,10,10,10,10,10,10,10,10,10

Dalip Choudhary
  • 546
  • 5
  • 18
  • try this while (iCnt < 10) { var value = iCnt; lstOfDelegate.Add(delegate { Console.WriteLine(value); }); iCnt++; } – Vivek Nuna Oct 16 '16 at 13:35
  • I have expanded my answer to add details about `whats going in background`. It should be noted the question to which this was marked as duplicate was asking for a workaround not for an explanation of what is going on. – Theraot Oct 16 '16 at 14:57

2 Answers2

2

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
Community
  • 1
  • 1
Theraot
  • 31,890
  • 5
  • 57
  • 86
1

That's because when you add the delegates to list it doesn't capture the value of the iCnt variable at the time you have added it to the list. When you invoke the delegates finally it evaluates the value of the iCnt variable at that time which is now 10 and hence your result.

Think of it as your stored delegate keeping a pointer to the iCnt variable (not exactly true but helps to picture it this way). Only when you invoke the delegate it goes to the pointed location, gets the value of iCnt and uses it .

Nikhil
  • 3,304
  • 1
  • 25
  • 42