0

封装可变状态部分的 F# WikiBook 上,有以下代码片段。

> let incr =
    let counter = ref 0
    fun () ->
        counter := !counter + 1
        !counter;;

val incr : (unit -> int)

> incr();;
val it : int = 1

> incr();;
val it : int = 2

> incr();;
val it : int = 3

起初,似乎很容易接受这样一个事实,即每次调用可变counter值都会递增。incr

但是想了一会儿,我无法理解的是什么时候counter从堆中释放出来,以及counter在递增之前如何仍然引用先前的值。它是如何通过多个函数调用counter在函数范围内生存的?incr

所以主要问题是:

  • 什么时候counter从堆中释放出来?
  • 不是counter内存泄漏吗?
4

3 回答 3

4

“词法范围”(名称在程序文本中具有意义)和“生命周期”(对象创建和销毁之间的运行时间)之间的区别有时会令人困惑,因为这两者通常高度相关。然而,这个例子演示的技术在函数式语言中很常见:你给一个实现细节一个小的词法范围(对调用者隐藏实现细节),但是通过在闭包中捕获它来延长它的生命周期(这样它的生命周期就变成了生命周期)封闭对象的 - 在这种情况下是“incr”函数)。这是在函数式编程中进行封装的常用方法(与面向对象编程中类中公共/私有的常用封装技术形成对比)。

现在,在这个特定的例子中,'incr' 看起来是一个顶级函数,这意味着它的值在程序的生命周期内持续存在(如果键入 fsi.exe,则为交互式会话)。您可以将其称为“泄漏”,但这取决于意图。如果您在整个程序的整个生命周期中都需要一些唯一的 id 计数器,那么您将不得不将该计数器变量存储在某个地方它持续整个程序。因此,这取决于“incr”的使用方式(您是否需要在整个程序的其余部分中使用该功能?),这是“泄漏”还是“设计特性”。无论如何,这里的关键点是'incr' 持有内存资源,所以如果你不会永远需要这些资源,你应该安排'incr' 引用的闭包在不再需要时变得不可访问。通常这可能是通过使其成为其他功能的本地化,例如

let MyComplicatedFuncThatNeedsALocalCounter args =
    let incr = 
        // as before
    // other code that uses incr
    // return some result that does not capture incr
于 2009-05-03T21:09:21.510 回答
3

在这种情况下,incr是一个顶级函数(如果我没记错的话,作为静态字段实现。)它包含一个闭包,而闭包又引用了名为counter. 只要这个闭包存在,这个ref单元就会被保存在内存中。

现在这个顶级绑定确实永远不会被垃圾收集,因为它是一个静态只读字段。(用 C# 术语)。但是,如果您有类似生命周期有限的闭包(在本地或对象中绑定),那么ref当闭包被垃圾收集时,单元格将被释放。

于 2009-05-03T21:14:35.447 回答
2

当 incr 不再可访问时,计数器从堆中释放。由于垃圾收集,这不是内存泄漏。

于 2009-05-03T20:50:40.370 回答