Skip to main content

C15 智能指针

https://doc.rust-lang.org/book/ch15-00-smart-pointers.html

指针是一种通用概念, 指的是这一类变量, 它们的值存储了内存地址, 而这个内存地址 "指向" 其他数据(即其他数据所在的内存地址). 而最常见的指针就是前面提到的引用(通过 & 表示), 它们没有额外开销, 而功能仅仅是指向另外的数据.

智能指针 指的是一种数据结构, 它类似指针的作用, 但有额外的附加功能(通过其存储的元数据和对应能力).

在 Rust 标准库中有许多智能指针, 包括引用计数指针 Rc, 原子引用计数指针 Arc, String, Vec<T> 等. 智能指针通常使用 struct 实现, 且通常都实现了 DerefDrop trait.

Deref 让智能指针使用时和使用引用方式一样, 而无需显式进行引用获取操作.

Drop 让我们可以自定义智能指针在出作用域后的行为.

由于智能指针种类众多, 且各种库中都包含自己实现的智能指针, 因此本章只挑选标准库中几个典型的来看:

  • Box<T>: 用于在堆上分配内存
  • Rc<T>: 用于进行引用计数, 从而实现多 ownership
  • Refer<T>RefMut<T>: 通过 RefCell<T> 进行访问, 让运行时的 borrow check 成为可能.

此外, 本章还会讲 内部可变(interior mutability) 模式, 这种模式为一个不可变类型提供改变其内部值的接口. 同时本章还会讲在使用引用计数时的一个典型问题: 循环引用, 以及如何避免循环引用所造成的内存泄露.

使用 Box<T> 分配堆内存

Box 用于将数据存储到堆上, 创建后, 获得一个栈上智能指针对象(内部除数据指针外, 还有其他元数据). 除分配堆内存外, Box 无额外开销. 它的典型使用场景如下:

  • 当有一个类型的大小无法在编译时预先知道.
  • 当有大量数据不希望被复制, 只是传递所有权时
  • 当表示一类只关心其实现何种 trait 类型的数据时(trait object)

Box 的简单使用如下所示:

let b = Box::new(5); // Box<i32>
println!("b={b}"); // 5

Box 的一个使用场景: 递归类型

当一个类型中包含其自身类型作为成员时, 这样的数据类型称为递归类型. 当在 Rust 中使用递归类型时, 编译时无法获知其具体占用内存大小(无法在栈上分配, 理论上递归类型可以无限自包含). 下面来看一个具体例子: con list.

con list 抽象表示如下:

(1, (2, (3, Nil)))

即它是一个包含 Value 和 Next 成员的结构体, 而 Next 成员可以没有, 也可以是 con list 类型. 它即是单链表的一种形式.

要在 Rust 中建立这种数据类型, 如果没有 Box, 如下的实现是无法通过编译的:

enum List {
Cons(i32, List),
Nil,
}

无法编译的原因是: 此类型没有一个确定的大小, 在栈上无法分配, 从而也就无法编译(编译器提示 "has infinite size").

这里引入 Box 的话, 则编译器可以知道该类型存储的确切大小(Box 类型栈上仅分配对应该结构体大小数据), 栈上值中包含头指针, 头指针指向的数据在堆上, 运行时分配内存:

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

// ...

// |1,->|2,->|3,Nil| 可以看到, 头指针在栈上, 后续指向的都是Box.
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Nil)))));

通过 Deref 将智能指针作为普通引用使用(自定义解引用 * 操作行为)

https://doc.rust-lang.org/book/ch15-02-deref.html

通过实现 Deref trait, 可以自定义 *(解引用操作符)的行为. 同时, 当为某个智能指针实现 Deref 后, 即可将智能指针直接作为普通引用使用, Rust 会自动对其进行内部处理.

下面首先来看 Deref 在普通引用上实现的方式, 然后通过实现一个和 Box<T> 类型类似的自定义类型, 并进行 Deref 实现, 说明 * 解引用操作符为什么无法在这类类型上像在引用类型上那样工作. 而后说明为什么 Deref trait 能够让智能指针像引用那样工作. 最后我们会看 Rust 的 deref coercion 功能(让我们在引用和智能指针上都可以进行相同形式的操作).

引用类型必须被解引用后才可获取其引用的值

普通引用和指针类似, 指针可以看作是指向某个数据的箭头, 如下我们创建一个队 i32 值的引用, 并使用解引用操作符 * 对其值进行访问.

let x = 5;
let y = &x;

assert_eq!(5, x);
assert_eq!(5, *y);
// 若我们直接将 5 和 y 进行比较, 则会报错, 因为 y 是引用类型, 它的值是地址而非真正引用的 i32 值.

Box<T> 也可像引用一样使用(解引用操作)

我们可以接着上面的例子, 将 Box 作为引用那样使用:

let x = 5;
let y = Box::new(x); // 将 x 复制后, 存放到堆上.
assert_eq!(5, x);
assert_eq!(5, *y); // Box 的解引用操作, 读取到堆上的值, 将智能指针像引用那样使用.

要说明为什么智能指针可以像引用那样使用, 我们建立一个自定义类型 MyBox 进行说明.

MyBox: 自定义智能指针

注意, 为简化例子, 我们不会实现 "堆上分配内存" 的行为.

实际 Box<T> 是一个 tuple struct, 它包含一个成员, 即 T 类型的成员.

而我们自定义的智能指针也是类似的结构:

struct MyBox<T>(T);

impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}

但如果我们想像之前那样去解引用它, 现在还无法达到目的:

let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y); // 报错! 提示无法解引用.

上述代码编译就会报错.

原因是我们没有在 MyBox<T> 上实现自定义的解引用行为, 而 Deref trait 正是为了实现自定义解引用行为服务的.

Deref: 实现自定义解引用行为

Deref 是标准库提供的 trait, 需要我们实现 deref 方法, 传入一个 &self 作为参数, 返回关联类型的引用 &Target:

use std::ops::Deref;

impl<T> Deref for MyBox<T> {
type Target = T;

fn deref(&self) -> &Self::Target {
&self.0
}
}

可以看到我们返回的是一个 &self.0, 因为我们只想返回被包含值的引用, 这样在使用 MyBox 进行解引用 * 操作时, 实际获取到的是内部值 T 的一个引用.

当实现了 Deref trait 后, 之前的代码即可通过编译. 当我们在执行 *y 时, 实际是在执行 *(y.deref()):

  • Deref 返回值的引用, 是因为所有权系统的约束, 我们如果返回值本身, 则实际值会被 move 出 self, 这是我们不希望发生的.
  • y.deref() 获取到的是值的引用, 我们再在其上进行解引用操作 *, 即可读取到值(且解引用不会触发 move, 不会将值 move 出 self, move 只在之前提到的几种情况下才会发生, 比如传参, 返回, 赋值).

Rust 的 Deref Coercions 功能(自动的引用类型链式转换)

还是以上面的 *y 为例, rust 会自动将 *y 解释为 *(y.deref()), 这样在任何场景下使用 *y 都可以自动进行期望的内部值解引用读取. 并且我们还需要注意, 对于实现了 Deref trait 的类型进行解引用操作, 获取到的类型并非该类型本身(如上面的 MyBox<i32> 上进行 * 获取到的是 i32 而非 MyBox).

回到 Deref Coercions 功能上, 此功能的作用是: 自动将一个实现了 Deref 类型的引用转换为另一类型的值, 比如 String 实现了 Deref, 则通过 Deref Coercions 功能, 可以自动将 &String 转换为 &str, 这种转换只会在满足如下条件时发生:

  1. 传参过程中遇到实现了 Deref trait 的类型参数,
  2. 参数是引用
  3. 传入的参数类型和参数类型不匹配(比如需要 &str 而传入的是 &String).
  4. 这种自动转换可以是链式的(最终转换为我们需要的参数类型, 实际就是多次调用 deref 方法的结果)

引入 Deref Coercions 的目的是解放开发者, 这样无需写各种获取引用并解引用的代码:

fn hello(s: &str) {
println!("hello {}", s);
}

// ...

let s = MyBox::new(String::from("world"));
hello(&s); // Deref Coercions 触发: MyBox 实现了 `Deref`, &s 可以自动 deref 获取到其中包含的 &String, 再通过 &String 的 deref 调用获取到 &str.

若 Rust 没有 Deref Coercions, 则我们在实现了 DerefMyBox 传入参数时, 需要像下面这样写:

let s = MyBox::new(String::from("world"));
hello(&(*s)[..]);

上面的代码:

  1. 首先计算 *s, 由于实现了 Deref trait, 实际被转换为 *(s.deref()), 即获取到内部值的引用后, 解引用获取到内部值, 这里就是 String 类型.
  2. 再通过 &[..] 共同作用到 String 上, 获取到对应的完整切片 &str

可见 Deref Coercions 极大减少了复杂代码出现的可能. 就一句话: 可以将实现了 Deref 协议的类型的引用, 作为链式调用 deref 后最终内部值的引用来使用.

关于 Deref Coercions 在 DerefDerefMut 上的引用类型转换行为总结

先简单介绍一下 DerefMut trait: Deref 有一个变体 DerefMut, Deref 获取到的是内部值的不可变引用, DerefMut 获取的是内部值的可变引用, 它需要实现 deref_mut 方法. DerefMut trait 的作用是自定义可变引用解引用时的行为:

let x = MyBox::new(5); // 假设 MyBox 实现了 DerefMut
let y = &mut x;

let z = *y; // 此时会自动调用 *(y.deref_mut)` 获取到内部值的可变引用后解引用获取可变的内部值.

而 Rust 的 Deref Coercions 在 DerefDerefMut 协议实现情况下, 是按如下规则对引用转换操作进行处理的:

  • Deref 普通情况: 当 T 实现了 Deref 且 Target 是 U 时, 可自动将 &T 转换为 &U
  • DerefMut 普通情况: 当 T 实现了 DerefMut 且 Target 是 U 时, 可自动将 &mut T 转换为 &mut U
  • Deref 特殊情况(可变引用转不可变引用): 当 T 实现了 Deref 且 Target 是 U 时, 可自动将 &mut T 转换为 &U

前两种都是正常情况, 实现了 Deref 可自动链式进行不可变引用转换, 实现了 DerefMut 可自动被链式进行可变引用转换. 第三种特殊情况是实现了 Deref 可自动将可变引用链式转换为不可变引用(需要注意此情况下无法自动进行不可变引用向可变引用的转换, 因为 Rust 无法知道是否这样的转换会破坏 borrow 规则).

使用 Drop trait 自定义值的释放行为

https://doc.rust-lang.org/book/ch15-03-drop.html

Drop trait 用于自定义当变量出作用域时候的行为, 在任意类型上均可实现 Drop, 比如可以在出作用域时通过 Drop 释放被占用的资源, 包括打开的文件或数据库连接等.

合着智能指针一起看 Drop trait 的原因是: 几乎所有智能指针实现时都会实现 Drop 来释放资源. 如 Box 就会在出作用域后, 在 Drop 的实现中将堆上内存释放.

某些语言中, 需要开发者手动释放资源, 但在 Rust 中通过 Drop 即可实现资源的自动释放, 在代码里再也不用看到到处是资源释放代码了, 且可以自信地认为资源使用完成后肯定是被正确释放了的.

Drop 需要实现的是 drop 方法, 它接受一个 &mut self 作为参数, 即可以改变 self 的内部状态.

当变量出作用域后, Rust 会自动调用 drop 方法, 而 drop 顺序规则可以通过下面的例子来看:

struct CustomSmartPointer {
data: String,
}

impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}

// ...

{// 单独的一个块作用域
let c = CustomSmartPointer {
data: String::from("my stuff"),
};
let d = CustomSmartPointer {
data: String::from("other stuff"),
};
println!("CustomSmartPointers created.");
}

上述代码执行后, 打印的顺序如下: 实际反映的是栈上变量的出栈顺序, 比如上面的代码中, 变量 d 是在栈顶的, 会先出栈.

  1. 先打印 CustomSmartPointers created., 因为此时两个变量均未出作用域, 不会开始释放.
  2. 而后打印变量 d 对应的 other stuff
  3. 再打印变量 c 对应的 my stuff

需要注意的是: Rust 不允许手动调用 drop, drop 必定是 Rust 自动触发的, 要控制资源释放时机, 可以看下面的方式.

关于手动控制提前释放资源: std::mem::drop 函数

通过手动调用 std::mem::drop, 我们可以自定义资源释放时机, 调用该函数实际是在触发 drop, 且 rust 不会再在后面重复做 drop 动作了(否则会出现 double free)

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

先回答这个问题: 为什么要使用引用计数指针?

为了实现这个场景: 在概念上一个值可能有多个所有者的情况下, 如果没有 Rc, 则此场景无法实现. 比如要实现一个图, 顶点是被多条边共享的, 只有在边全部没有了后, 该顶点才可以被释放.

使用 Rc<T> 后, 值的实际 Owner 是该 Rc(匹配 Rust 的 Ownership 规则), 在 Rc 中, 会记录下 "概念上的所有者" 个数, 只有在该个数变为 0 后, 对应的值才会被释放.

通常我们会通过 Rc 在 heap 分配内存存放数据, 分享给程序中其他部分只读访问, 此时就不存在值被 move 的情况了, 这种和引用的效果一样, 且可以保证值不会被意外释放, 其他部分靠增加引用计数而得到访问权, 访问者增加, 意味着引用计数 +1. 访问者出作用域被释放, 意味着引用计数 -1. 当引用计数变为 0 后, 值才会被释放.

Rc 非线程安全, 即我们只能将它用在单线程环境中, 因为内部没有提供多线程同步的相关代码, 但也因这样性能会很高.

比如下面的代码:

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

习惯使用 Rc::clone 进行 Rc 的共享, 而使用 clone 方法进行深拷贝, 这样可以很容易在代码区分二者, 到底是深拷贝还是引用计数增加

结点逻辑结构如下:

通过 Rcstrong_countweak_count 可以清楚知道当前的引用计数个数.

此外需要注意: Rc 提供的是只读引用的访问, 要提供读写引用访问, 需要用到 RefCell, 下面就来看看 RefCell.

内部可变模式: RefCell<T>

https://doc.rust-lang.org/book/ch15-05-interior-mutability.html

内部可变模式指的是在 Rust 中即便是只获取到了值的不可变引用, 也可以对其进行改变. 一般来说这个操作在 Rust 中是被 borrow rule 禁止的, 为了改变数据, 内部可变模式使用 unsafe 代码来绕过 Rust 的 borrow 和 mutation 管理. unsafe 意味着编译器不会对这些代码在编译时进行检查.

我们可以使用一些实现了上面规则的类型, 让 Rust 在运行时检查 borrow 规则, 这些类型提供了 safe 的接口来包裹 unsafe 代码, 而 RefCell<T> 就是一个典型例子.

RefCell 仍然表示单一所有权, 同为智能指针, 它和 Box 有什么区别呢? 当使用 Box 并获取到引用时, Rust 的 borrow 规则在编译时就能得到保障, 而使用 RefCell 时, borrow 规则是在运行时去保证的. 当使用 RefCell 提供的引用且 borrow 规则被违反, 在运行时程序会 panic 而不会出现未知行为.

在编译时检查 borrow 规则的好处是和引用有关的错误可以在开发过程中去修复, 因此这样的做法是最佳的选择. 但某些场景下不允许我们实现在编译时检查的代码结构(必须使用内部可变模式的情况下, 若仍然使用编译时检查, 则可能原本正确的逻辑代码会被编译器拒绝), 就必须要在编译时进行 borrow 规则保证了. RefCell 即可用在当你自己确信代码是满足 borrow 规则但编译器无法理解这类代码时的场景.

Rc 类似, RefCell 也只能在单线程环境下保证安全, 不同线程共享 RefCell 无法通过编译.

下面来看一些使用 RefCell 实现内部可变模式的例子:

内部可变模式: 不可变值的可变引用

下面的代码无法通过编译, 因无法在不可变值上创建可变引用:

let x = 5;
let y = &mut x; //无法通过编译

但有时需要通过某个类型对象提供的方法, 改变它内部的值, 而其它部分的代码则无法直接改变其内部值时(类似 setter), 此时就可以使用 RefCell 达到目的.

下面来看一个通过 RefCell 实现内部可变模式的实际例子.

内部可变模式实例: Mock 对象

比如在单元测试时, 经常用到 Mock 对象, 这类对象提供模拟接口的能力, 替换真实依赖以观察待测对象行为是否符合预期.

而 Mock 对象的一个特点就是它能够记录下测试过程中经过待测对象操作后所发生的变化(数据或动作).

假设我们想实现一个 Mock 类型, 用于模拟我们库中的类型, 该类型可以判断当前值和最大值的差异, 并根据当前值和最大值间的接近程度发送对应消息. 比如用它可以记录用户 API 的使用配额, 接近限制或达到限制则发送消息.

根据这个类型的功能, 我们可以定义一个 trait Messenger, 它提供 send 接口用于发送消息, 而消息发送时机/发送方式/发送内容都可以自定义:

/// Messenger 接口: 用于发送消息
pub trait Messenger {
fn send(&self, msg: &str);
}

/// 限制追踪器: 内部依赖实现了 Messenger 接口的对象引用
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T, // 接口依赖
value: usize,
limit: usize,
}

impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, limit: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
limit,
}
}

pub fn set_value(&mut self, value: usize) {
self.value = value;
let percent_of_max = self.value as f64 / self.limit as f64;
if percent_of_max >= 1.0 {
self.messenger.send("超限!!");
} else if percent_of_max >= 0.9 {
self.messenger.send("快超限了: 90% !!");
} else if percent_of_max >= 0.75 {
self.messenger.send("离超限很近了: 75% !!");
}
}
}

上述代码中, 在 LimitTrackset_value 方法中, 我们没有返回任何可以表征状态的结果, 这样一来在测试时不易对结果进行假设测试. 此时就可以用到 Mock 对象, 测试时, 输入对应数值, 记录下发送的内容是什么, 这样就能进行结果断言了:

#[cfg(test)]
mod tests {
use super::*;

/// Mock 对象, 用于记录被发送的消息, 用作依赖模拟, 实现 Messenger trait.
struct MockMessenger {
sent_messages: Vec<String>,
}

impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: vec![],
}
}
}

impl Messenger for MockMessenger {
fn send(&self, msg: &str) {
// 无法通过编译, 原因是 send 方法给到的是 self 不可变引用, 此时无法对 sent_messages 进行任何修改.
self.sent_messages.push(String::from(msg)); // !! 无法编译
}
}

#[test]
fn it_sends_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.len(), 1);
}
}

可以看到上述代码中, MockMessenger 的 send 接口实现方法无法通过编译, 原因是传入 self 的不可变引用, 无法改变 self 的成员. 此时我们就可以使用 RefCell 引入内部可变, 从而在只拿到不可变引用时仍然可以改变其内部值:

// 首先将上方 MockMessenger 中记录消息发送的 sent_messages 成员包裹在 RefCell 中:
struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}
//...
// 然后 MockMessenger 的 new 函数对应修改:
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
// ...
// send 方法实现中, 就可以使用内部可变模式了: 调用 RefCell 的 borrow_mut 获取内部成员的可变引用(即使自己是不可变的)
fn send(&self, msg: &str) {
// 使用 borrow_mut 时, 我们是知道运行时合法: 单线程环境下不会同时出现其他地方去获取该 RefCell 的可变引用, 且此时也不会有另外的地方在读取其不可变引用.
self.sent_messages.borrow_mut().push(String::from(msg));
}
// ...
// 最后我们的测试方法中, 我们同样使用 borrow 获取到不可变引用: 我们知道执行这句话时, 也不会有任何其他地方有可变引用的写.
#[test]
fn it_sends_over_75_percent_warning_message() {
// ...
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}

使用 RefCell 即可达到目的, 详细改动见上方注释. 下面我们来看看 RefCell 的原理.

RefCell 原理: 运行时保持 borrow 的追踪

和使用 &&mut 创建不可变/可变引用类似, RefCell 对应提供 borrowborrow_mut 方法, borrow 返回 Ref<T>, borrow_mut 返回 RefMut<T>, 这两个返回类型均实现了 DeRef trait, 这样可以将它们看作是普通的引用使用.

RefCell 会记录下其上被创建的正在使用的不可变/可变引用个数, 比如调用 borrow 得到 Ref<T> 后, 不可变引用的个数 +1, 当被创建的 Ref<T> 出作用域被 Drop 后, 不可变引用个数 -1, 而 RefMut<T> 创建后也同理. 这样就可以在运行时保证和 borrow checker 一致的规则检查, 即多个不可变引用或一个可变引用的存在检查.

当 borrow 规则被违反后, RefCell 就会触发 panic, 比如下面的代码:

fn send(&self, message: &str) {
// 下面的代码会触发 panic, 原因是同时存在两个可变引用, panic 信息类似 "already borrowed: BorrowMutError"
let mut one_borrow = self.sent_messages.borrow_mut();
let mut two_borrow = self.sent_messages.borrow_mut();
// ...
}

技巧: 结合 RcRefCell 实现概念多所有权下的内部可变模式

还是回到之前说的图顶点被多条边"拥有", 多条边都可以修改顶点的情况下, 为保证代码可读和可维护, 牺牲少许性能获取更简单直接的设计是十分划算的: 结合内部可变模式去实现一些顶点方法(类似其中的 setter).

引用计数造成内存泄露: 循环引用问题

https://doc.rust-lang.org/book/ch15-06-reference-cycles.html

Rust 会尽最大努力确保内存安全, 但在某些情况下, 我们可能自己造成内存泄露, 而内存泄露在 Rust 并非内存安全问题, 因在某些时候需要我们去泄露内存确保值一直存在于内存中.

但在使用 Rc 的过程中, 我们想要的是值在引用个数变为 0 的时候被自动释放, 但可能存在循环引用导致值无法被释放(当然前提是内存是在堆上分配的). 下面就来看看循环引用是什么, 以及如何避免循环引用.

下面这个例子, 构造了一个循环引用:

use std::{cell::RefCell, rc::Rc};

#[derive(Debug)]
pub enum List {
Cons(i32, RefCell<Rc<List>>),
Nil,
}

impl List {
fn tail(&self) -> Option<&RefCell<Rc<List>>> {
match self {
List::Cons(_, ref_cell) => Some(ref_cell),
List::Nil => None,
}
}

pub fn create_ref_circle() {
let a = Rc::new(List::Cons(1, RefCell::new(Rc::new(List::Nil))));
println!("a initial rc count = {}", Rc::strong_count(&a));
println!("a next item = {:?}", a.tail());

let b = Rc::new(List::Cons(2, RefCell::new(Rc::clone(&a))));

println!("a rc count after b creation = {}", Rc::strong_count(&a));
println!("b initial rc count = {}", Rc::strong_count(&b));
println!("b next item = {:?}", b.tail());

// 将 a 和 b 反向连接起来
if let Some(link) = a.tail() {
*link.borrow_mut() = Rc::clone(&b);
}
println!("b rc count after changing a = {}", Rc::strong_count(&b));
println!("a rc count after changing a = {}", Rc::strong_count(&a));

// println!("a tail = {:?}", a.tail());
}
}

上面的例子中, 构造了一个 b 指向 a, 然后 a 的 tail 又被修改指向到 b. 这样就构造了一个循环引用. 要解决也比较简单, 就是在循环的一条边修改为 Weak 即可, 实际用到的是 Rc::downgrade 获取 Weak 实例, 它会引起 Rc 的 weak 引用计数 +1, 但 weak 引用计数只是为了保证 borrow 规则, 不会对最终释放造成影响.

下面来看一个树的实现例子, 详细看看如何通过 Weak 引用避免造成引用循环.

实现一个多叉树数据结构

我们首先定义树结点并实现一些典型方法/关联函数:

use std::{
cell::RefCell,
rc::{Rc, Weak},
};

type WeakNode<T> = Weak<Node<T>>;
type StrongNode<T> = Rc<Node<T>>;

#[derive(Debug)]
pub struct Node<T> {
pub value: T,
pub parent: RefCell<Option<WeakNode<T>>>,
pub children: RefCell<Option<Vec<StrongNode<T>>>>,
}

impl<T> Node<T> {
pub fn new(
value: T,
parent: Option<WeakNode<T>>,
children: Option<Vec<StrongNode<T>>>,
) -> Node<T> {
Node {
value,
parent: RefCell::new(parent),
children: RefCell::new(children),
}
}

pub fn set_parent(&self, parent: WeakNode<T>) {
*self.parent.borrow_mut() = Some(parent);
}

pub fn set_children(&self, children: Vec<StrongNode<T>>) {
*self.children.borrow_mut() = Some(children);
}

pub fn add_child(&self, child: StrongNode<T>) {
self.children
.borrow_mut()
// 没有创建 Vec 则自动创建
.get_or_insert(Default::default())
.push(child);
}

pub fn get_value(&self) -> &T {
&self.value
}
}

如下是使用实例:

use std::rc::Rc;

use node::Node;

mod node;

pub fn create_tree() {
// 创建两个结点, 然后定义它们二者的关系

let root = Rc::new(Node::new(11, None, None));
let leaf = Rc::new(Node::new(32, None, None));
leaf.set_parent(Rc::downgrade(&root));
root.add_child(Rc::clone(&leaf));
leaf.set_children(Vec::new()); //仅测试

let p = leaf.parent.borrow();
if let Some(p) = p.as_ref() {
// Weak 必须进行 upgrade 才能够获取到可能的值, 因为它并不知道是否对应的内存已被释放. upgrade 会返回一个 Option
if let Some(node) = p.upgrade() {
println!("node value: {}", node.get_value());
}
}
}

上述即使用 Rc + RefCell 内部可变模式实现的一个多叉树结构. 这个结构已固定所有结点均存放在堆上. 因上述结构是一个递归数据结构(自己包含自己), 因此无法避免在堆上分配内存.