假设我有一个带有深度绑定的静态/词法范围的语言,我创建了一个闭包。闭包将包含我想要执行的语句加上所谓的引用环境,或引用this post,可以使用的变量集合。
这个引用环境实际上看起来像实现方式?我最近在阅读有关ObjectiveC的块实现的文章,作者建议在幕后获得堆栈上所有变量的副本以及堆对象的所有引用。该解释声称您在关闭创建的时间点获得了引用环境的“快照”。
我认为误解其中一些概念可能会导致棘手的问题,例如Eric Lippert在this blog post中提到的问题。这很有意思,因为你认为保持对一个值类型的引用没有意义,这个值类型可能在调用闭包时消失,但我猜测在C#中编译器会发现变量稍后需要将它放入堆中。
似乎在大多数内存管理语言中,一切都是引用,因此ObjectiveC是一种独特的情况,必须处理复制堆栈中的内容。
答案 0 :(得分:3)
这是一个类似伪javascript语法的运行示例。
function f(x) {
var y = ...;
function g(z) {
function h(w) {
.... y, z, w ....
}
.... x, h ....
}
.... x, g ....
}
一种表示是一种链接的环境链。也就是说,闭包由一个代码指针,一些槽和一个封闭闭包或顶层环境的引用组成。在此表示中,
f = [<code>, <top-level-env>]
g = [<code>, f, x, y]
h = [<code>, g, z]
除非有时最好让每个函数都直接引用顶层环境,因为它经常被使用:
f = [<code>, <top-level-env>]
g = [<code>, <top-level-env>, f, x, y]
h = [<code>, <top-level-env>, g, z]
(还有其他变种。)
这种表示的一个优点是你可以在闭包中存储可变变量。 (好吧,也许,取决于你如何表示函数激活。)一个缺点是,如果你有深度嵌套的闭包,一些变量可能需要多次跳转才能达到。另一个缺点是,如果一个闭包超过其父级(例如,g
返回h
),那么这种表示可能会阻止GC收集大部分甚至完全无法访问的环境帧。
另一种表示是“平面闭包”:每个闭包包含一个代码指针和所有代码自由变量的槽。
g = [<code>, x, y]
h = [<code>, y, z]
此表示修复了空间/ GC问题;没有关闭引脚在内存中另一个关闭。另一方面,自由变量槽被复制而不是共享,所以如果有一个带有许多自由变量的嵌套闭包---或嵌套闭包的许多实例 - 整体内存使用可能会更高。此外,这种表示通常要求存储可变变量以进行堆分配(但仅适用于实际变异的变量,并且仅在不能自动重写变异时)。
还有混合方法。例如,您可能拥有大多数平坦的闭包,但特别对待顶级环境:
g = [<code>, <top-level-env>, x, y]
或者你可能有一个“足够聪明”(或至少“足够雄心勃勃”)的编译器,它试图根据自由变量的数量,嵌套深度等来选择表示。
答案 1 :(得分:2)
在Smalltalk中,闭包可以引用“外部上下文”。外部上下文通常是创建闭包的方法的堆栈框架,但对于嵌套闭包,它可能是另一个闭包。
持有对外部上下文的引用的闭包很昂贵,因为(我猜)它们会阻止相应的堆栈被垃圾收集。因此,闭包仅在真正需要时引用外部上下文:
清理闭包:没有引用任何本地的闭包。他们不需要引用外部背景。
E.g。 [ Transcript show: 'something' ]
复制闭包:闭包,引用变量,在创建闭包后不会更改。创建闭包时变量的值将复制到闭包本身中。然后,不需要保持对外部上下文的引用。例如。
|清单|
list:= OrderedCollection new。
1:5做:[:i | list add:i]。
完全闭包:保留对外部上下文的引用的闭包。 E.g。
|计数器|
counter:= 0.
1:5做:[:i | counter:= counter + 1]。
如果关闭的变量在创建闭包后发生变异,则需要完全闭包,但非本地返回也是如此。关于非本地回报,您可能会喜欢blog post from Neal Gafter。
关于即将到来的JDK 7的关闭,Brian Goetz'State of the Lambda的好读也是如此。我发现有趣的讨论为什么他们会坚持使用Java limation来捕获最终的最终变量并禁止捕获可变局部变量。上面的完全关闭示例将不受支持。他们声称的论点是,它主要是连续成语。