指针(Pointer)是一个包含内存地址的变量,这个地址指向存储在内存中的其他数据。Rust 中最常见的指针是引用(reference),用 & 符号表示,它们只借用数据而不拥有所有权。
而智能指针(Smart Pointer)则是一种数据结构,不仅像指针一样工作,还拥有额外的元数据和功能。智能指针的概念并非 Rust 独有,它起源于 C++,也存在于其他编程语言中。
Rust 的所有权和借用机制让智能指针有了额外的特性:引用只借用数据,而智能指针通常拥有它们指向的数据。
比如 String 和 Vec<T>。它们都拥有一些内存并允许你操作它们,同时还有元数据(如容量)和额外的能力(如 String 确保数据始终是有效的 UTF-8)。
1. 智能指针的特征
智能指针通常使用结构体实现,但与普通结构体不同的是,它们实现了 Deref 和 Drop trait:
Dereftrait:允许智能指针实例像引用一样使用,使你的代码可以同时适用于引用和智能指针Droptrait:允许你自定义智能指针离开作用域时运行的代码
本章将介绍标准库中最常用的智能指针:
Box<T>:在堆上分配值Rc<T>:引用计数类型,支持多所有权Ref<T>和RefMut<T>:通过RefCell<T>访问,在运行时而非编译时执行借用规则
此外,我们还会讨论内部可变性模式(interior mutability pattern),以及如何避免引用循环导致的内存泄漏。
2. Box - 堆上的数据
Box<T> 是最简单的智能指针,它允许你将数据存储在堆上而不是栈上。栈上只保留指向堆数据的指针。
2.1 基本使用
1 | fn main() { |
在这个例子中,数字 5 被存储在堆上,b 是指向它的指针。当 b 离开作用域时,堆上的数据会被自动释放。
2.2 使用场景
递归类型
Rust 不允许直接在栈上存储递归类型,因为它们的大小无法在编译时确定。但是 Box<T> 可以帮助我们创建递归数据结构:
1 | enum List { |
这里的 List 是一个递归类型,每个 Cons 变体都包含一个 Box<List>,使得 List 可以递归嵌套,同时编译器能够确定所需的空间大小。
大型数据转移所有权
当你有大量数据需要转移所有权,但又不想复制数据时,可以使用 Box<T>:
1 | fn main() { |
3. Rc - 引用计数智能指针
有时候,一个值可能有多个所有者。例如在图数据结构中,多条边可能指向同一个节点,这个节点从概念上讲被所有指向它的边所拥有。Rc<T> 就是为这种场景设计的。
Rc<T> 是 reference counting(引用计数)的缩写。它通过跟踪值的引用数量来判断这个值是否仍在使用。当引用数量变为 0 时,该值就可以被清理。
注意:Rc<T> 只能用于单线程场景。
3.1 基本使用
1 | use std::rc::Rc; |
在这个例子中:
a是一个包含 5 和 10 的列表b和c都共享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 | use std::cell::RefCell; |
RefCell<T> 提供了两个方法:
borrow():返回不可变引用Ref<T>borrow_mut():返回可变引用RefMut<T>
这两个返回值都是智能指针,实现了 Deref trait。
4.5 运行时借用检查
如果违反借用规则,程序会在运行时 panic:
1 | use std::cell::RefCell; |
运行时会出现类似错误:
1 | thread 'main' panicked at 'already borrowed: BorrowMutError' |
5. Rc<T> 与 RefCell<T> 结合使用
Rc<T> 允许多个所有者,但只提供不可变访问。RefCell<T> 允许可变访问,但只能有一个所有者。将它们结合使用,可以创建有多个所有者且可修改的值:
1 | use std::cell::RefCell; |
输出中,a、b、c 中的共享值都会是 15,因为它们都指向同一个 RefCell<i32>。
6. 引用循环与内存泄漏
6.1 什么是引用循环
使用 Rc<T> 和 RefCell<T> 可能会创建引用循环:两个 Rc<T> 值互相引用,导致引用计数永远不会变为 0,从而造成内存泄漏。
1 | use std::cell::RefCell; |
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 | use std::rc::{Rc, Weak}; |
7. Deref trait - 像引用一样使用
实现 Deref trait 允许你自定义解引用运算符 * 的行为。通过实现 Deref,智能指针可以像常规引用一样被处理。
1 | use std::ops::Deref; |
7.1 Deref 强制转换
当把某个类型的引用传递给函数或方法,但它的类型与参数类型不匹配时,Rust 会自动进行 Deref 强制转换:
1 | fn hello(name: &str) { |
Rust 会自动调用 deref 方法,将 &MyBox<String> 转换为 &String,再转换为 &str。
8. Drop trait - 清理代码
Drop trait 允许你自定义值离开作用域时的行为。智能指针通常会实现 Drop trait 来释放资源:
1 | struct CustomSmartPointer { |
输出:
1 | CustomSmartPointers 已创建 |
注意:变量以创建时相反的顺序被丢弃(先创建的后销毁)。
8.1 提前丢弃值
如果需要提前清理值,不能直接调用 drop 方法,而应该使用 std::mem::drop 函数:
1 | fn main() { |
总结
智能指针是 Rust 中管理内存和所有权的强大工具:
Box<T>:适用于堆上分配和单一所有权场景,特别是递归类型Rc<T>:适用于单线程环境中需要多个所有者共享数据的场景RefCell<T>:适用于需要内部可变性和运行时借用检查的场景Rc<RefCell<T>>:结合两者优势,实现多所有权的可变数据Weak<T>:避免Rc<T>引用循环导致的内存泄漏
通过 Deref 和 Drop trait,智能指针可以像普通引用一样使用,并在离开作用域时自动清理资源。
掌握智能指针的使用,对于编写安全、高效的 Rust 代码至关重要。它们为我们提供了更灵活的内存管理方式,同时保持了 Rust 的内存安全保证。
Hooray!智能指针小节完成!!!