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

Why does this output 10 for each loop? Isn't int a value 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 不是引用而是在委托内部吗?

注意:我不是要在这里解决问题,只是了解它是如何工作的,以一种可重复应用的方式。

编辑:我的困惑示例

int i = 9;
int d = 8;
d = i;
i++;
Console.WriteLine(d);

这表明 i 是作为值而不是引用传递的。我在闭包内部也有同样的预期,但很惊讶。


感谢您的评论,我现在了解得更多了,委托中的代码直到之后才执行,它使用 i 存在于它之外的通用 class 中编译器?

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

你拥有的是一个闭包。这是当你创建一个匿名函数并在其中使用局部范围的变量时。

它不会复制这些变量。它使用这些变量。由于您一直 i 增加到 10,这些匿名函数将 运行 使用相同的变量 i.

如果你想让它实际数到10,你可以为闭包创建一个新变量。

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

查看此问题了解更多信息: What are 'closures' in .NET?

在这里,Delegate 被添加了 10 次,并且引用了变量 i。调用委托时 - 它正在考虑 i 的最后一个值,即 for 循环之后的 10。有关更多信息,请查看闭包。

您可能有兴趣看看这段代码实际上是如何被编译器重写的,因为它有助于理解发生了什么。如果你编译然后在一些反编译器(比如 dotPeek)中查看 dll 并禁用 "fancy view",你会看到这个(一些名称被更改因为它们不可读):

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

我认为大多数答案都很好,评论也很好,但我建议查看转换为 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 已移出 for 循环。因此它可用于委托。

还值得查看 class 的 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

代码很长但是值得注意的是这个class里面有public int字段

.field public int32 i

此时开始变得有趣了:P.

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

当您打印变量时,您正在访问委托中的 public 字段,即 i.

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

现在你应该挠头了,不知道发生了什么,因为我们没有在这个私有class中分配我。但是这个 i 字段是 public 并且它正在 Program 的 main 方法中被修改。

.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 方法的变量。它是您的委托使用的方法的 public 变量。
  2. 您的委托正在使用的方法在 Main 的私有嵌套 class 中。
  3. 我不知道 C# 编译器的内部结构,但这很有趣。如果您想亲眼看到它,我建议您使用 dnSpy。

编辑:@evk 更快 :P.

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