5分钟速读之Rust权威指南(二十七)Rc<T>

312 阅读3分钟

使用Rc<T>共享数据

前面我们有使用Box实现链表,本节我们以一道题开始,先试着实现图中的列表:

WX20210622-193910@2x.png

图中列表 a 包含 5 ,后面跟着的是 10,列表 b 从 3 开始,列表 c 从 4 开始。b 和 c 会接上包含 5 和 10 的列表 a。换句话说,这两个列表会尝试共享a列表所包含的 5 和 10。

使用Box实现

我们先尝试使用已经学过的Box类型来实现:

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

// 创建a列表
let a = List::Cons(5,
  Box::new(List::Cons(10,
    Box::new(List::Nil))
  )
);

// 创建b列表,并连接a列表
let b = List::Cons(3, Box::new(a));

// 创建c列表,并连接a列表
let c = List::Cons(4, Box::new(a)); // 报错,a的所有权已经被移动

利用引用来解决a的所有权转移的问题

我们可以将Box中的列表类型改为引用类型,这样b和c列表就可以同时引用a列表了:

#[derive(Debug)]
enum List<'a> {
  Cons(i32, Box<&'a List<'a>>),
  Nil,
}

引用方式一:

let a = List::Cons(5,
  Box::new(&List::Cons(10,
    Box::new(&List::Nil)) // 报错,创建临时引用&Nil,但是在a赋值时已经被销毁
  )
); // &Nil在这里的结束语句被销毁,此时a还没创建完成
println!("{:?}", a);

引用方式二:

let a2 = &List::Nil;
let a1 = &List::Cons(10, Box::new(a2));
let a = &List::Cons(5, Box::new(a1));

let b = List::Cons(3, Box::new(a));
let c = List::Cons(4, Box::new(a));
println!("{:?}", b); // Cons(3, Cons(5, Cons(10, Nil)))
println!("{:?}", c); // Cons(4, Cons(5, Cons(10, Nil)))

虽然可以实现,但是嵌套关系不是很直观。

使用Rc<T>来解决

Rc<T>支持多重所有权,它名称中的Rc是Referencecounting(引用计数)的缩写。对于了解垃圾回收概念的同学理解起来会更顺畅一些,Rc<T>类型会在内部维护一个引用次数计数器,用于确认这个值是否仍在使用。如果对一个值的引用次数为零,那么就意味着这个值可以被安全地清理掉,而不会触发引用失效的问题:

use std::rc::Rc; // 引入Rc
#[derive(Debug)]
enum List {
  Cons(i32, Rc<List>), // 替换Box为Rc
  Nil
}

// 创建Rc实例
let a = Rc::new(List::Cons(5,
  Rc::new(List::Cons(10,
    Rc::new(List::Nil)
  ))
));

// 这里的Rc::clone只是增加的引用计数,虽然使用a.clone也可以实现,但是数据会被深拷贝
let b = List::Cons(3, Rc::clone(&a));

// 再次增加引用次数
let c = List::Cons(4, Rc::clone(&a));

println!("{:?}", b); // Cons(3, Cons(5, Cons(10, Nil)))
println!("{:?}", c); // Cons(4, Cons(5, Cons(10, Nil)))

观察引用计数

通过Rc<T>提供的方法可以获取到当前的引用数量:

let a = Rc::new(List::Cons(5,
  Rc::new(List::Cons(10,
    Rc::new(List::Nil)
  ))
));

println!("创建a之后的引用计数:{}", Rc::strong_count(&a));
// 创建a之后的引用计数:1

let b = List::Cons(3, Rc::clone(&a));
println!("创建b之后的引用计数:{}", Rc::strong_count(&a));
// 创建b之后的引用计数:2

// 进入新的作用域
{
	let c = List::Cons(2, Rc::clone(&a));
	println!("创建c之后的引用计数:{}", Rc::strong_count(&a));
  // 创建c之后的引用计数:3
} // c离开作用域时自动将引用计数减1。

println!("销毁c之后的引用计数:{}", Rc::strong_count(&a));
// 销毁c之后的引用计数:2

Rc<T>通过不可变引用使得程序的不同部分之间可以共享只读数据。如果Rc<T>也允许持有多个可变引用的话,那么它就会违反借用规则:多个指向同一区域的可变借用会导致数据竞争及数据不一致。