3

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

Douglas Gaskell
  • 9,017
  • 9
  • 71
  • 128
  • 1
    what do you expect..? you are starting the counter at 0, C# is `Zero based` if you write out `0,1,2,3,4,5,6,7,8,9` how many values are there.. `9` or `10`..? – MethodMan Nov 17 '17 at 22:26
  • `d` *is* set to whatever `i` is at that time. "That time" is when the delegate is invoked though, not when the delegate is created. –  Nov 17 '17 at 22:27
  • not an expert in c# but I think you've pointed the problem: `d` is equal to `i` at the time it is executed. And because of the `delegate`, `d=i` is executed after the loop has finished. – ValLeNain Nov 17 '17 at 22:27
  • 4
    `i` is captured, not `d`. You need to move `d` outside the delegate. – Blorgbeard Nov 17 '17 at 22:27
  • @MethodMan I don't think you understood the question. I'm asking why `d` is not the value of `i` when the delegate is made, but instead the value of `i` when the delegates are invoked. I would have thought `d` would be set to the value of `i`, and not reference it since `i` is a value type – Douglas Gaskell Nov 17 '17 at 22:29
  • There are a lot of questions about this here, for example: https://stackoverflow.com/q/271440/5311735 – Evk Nov 17 '17 at 22:29
  • 1
    @DouglasGaskell because of captured variables, this code is compiled not the way you might expect. Because `i` is captured - it goes to the field of compiler-generated class which is then used in delegate. So `var d = i` is really `var d = generatedClass.i`. This field is modified by loop so when you invoke delegates - it has value 10. Whether it's value type or not is not relevant. – Evk Nov 17 '17 at 22:36
  • @Evk Ah! Thank you, that's exactly what I'm looking for, none of the explanations that I've been finding explain it. They just end up saying "Because that's how it is" but with more words. – Douglas Gaskell Nov 17 '17 at 22:42
  • Possible duplicate of [Captured variable in a loop in C#](https://stackoverflow.com/questions/271440/captured-variable-in-a-loop-in-c-sharp) – user4003407 Nov 17 '17 at 22:48
  • In javascript it is\was the same by the way (unless you use `let`): https://stackoverflow.com/q/750486/5311735 – Evk Nov 17 '17 at 22:59
  • Ah, I used let instead of var, mainly because the `var` leaks it's scope outside the block it's in afaik. – Douglas Gaskell Nov 17 '17 at 23:00
  • Accepted answer to that question says that "IE9-IE11 and Edge prior to Edge 14" does this wrong even with `let`, so beware. Well, it's javascript, you should be cautios there at everything. – Evk Nov 17 '17 at 23:02

5 Answers5

5

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);
        }
    }
}
Evk
  • 98,527
  • 8
  • 141
  • 191
  • This is perfect, and actually describes what is happening, and why this same code behaves differently in different languages even if they use similar value and reference types. – Douglas Gaskell Nov 17 '17 at 22:56
3

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?

  • Sam, `int i = 9;int d = 8;d = i;i++;Console.WriteLine(d);` shows that i is passed as a value, which is where my confusion sets in. For the closures at first glance it seemed like it was using a pointer to `i` instead? – Douglas Gaskell Nov 17 '17 at 22:46
  • @DouglasGaskell `i` is not even passed into the function as a parameter. – Sam I am says Reinstate Monica Nov 17 '17 at 22:48
  • It's because the assignment to d from I happens in the delegate (the closure) that bit of code is actually only executed when you called the delegates in the second loop at that point the state object that was created for the closure has a property called i that is now 10 and remains 10 – Dave Nov 17 '17 at 22:49
  • @DouglasGaskell `var d = i` is executed when you call `printer();` Notice how you are not calling like `printer(i);`. – Sam I am says Reinstate Monica Nov 17 '17 at 22:51
  • 1
    @Dave It just clicked, wow. Of course that makes plenty of sense now.. I was thinking in javascript. Where this does output 0-9 – Douglas Gaskell Nov 17 '17 at 22:52
  • @DouglasGaskell [No, It just outputs 10's in javascript](https://jsfiddle.net/2mrfh2cw/). In fact, closures are even more used in javascript than in c# – Sam I am says Reinstate Monica Nov 17 '17 at 22:56
  • @SamIam I used `let` https://jsfiddle.net/L21xLaq0/2/ which is more like a field in C# afaik as it maintains it's scope. – Douglas Gaskell Nov 17 '17 at 22:57
1

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.

Petter Hesselberg
  • 5,062
  • 2
  • 24
  • 42
Shoter
  • 976
  • 11
  • 23
0

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.

HelloWorld
  • 63
  • 1
  • 8
-1

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.