Rust 学习笔记(28)-Rc

参考章节《Rust 程序设计语言》第15.4章 Rc<T> 引用计数智能指针

Rust 有一个叫做 Rc<T> 的类型。其名称为 引用计数(reference counting)的缩写。
引用计数意味着记录一个值引用的数量来知晓这个值是否仍在被使用。如果某个值有0个引用,就代表没有任何有效引用并可以被清理。

书上叫它引用计数智能指针,我更喜欢把它叫做可克隆型智能指针

一想到可克隆就能想到Rc::clone,就能想到多有权,就能想到引用计数 (PS:这只是我的叫法,大家可根据自己的理解随意发挥)

大部分情况下所有权是非常明确的:可以准确地知道哪个变量拥有某个值。然而,有些情况单个值可能会有多个所有者

我们来看看下图

rc

书上对这图的解释我也是醉了

列表 a 包含 5 之后是 10,之后是另两个列表:b3 开始而 c4 开始。bc 会接上包含 510 的列表 a

我晕,你直接说 bc 包含 a 不就行了,你妹的废话一大堆

来看看这幅图对应的代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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));
}

这段代码并不能编译

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$ 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

For more information about this error, try `rustc --explain E0382`.
error: could not compile `cons-list` due to previous error

原因很简单,当创建 b 列表时,a 被移动进了 b 这样 b 就拥有了 a
接着当再次尝试使用 a 创建 c 时,这不被允许,因为 a 的所有权已经被移动。

那么如何解决这个问题

  1. 一种方法是改变 Cons 的定义来存放一个引用,不过接着必须指定生命周期参数

通过指定生命周期参数,表明列表中的每一个元素都至少与列表本身存在的一样久。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
enum List<'a> {
    Cons(i32, Box<&'a List<'a>>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let v = Cons(10, Box::new(&Nil));

    let a = Cons(5, Box::new(&v));
    let b = Cons(3, Box::new(&a));
    let c = Cons(4, Box::new(&a));
}
  1. 第二种办法是使用 Rc<T> 智能指针

我们修改 List 的定义为使用 Rc<T> 代替 Box<T>,当创建 a 时,Rc 的引用计数器为 1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
enum List {
    Cons(i32, Rc<List>), // 用 Rc<T> 代替 Box<T>
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc; // 需要使用 use 语句将 Rc<T> 引入作用域,因为它不在 prelude 中。

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); // 当创建 a 时,Rc 的引用计数器为 1
    let b = Cons(3, Rc::clone(&a)); // 调用 Rc::clone(&a) 克隆 a 的所有权,这会将引用计数从 1 增加到 2
    let c = Cons(4, Rc::clone(&a)); // 调用 Rc::clone(&a) 克隆 a 的所有权,这会将引用计数从 2 增加为 3
}

当创建 b 时会克隆 a,这会将引用计数从 1 增加到 2 并允许 ab 共享 Rc<List> 中数据的所有权
当创建 c 时会克隆 a,这会将引用计数从 2 增加为 3 并允许 ac 共享 Rc<List> 中数据的所有权
当调用 Rc::clone时,Rc<List> 中数据的引用计数都会增加,直到有0个引用之前其数据都不会被清理

总结一下

  1. Rc<T> 允许我们克隆所有权
  2. 调用 Rc::clone 时会增加 引用计数器
updatedupdated2025-03-012025-03-01