闭包变量存储在哪里?

9

这是一篇关于Eric Lippert的文章"不要在循环变量上使用闭包"。阅读起来很有意思,Eric解释了为什么在这段代码之后,所有的函数都会返回v中最后一个值:

 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变量都将占用同一块内存。
我最初的想法是装箱,每个函数成员保留对装箱v2的引用。但这不能解释第一个情况。

好的,在阅读了我的问题后,我认为可以这样解释:在第一个版本中,v 被装箱一次并且引用被重复使用。但是我想看到一个更权威的答案。 - H H
2个回答

6
通常情况下,变量v2会在代码块开始时在堆栈上分配一些空间。在代码块结束时(即迭代结束时),堆栈会被回卷(我描述的是逻辑情况,而不是优化后的实际行为)。因此,每个v2实际上都是与上一次迭代不同的v2,尽管它最终会占用相同的内存位置。
然而,编译器发现v2被lambda创建的匿名函数使用。编译器所做的是提升v2变量。编译器创建一个新的类,其中包含一个Int32字段来保存v2的值,它没有在堆栈上分配位置。它还将匿名函数作为这个新类型的方法。(为简单起见,我将这个无名类称为“Thing”)。
在每次迭代中,都会创建一个“Thing”的新实例,并且当给v2赋值时,实际上是分配了Int32字段,而不仅仅是堆栈内存中的指针。匿名函数表达式(lambda)现在将返回一个委托,该委托具有非空实例对象引用,此引用将指向“Thing”的当前实例。
当调用匿名函数的委托时,它将作为“Thing”实例的实例方法执行。 因此,v2可用作成员字段,并且将具有在创建此“Thing”实例期间赋予它的值。

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接