Rc<T>、引用计数智能指针

在大多数情况下,所有权是明确的:您确切地知道哪个变量 拥有给定的值。但是,在某些情况下,单个值可能具有 多个所有者。例如,在图形数据结构中,多个边可能会 指向同一节点,并且该节点在概念上由所有边拥有 指向它。除非节点没有任何 edges 指向它,因此没有所有者。

您必须使用 Rust 类型显式启用多个所有权Rc<T>,它是 reference counting 的缩写。这Rc<T>类型 跟踪对值的引用数以确定 该值仍在使用中。如果对某个值的引用为零,则值 可以清理而不会使任何引用无效。

想象Rc<T>作为家庭活动室的电视。当一个人进来看电视时, 他们打开了它。其他人可以进入房间看电视。当最后一个 人们离开房间,他们关掉了电视,因为它不再被使用。 如果有人在其他人仍在观看电视时关闭电视,就会有 其余电视观众的哗然!

我们使用Rc<T>type 时,我们想在堆上为 要读取程序的多个部分,并且我们在编译时无法确定 哪个部分将最后完成对数据的使用。如果我们知道哪个部分会完成 最后,我们可以将这部分设为数据的所有者,而 Normal 所有权 在编译时强制执行的规则将生效。

请注意,Rc<T>仅适用于单线程方案。当我们讨论 并发性,我们将介绍如何在 多线程程序。

Rc<T>共享数据

让我们回到示例 15-5 中的 cons 列表示例。回想一下,我们定义了 它使用Box<T>.这一次,我们将创建两个共享所有权的列表 第三个列表。从概念上讲,这类似于图 15-3:

两个列表共享第三个列表的所有权

图 15-3:两个列表bc、共享 的所有权 第三个列表a

我们将创建 lista,其中包含 5,然后是 10。然后我们再制作两个 列表:b以 3 开头,c从 4 开始。双bc然后,列表将继续执行第一个a包含 5 和 10 的列表。在其他 words,则两个列表将共享包含 5 和 10 的第一个列表。

尝试使用我们的ListBox<T>不起作用,如示例 15-17 所示:

文件名: src/main.rs

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));
}

示例 15-17:证明我们不允许拥有 两个列表Box<T>试图分享第三个列表的所有权

当我们编译此代码时,我们收到此错误:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0382]: use of moved value: `a`
  --> src/main.rs:11:30
   |
9  |     let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
   |         - move occurs because `a` has type `List`, which does not implement the `Copy` trait
10 |     let b = Cons(3, Box::new(a));
   |                              - value moved here
11 |     let c = Cons(4, Box::new(a));
   |                              ^ value used here after move

For more information about this error, try `rustc --explain E0382`.
error: could not compile `cons-list` (bin "cons-list") due to 1 previous error

Cons变体拥有它们持有的数据,因此当我们创建b列表a已移至bb拥有a.然后,当我们尝试使用aagain when 创建c,我们不允许这样做,因为a已移动。

我们可以更改Cons来保留引用,但随后 我们必须指定生命周期参数。通过指定生命周期 参数,我们将指定列表中的每个元素都将位于 至少与整个列表一样长。元素和列表就是这种情况 在示例 15-17 中,但并非在每个场景中。

相反,我们将更改List使用Rc<T>代替Box<T>,如示例 15-18 所示。每Consvariant 现在将保存一个值 以及一个Rc<T>指向List.当我们创建b,而不是采用 所有权a,我们将克隆Rc<List>a是持有,因此 将引用的数量从 1 增加到 2,然后让ab共享该数据Rc<List>.我们还将克隆a什么时候 创建c,将引用数量从 2 个增加到 3 个。每次 我们调用Rc::clone,则对Rc<List>将 increase,除非对 它。

文件名: src/main.rs

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

use crate::List::{Cons, Nil};
use std::rc::Rc;

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

示例 15-18: 的定义List使用Rc<T>

我们需要添加一个use要带来的声明Rc<T>into 范围,因为它不是 在序曲中。在main,我们创建包含 5 和 10 的列表并将其存储在 一个新的Rc<List>a.然后,当我们创建bc,我们调用Rc::clone函数,并将引用传递给Rc<List>a作为 论点。

我们本可以调用a.clone()而不是Rc::clone(&a),但 Rust 的 约定是使用Rc::clone在这种情况下。的实现Rc::clone不会像大多数类型那样对所有数据进行深层复制” 的实现clone做。对Rc::clone只会递增 reference count 的 Count,这不会花费太多时间。数据的深层副本可以采用 很多时间。通过使用Rc::clone对于参考计数,我们可以直观地 区分深拷贝类型的克隆和以下类型的克隆 增加引用计数。在 代码中,我们只需要考虑 deep-copy 克隆,并且可以忽略对Rc::clone.

克隆Rc<T>增加引用计数

让我们更改示例 15-18 中的工作示例,以便我们可以看到参考 计数会发生变化,因为我们创建和删除对Rc<List>a.

在示例 15-19 中,我们将更改main所以它有一个内部作用域 around listc; 那么我们可以看到引用计数是如何变化的:c超出范围。

文件名: src/main.rs

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

use crate::List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("count after creating a = {}", Rc::strong_count(&a));
    let b = Cons(3, Rc::clone(&a));
    println!("count after creating b = {}", Rc::strong_count(&a));
    {
        let c = Cons(4, Rc::clone(&a));
        println!("count after creating c = {}", Rc::strong_count(&a));
    }
    println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}

示例 15-19:打印引用计数

在程序中引用计数发生变化的每个点,我们都会打印 引用计数,我们通过调用Rc::strong_count功能。这 函数被命名为strong_count而不是count因为Rc<T>类型 还有一个weak_count;我们拭目以待weak_count用于“防止参考循环:转动Rc<T>转换为Weak<T>部分。

此代码打印以下内容:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.45s
     Running `target/debug/cons-list`
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2

我们可以看到,Rc<List>a初始引用计数为 1;然后 每次我们调用clone,则计数增加 1。什么时候c超出范围, 计数减少 1。我们不必调用函数来减少 引用计数,就像我们必须调用Rc::clone增加引用 count:的Droptrait 减少引用计数 当Rc<T>value 超出范围。

在这个例子中,我们看不到的是,当b然后a超出范围 在main,则计数为 0,并且Rc<List>已清理 完全。用Rc<T>允许单个值具有多个所有者,并且 count 可确保只要任何所有者 仍然存在。

通过不可变引用,Rc<T>允许您在多个 程序的部件仅用于读取。如果Rc<T>允许您拥有多个 可变引用,则可能会违反所讨论的借用规则之一 在第 4 章中:对同一位置的多个可变借用会导致数据竞争 和不一致。但是能够更改数据非常有用!在下一个 部分中,我们将讨论内部可变性模式和RefCell<T>type 中,您可以将其与Rc<T>使用这个 不可变性限制。

本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准