4

The Rust programming language 读书笔记——智能指针

 2 years ago
source link: https://www.starky.ltd/2021/07/26/the-rust-programming-language-reading-notes-smart-pointer/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

The Rust programming language 读书笔记——智能指针

发表于 2021-07-26

| 分类于 Program

| 0

| 阅读次数: 2

字数统计: 20k

|

阅读时长 ≈ 0:20

指针(pointer)是一个通用概念,用来指代那些包含内存地址的变量。这些地址“指向”内存中的其他数据。
Rust 中最常见的指针是引用,会借用它所指向的数据。除此之外,引用没有任何其他功能和额外的开销。
智能指针(smart pointer)是一种数据结构,它的行为类似于指针但拥有额外的元数据和附加功能

在拥有所有权和借用概念的 Rust 中:引用只是用来借用数据的指针;而大多数智能指针本身就拥有它们指向的值
比如 StringVec<T> 类型就都可以被算作智能指针。它们都拥有一片内存区域并允许用户对其进行操作,拥有元数据(如容量等),能提供额外的功能或保障(如 String 会保证其中的数据必是合法的 UTF-8 编码)。

通常使用结构体来实现智能指针。区别于普通的结构体,智能指针会实现 DerefDrop 这两个 trait。Deref trait 使得智能指针的实例拥有与引用一致的行为;Drop trait 使得用户可以自定义智能指针离开作用域时运行的代码。

标准库中最为常见的智能指针如下:

  • Box<T>:可用于在堆上分配数据
  • Rc<T>:具备多重所有权的引用计数类型
  • Ref<T>ReMut<T>:可以通过 RefCell<T> 访问,是一种可以在运行时而不是编译时执行借用规则的类型

使用 Box<T> 在堆上分配数据

装箱(box)是最简单的一种智能指针,其类型为 Box<T>。它使我们可以将数据存储在堆上,并在栈中保留一个指向堆数据的指针。

装箱常被用于以下场景:

  • 拥有一个无法在编译时确定大小的类型,但又想在一个要求固定尺寸的上下文环境中使用这个类型
  • 需要传递大量数据的所有权,但又不希望产生大量数据的复制行为
  • 希望拥有一个实现了指定 trait 的类型值,但又不关心具体的类型

使用装箱在堆上存储一个 i32 值:

fn main() {
let b = Box::new(5);
println!("b = {}", b);
}

将单一值存放在堆上没有太大用处,大部分情况下都可以将类似的单个 i32 值默认放置在栈上。

使用装箱定义递归类型

Rust 必须在编译时知道每一种类型占据的空间大小,但有一种被称为递归的类型无法在编译时确定具体大小。
递归类型的值可以在自身中存储另一个相同类型的值,这种嵌套理论上可以无穷无尽地进行下去,根本无法计算出具体的空间大小。

链接列表(cons list)是一种在函数式语言中非常常见的数据类型。cons 函数会将两个参数组成一个二元组,而这个元组通常由一个值和另一个二元组组成,通过这种不断嵌套元组的形式最终组成一个列表。
使用枚举来定义一个链接列表:

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

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

fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil)));
}

上述代码在编译时会报出 error[E0072]: recursive type `List` has infinite size 错误。

Rust 在计算枚举类型需要的空间大小时,会遍历枚举中的每一个成员来找到需要最大空间的那个变体。因为在每个时间点,只会有一个变体存在。
对于递归类型大小的计算,以前面的 List 为例,编译器会先检查 Cons 变体,它持有一个 i32 类型的值及另外一个 List 类型;为了确定此处List 的大小,编译器又会从 Cons 开始遍历其下的所有变体,这样的检查将永远进行下去。
包含无穷多 Cons 变体的无穷 List

可以使用 Box<T> 将递归类型的大小固定下来。
Box<T> 是一个指针,其大小总是恒定的,不会因为指向数据的大小而发生变化。我们可以在 Cons 变体中存放一个 Box<T> 指针,Box<T> 指向下一个存储在堆上的 List。即嵌套的 List 并没有直接存放在 Cons 变体中,而是放置在堆上,打破了无限递归的过程。
此时任意的 List 值都只需要占用一个 i32 值加上一个装箱指针的大小。
Box in Cons

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

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

fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}

装箱除了间接访问内存和堆分配,没有提供任何其他的特殊功能,也没有这些特殊功能附带的性能开销。因此正好能被用在类似于链接列表这类只是需要间接访问的场景中。

通过 Deref trait 将智能指针视作常规引用

实现 Deref trait 使我们可以自定义解引用运算符 * 的行为,这意味着原本用于处理引用的代码可以不加修改地用于处理智能指针。

使用解引用跳转到指针指向的值

指针可以被理解为一种箭头,会指向存储在别处的值。

fn main() {
let x = 5;
let y = &x;

assert_eq!(5, x);
assert_eq!(5, *y);
}

数字和引用是两种不同的类型,所以不能直接比较 5y。必须使用 *y 来跳转到引用指向的值。

Box<T> 当成引用来操作
fn main() {
let x = 5;
let y = Box::new(x);

assert_eq!(5, x);
assert_eq!(5, *y);
}
自定义智能指针
struct MyBox<T>(T);

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

use std::ops::Deref;

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

fn deref(&self) -> &T {
&self.0
}
}

fn main() {
let x = 5;
let y = MyBox::new(x);

assert_eq!(5, x);
assert_eq!(5, *y);
}

上述代码中的 MyBox 是一个拥有 T 类型的元组结构体,其关联函数 MyBox::new 接收一个 T 类型的参数,并返回一个存储有传入值的 MyBox 实例作为结果。

为了为 MyBox<T> 类型实现解引用功能,代码中实现了 Deref trait
标准库中的 Deref trait 要求我们实现一个 deref 方法,该方法会借用 self 并返回一个指向内部数据的引用。
deref 方法体中的 &self.0 意味着 deref 会返回一个指向值的引用,进而允许调用者通过 * 运算符访问值。
代码中的 *y 会被 Rust 隐式地展开为 *(y.deref())。使得我们可以用完全相同的方式编写代码来处理常规引用及实现了 Deref trait 的类型。

函数和方法的隐式解引用转换

解引用转换是 Rust 为函数和方法的参数提供的一种编程特性。当某个类型 T 实现了 Deref trait 时,它能够将 T 的引用转换为 T 经过 Deref 操作后生成的引用。
当我们将某个类型的值引用作为参数传递给类型或方法,但传入的类型与参数类型不一致时,解引用转换就会自动发生。编译器会插入一系列的 deref 方法来将我们提供的类型转换为参数所需的类型。

解引用转换使程序员在调用函数或方法时无需多次显式地使用 &* 操作符来进行引用和解引用操作。我们因而可以更多地编写出能够同时作用于常规引用和智能指针的代码。

struct MyBox<T>(T);

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

use std::ops::Deref;

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

fn deref(&self) -> &T {
&self.0
}
}

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

fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
// => Hello, Rust!
hello(&(*m)[..]);
// => Hello, Rust!
}

&m 是一个指向 MyBox<String> 值的引用。因为 MyBox<T> 实现了 Deref trait,Rust 可以通过调用 deref 将 &MyBox<String> 转换为 String;因为标准库为 String 提供的 Deref 实现会返回字符串切片,所以 Rust 可以继续调用 deref 将 &String 转换为 &str,最终与 hello 函数的定义匹配。

解引用转换与可变性

使用 Deref trait 能够重载不可变引用的 * 运算符,使用 DerefMut trait 能够重载可变引用的 * 运算符。

Rust 会在类型与 trait 满足下面 3 种情况时执行解引用转换:

  • T: Deref<Target=U> 时,允许 &T 转换为 &U
  • T: DerefMut<Target=U> 时,允许 &mut T 转换为 &mut U
  • T: Deref<Target=U> 时,允许 &mut T 转换为 &U

借助 Drop trait 在清理时运行代码

Drop trait 允许我们在变量离开作用域时执行某些自定义操作。可以为任意类型实现一个 Drop trait,它常常被用来释放诸如文件、网络连接等资源。
几乎每一种智能指针的实现都会用到这一 trait,比如 Box<T> 通过自定义 Drop 来释放装箱指针指向的堆内存空间。

struct CustomSmartPointer {
data: String,
}

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

fn main() {
let c = CustomSmartPointer {
data: String::from("my stuff"),
};
let d = CustomSmartPointer {
data: String::from("other stuff"),
};
println!("CustomSmartPointer created.")
}

// => CustomSmartPointer created.
// => Dropping CustomSmartPointer with data `other stuff`!
// => Dropping CustomSmartPointer with data `my stuff`!
使用 std::mem::drop 提前丢弃值
struct CustomSmartPointer {
data: String,
}

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

fn main() {
let c = CustomSmartPointer {
data: String::from("some data"),
};
println!("CustomSmartPointer created.");
drop(c);
println!("CustomSmartPointer dropped before the end of main");
}

// => CustomSmartPointer created.
// => Dropping CustomSmartPointer with data `some data`!
// => CustomSmartPointer dropped before the end of main

基于引用计数的智能指针 Rc

所有权在大多数情况下都是清晰的,对于一个给定的值,可以准确地判断出哪个变量拥有它。
但在某些场景中,单个值也可能同时被多个所有者持有。
比如在图数据结构中,多个边可能会指向相同的节点,这个节点同时属于所有指向它的边。一个节点只要在任意指向它的边还存在时就不应该被清理掉。
Rust 提供了一种名为 Rc<T> 的类型来支持多重所有权。Rc<T> 类型的实例会在内部维护一个用于记录值引用次数的计数器,从而确认这个值是否仍在使用。若对一个值的引用次数为零,就意味着这个值可以被安全地清理掉。

当你希望将堆上的一些数据分享给程序的多个部分同时使用,而又无法在编译期确定哪个部分会最后释放这些数据时,就可以使用 Rc<T> 类型。
相反地,若我们能够在编译期确定哪一部分最后会释放数据,那么就只需要让这部分代码成为数据的所有者即可。
Rc<T> 只能被用于单线程场景中

使用 Rc<T> 共享数据

创建两个链接列表,并让它们同时持有第三个列表的所有权。
Cons List

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

尝试编译上述代码会报出 error[E0382]: use of moved value: a 错误。原因是 a 列表会在创建 b 列表时被移动至 b 中。即 b 列表持有了 a 列表的所有权。随后再次尝试使用 a 来创建 c 列表时就会出现编译错误,因为 a 已经被移走了。

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

上述代码中,每个 Cons 变体都持有一个值及一个指向 List 的 Rc<T>。我们只需要在创建 b 的过程中克隆 aRc<List> 智能指针即可,无需获取 a 的所有权。
这使得 ab 可以共享 Rc<List> 数据的所有权,并使智能指针中的引用计数从 1 增加到 2。随后创建 c 时也会同样克隆 a 并将引用计数从 2 增加到 3。
每次调用 Rc::clone 都会使引用计数增加,而 Rc<List> 中的数据只有在引用计数器减少到 0 时才会被真正清理掉。

克隆 Rc<T> 会增加引用计数
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)))));
println!("count after creating a = {}", Rc::strong_count(&a));
let b = Cons(4, 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));
}

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

在上面的代码中,a 存储的 Rc<List> 拥有初始引用计数 1,并在随后每次调用 clone 时增加 1。当 c 离开作用域被丢弃时,引用计数减少 1。Rc<T>Drop 实现会在 Rc<T> 离开作用域时自动将引用计数减 1。

使用 Rc<T> 可以使单个值拥有多个所有者,而引用计数机制则保证了这个值会在其所有者存活时一直有效,并在所有者全部离开作用域时被自动清理。
Rc<T> 通过不可变引用使你可以在程序的不同部分之间共享只读数据。但如果 Rc<T> 也允许持有多个可变引用的话,就会违反一个借用原则:多个指向同一区域的可变借用会导致数据竞争及数据不一致。
但在实际开发中,允许数据可变是非常有用的。实际上可以通过 RefCell<T>Rc<T> 联合使用来绕开不可变的限制。

RefCell 和内部可变性模式

内部可变性 是 Rust 的设计模式之一,它允许你在只持有不可变引用的前提下对数据进行修改。为了能够改变数据,此模式在它的数据结构中使用了 unsafe 代码来绕过 Rust 正常的可变性和借用规则。
假如我们能够保证自己的代码在运行时符合借用规则,就可以在即使编译器无法在编译阶段保证符合借用规则的前提下,使用那些采用了内部可变性模式的类型。实现过程中涉及的那些不安全代码会被妥善地封装在安全的 API 内,而类型本身从外部看依然是不可变的。

使用 RefCell<T> 在运行时检查借用规则

Rust 中的借用规则如下:

  • 在任何给定的时间内,只能拥有一个可变引用或者任意数量的不可变引用
  • 引用总是有效的

对于一般引用和 Box<T> 的代码,Rust 会在编译阶段强制代码遵守这些借用规则。而对于使用 RefCell<T> 的代码,Rust 只会在运行时检查这些规则,并在违反的时候触发 panic 来提前终止程序。

借用规则的检查放在编译阶段不仅会帮助我们在开发阶段尽早暴露问题,并且不会带来任何运行时开销。对于大多数场景都是最佳的选择。
在运行时检查借用规则可以使我们实现某些特定的内存安全场景,即便这些场景无法通过编译时检查。因为某些静态分析是根本无法完成的。这类编译器无法理解代码,但开发者可以保证借用规则能够满足的场景,就适用于 RefCell<T>

可变地借用一个不可变的值

内部可变性模式允许用户更改一个不可变值的内部数据
借用规则限制用户可变地借用一个不可变的值,如下面的代码无法通过编译:

fn main() {
let x = 5;
let y = &mut x;
}

但是在某些情况下,我们也需要一个值对外保持不可变性的同时能够在方法内部修改自身。除了这个值本身的方法,其余的代码仍不能修改这个值。
使用 RefCell<T> 可以获得这种内部可变性。
这种内部可变性的机制把借用规则的检查从编译期延后到了运行阶段,若违反了借用规则,只会在运行时触发 panic!

内部可变性的应用场景:模拟对象
测试替代是一种通用的编程概念,代表了那些在测试工作中被用作其他类型替代品的类型。而模拟对象则指代了测试替代中某些特定的类型,会承担起记录测试过程的工作。

Rust 没有和其他语言中类似的对象概念,也没有在标准库中提供模拟对象的测试功能。但是可以自定义一个结构体来实现与模拟对象相同的效果。
比如我们希望开发一个库,会基于当前值与最大值之间的接近程度向外传递信息。比如可以记录用户调用不同 API 的次数,并与设置的调用限额作比较。
使用这个库的应用程序需要自行实现发送消息的功能,例如在应用程序中打印信息、发送邮件、发送文字短信等。
源代码如下:

pub trait Messenger {
fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: 'a + Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}

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

pub fn set_value(&mut self, value: usize) {
self.value = value;

let percentage_of_max = self.value as f64 / self.max as f64;

if percentage_of_max >= 1.0 {
self.messenger.send("Error: You're over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used 75% of your quota!");
}
}
}

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

struct MockMessenger {
sent_messages: Vec<String>,
}

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

impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.push(String::from(message));
}
}

#[test]
fn it_sends_an_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);
}
}

其中 Messenger trait 的 send 方法可以接收 self 的不可变引用及一条文本消息作为参数。我们需要在测试中确定的是,当某段程序使用一个实现了 Messenger trait 的模拟对象与一个 max 值来创建 LimitTracker 实例时,传入的不同 value 值能够触发 messenger 发送不同的信息。
此处的模拟对象 MockMessenger 在调用 send 时只需要将收到的消息存档记录即可,不需要真的去发送邮件或短信。使用模拟对象创建 LimitTracker 实例后,就可以通过调用 set_value 方法检查模拟对象中是否存储了我们希望见到的消息。

尝试编译(cargo test)上述代码会报出如下错误:
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference

send 方法接收了 self 的不可变引用,因此我们无法通过修改 MockMessenger 的内容来记录消息。我们也不能将函数签名修改为 &mut self,因为修改后的签名与 Messenger trait 定义的 send 签名不符。
此时就是一个非常适合内部可变性的场景。只要在 RefCell<T> 中存入 sent_messagessend 方法就能够修改 sent_messages

修改后的代码如下:

pub trait Messenger {
fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: 'a + Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}

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

pub fn set_value(&mut self, value: usize) {
self.value = value;

let percentage_of_max = self.value as f64 / self.max as f64;

if percentage_of_max >= 1.0 {
self.messenger.send("Error: You're over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used 75% of your quota!");
}
}
}

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

struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}

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

impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.borrow_mut().push(String::from(message));
}
}

#[test]
fn it_sends_an_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.borrow().len(), 1);
}
}

其中 sent_messages 字段的类型变为了 RefCell<Vec<String>>。对于 send 方法的实现,其第一个参数依然是 self 的不可变借用,与 trait 的定义保持一致。后面的代码调用 self.messages.borrow_mut 方法获取 RefCell<Vec<String>> 内部值(也就是动态数组)的可变引用,再调用其 push 方法存入数据。
在断言语句中,调用了 RefCell<Vec<String>>borrow 方法来取得动态数组的不可变引用。

对于 RefCell<T> 而言,我们可以使用 borrowborrow_mut 分别创建不可变和可变引用。这两个方法会分别返回 Ref<T>RefMut<T> 两种智能指针,可以被当作一般的引用来对待。
RefCell<T> 会记录当前存在多少个活跃的 Ref<T>RefMut<T>,维护和编译器同样的借用规则:任何给定的时间都只允许拥有多个不可变借用或一个可变借用。
当借用规则被违背时,RefCell<T> 会在运行时触发 panic。

Rc<T>RefCell<T> 实现一个拥有多重所有权的可变数据

Rc<T> 允许多个所有者持有同一数据,但只能提供针对数据的不可变访问。若在 Rc<T> 内存储了 RefCell<T>,就可以定义出拥有多个所有者且能够进行修改的值了。

Cons 定义中使用 RefCell<T> 来实现修改现有列表内数值的功能:

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

use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

fn main() {
let value = Rc::new(RefCell::new(5));
let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));
let b = Cons(Rc::new(RefCell::new(6)), Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(10)), Rc::clone(&a));

*value.borrow_mut() += 10;

println!("a after = {:?}", a);
println!("b after = {:?}", b);
println!("c after = {:?}", c);
}

循环引用与内存泄漏

与数据竞争不同,在编译期彻底防止内存泄漏并不是 Rust 做出的保证。这意味着内存泄漏在 Rust 中是一种内存安全行为。
可以通过使用 Rc<T>RefCell<T> 创建出相互引用成环状的实例。由于环中每一个指针的引用计数都不可能减少到 0,对应的值也不会被释放丢弃,最终造成内存泄漏。

代码中先创建了一个 Rc<List> 实例并存储至变量 a,其中的 List 初始值为 5, Nil
之后又创建了一个 Rc<List> 实例并存储至变量 b,其中的 List 包含数值 10 及指向列表 a 的指针。
接着将 a 指向的下一个元素 Nil 修改为 b,此时即创建出了循环引用。

在完成 a 指向 b 的操作后,这两个 Rc<List> 实例的引用计数都变为了 2。而在 main 函数结尾处,Rust 会首先释放 b,并使 b 存储的 Rc<List> 实例的引用计数减少 1。
但由于 a 仍然有一个指向 bRc<List> 的引用,这个 Rc<List> 的引用计数仍然是 1 而不是 0。因此该 Rc<List> 在堆上的内存不会被释放,这块内存会永远以引用计数为 1 的状态保留在堆上。
a 和 b 相互指向的循环引用

假如去除最后一行 println! 的注释并再次运行程序,Rust 会在尝试将循环引用打印出来的过程中反复地从 a 跳转到 b,再从 b 跳转至 a,直到发生栈溢出为止。

如果程序中存在 RefCell<T> 包含 Rc<T> 或其他联用了内部可变性与引用计数指针的情形,就需要自行确保不会在代码中创建出循环引用。
创建出循环引用意味着代码逻辑有 bug,可以通过自动化测试及其他软件开发手段来尽可能地避免。

使用 Weak<T> 替代 Rc<T> 来避免循环引用

调用 Rc::clone 会增加 Rc<T> 实例的 strong_count 引用计数,而 Rc<T> 实例只有在 strong_count 为 0 时才会被清理。
除此之外,我们还可以调用 Rc::downgrade 函数来创建 Rc<T> 实例中值的弱引用。使用 Rc<T> 的引用来调用 Rc::downgrade 会返回一个类型为 Weak<T> 的智能指针,这一操作会让 Rc<T>weak_count 计数增加 1。Rc<T> 类型使用 weak_count 来记录当前存在多少个 Weak<T> 引用,但不会在执行清理操作前要求 weak_count 必须为 0。
强引用可以被用来共享一个 Rc<T> 实例的所有权,而弱引用则不会表达所有权关系。一旦强引用计数为 0,任何由弱引用组成的循环就会被打破。弱引用不会造成循环引用
我们无法确定 Weak<T> 引用的值是否已经被释放,因此需要在使用 Weak<T> 指向的值之前确保它依然存在。可以调用 Weak<T> 实例的 upgrade 方法来完成这一验证。此函数返回的 Option<Rc<T>> 会在 Rc<T> 值依然存在时表达为 Some,在 Rc<T> 值被释放时表达为 None。Rust 能够保证 SomeNone 两个分支都得到妥善的处理,不会产生无效指针之类的问题。

创建树状结构体:带有子节点的 Node

源代码如下:

use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
struct Node {
value: i32,
children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
let leaf = Rc::new(Node {
value: 3,
children: RefCell::new(vec![]),
});

let branch = Rc::new(Node {
value: 5,
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
}

我们希望 Node 持有自身所有子节点并通过变量来共享它们的所有权,从而可以直接访问树中的每个 Node。因此将 Vec<T> 的元素定义为 Rc<Node> 类型的值。
我们还希望在 children 字段中使用 RefCell<T> 包裹 Vec<Rc<Node>> 来实现内部可变性。
我们使用上述结构体定义一个值为 3 且没有子节点的 Node 实例,并将其作为叶子节点存入 leaf 变量。再定义一个值为 5 且将 leaf 作为子节点的 branch 实例。接着克隆 leafRc<Node> 实例,并将其存入 branch
此时我们可以使用 branch.children 来从 branch 访问 leaf,但是反之则不行。因为 leaf 并不持有 branch 的引用,它甚至对两个节点之间存在父子关系一无所知。

增加子节点指向父节点的引用

为了让子节点意识到父节点的存在,可以为 Node 结构体添加一个 parent 字段。但 parent 的类型不能是 Rc<T>,会创建循环引用。branch.children 指向 leaf 的同时使 leaf.parent 指向 branch 会导致两者的 strong_count 都无法清零。

父节点自然拥有子节点的所有权,因为父节点被丢弃时,子节点也应该随之被丢弃;但子节点却不应该拥有父节点的所有权,即父节点的存在不会因为丢弃子节点而受到影响。
这恰好是使用弱引用的场景。

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

#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});

println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());

let branch = Rc::new(Node {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);

println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}

// => leaf parent = None
// => leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) }, children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) }, children: RefCell { value: [] } }] } })

在上面的代码中,branch 创建完毕后,我们通过 RefCell<Weak<Node>>borrow_mut 方法取出 leafparent 字段的可变借用,再使用 Rc::downgrade 函数获取 branchRc<Node>Weak<Node> 引用,将其存入 leafparent 字段中。
最后在打印 leaf 的父节点时,便可以看到一个包含了 branch 实际内容的 Some 变体,即表明 leaf 可以访问其父节点。另外,此时打印 leaf 还可以避免之前因循环引用导致的栈溢出故障,因为 Weak<Node> 引用会被直接打印为 (Weak)

The Rust Programming Language


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK