为什么每个循环输出10?不是int值类型?

时间:2017-11-17 22:23:42

标签: c# value-type reference-type

我正在浏览一些C#示例并且发现了这个:

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

我希望这会输出09,因为int是值类型,而d应设置为i所在的值10时间。

然而,这会导致int i = 9; int d = 8; d = i; i++; Console.WriteLine(d); 十次。

这是为什么?在委托中,int不是引用吗?

注意:我不是要在这里解决问题,只是以可重用的方式了解其工作原理。

编辑:我的困惑示例

i

这表明i作为值传递,而不是引用。我预计关闭内部也会如此,但很惊讶。

感谢您的评论,我现在更了解,代理中的代码直到之后才执行,并且在编译器生成的泛型类中使用了{{1}}之外的代码?

在javascript中,这种类型的代码输出1-9,这是我在C#中所期望的。 https://jsfiddle.net/L21xLaq0/2/

5 个答案:

答案 0 :(得分:3)

您可能有兴趣了解编译器是如何实际重写此代码的,因为它有助于了解正在发生的事情。如果你编译然后在一些带有禁用“花式视图”的反编译器(如dotPeek)中查看dll,你会看到这个(某些名称因为它们不可读而被更改):

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

答案 1 :(得分:2)

你拥有的是一个封闭物。在您创建匿名函数并在其中使用本地范围的变量时。

它没有复制这些变量。它使用那些变量。由于您将i一直增加到10,因此这些匿名函数将使用相同的变量i运行。

如果您希望它实际计数到10,您可以为闭包创建一个新变量。

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

有关详细信息,请参阅此问题: What are 'closures' in .NET?

答案 2 :(得分:1)

我认为大多数答案都很好并且评论很好,但我建议将反编译代码转换为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();
        }

dnSpy是如何从IL指令中读取我的代码的。乍一看,您必须了解有关您添加的委托的两件事:

  1. 添加委托时,Add中没有赋值,因为您没有执行代码。
  2. 你的int被移到外面循环。因此,它可供代表使用。
  3. 同样值得查看自动生成以表示委托的类的IL代码。它将彻底揭示引擎盖下的内容:

    .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
    

    代码很长但值得注意的是这个类里面有公共int字段。

    .field public int32 i
    

    此时此刻变得有趣:P。

    您还可以看到一个什么都不做的构造函数。创建对象时没有任何赋值或其他任何内容。除了创建Object之外没什么特别的。

    当您打印变量时,您正在访问代理中的公共字段,即i。

    ldfld     int32 ConsoleApp1.Program/'<>c__DisplayClass1_0'::i
    

    现在你应该挠头,不知道发生了什么,因为我们没有在这个私人课程中分配我。但是这个i字段是公开的,它在Program的主要方法中被修改。

    .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
    

    代码由我评论。但简而言之。

    1. 您的i不是Main方法的变量。它是您的代理使用的方法的公共变量。
    2. 您的委托使用的方法位于Main的私有嵌套类中。
    3. 我不知道C#编译器的内部结构,但这很有趣。如果你想亲眼看看,我推荐使用dnSpy。
    4. 编辑:@evk更快:P。

答案 3 :(得分:0)

这里,代表被添加了10次,并引用了变量i。当调用委托时 - 它正在考虑i的最后一个值,它在for循环之后将是10。有关更多信息,请查看结束。

答案 4 :(得分:-1)

输出将是数字“ 10”的十倍。将委托添加到for循环中,并存储对i变量的引用,而不是值本身。因此,在退出循环之后,变量i已设置为“ 10”(循环中i的最后状态),并且在调用每个委托时,所有变量所使用的值为“ 10”。这种行为称为闭包。