Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

在大多数情况下,所有权是明确的:你清楚地知道哪个变量拥有某个值。然而,有些情况下一个值可能会有多个所有者。例如,在图数据结构中,多条边可能指向同一个节点,而这个节点在概念上被所有指向它的边所拥有。一个节点只有在没有任何边指向它、也就是没有任何所有者时,才应该被清理。

你需要使用 Rust 的 Rc<T> 类型来显式地启用多重所有权,Rc<T>引用计数(reference counting)的缩写。Rc<T> 类型会追踪一个值的引用数量,以此判断该值是否仍在使用中。如果一个值的引用数量为零,那么这个值就可以被安全地清理,而不会导致任何引用失效。

可以把 Rc<T> 想象成客厅里的一台电视。当一个人进来看电视时,他会打开电视。其他人也可以进来一起看。当最后一个人离开房间时,他会关掉电视,因为电视已经没人看了。如果有人在其他人还在看的时候就把电视关了,剩下的人肯定会不高兴!

当我们希望在堆上分配一些数据供程序的多个部分读取,而又无法在编译时确定哪个部分会最后使用完这些数据时,就可以使用 Rc<T> 类型。如果我们能确定哪个部分最后使用完,那只需让那个部分成为数据的所有者就好了,编译时的常规所有权规则就会生效。

注意 Rc<T> 只适用于单线程场景。当我们在第 16 章讨论并发时,会介绍如何在多线程程序中进行引用计数。

共享数据

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

A linked list with the label 'a' pointing to three elements. The first element contains the integer 5 and points to the second element. The second element contains the integer 10 and points to the third element. The third element contains the value 'Nil' that signifies the end of the list; it does not point anywhere. A linked list with the label 'b' points to an element that contains the integer 3 and points to the first element of list 'a'. A linked list with the label 'c' points to an element that contains the integer 4 and also points to the first element of list 'a' so that the tails of lists 'b' and 'c' are both list 'a'.

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

我们将创建一个包含 510 的列表 a。然后再创建两个列表:b3 开头,c4 开头。bc 两个列表接下来都会连接到包含 510 的第一个列表 a。换句话说,两个列表将共享包含 510 的第一个列表。

尝试使用 Box<T> 定义的 List 来实现这个场景是行不通的,如示例 15-17 所示。

Filename: 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));
}
Listing 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
   |
note: if `List` implemented `Clone`, you could clone the value
  --> src/main.rs:1:1
   |
 1 | enum List {
   | ^^^^^^^^^ consider implementing `Clone` for this type
...
10 |     let b = Cons(3, Box::new(a));
   |                              - you could clone this value

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 被移动到了 b 中,b 拥有了 a。接着,当我们尝试在创建 c 时再次使用 a,这是不被允许的,因为 a 已经被移动了。

我们可以将 Cons 的定义改为持有引用,但那样就必须指定生命周期参数。通过指定生命周期参数,我们将要求列表中的每个元素至少与整个列表存活一样长。对于示例 15-17 中的元素和列表来说确实如此,但并非所有场景都是这样。

我们换一种方式,将 List 的定义改为使用 Rc<T> 来代替 Box<T>,如示例 15-18 所示。现在每个 Cons 变体将持有一个值和一个指向 ListRc<T>。当我们创建 b 时,不再获取 a 的所有权,而是克隆 a 持有的 Rc<List>,从而将引用计数从一增加到二,让 ab 共享该 Rc<List> 中数据的所有权。创建 c 时我们也会克隆 a,将引用计数从二增加到三。每次调用 Rc::clone 时,Rc<List> 中数据的引用计数都会增加,只有当引用计数为零时数据才会被清理。

Filename: 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));
}
Listing 15-18: 使用 Rc<T> 定义的 List

我们需要添加一条 use 语句将 Rc<T> 引入作用域,因为它不在 prelude 中。在 main 中,我们创建了包含 510 的列表,并将其存储在 a 中的一个新 Rc<List> 里。然后,当我们创建 bc 时,调用 Rc::clone 函数并传入 aRc<List> 的引用作为参数。

我们也可以调用 a.clone() 而不是 Rc::clone(&a),但 Rust 的惯例是在这种情况下使用 Rc::cloneRc::clone 的实现不会像大多数类型的 clone 实现那样对所有数据进行深拷贝。调用 Rc::clone 只会增加引用计数,这不会花费太多时间。而数据的深拷贝可能会非常耗时。通过使用 Rc::clone 进行引用计数,我们可以在视觉上区分深拷贝类型的克隆和增加引用计数类型的克隆。在排查代码中的性能问题时,我们只需要关注深拷贝的克隆,而可以忽略对 Rc::clone 的调用。

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

让我们修改示例 15-18 中的工作示例,以便观察在创建和丢弃 aRc<List> 的引用时,引用计数是如何变化的。

在示例 15-19 中,我们将修改 main,在列表 c 周围添加一个内部作用域;这样我们就能看到当 c 离开作用域时引用计数是如何变化的。

Filename: src/main.rs
enum List {
    Cons(i32, Rc<List>),
    Nil,
}

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

// --snip--

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));
}
Listing 15-19: 打印引用计数

在程序中引用计数发生变化的每个位置,我们都打印了引用计数,这个值通过调用 Rc::strong_count 函数获得。这个函数命名为 strong_count 而不是 count,是因为 Rc<T> 类型还有一个 weak_count;我们将在“使用 Weak<T> 避免引用循环”中介绍 weak_count 的用途。

这段代码会打印如下内容:

$ 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

我们可以看到 a 中的 Rc<List> 的初始引用计数为 1;然后每次调用 clone 时,计数加 1。当 c 离开作用域时,计数减 1。我们不需要像调用 Rc::clone 增加引用计数那样调用一个函数来减少引用计数:Drop trait 的实现会在 Rc<T> 值离开作用域时自动减少引用计数。

在这个例子中我们看不到的是,当 bamain 末尾离开作用域时,计数变为 0,Rc<List> 被完全清理。使用 Rc<T> 允许一个值拥有多个所有者,而引用计数确保只要任何一个所有者仍然存在,该值就保持有效。

通过不可变引用,Rc<T> 允许你在程序的多个部分之间共享只读数据。如果 Rc<T> 也允许你拥有多个可变引用,你可能会违反第 4 章讨论的借用规则之一:对同一位置的多个可变借用可能导致数据竞争和不一致。但是能够修改数据是非常有用的!在下一节中,我们将讨论内部可变性模式和 RefCell<T> 类型,你可以将它与 Rc<T> 结合使用来突破这个不可变性限制。