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

问题描述 投票:0回答:3

为了说明

Rc<T>
的必要性,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));
}

然后声称(强调我的)

我们可以更改

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

好吧,不完全是。以下代码片段在

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
。此外,我可以将
b
c
都指向
a
,这给了
a
多个概念所有者(实际上是借款人)。

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

Rc<T>

rust reference immutability borrow-checker ownership
3个回答
8
投票

对于“普通”借用,您可以非常粗略地想到静态证明的按关系排序,其中编译器需要证明某物的所有者 always 在任何借用之前复活并且 always 在所有借用结束后死亡(

a
拥有
String
,它在借用
b
a
之前复活,然后
b
死,然后
a
死;有效)。对于很多用例,这是可以做到的,这是 Rust 使借用系统实用的洞察力。

有些情况下这不能静态完成。在您给出的示例中,您有点作弊,因为所有借用都有

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

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


0
投票

即使是单线程,也有销毁顺序是动态确定的,而借用检查器要工作,必须有一个确定的生命周期树(堆栈)。

fn run() {
    let writer = Rc::new(std::io::sink());
    let mut counters = vec![
        (7, Rc::clone(&writer)),
        (7, writer),
    ];
    while !counters.is_empty() {
        let idx = read_counter_index();
        counters[idx].0 -= 1;
        if counters[idx].0 == 0 {
            counters.remove(idx);
        }
    }
}

fn read_counter_index() -> usize {
    unimplemented!()
}

如您在本例中所见,销毁顺序由用户输入决定。

使用智能指针的另一个原因是简单。借用检查器确实会带来一些代码复杂性。例如,使用智能指针,您能够以很小的开销解决 self-referential struct 问题。

struct SelfRefButDynamic {
    a: Rc<u32>,
    b: Rc<u32>,
}

impl SelfRefButDynamic {
    pub fn new() -> Self {
        let a = Rc::new(0);
        let b = Rc::clone(&a);
        Self { a, b }
    }
}

这对于静态(编译时)引用是不可能的:

struct WontDo {
    a: u32,
    b: &u32,
}

0
投票

user2722968 的回答帮助我理解了这个问题并创建了一个超级简单的例子来向我自己解释逻辑。

如果

a
的所有者超出范围,nalzok 的原始参考解决方案将不起作用:

#[derive(Debug)]
enum List<'a> {
    Cons(i32, &'a List<'a>),
    Nil,
}

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

fn main() {
    let b;
    {
        let a = Cons(5, &Cons(10, &Nil));
        b = Cons(3, &a);
//                  ^^ borrowed value does not live long enough
    }
//  - `a` dropped here while still borrowed

    println!("{:?}", b);
//                   - borrow later used here
}

使用

Rc
时,您将获得完全相同的代码运行,因为这样您就不必关心所有者和借用者的生命周期:

use std::rc::Rc;

#[derive(Debug)]
enum List {
    Cons(i32, Rc<List>),
    Nil,
}

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

fn main() {
    let b;
    {
        let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
        b = Cons(3, Rc::clone(&a));
    }

    println!("{:?}", b);
}
© www.soinside.com 2019 - 2024. All rights reserved.