封闭变量如何|在哪里存储?
这是基于文章 “关闭被认为有害的循环变量” 作者:Eric Lippert。
这是一本很好的书,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 的引用。但这并不能解释第一种情况。
This is a question based on the article "Closing over the loop variable considered harmful" by Eric Lippert.
It is a good read, Eric explains why after this piece of code all funcs will return the last value in v:
var funcs = new List<Func<int>>();
foreach (var v in values)
{
funcs.Add(() => v);
}
And the correct version looks like:
foreach (var v in values)
{
int v2 = v;
funcs.Add(() => v2);
}
Now my question is how and where are those captured 'v2' variables stored. In my understanding of the stack, all those v2 variables would occupy the same piece of memory.
My first thought was boxing, each func member keeping a reference to a boxed v2. But that would not explain the first case.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
通常,变量
v2
会在其所在代码块的开头在堆栈上分配一些空间。在代码块的末尾(即迭代结束),堆栈会回退(我描述的是逻辑场景,而不是优化的实际行为)。因此,每个v2
实际上是与上一次迭代不同的v2
,尽管它最终会占用相同的内存位置。然而,编译器发现
v2
正在被 lambda 创建的匿名函数使用。编译器的作用是 提升v2
变量。编译器创建一个新类,它有一个 Int32 字段来保存v2
的值,但它没有在堆栈上分配位置。它还使匿名函数成为这种新类型的方法。 (为了简单起见,我将为这个未命名的类命名,我们称之为“Thing”)。现在,在每次迭代中,都会创建“Thing”的新实例,并且当为
v2
分配其Int32字段时,该字段实际上被分配的不仅仅是一个堆栈内存中的点。匿名函数表达式(lambda)现在将返回一个具有非空实例对象引用的委托,该引用将指向“Thing”的当前实例。当调用匿名函数的委托时,它将作为“Thing”实例的实例方法执行。因此,
v2
可用作成员字段,并且将在创建“Thing”实例的迭代期间为其赋予值。Ordinarily the variable
v2
would be allocated some space on the stack at the start of the code block its found in. At the end of the code block (i.e. the end of the iteration) the stack is wound back (I'm describing the logical scenario not an optimised actual behaviour). Hence eachv2
is in effect a differentv2
from the previous iteration although its true that it would end up occupying the same memory location.However the compiler spots that
v2
is being used by an anonymous function created by the lambda. What the compiler does is hoist thev2
variable. The compiler creates a new class that has an Int32 field to hold the value ofv2
, it is not allocated a place on the stack. It also makes the anonymous function a method of this new type. (For simplicity I'll give this un-named class a name, lets call it "Thing").Now in each iteration a new instance of "Thing" is created and when
v2
is assigned its the Int32 field which is actually assigned not just a point in stack memory. The anonymous function expression (the lambda) will now return a delegate which has non-null instance object reference, this reference will be to the current instance of "Thing".When the delegate for anonymous function is invoked it will execute as an instance method of a "Thing" instance. Hence
v2
is available as a member field and will have the value give it during the iteration this instance of "Thing" was created.除了尼尔和安东尼的答案之外,下面是在这两种情况下可能自动生成的代码示例。
(注意,这只是为了演示原理,实际编译器生成的代码不会完全像这样。如果你想看到真正的代码,那么你可以使用 Reflector 看一下。)
Further to the answers from Neil and Anthony, here's an example of the code that might be auto-generated in both cases.
(Note that this is only to demonstrate the principle, the actual compiler-generated code won't look exactly like this. If you want to see the real code then you can take a look using Reflector.)