这是一个基于Eric Lippert的文章"Closing over the loop variable considered harmful"的问题 这是一个很好的阅读,Eric解释了为什么在这段代码之后所有的funcs都将返回v中的 last 值:
var funcs = new List<Func<int>>();
foreach (var v in values)
{
funcs.Add(() => v);
}
正确的版本如下:
foreach (var v in values)
{
int v2 = v;
funcs.Add(() => v2);
}
现在我的问题是如何以及在何处存储捕获的'v2'变量。在我对堆栈的理解中,所有这些v2变量都会占用同一块内存。
我的第一个想法是拳击,每个func成员保持对盒装v2的引用。但这并不能解释第一种情况。
答案 0 :(得分:6)
通常,变量v2
将在其找到的代码块的开头在堆栈上分配一些空间。在代码块的末尾(即迭代结束),堆栈被回收(我描述的逻辑场景不是优化的实际行为)。因此,每个v2
实际上与前一次迭代的v2
不同,尽管它最终会占用相同的内存位置。
然而,编译器发现lambda创建的匿名函数正在使用v2
。编译器的作用是hoist v2
变量。编译器创建一个新类,其具有Int32字段以保存v2
的值,它不会在堆栈上分配一个位置。它还使匿名函数成为这种新类型的方法。 (为简单起见,我将给这个未命名的类命名,我们称之为“Thing”)。
现在,在每次迭代中,会创建新“Thing”实例,并且当v2
被分配时,其Int32字段实际上不仅仅是指向堆栈内存。匿名函数表达式(lambda)现在将返回一个具有非null实例对象引用的委托,此引用将指向“Thing”的当前实例。
当调用匿名函数的委托时,它将作为“Thing”实例的实例方法执行。因此v2
可用作成员字段,并且在迭代期间将为其创建“Thing”实例的值。
答案 1 :(得分:4)
除了Neil和Anthony的答案之外,这里是两个案例中可能自动生成的代码示例。
(注意,这只是为了演示原理,实际的编译器生成的代码看起来不完全如此。如果你想看到真正的代码,那么你可以使用Reflector来看看。)
// first loop
var captures = new Captures();
foreach (var v in values)
{
captures.Value = v;
funcs.Add(captures.Function);
}
// second loop
foreach (var v in values)
{
var captures = new Captures();
captures.Value = v;
funcs.Add(captures.Function);
}
// ...
private class Captures
{
public int Value;
public int Function()
{
return Value;
}
}