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

Viewed 1623

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

5 Answers

You may be interested to see how this code is actually rewritten by compiler, because it helps to understand what's going on. If you compile and then view dll in some decompiler (like dotPeek) with disabled "fancy view", you will see this (some names are changed because they are not readable):

class Program {
    delegate void Printer();

    private static void Main() {
        List<Program.Printer> printerList = new List<Program.Printer>();
        // closure object which holds captured variables
        Program.DisplayClass10 cDisplayClass10 = new Program.DisplayClass10();
        int i;
        // loop assigns field of closure object
        for (cDisplayClass10.i = 0; cDisplayClass10.i < 10; cDisplayClass10.i = i + 1) {
            // your delegate is method of closure object
            printerList.Add(new Program.Printer(cDisplayClass10.CrypticFunctionName));
            i = cDisplayClass10.i;
        }
        // here, cDisplayClass10.i is 10
        foreach (Program.Printer printer in printerList)
            printer();
        Console.ReadLine();
    }

    // class for closure object
    [CompilerGenerated]
    private sealed class DisplayClass10 {
        public int i;

        internal void CrypticFunctionName() {
            Console.WriteLine(this.i);
        }
    }
}

What you have is a closure. It's when you make an anonymous function and use locally-scoped variables in it.

It doesn't make a copy of those variables. It uses those variables. Since you're increased i all the way up to 10, those anonymous functions will run using the same variable i.

If you want it to actually count to 10, you can make a new variable for the closure.

var j = i;
printers.Add(delegate { var d = j; Console.WriteLine(d); });

See this question for more info: What are 'closures' in .NET?

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.

Here, Delegate was added 10 times and reference to the variable i is taken. when calling a delegate - it is considering the last value of i which would be 10 after the for loop. For More information, check it out closure.

The output will be the number "10" ten times. The delegate is added in the for loop and reference to i variable is stored, rather than the value itself. So, after we exit the loop, the variable i has been set to "10" (last state of i in the loop) and by the time each delegate is invoked, the value used by all of them is "10". This behavior known as closure.

Related