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

ch15-04-rc.md
commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

大部分情况下所有权是非常明确的:可以准确的知道哪个变量拥有某个值。然而并不总是如此;有时确实可能需要多个所有者。为此,Rust 有一个叫做Rc<T>的类型。它的名字是引用计数reference counting)的缩写。引用计数意味着它记录一个值引用的数量来知晓这个值是否仍在被使用。如果这个值有零个引用,就知道可以在没有有效引用的前提下清理这个值。

根据现实生活场景来想象的话,它就像一个客厅的电视。当一个人进来看电视时,他打开电视。其他人也会进来看电视。当最后一个人离开房间时,他关掉电视因为它不再被使用了。如果某人在其他人还在看的时候关掉了电视,正在看电视人肯定会抓狂的!

Rc<T>用于当我们希望在堆上分配一些内存供程序的多个部分读取,而且无法在编译时确定程序的那一部分会最后结束使用它。如果我们知道的话那么常规的所有权规则会在编译时强制起作用。

注意Rc<T>只能用于单线程场景;下一章并发会涉及到如何在多线程程序中进行引用计数。如果尝试在多线程中使用Rc<T>则会得到一个编译错误。

使用Rc<T>分享数据

让我们回到列表 15-5 中的 cons list 例子。在列表 15-11 中尝试使用Box<T>定义的List。首先创建了一个包含 5 接着是 10 的列表实例。之后我们想要创建另外两个列表:一个以 3 开始并后接第一个包含 5 和 10 的列表,另一个以 4 开始其后是第一个列表。换句话说,我们希望这两个列表共享第三个列表的所有权,概念上类似于图 15-10:

Two lists that share ownership of a third list

Figure 15-10: Two lists, b and c, sharing ownership of a third list, a

尝试使用Box<T>定义的List并不能工作,如列表 15-11 所示:

Filename: src/main.rs

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

use 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-11: Having two lists using Box<T> that try to share ownership of a third list won't work

编译会得出如下错误:

error[E0382]: use of moved value: `a`
  --> src/main.rs:13:30
   |
12 |     let b = Cons(3, Box::new(a));
   |                              - value moved here
13 |     let c = Cons(4, Box::new(a));
   |                              ^ value used here after move
   |
   = note: move occurs because `a` has type `List`, which does not
   implement the `Copy` trait

Cons成员拥有其储存的数据,所以当创建b列表时将a的所有权移动到了b。接着当再次尝使用a创建c时,这不被允许因为a的所有权已经被移动。

相反可以改变Cons的定义来存放一个引用,不过接着必须指定生命周期参数,而且在构造列表时,也必须使列表中的每一个元素都至少与列表本身存在的一样久。否则借用检查器甚至都不会允许我们编译代码。

如列表 15-12 所示,可以将List的定义从Box<T>改为Rc<T>

Filename: src/main.rs

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

use 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, a.clone());
    let c = Cons(4, a.clone());
}

Listing 15-12: A definition of List that uses Rc<T>

注意必须为Rc增加use语句因为它不在 prelude 中。在main中创建了存放 5 和 10 的列表并将其存放在一个叫做a的新的Rc中。接着当创建bc时,我们对a调用了clone方法。

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

之前我们见过clone方法,当时使用它来创建某些数据的完整拷贝。但是对于Rc<T>来说,它并不创建一个完整的拷贝。Rc<T>存放了引用计数,也就是说,一个存在多少个克隆的计数器。让我们像列表 15-13 那样在创建c时增加一个内部作用域,并在不同的位置打印出关联函数Rc::strong_count的结果。Rc::strong_count返回传递给它的Rc值的引用计数,而在本章的稍后部分介绍避免引用循环时讲到它为什么叫做strong_count

Filename: src/main.rs

# enum List {
#     Cons(i32, Rc<List>),
#     Nil,
# }
#
# use List::{Cons, Nil};
# use std::rc::Rc;
#
fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("rc = {}", Rc::strong_count(&a));
    let b = Cons(3, a.clone());
    println!("rc after creating b = {}", Rc::strong_count(&a));
    {
        let c = Cons(4, a.clone());
        println!("rc after creating c = {}", Rc::strong_count(&a));
    }
    println!("rc after c goes out of scope = {}", Rc::strong_count(&a));
}

Listing 15-13: Printing out the reference count

这会打印出:

rc = 1
rc after creating b = 2
rc after creating c = 3
rc after c goes out of scope = 2

不难看出a的初始引用计数是一。接着每次调用clone,计数会加一。当c离开作用域时,计数减一,这发生在Rc<T>Drop trait 实现中。这个例子中不能看到的是当b接着是amain函数的结尾离开作用域时,包含 5 和 10 的列表的引用计数会是 0,这时列表将被丢弃。这个策略允许拥有多个所有者,而引用计数会确保任何所有者存在时这个值保持有效。

在本部分的开始,我们说Rc<T>只允许程序的多个部分读取Rc<T>T的不可变引用。如果Rc<T>允许一个可变引用,我们将遇到第四章讨论的借用规则所不允许的问题:两个指向同一位置的可变借用会导致数据竞争和不一致。不过可变数据是非常有用的!在下一部分,我们将讨论内部可变性模式和RefCell<T>类型,它可以与Rc<T>结合使用来处理不可变性的限制。