概述
Box<T> 是 Rust 中最简单、最直接的一种智能指针。它的核心作用很明确:
- 把数据存储到堆(heap)上
- 在栈上只保留一个指向堆数据的指针
和其他更“聪明”的智能指针相比,Box<T> 的额外能力并不多。它几乎只做两件事:
- 提供一层间接访问
- 负责堆内存的分配和释放
也正因为如此,Box<T> 很轻量,也非常适合作为学习智能指针的起点。
什么时候会用到 Box<T>
单纯把一个值塞进 Box<T> 里,看起来似乎没什么特别,但它在下面这些场景里非常有用:
1. 编译时无法知道大小的类型
如果一个类型在编译期无法确定大小,但你又必须把它放到“要求大小固定”的上下文中,就可以使用 Box<T>。
最典型的例子就是递归类型。
2. 希望转移大块数据的所有权,但不想复制整块数据
如果一个值很大,直接在栈上移动它可能会带来较高的复制成本。
这时可以把数据放进 Box<T>:
- 栈上只移动一个指针
- 真正的大块数据仍然留在堆上的原位置
这样转移所有权时,代价会小很多。
3. 只关心值实现了某个 trait,而不关心具体类型
这种场景会发展到后面的trait object(trait 对象),例如 Box<dyn Draw> 这样的形式。
这里先知道一件事就够了:
Box<T> 不只是“装一个值”,它也经常出现在“隐藏具体类型、只暴露行为”的设计里。
在堆上存储数据
先看最简单的 Box<T> 示例:
fn main() {
let b = Box::new(5);
println!("b = {b}");
}
这里发生的事可以拆成两步理解:
5这个值被放到了堆上- 变量
b本身留在栈上,保存的是一个指向堆上数据的指针
虽然这个值在堆上,但我们使用它时几乎和普通值没什么区别:
println!("b = {b}");
输出仍然是:
b = 5
这也是 Box<T> 很容易上手的地方之一:存储位置变了,但使用方式尽量保持自然。
离开作用域时会发生什么
当 b 离开作用域时:
- 栈上的
Box自身会被销毁 Box指向的堆内存也会被一并释放
所以 Box<T> 的一个重要价值是:它帮我们自动管理堆上的数据,不需要手动释放。
这个例子为什么不算特别实用
把一个单独的 i32 放到堆上,其实没太大意义。
因为像 i32 这样的简单值:
- 体积很小
- 默认放栈上就很好
- 不需要专门为了它做堆分配
所以单值示例主要是为了熟悉 Box::new(...) 的语法,而不是为了说明真实开发中最常见的用途。
Box<T> 为什么能解决递归类型的问题
接下来才是 Box<T> 最经典、也最值得掌握的用法:
为递归类型提供“已知大小”。
什么是递归类型
递归类型指的是:一个类型的值内部,又包含了同类型的值。
例如链表、树这类结构,天然就带有递归定义特征。
如果我们直接写一个递归枚举:
enum List {
Cons(i32, List),
Nil,
}
这段代码不能编译。
原因不是 Rust 不支持递归数据结构,而是:
Rust 必须在编译时知道每个类型到底占多少空间。
为什么上面的 List 会变成“无限大”
来看这个定义:
enum List {
Cons(i32, List),
Nil,
}
如果编译器要计算 List 的大小,就必须先看它最大的变体。
其中 Nil 很简单,几乎不存什么数据;但 Cons 变体包含:
- 一个
i32 - 一个
List
于是问题来了:
要算 List 的大小,得先算 Cons 的大小;要算 Cons 的大小,又得先算里面那个 List 的大小;而那个 List 里面又还有一个 Cons…
这个过程会无限套下去:
List
= i32 + List
= i32 + (i32 + List)
= i32 + (i32 + (i32 + List))
= ...
所以编译器最终只能得出一个结论:
这个类型没有已知大小。
这就是为什么会报出类似下面的错误:
error[E0072]: recursive type `List` has infinite size
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
编译器这里给出的建议非常关键:
insert some indirection
意思是:不要直接把下一个 List 值内嵌进来,而是通过某种“间接方式”指向它。
间接存储是什么意思
所谓间接存储(indirection),可以简单理解成:
- 不直接存值本身
- 而是存一个指向这个值的指针
这正是 Box<T> 擅长的事。
因为无论 T 有多大,Box<T> 自身的大小都是固定的。编译器只需要知道:
- 一个
i32占多大 - 一个
Box<List>指针占多大
这样 Cons 的大小就确定了。
也就是说,Box<T> 不是帮我们“消灭递归”,而是帮我们把递归从“直接包含”改成“通过指针连接”。
用 Box<T> 改写递归类型
把前面的定义改成下面这样:
enum List {
Cons(i32, Box<List>),
Nil,
}
这时 Cons 变体中保存的是:
- 当前节点的值
i32 - 指向下一个节点的
Box<List>
于是整个类型大小就变成了:
List 最大大小 = i32 + 指针大小
这已经是编译器可以接受的固定大小了。
用 Box<T> 构造一个 cons list
有了上面的定义后,就可以表示列表 1, 2, 3:
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))
)
)
)
);
}
这个结构可以从外向里理解:
- 第一个
Cons保存1 - 并指向下一个
List - 下一个
List是保存2的Cons - 再往后是保存
3的Cons - 最后用
Nil表示链表结束
如果写成更接近数学结构的形式,它表达的是:
(1, (2, (3, Nil)))
这就是一个典型的 cons list。
Box<T> 在这里到底解决了什么
可以把这个问题压缩成一句话:
递归类型本身没问题,问题在于“直接递归包含”会让大小无法确定。
而 Box<T> 的作用就是:
- 让递归结构中的下一层节点放到堆上
- 当前层只保存一个固定大小的指针
- 从而让整个类型在编译期拥有确定大小
所以这里真正重要的不是“堆”本身,而是:
Box<T> 提供了固定大小的间接层。
Box<T> 的特点:能力少,但很有用
和后面要学的 Rc<T>、RefCell<T> 相比,Box<T> 并不复杂。
它没有:
- 引用计数
- 共享所有权
- 运行时借用检查
它做的事情非常朴素:
- 在堆上存储值
- 通过指针间接访问值
- 在离开作用域时自动清理堆数据
也正因为能力边界很清楚,Box<T> 在很多场景下反而是最合适的选择。
为什么说 Box<T> 也是智能指针
Box<T> 之所以属于智能指针,不只是因为它“像指针”,还因为它实现了智能指针最重要的两个 trait:
Deref
Deref 让 Box<T> 的值可以像引用一样使用。
这也是为什么我们经常感觉 Box<T> 用起来并不笨重。
Drop
Drop 让 Box<T> 在离开作用域时,自动释放它所指向的堆数据。
这保证了我们既能使用堆内存,又不需要手动管理释放逻辑。
后面学习 Deref 和 Drop 时,你会更清楚这两个 trait 为什么是智能指针的基础。
小结
Box<T> 是 Rust 中最基础的智能指针。它最重要的价值不是“把一个值放上堆”这么简单,而是:
- 通过堆分配实现间接存储
- 让大型数据的所有权转移更轻量
- 为递归类型提供固定大小
- 为后面的 trait object 打基础
如果只记住这一节的一句话,可以记成:
Box<T> 的核心意义,是用一个固定大小的指针,把原本不好直接存放的数据安全地接起来。
知识自测
尝试独立回答下面这些课后习题,看看这一节的关键点是否已经真正掌握。
第 1 题:Box<T> 最核心的作用是什么?它和普通引用最大的区别是什么?
第 2 题:为什么下面这个递归类型不能编译?
enum List {
Cons(i32, List),
Nil,
}
第 3 题:编译器提示里的 insert some indirection 是什么意思?为什么把 List 改成 Box<List> 之后,编译器就能计算它的大小了?
第 4 题:Box<T> 最常见的使用场景有哪些?哪些场景只是“能用”,哪些场景才是“真正有价值”的用法?
第 5 题:为什么说 Box<T> 也是智能指针?Deref 和 Drop 在这里分别起什么作用?
第 6 题:如果用一句话总结 Box<T> 在这一节里的价值,你会怎么说?
📖 点击查看答案
第 1 题答案
Box<T> 的核心作用是把数据放到堆上,并在栈上保留一个固定大小的指针。它和普通引用最大的区别是:引用只是借用数据,而 Box<T> 通常拥有它所指向的数据,并负责这块堆内存的生命周期。
第 2 题答案
因为 Cons 变体里直接包含了另一个 List。编译器为了计算 List 的大小,就必须先计算里面那个 List 的大小,而里面又有一个 List,于是会无限递归下去,最终得到“类型大小无限”的错误。
第 3 题答案
它的意思是:不要直接把值本身嵌进去,而要通过某种“间接层”去指向它,比如 Box、Rc 或引用。对于 Box<List> 来说,编译器不需要知道整个递归结构到底有多深,它只需要知道一个 Box 指针的大小是固定的。于是 Cons 的大小就能确定为“一个 i32 + 一个固定大小的指针”。
第 4 题答案
常见场景有三类:
- 编译时无法确定大小的类型,例如递归类型
- 希望转移大块数据的所有权,但不想复制整块数据
- 只关心实现了某个 trait 的行为,而不关心具体类型,为后面的 trait object 做准备
其中最有代表性的用法是递归类型和大数据所有权转移。把单个小值例如 i32 放进 Box<T> 虽然能用,但主要是语法演示,不是最有价值的实际场景。
第 5 题答案
因为 Box<T> 不只是一个普通结构体,它还能像指针一样工作,并且实现了智能指针最关键的两个 trait:
Deref:让它在很多场景下像引用一样使用Drop:让它离开作用域时自动清理堆上的数据
第 6 题答案
可以这样总结:
Box<T> 用一个固定大小的堆指针,把原本不适合直接内嵌的数据安全地组织起来,尤其适合递归类型和大数据所有权转移。
参考资料
- [The Rust Programming Language - Using Box
to Point to Data on the Heap](https://doc.rust-lang.org/book/ch15-01-box.html) - std::boxed::Box