Rust —— 智能指针

指针(Pointer)是一个包含内存地址的变量,这个地址指向存储在内存中的其他数据。Rust 中最常见的指针是引用(reference),用 & 符号表示,它们只借用数据而不拥有所有权。

而智能指针(Smart Pointer)则是一种数据结构,不仅像指针一样工作,还拥有额外的元数据和功能。智能指针的概念并非 Rust 独有,它起源于 C++,也存在于其他编程语言中。

Rust 的所有权和借用机制让智能指针有了额外的特性:引用只借用数据,而智能指针通常拥有它们指向的数据

比如 StringVec<T>。它们都拥有一些内存并允许你操作它们,同时还有元数据(如容量)和额外的能力(如 String 确保数据始终是有效的 UTF-8)。

1. 智能指针的特征

智能指针通常使用结构体实现,但与普通结构体不同的是,它们实现了 DerefDrop trait:

  • Deref trait:允许智能指针实例像引用一样使用,使你的代码可以同时适用于引用和智能指针
  • Drop trait:允许你自定义智能指针离开作用域时运行的代码

本章将介绍标准库中最常用的智能指针:

  • Box<T>:在堆上分配值
  • Rc<T>:引用计数类型,支持多所有权
  • Ref<T>RefMut<T>:通过 RefCell<T> 访问,在运行时而非编译时执行借用规则

此外,我们还会讨论内部可变性模式(interior mutability pattern),以及如何避免引用循环导致的内存泄漏。

2. Box - 堆上的数据

Box<T> 是最简单的智能指针,它允许你将数据存储在堆上而不是栈上。栈上只保留指向堆数据的指针。

2.1 基本使用

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

在这个例子中,数字 5 被存储在堆上,b 是指向它的指针。当 b 离开作用域时,堆上的数据会被自动释放。

2.2 使用场景

递归类型

Rust 不允许直接在栈上存储递归类型,因为它们的大小无法在编译时确定。但是 Box<T> 可以帮助我们创建递归数据结构:

1
2
3
4
5
6
7
8
9
10
enum List {
Cons(i32, Box<List>),
Nil,
}

use List::{Cons, Nil};

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

这里的 List 是一个递归类型,每个 Cons 变体都包含一个 Box<List>,使得 List 可以递归嵌套,同时编译器能够确定所需的空间大小。

大型数据转移所有权

当你有大量数据需要转移所有权,但又不想复制数据时,可以使用 Box<T>

1
2
3
4
5
fn main() {
let large_data = Box::new([0; 1000000]);
// 转移所有权时只复制指针,不复制数据
let new_owner = large_data;
}

3. Rc - 引用计数智能指针

有时候,一个值可能有多个所有者。例如在图数据结构中,多条边可能指向同一个节点,这个节点从概念上讲被所有指向它的边所拥有。Rc<T> 就是为这种场景设计的。

Rc<T>reference counting(引用计数)的缩写。它通过跟踪值的引用数量来判断这个值是否仍在使用。当引用数量变为 0 时,该值就可以被清理。

注意Rc<T> 只能用于单线程场景。

3.1 基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use std::rc::Rc;

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

use List::{Cons, Nil};

fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
println!("计数:{}", Rc::strong_count(&a)); // 1

let b = Cons(3, Rc::clone(&a));
println!("计数:{}", Rc::strong_count(&a)); // 2

{
let c = Cons(4, Rc::clone(&a));
println!("计数:{}", Rc::strong_count(&a)); // 3
}

println!("计数:{}", Rc::strong_count(&a)); // 2
}

在这个例子中:

  • a 是一个包含 5 和 10 的列表
  • bc 都共享 a 的所有权
  • 使用 Rc::clone(&a) 增加引用计数,而不是深拷贝数据
  • 每次 Rc::clone 只增加引用计数,不会复制堆上的数据,所以速度很快

3.2 Rc::clone vs .clone()

虽然可以使用 a.clone() 代替 Rc::clone(&a),但 Rust 的惯例是使用 Rc::clone,因为:

  • Rc::clone 只增加引用计数,非常快
  • 普通的 .clone() 通常会深拷贝数据,耗时较多
  • 使用 Rc::clone 能清楚地表明这只是增加引用计数

4. RefCell 和内部可变性

4.1 什么是内部可变性

内部可变性(interior mutability)是 Rust 的一种设计模式,它允许你在只有不可变引用的情况下修改数据。这通常是借用规则所不允许的。

为了改变数据,该模式在数据结构内部使用 unsafe 代码来绕过 Rust 的可变性和借用规则。我们可以使用这些类型,只要能确保在运行时遵循借用规则,即使编译器无法保证这一点。

4.2 RefCell<T> 的特点

  • RefCell<T> 代表其持有数据的唯一所有权

  • Rc<T> 类似,RefCell<T> 只能用于单线程场景

  • 编译时检查 vs 运行时检查

    • Box<T> 的借用规则在编译时检查
    • RefCell<T> 的借用规则在运行时检查
    • 如果违反借用规则,RefCell<T> 会在运行时 panic

4.3 Box<T> vs Rc<T> vs RefCell<T>

类型 所有权 借用检查时机 可变性
Box<T> 单一所有者 编译时 可变或不可变借用
Rc<T> 多个所有者 编译时 只能不可变借用
RefCell<T> 单一所有者 运行时 可变或不可变借用

4.4 基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use std::cell::RefCell;

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

// 获取可变借用
{
let mut y = x.borrow_mut();
*y += 1;
}

// 获取不可变借用
let z = x.borrow();
println!("x = {}", z); // 输出: x = 6
}

RefCell<T> 提供了两个方法:

  • borrow():返回不可变引用 Ref<T>
  • borrow_mut():返回可变引用 RefMut<T>

这两个返回值都是智能指针,实现了 Deref trait。

4.5 运行时借用检查

如果违反借用规则,程序会在运行时 panic:

1
2
3
4
5
6
7
8
use std::cell::RefCell;

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

let _y = x.borrow_mut();
let _z = x.borrow_mut(); // panic! 已经有一个可变借用了
}

运行时会出现类似错误:

1
thread 'main' panicked at 'already borrowed: BorrowMutError'

5. Rc<T>RefCell<T> 结合使用

Rc<T> 允许多个所有者,但只提供不可变访问。RefCell<T> 允许可变访问,但只能有一个所有者。将它们结合使用,可以创建有多个所有者且可修改的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::cell::RefCell;
use std::rc::Rc;

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

use List::{Cons, Nil};

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(3)), Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));

// 修改共享的值
*value.borrow_mut() += 10;

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

输出中,abc 中的共享值都会是 15,因为它们都指向同一个 RefCell<i32>

6. 引用循环与内存泄漏

6.1 什么是引用循环

使用 Rc<T>RefCell<T> 可能会创建引用循环:两个 Rc<T> 值互相引用,导致引用计数永远不会变为 0,从而造成内存泄漏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
use std::cell::RefCell;
use std::rc::Rc;

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

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

use List::{Cons, Nil};

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

// 创建循环引用
if let Some(link) = a.tail() {
*link.borrow_mut() = Rc::clone(&b);
}

// a 和 b 形成循环,引用计数永远不会为 0
}

6.2 使用 Weak<T> 避免循环引用

为了避免引用循环,Rust 提供了 Weak<T>

  • Rc::clone 增加 strong_count(强引用计数)
  • Rc::downgrade 创建 Weak<T>,增加 weak_count(弱引用计数)
  • 只有当 strong_count 为 0 时,值才会被清理,不管 weak_count 是多少
  • 使用 weak.upgrade() 获取 Option<Rc<T>>,如果值已被清理则返回 None
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
use std::rc::{Rc, Weak};
use std::cell::RefCell;

#[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![]),
});

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

// 使用 Weak 引用父节点,避免循环
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
}

7. Deref trait - 像引用一样使用

实现 Deref trait 允许你自定义解引用运算符 * 的行为。通过实现 Deref,智能指针可以像常规引用一样被处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::ops::Deref;

struct MyBox<T>(T);

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

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

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

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

assert_eq!(5, x);
assert_eq!(5, *y); // Deref 让这成为可能
}

7.1 Deref 强制转换

当把某个类型的引用传递给函数或方法,但它的类型与参数类型不匹配时,Rust 会自动进行 Deref 强制转换

1
2
3
4
5
6
7
8
fn hello(name: &str) {
println!("Hello, {name}!");
}

fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m); // &MyBox<String> -> &String -> &str
}

Rust 会自动调用 deref 方法,将 &MyBox<String> 转换为 &String,再转换为 &str

8. Drop trait - 清理代码

Drop trait 允许你自定义值离开作用域时的行为。智能指针通常会实现 Drop trait 来释放资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct CustomSmartPointer {
data: String,
}

impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("销毁 CustomSmartPointer,数据:`{}`", self.data);
}
}

fn main() {
let c = CustomSmartPointer {
data: String::from("my stuff"),
};
let d = CustomSmartPointer {
data: String::from("other stuff"),
};
println!("CustomSmartPointers 已创建");
} // d 和 c 离开作用域,自动调用 drop

输出:

1
2
3
CustomSmartPointers 已创建
销毁 CustomSmartPointer,数据:`other stuff`
销毁 CustomSmartPointer,数据:`my stuff`

注意:变量以创建时相反的顺序被丢弃(先创建的后销毁)。

8.1 提前丢弃值

如果需要提前清理值,不能直接调用 drop 方法,而应该使用 std::mem::drop 函数:

1
2
3
4
5
6
7
8
fn main() {
let c = CustomSmartPointer {
data: String::from("some data"),
};
println!("CustomSmartPointer 已创建");
drop(c); // 提前清理
println!("CustomSmartPointer 在 main 结束前被丢弃");
}

总结

智能指针是 Rust 中管理内存和所有权的强大工具:

  • Box<T>:适用于堆上分配和单一所有权场景,特别是递归类型
  • Rc<T>:适用于单线程环境中需要多个所有者共享数据的场景
  • RefCell<T>:适用于需要内部可变性和运行时借用检查的场景
  • Rc<RefCell<T>>:结合两者优势,实现多所有权的可变数据
  • Weak<T>:避免 Rc<T> 引用循环导致的内存泄漏

通过 DerefDrop trait,智能指针可以像普通引用一样使用,并在离开作用域时自动清理资源。

掌握智能指针的使用,对于编写安全、高效的 Rust 代码至关重要。它们为我们提供了更灵活的内存管理方式,同时保持了 Rust 的内存安全保证。

Hooray!智能指针小节完成!!!