Rust智能指针Rc<T>

有的时候单个值会有多个所有者,例如图数据结构中,多个边可能指向相同的节点,节点在没有任何边指向它从而没有任何所有者之前,都不应该被清理掉。

启用多所有权需要使用Rc<T>,这是引用计数(reference counting)的缩写,它记录了一个值的引用数量来知晓这个值是否仍在被使用,如果这个值有零个引用,就代表没有任何有效引用并可以被清理。

Rc<T>用于我们希望在堆上面分配一些内存供程序多个部分读取,且无法在编译时确定程序的哪一个部分会最后结束使用它的时候。如果知道哪部分最后一个结束使用,就可以令其成为数据的所有者,正常的所有权规则就可以在编译时生效。

🔥 谨慎

Rc<T>只能用于单线程场景,多线程场景将在后面介绍。

使用Rc共享数据🔗

假如我们想创建一个有共享所有权的列表,尝试使用Box<T>:

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

这段代码会报错,因为创建b列表时,a的所有权已经被移动,所以尝试使用a创建c时不被允许。

可以改变Cons的定义来存放一个引用,不过需要指定生命周期参数,这样可以使列表中每一个元素都至少与列表本身存在的一样久,然而并不是所有情况都是如此。

下面我们使用Rc<T>来解决这个问题:

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

这里创建b时,不同于获取a的所有权,而是克隆a所包含的Rc<List>,这会使引用计数从1增加到2并允许b和a共享Rc<List>中的数据的所有权,创建c时也一样。每次调用Rc::clone时,引用计数就会增加1,知道有零个引用之前数据都不会被清理。

这里先使用use语句将Rc<T>引入作用域,先创建a,然后创建b和c,Rc::clone接受一个Rc<T>的引用并返回一个新的Rc<T>。这里也可以调用a.clone()而不是Rc::clone(&a),不过这里习惯是使用Rc::clone。Rc::clone不像大部分类型的clone那样对所有数据进行深拷贝,而是增加了引用计数。这会节省很多时间。

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

这段代码可以观察到引用计数的变化,这里通过调用Rc::strong_count函数来获取引用计数。这个函数叫strong_count而不是count是因为Rc<T>也有weak_count这个函数。该代码会打印如下内容:

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

可以看到,a中初始的引用计数为1,后面每次调用Rc::clone时,引用计数就会增加1,直到c离开作用域时,引用计数就会减少1。不用像调用Rc<T>一样调用函数来减少计数,因为Rc<T>实现了Drop trait,当Rc<T>离开作用域时,引用计数就会减少1。

在最后,a和b离开作用域时,引用计数会减少到0,同时Rc<T>也会被完全清理。

通过不可变引用,Rc<T>可以允许程序的多个部分只读地共享数据,后面会介绍内部可变性模式和RefCell<T>类型,它可以与Rc<T>结合使用来处理不可变性的限制。