Search code examples
c#value-typereference-type

Why does this output 10 for each loop? Isn't int a value type?


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/.


Solution

  • 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:

    1. When you add delegate there is no assignment inside Add because you do not execute code.
    2. Your int is moved outside for loop. Because of that it is available for use for delegate.

    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.

    1. Your i is not an variable of Main method. It is public variable of a method that your delegate use.
    2. Method that is being used by your delegate is inside private nested class in Main.
    3. I do not know internals of C# compiler but this was pretty interesting to see. If you want to see it with your own eyes i recommend using dnSpy.

    edit: @evk was faster :P.