一句话总结
Box 是最朴素的智能指针: 把数据放到堆上, 栈上只留一个指针.
它遵守所有权规则(唯一所有者, 离开作用域自动释放), 不增加任何运行时开销.
它解决两类问题: 编译期大小未知的类型(如递归类型)和把大数据搬到堆上以廉价移动.
"智能"则来自它实现的 Deref 与 Drop 两个 trait.
什么是智能指针
从本篇起进入智能指针. 先建立一个统领全阶段的视角: 智能指针是"行为像指针, 但附带额外能力"的类型. 它们通常拥有所指向的数据(而普通引用 & 只是借用), 并通过实现特定 trait 获得"智能":
Deref: 让它能像普通引用一样用*解引用, 自动当作&T使用.Drop: 让它离开作用域时自动执行清理(如释放堆内存).
普通引用 &T 像一张写着地址的便签, 只指路, 不负责房子; 智能指针像带着房产证和物业服务的钥匙: 它拥有房子(数据), 还自带"到期自动退租清理"(Drop)和"用起来跟住在里面一样方便"(Deref)的服务.
本阶段的 Box, Rc, RefCell 都是不同物业套餐的钥匙.
Box: 把数据放到堆上
Box<T> 的作用一句话概括: 在堆上分配一个 T, 栈上持有指向它的指针. 用法极简:
|
对 i32 这种小数据用 Box 没意义(本就该在栈上). Box 的价值体现在两类真实场景.
场景一: 编译期大小未知的递归类型
这是 Box 最经典的用途. 设想要定义一个链表节点--它包含"下一个同类型节点":
|
编译器报 "recursive type has infinite size": 要算 List 占多少字节, 得先知道里面那个 List 占多少字节...无限套娃. Box 打破这个循环--指针的大小是固定的(就一个机器字), 无论它指向多大的数据:
|
思想: 用'固定大小的指针'换'无限大小的数据'
递归类型的大小之所以无法确定, 是因为数据被内联展开. 把递归部分包进 Box, 等于说"这里存的不是下一个节点本身, 而是它的地址"--地址大小恒定, 递归的大小问题迎刃而解.
任何"编译期不知道有多大"的类型, Box 都能把它"装箱"成已知大小.
场景二: 廉价移动大数据
第二类场景: 当一个值很大(比如一个含大数组的结构体), 按值移动/传递会复制栈上的全部字节. 把它装进 Box, 移动时只需复制那个指针(一个机器字), 数据本身在堆上原地不动:
|
Deref: 为什么 Box 用起来像普通值
前面 println!("{b}") 直接打印了 Box 里的 5, *b 也能取出值--这都靠 Deref trait. 它定义了"当对这个类型用 * 时, 该返回什么". Box<T> 实现了 Deref<Target = T>, 于是 *b 得到里面的 T.
|
更妙的是解引用强制转换(Deref coercion): 编译器会自动把 &Box<T> 转成 &T. 所以一个接收 &str 的函数, 能直接传 &Box<String> 进去:
|
Deref coercion 像一套自动适配的转接头: 拿着 &Box 这个"三层插头", 函数要的是 &str 这个"标准接口", 编译器默默套上转接头让它们对接.
这让智能指针用起来几乎和裸值无差别--"智能"二字名副其实.
Drop: 离开作用域自动清理
Drop trait 定义"值离开作用域时要做的清理". Box 实现它来释放堆内存, 无需手动 free. 也能为自己的类型实现 Drop, 挂上自定义清理逻辑(关文件, 断连接等):
|
不能手动调用 .drop()
不能写 g.drop() 手动清理(编译器禁止, 否则会导致离开作用域时二次清理). 需要提前释放, 用标准库函数 std::mem::drop(g).
这与 Go 的 defer 形成对比: Go 的清理靠显式 defer 且可控制时机, Rust 的 Drop 由作用域自动, 确定性触发.
快速回顾
- 智能指针: 行为像指针, 通常拥有数据, 靠实现
Deref/Drop获得"智能". - Box: 数据放堆上, 栈上留指针, 遵守所有权, 零运行时开销.
- 两大用途: 1. 让递归等"编译期大小未知"的类型变成已知大小; 2. 廉价移动大数据(只移指针).
- Deref: 让
*box取值, 并支持解引用强制转换(&Box<String>自动当&str用). - Drop: 离开作用域自动清理; 不能手动
.drop(), 提前释放用std::mem::drop.
动手练习
-
下面的枚举为什么编译不过? 如何用 Box 修复?
enum Tree {
Node(i32, Tree, Tree),
Leaf,
}
参考答案
递归类型大小无限: Tree 含两个 Tree, 要算它的大小需先知道子 Tree 的大小, 无限递归. 用 Box 把递归部分变成固定大小的指针:
|
-
下面代码会打印什么? 顺序如何?
struct D(i32);
impl Drop for D {
fn drop(&mut self) { println!("drop {}", self.0); }
}
fn main() {
let _a = D(1);
let _b = D(2);
println!("end");
}
参考答案
|
先打印 end. 然后 main 结束, 局部变量按声明的逆序(后声明的先 drop)依次清理: _b(2)先于 _a(1). 这种 LIFO 的确定性析构顺序是 Rust Drop 的重要特性.
- 一个函数
fn print_len(s: &str), 手上有let b: Box<String> = Box::new("hi".into());. 能直接print_len(&b)吗? 为什么?
参考答案
能. 靠解引用强制转换(Deref coercion): 编译器自动把 &Box<String> 转成 &String, 再转成 &str. 无需手动解引用, 智能指针用起来和裸值几乎无差别.
- 想在某个变量作用域结束之前就触发它的 Drop, 该怎么做? 为什么不能调
x.drop()?
参考答案
用 std::mem::drop(x) 提前释放. 不能调 x.drop(), 是因为那样会导致变量在作用域结束时被二次 drop(析构两次), Rust 为防止这种内存错误而禁止手动调用 Drop::drop.
std::mem::drop 通过获取所有权并立即结束其作用域来安全地提前清理.