当不可变引用可以完成这项工作时,为什么我们需要 Rc<T>?

nal*_*zok 10 reference immutability ownership rust borrow-checker

为了说明 的必要性Rc<T>本书提供了以下代码片段(剧透:它不会编译)来表明,如果没有Rc<T>.

\n
enum List {\n    Cons(i32, Box<List>),\n    Nil,\n}\n\nuse crate::List::{Cons, Nil};\n\nfn main() {\n    let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));\n    let b = Cons(3, Box::new(a));\n    let c = Cons(4, Box::new(a));\n}\n
Run Code Online (Sandbox Code Playgroud)\n

然后它声称(强调我的)

\n
\n

我们可以更改 的定义来Cons保存引用,但随后我们必须指定生命周期参数。通过指定生命周期参数,我们将指定列表中的每个元素的生存时间至少与整个列表一样长。例如,借用检查器不会让我们进行编译,因为在 a 可以引用它之前,临时值将被删除。let a = Cons(10, &Nil);Nil

\n
\n

嗯,不完全是。以下代码片段在下编译rustc 1.52.1

\n
enum List<\'a> {\n    Cons(i32, &\'a List<\'a>),\n    Nil,\n}\n\nuse crate::List::{Cons, Nil};\n\nfn main() {\n    let a = Cons(5, &Cons(10, &Nil));\n    let b = Cons(3, &a);\n    let c = Cons(4, &a);\n}\n
Run Code Online (Sandbox Code Playgroud)\n

请注意,通过引用,我们不再需要Box<T>间接来保存嵌套的List. 此外,我可以同时指向和b,这给出了多个概念所有者(实际上是借款人)。caa

\n

Rc<T>问题:当不可变引用可以完成这项工作时,为什么我们需要它?

\n

use*_*968 12

对于“普通”借用,您可以非常粗略地想到静态证明的按关系排序,其中编译器需要证明某物的所有者总是在任何借用之前生存,并且总是在所有借用死亡后死亡(aowns String,它来了b借用之前的生命a,然后b死亡,然后a死亡;有效)。对于很多用例来说,这是可以做到的,这是 Rust 使借用系统变得实用的洞察力。

在某些情况下,这无法静态完成。在您给出的示例中,您有点作弊,因为所有借用都有一个'static生命周期;因此,'static项目可以在任何无限之前或之后“排序” - 所以实际上一开始就没有限制。List<'a>当您考虑不同的生命周期(许多、List<'b>等)时,该示例会变得更加复杂。当您尝试将值传递给函数并且这些函数尝试添加项目时,此问题将变得明显。这是因为在函数内部创建的值在离开其作用域后(即当封闭函数返回时)将会消亡,因此我们不能在之后保留对它们的引用,否则将会出现悬空引用。

Rc当一个人无法静态地证明谁是原始所有者,其生命周期在任何其他人之前开始并在任何其他人之后结束时(!),就会出现。一个典型的例子是从用户输入派生的图形结构,其中多个节点可以引用另一个节点。它们需要与它们在运行时引用的节点形成“生于之后,死于之前”的关系,以保证它们永远不会引用无效数据。这Rc是一个非常简单的解决方案,因为一个简单的计数器就可以表示这些关系。只要计数器不为零,一些“生于之后,死于之前”的关系仍然活跃。这里的关键见解是,节点创建和消亡的顺序并不重要,因为任何顺序都是有效的。只有两端的点 - 计数器达到 0 的地方 - 实际上很重要,其间的任何增加或减少都是相同的 (0=+1+1+1-1-1-1=0与 相同0=+1+1-1+1-1-1=0)Rc当计数器达到零时被破坏。在图表示例中,这是当不再引用节点时。这告诉该节点的所有者Rc(最后一个节点指的是)“哦,事实证明是底层节点的所有者 - 没有人知道! - 我可以摧毁它”。