一个引用环境通常如何实现闭包?

3
假设我有一种静态/词法作用域的语言,具有深层绑定,并且我创建了一个闭包。该闭包将包含我想执行的语句以及所谓的引用环境,或者引用此帖子中所引述的可用变量集合。
从实现的角度来看,这个引用环境实际上是什么样子?我最近阅读了有关ObjectiveC块实现的文章,作者提到在幕后,您将获取堆栈上所有变量以及对堆对象的所有引用的副本。解释称,您会在闭包创建时获取引用环境的“快照”。
1.那是否更多或更少是发生了什么,还是我理解有误? 2.是否需要“冻结”堆对象的单独副本?或者可以安全地假设如果它们在闭包创建和执行闭包之间被修改,则闭包将不再操作对象的原始版本? 3.如果确实进行了复制,请问当需要创建大量闭包并将它们存储在某个位置时,是否存在内存使用考虑因素?
我认为对一些概念的误解可能会导致棘手的问题,例如Eric Lippert在这篇博客文章中提到的问题。这很有趣,因为您认为保留可能在闭包被调用时消失的值类型的引用是没有意义的,但我猜想在C#中编译器会发现变量稍后需要并将其放入堆中。
似乎在大多数内存管理语言中,所有内容都是引用,因此ObjectiveC是一种相对独特的情况,必须处理复制堆栈上的内容。
2个回答

4

以下是伪代码的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)您知道在哪里可以找到更多像我们在这里讨论的那样的细节吗?互联网上对于这些相当晦涩的概念并不是很丰富。也许有一些特定的书籍吗? - Alexandr Kurilin

2
在Smalltalk中,闭包可以保持对“外部上下文”的引用。外部环境通常是创建闭包的方法的堆栈帧,但对于嵌套闭包来说,它可能是另一个闭包。
保持对外部上下文的引用的闭包是昂贵的,因为(我猜)它们会防止相应的堆栈被垃圾回收。因此,闭包仅在真正需要时才引用外部上下文:
干净的闭包:没有引用任何本地内容的闭包。它们不需要引用外部上下文。
例如:[Transcript show:'something'] 复制闭包:引用自创建闭包后不会更改的变量的闭包。在创建闭包时,变量的值被复制到闭包本身中。然后,就不需要保留对外部上下文的引用。例如: | list | list := OrderedCollection new. 1 to: 5 do: [: i | list add: i]。
完整闭包:保持对外部上下文的引用的闭包。例如: | counter | counter := 0。 1 to: 5 do: [: i | counter := counter + 1]。
如果在创建闭包后对封闭的变量进行了更改,则需要完整闭包,但也需要用于非局部返回。关于非局部返回,您可以参考Neal Gafter的博客文章
Brian Goetz的Lambda状态是关于即将推出的JDK 7中闭包的讨论,其中包括为什么他们将坚持Java限制来捕获只有最终状态的变量并禁止捕获可变局部变量的讨论。不支持上面的完整闭包示例。他们声称这是大多数串行用法的参数。

感谢提供链接和解释。希望我能接受多个答案! - Alexandr Kurilin

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