我正在浏览一些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 i = 9;
int d = 8;
d = i;
i++;
Console.WriteLine(d);
十次。
这是为什么?在委托中,int不是引用吗?
注意:我不是要在这里解决问题,只是以可重用的方式了解其工作原理。
编辑:我的困惑示例
i
这表明i
作为值传递,而不是引用。我预计关闭内部也会如此,但很惊讶。
感谢您的评论,我现在更了解,代理中的代码直到之后才执行,并且在编译器生成的泛型类中使用了{{1}}之外的代码?
在javascript中,这种类型的代码输出1-9,这是我在C#中所期望的。 https://jsfiddle.net/L21xLaq0/2/
答案 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指令中读取我的代码的。乍一看,您必须了解有关您添加的委托的两件事:
同样值得查看自动生成以表示委托的类的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
代码由我评论。但简而言之。
编辑:@evk更快:P。
答案 3 :(得分:0)
这里,代表被添加了10次,并引用了变量i。当调用委托时 - 它正在考虑i的最后一个值,它在for循环之后将是10。有关更多信息,请查看结束。
答案 4 :(得分:-1)
输出将是数字“ 10”的十倍。将委托添加到for循环中,并存储对i变量的引用,而不是值本身。因此,在退出循环之后,变量i已设置为“ 10”(循环中i的最后状态),并且在调用每个委托时,所有变量所使用的值为“ 10”。这种行为称为闭包。