如何|存储封闭变量的位置?

时间:2009-11-17 09:53:03

标签: c# linq clr closures

这是一个基于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的引用。但这并不能解释第一种情况。

2 个答案:

答案 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;
    }
}