智能指针
最直接的智能指针是 box,类型写作
Box<T>。Box 让你把数据存在堆上而不是栈上,留在栈上的只是指向堆数据的指针。
Box 做什么用
除了把数据放堆上以外没有性能开销,但也没有额外能力。最常用于这几种情况:
有一个编译期无法确定大小的类型,却要在需要确切大小的上下文里用它
有一大块数据,想转移所有权但确保数据不会被复制
想拥有一个值,只关心它实现了某个 trait,而不关心具体是哪个类型
第一种情况下面会演示。第二种情况下,转移一大块数据的所有权可能要很久,因为数据在栈上被来回复制。把大数据放进 box,栈上只复制小小的指针数据,数据本身留在堆上一个地方。第三种情况叫 trait object。
把数据存在堆上
fn main() { |
b 是一个 Box,指向堆上的值 5。访问 box 里的数据跟数据在栈上时一样。跟任何拥有的值一样,box 出作用域时被释放 —— 栈上的 box 和它指向的堆数据都会被回收。
单把一个值放堆上没什么用,大多数情况下 i32 放在栈上更合适。
用 Box 支持递归类型
递归类型的值可以包含同类型的另一个值。递归类型有问题是因为 Rust 需要在编译期知道一个类型占多少空间 —— 但递归类型的嵌套理论上可以无限继续,Rust 算不出需要多少空间。因为 box 的大小是已知的,插入一个 box 就能打破递归。
以 cons list 为例。cons list 是函数式语言里常见的数据类型:
enum List { |
这段代码还编译不过,因为 List 类型大小未知:
error[E0072]: recursive type `List` has infinite size |
错误说这个类型「有无限大小」。List 的一个变体直接递归地持有另一个自己,Rust 算不出存储一个 List 值需要多少空间。
间接层
编译器建议的间接层(indirection)意思是:不直接存储值,而是存一个指向它的指针。
enum List { |
Cons 变体需要 i32 的空间加上 box 指针数据的空间。Nil 变体不存任何值,占的栈空间比 Cons 少。现在我们知道任何 List 值都占 i32 的大小加上一个 box 指针的大小 —— 递归链被打断,编译器能算出尺寸。
Cons 变体不再直接持有另一个 List 值,Box<T> 指向堆上的下一个 List 值。
用 Box 写一个实际的链表
enum List { |
输出:
first = 1 |
写这种嵌套的 match 很啰嗦,但目前只能这样 —— 后面会看到 impl Trait 怎么改善它。
Box 提供的两个 trait
Box 只提供间接层和堆分配,没有别的特殊能力,也没有其他智能指针那种性能开销。所以在只需要间接层的地方(比如这个 cons list)它很有用。
但 Box<T> 之所以是智能指针,是因为它实现了两个 trait:
Deref—— 让Box<T>能像引用一样使用Drop——Box<T>出作用域时,堆上的数据也会被清理
use std::ops::Deref; |
Deref 的强制转换让 &Box<String> 能传给要 &str 的地方 —— 编译器自动加了一层 *b。这两个 trait 对后面其他智能指针的功能都很重要。