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

Why do we need Rc<T> when immutable references can do the job?

为了说明 Rc<T> 的必要性,the Book 提供了以下代码片段(剧透:它不会编译)以表明我们不能在没有 Rc<T> 的情况下启用多重所有权。

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
    let b = Cons(3, Box::new(a));
    let c = Cons(4, Box::new(a));
}

然后声称(强调我的)

We could change the definition of Cons to hold references instead, but then we would have to specify lifetime parameters. By specifying lifetime parameters, we would be specifying that every element in the list will live at least as long as the entire list. The borrow checker wouldn’t let us compile let a = Cons(10, &Nil); for example, because the temporary Nil value would be dropped before a could take a reference to it.

嗯,不完全是。以下片段在 rustc 1.52.1

下编译
enum List<'a> {
    Cons(i32, &'a List<'a>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let a = Cons(5, &Cons(10, &Nil));
    let b = Cons(3, &a);
    let c = Cons(4, &a);
}

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

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

对于“普通”借用,您可以非常粗略地想到静态证明的按关系排序,其中编译器需要证明某些东西的所有者 always 会出现在任何借用之前并且总是在所有借用死亡之后死亡(a拥有String,它在借用ab之前复活,然后 b 死亡,然后 a 死亡;有效)。对于很多用例,这是可以做到的,这是 Rust 使借用系统变得实用的见解。

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

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