本篇导读
第二卷开始,我们进入 Rust 指针的世界。前面大量使用的 &T 只是普通引用,它借用数据、不拥有所有权。而智能指针(smart pointers)不仅指向数据,还拥有数据、携带额外元数据和能力,离开作用域时会像普通值一样自动清理——这个概念源自 C++。
本篇先学最基础的 Box<T>:把值放到堆上。接着研究两个支撑智能指针行为的 trait:Deref(让指针能像引用一样用 * 解引用,并支持强制转换)和 Drop(自定义资源释放)。最后用 Box 解决递归类型的大小问题。
一、什么是智能指针
在 Rust 中,常见的智能指针包括:Box<T>、Rc<T>、RefCell<T>。其实 String 和 Vec<T> 也属于智能指针——它们拥有堆数据、带元数据、在 drop 时释放内存。
智能指针通常用结构体实现,区别于普通结构体的关键是:它们实现了 Deref 和 Drop 两个 trait。
二、Box<T>:把值放到堆上
Box<T> 是最简单的智能指针,它把一个值分配在堆上,栈上只留一个指针。
1. 基本用法
fn main() {
let b = Box::new(5); // 5 被分配在堆上
println!("b = {b}");
} // b 离开作用域:先 drop Box(释放堆上的 5)
单独为一个整数装箱意义不大(栈上更快)。Box 主要用于三种场景。
2. 场景一:编译期大小未知的递归类型
Rust 必须在编译期知道每个类型占多大空间,但递归类型(如链表、树)理论上大小无限。Box 大小固定(一个指针),用它“间接”存放递归部分即可。
以经典的 cons list(函数式语言链表)为例:
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))))));
}
Box 只占一个指针的位置,于是编译器能确定 List 的大小。没有 Box,编译器会报 "infinite size" 错误,因为它试图为无限递归的类型分配空间。
3. 场景二:转移大量数据的所有权,避免拷贝
当你有一块很大的数据,想转移所有权但又不希望在栈上复制,装箱后只需拷贝一个指针。
4. 场景三:拥有 trait 对象
第 11 篇的 Box<dyn Trait> 用 Box 承载动态分发的值。
三、Deref trait:自定义解引用行为
1. 普通引用的解引用
let x = 5;
let y = &x;
assert_eq!(5, *y); // * 顺着引用找到值
2. Box 也能解引用
let x = 5;
let y = Box::new(x);
assert_eq!(5, *y); // Box 实现了 Deref
3. 实现自己的智能指针
定义一个类似 Box 的元组结构体 MyBox,实现 Deref:
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 y = MyBox::new(5);
assert_eq!(5, *y); // *(y.deref())
}
type Target = T 定义 Deref 关联类型;
deref 返回内部值的引用;
*y 实际是 *(y.deref())。
4. 函数和方法的 Deref 强制转换
Deref coercion(解引用强制转换)是 Rust 的便利机制:当你传一个类型,但函数需要引用另一类型时,Rust 自动插入 deref 调用进行转换。
fn hello(name: &str) {
println!("你好,{name}");
}
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
// &MyBox<String> 自动 deref 成 &String,再 deref 成 &str
}
没有这个机制,你得写 hello(&(*m)[..]);。强制转换发生在编译期、没有运行时开销。
5. Deref 强制转换的三种情形
- 从
&T 到 &U(T: Deref<Target=U>);
- 从
&mut T 到 &mut U(T: DerefMut<Target=U>);
- 从
&mut T 到 &U(可变借用可降级为共享,但反过来不行)。
四、Drop trait:资源清理的时机
Drop trait 让你指定值离开作用域时该做什么,常用于释放文件、网络连接、锁、内存等资源。
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("清理 CustomSmartPointer 的数据:{}", self.data);
}
}
fn main() {
let c = CustomSmartPointer { data: String::from("我的东西") };
let d = CustomSmartPointer { data: String::from("别的东西") };
println!("两个智能指针已创建");
} // 逆序 drop:先 d 后 c
输出顺序:
两个智能指针已创建
清理 CustomSmartPointer 的数据:别的东西
清理 CustomSmartPointer 的数据:我的东西
注意 drop 顺序与创建顺序相反(栈式)。
提前丢弃:std::mem::drop
你不能手动调用 c.drop()(会导致 double free),但可以用标准库函数 drop(c) 提前强制释放:
let c = CustomSmartPointer { data: String::from("提前结束") };
drop(c); // 立即清理
println!("后面的代码");
std::mem::drop 本质就是拿走所有权、什么都不做,让值在函数结束时被 drop。
五、综合示例:用 Box 实现一个简单二叉树
#[derive(Debug)]
struct Tree {
value: i32,
left: Option<Box<Tree>>,
right: Option<Box<Tree>>,
}
impl Tree {
fn leaf(value: i32) -> Box<Tree> {
Box::new(Tree { value, left: None, right: None })
}
fn new(value: i32, left: Option<Box<Tree>>, right: Option<Box<Tree>>) -> Box<Tree> {
Box::new(Tree { value, left, right })
}
fn contains(&self, target: i32) -> bool {
if self.value == target {
return true;
}
let in_left = self.left.as_ref().map_or(false, |n| n.contains(target));
let in_right = self.right.as_ref().map_or(false, |n| n.contains(target));
in_left || in_right
}
}
fn main() {
let root = Tree::new(
5,
Some(Tree::leaf(3)),
Some(Tree::new(
8,
Some(Tree::leaf(7)),
None,
)),
);
println!("包含 7 吗?{}", root.contains(7)); // true
println!("包含 10 吗?{}", root.contains(10)); // false
println!("{root:#?}");
}
这展示了 Box 在树形递归结构中的标准用法:Option<Box<Tree>> 表示可能为空的子节点,Box 提供固定大小、并在整棵树 drop 时递归释放全部节点。
六、本篇小结
- 智能指针拥有数据、实现 Deref 与 Drop;String、Vec 也是智能指针;
Box<T> 在堆上分配值,用于递归类型、转移大数据、trait 对象;
Deref trait 让自定义指针可用 *,deref 返回内部值引用;
- Deref 强制转换在函数/方法调用时自动把
&MyBox<String> 转成 &str,零开销;
Drop trait 自定义离开作用域的清理逻辑,逆序执行;提前释放用 std::mem::drop,不能手动调用 drop 方法;
- 递归类型用 Box(或其他固定大小指针)间接存放,解决无限尺寸问题。
七、动手练习
- 用 Box 创建一个 cons list 并实现一个递归函数求所有元素之和;
- 实现自己的
MyBox,验证 Deref 强制转换能传给 &str 参数;
- 实现 Drop 的结构体,在多个实例场景观察 drop 的逆序;
- 用
std::mem::drop 提前释放一个值,并验证之后无法使用;
- (选做)用 Box 实现一个单链表,支持 push、pop、len(第 17 篇后可再用 Rc 改造)。
下篇预告:第 17 篇|智能指针(下):Rc、RefCell 与 Weak
Box 是独占所有权的。但有时一份数据需要有多个所有者(如图结构中被多处引用的节点),有时又需要在持有不可变引用的情况下修改内部数据(内部可变性)。
下一篇我们学习引用计数指针 Rc<T>、内部可变性 RefCell<T>,以及用 Weak<T> 打破引用循环导致的内存泄漏。
如果你在 Rust 学习中遇到问题,欢迎到 云栈社区 与其他开发者一起交流讨论。