有的时候单个值会有多个所有者,例如图数据结构中,多个边可能指向相同的节点,节点在没有任何边指向它从而没有任何所有者之前,都不应该被清理掉。
启用多所有权需要使用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>结合使用来处理不可变性的限制。