在纯函数式语言中,数据是不可变的。使用引用计数时,创建一个引用循环需要更改已经创建的数据。看起来纯函数式语言可以使用引用计数而不必担心可能会出现循环引用。这是对的吗?如果是,为什么它们没有使用引用计数?
我知道在许多情况下,引用计数比GC慢,但至少它减少了暂停时间。在暂停时间较长的情况下,有选择地使用引用计数将是不错的选择。
在纯函数式语言中,数据是不可变的。使用引用计数时,创建一个引用循环需要更改已经创建的数据。看起来纯函数式语言可以使用引用计数而不必担心可能会出现循环引用。这是对的吗?如果是,为什么它们没有使用引用计数?
我知道在许多情况下,引用计数比GC慢,但至少它减少了暂停时间。在暂停时间较长的情况下,有选择地使用引用计数将是不错的选择。
您的问题基于错误的假设。具有循环引用和不可变数据是完全可能的。请看下面这个使用不可变数据创建循环引用的C#示例。
class Node {
public readonly Node other;
public Node() {
other = new Node(this);
}
public Node(Node node) {
other = node;
}
}
这种技巧在很多函数式语言中都能实现,因此任何集合机制都必须处理可能存在的循环引用。我并不是说循环引用不能使用引用计数机制,只是必须处理好它。
回应评论... 在Haskell中这很简单。
data Node a = Node { other :: Node a }
recursiveNode = Node { other = recursiveNode }
在SML中几乎不需要更多的努力。
datatype 'a node = NODE of unit -> 'a node
val recursiveNode : unit node =
let fun mkRecursiveNode () = NODE mkRecursiveNode
in mkRecursiveNode () end
无需进行变异。
我是正确的吗?
不完全正确。你可以使用纯函数式编程创建循环数据结构,只需同时定义相互递归的值即可。例如,在OCaml中:
let rec xs = 0::ys and ys = 1::xs
然而,设计成不可能创建循环结构的语言是有可能的。这种结果被称为单向堆,其主要优点在于垃圾收集可以像引用计数一样简单。
如果是这样,为什么其他编程语言没有这样做呢?
有些语言禁止使用循环结构并使用引用计数。例如Erlang和Mathematica。
例如,在Mathematica中,当您引用一个值时,会对其进行深拷贝,因此更改原始值不会更改副本:
In[1] := xs = {1, 2, 3}
Out[1] = {1, 2, 3}
In[2] := ys = xs
Out[2] = {1, 2, 3}
In[3] := xs[[1]] = 5
Out[3] = 5
In[4] := xs
Out[4] = {5, 2, 3}
In[5] := ys
Out[5] = {1, 2, 3}
我认为有几件事情需要注意。
(曾经我可能真的“知道”这些,但现在我正在试图回忆或推测,因此不要将其视为任何权威意见。)
std::shared_ptr
,还是弱引用,像C++的std::weak_ptr
? - Max Barraclough考虑这个寓言,它是关于Lisp机器的发明家David Moon的故事:
一天,一名学生来找Moon并说:“我知道如何制作更好的垃圾收集器。我们必须保留对每个cons指针的引用计数。”
Moon耐心地告诉学生以下故事:
一天,一名学生来找Moon并说:“我知道如何制作更好的垃圾收集器...
引用计数比垃圾回收慢得多,因为它对CPU不利。而垃圾回收大部分时间可以等待空闲时间,而且垃圾回收可以是并发的(在另一个线程上)。所以这就是问题所在——垃圾回收是最小的恶,很多尝试都表明了这一点。