为什么每个循环输出 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();
}
}
}
我希望它输出 0
到 9
,因为 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 指令中读取我的代码的方式。乍一看,关于您添加的委托,您必须了解两件事:
- 当您添加委托时,Add 中没有赋值,因为您不执行代码。
- 您的 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
代码是我评论的。但总之。
- 你的 i 不是 Main 方法的变量。它是您的委托使用的方法的 public 变量。
- 您的委托正在使用的方法在 Main 的私有嵌套 class 中。
- 我不知道 C# 编译器的内部结构,但这很有趣。如果您想亲眼看到它,我建议您使用 dnSpy。
编辑:@evk 更快 :P.
输出将是数字“10”十次。委托被添加到 for 循环中并存储对 i 变量的引用,而不是值本身。因此,在我们退出循环后,变量 i 已设置为“10”(循环中 i 的最后状态),并且在调用每个委托时,所有委托使用的值都是“10”。这种行为称为关闭。
我浏览了一些 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();
}
}
}
我希望它输出 0
到 9
,因为 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 指令中读取我的代码的方式。乍一看,关于您添加的委托,您必须了解两件事:
- 当您添加委托时,Add 中没有赋值,因为您不执行代码。
- 您的 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
代码是我评论的。但总之。
- 你的 i 不是 Main 方法的变量。它是您的委托使用的方法的 public 变量。
- 您的委托正在使用的方法在 Main 的私有嵌套 class 中。
- 我不知道 C# 编译器的内部结构,但这很有趣。如果您想亲眼看到它,我建议您使用 dnSpy。
编辑:@evk 更快 :P.
输出将是数字“10”十次。委托被添加到 for 循环中并存储对 i 变量的引用,而不是值本身。因此,在我们退出循环后,变量 i 已设置为“10”(循环中 i 的最后状态),并且在调用每个委托时,所有委托使用的值都是“10”。这种行为称为关闭。