【问题标题】:How|Where are closed-over variables stored?如何|封闭变量存储在哪里?
【发布时间】:2010-12-17 09:23:30
【问题描述】:

这是基于 Eric Lippert 的文章 "Closing over the loop variable considered harmful" 提出的问题。
这是一本好书,Eric 解释了为什么在这段代码之后所有 func 都会返回 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 的引用。但这并不能解释第一种情况。

【问题讨论】:

  • 好的,在阅读了我自己的问题后,我想它会被解释为:在第一个版本中,v 被装箱一次,并且引用被重新使用。但我希望看到更权威的答案。

标签: c# linq clr closures


【解决方案1】:

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

【讨论】:

    【解决方案2】:

    通常变量v2 会在它所在的代码块的开头分配一些空间。在代码块的末尾(即迭代结束),堆栈回退(我'正在描述逻辑场景而不是优化的实际行为)。因此,每个v2 实际上是与前一次迭代不同的v2,尽管它最终会占用相同的内存位置。

    但是,编译器发现 v2 正被 lambda 创建的匿名函数使用。编译器所做的是hoist v2 变量。编译器创建一个新类,该类具有一个 Int32 字段来保存 v2 的值,它没有在堆栈上分配一个位置。它还使匿名函数成为这种新类型的方法。 (为简单起见,我会给这个未命名的类起一个名字,我们称之为“Thing”)。

    现在,在 每个 迭代中,会创建一个 new “Thing”实例,当 v2 被分配时,它的 Int32 字段实际上被分配的不仅仅是一个点堆栈内存。匿名函数表达式(lambda)现在将返回一个具有非空实例对象引用的委托,该引用将指向“Thing”的当前实例

    当调用匿名函数的委托时,它将作为“事物”实例的实例方法执行。因此v2 可用作成员字段,并且在迭代期间将具有赋予它的值,这个“Thing”实例被创建。

    【讨论】:

      猜你喜欢
      • 2017-03-26
      • 2010-10-02
      • 2018-02-01
      • 2011-04-11
      • 2010-09-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多