【Rust自学】15.1. 使用Box<T>智能指针来指向堆内存上的数据
15.1 使用Box T 智能指针来指向堆内存上的数据15.1.1BoxTBoxT可以被简单地理解为一个箱子。它是最简单的智能指针允许你把数据存储在堆上而不是栈上。具体实现是BoxT在栈上有一小块内存存放指向堆上数据的指针。也就是说实际数据存储在堆上。除了把数据放在堆上之外它没有其他开销代价是也没有额外功能。乍看之下BoxT和普通指针好像没什么区别但其真正不同之处在于BoxT实现了Deref和Drop这两个 trait。15.1.2BoxT的常见场景当某个类型的大小无法在编译时确定但使用它的上下文又需要知道其确切大小时BoxT是一个好选择。当你有大量数据想移交所有权但又需要确保在操作过程中不会被复制时。当你使用某个值时只关心它是否实现了特定的 trait而不关心其具体类型时。15.1.3 使用BoxT在堆上存储数据看个例子fn main() { let b Box::new(5); println!(b {b}); }我们将变量b定义为一个包含Box的值该Box指向分配在堆上的值5。这个程序会打印b 5。和其他任何拥有所有权的值一样当b离开作用域时它会像其他拥有所有权的值一样释放内存——作用域结束时堆上和栈上的内存都会被释放。15.1.4 使用BoxT赋能递归类型在编译时Rust 需要知道一个类型所占的空间大小。但是有一种被称为递归的类型它的大小无法在编译时确定。以这个图为例Cons类型有两个字段一个字段是i32另一个字段是Cons类型本身。在编译时Rust 需要知道类型的大小。i32的大小是固定的但第二个Cons字段——也就是Cons类型本身——的大小无法确定。针对这种情况可以使用BoxT。对于递归类型BoxT使其大小可以被确定。这种东西在函数式语言中是存在的叫做Cons List。15.1.5 关于Cons ListCons List是来自 Lisp 语言的一种数据结构。在这种结构中每个成员由两个元素组成一个是当前项的值比如上图中的i32另一个是下一个元素。这种数据结构就这样一直递归下去直到最后一个元素。最后一个成员只包含一个Nil值没有下一个元素而Nil值充当终止标记。Nil和None的概念不一样。None表示无效或缺失的值而Nil是一个终止标记。从上图可以看出Cons List是一种链表。15.1.6Cons List在 Rust 中的替代者Cons List并不是 Rust 中的常用集合。通常情况下VecT是更好的选择。下面这个List定义与上图中的Cons List结构相匹配enum List { Cons(i32, List), Nil, }List枚举有两个变体Cons和Nil。Cons变体附带了两份数据一个是i32类型一个是List类型。这么写逻辑上没问题但编译时会报错$ cargo run Compiling cons-list v0.1.0 (/tmp/ch15-refresh/cons-list) error[E0072]: recursive type List has infinite size -- src/main.rs:1:1 | 1 | enum List { | ^^^^^^^^^ 2 | Cons(i32, List), | ---- recursive without indirection | help: insert some indirection (e.g., a Box, Rc, or ) to break the cycle | 2 | Cons(i32, BoxList), | For more information about this error, try rustc --explain E0072. error: could not compile cons-list (bin cons-list) due to 1 previous error这是因为 Rust 需要知道类型的大小但无法计算出递归类型的大小。15.1.7 Rust 计算类型大小的方法先看看 Rust 如何确定类型所占的空间大小。举个例子enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), }为了确定为Message值分配多少空间Rust 会遍历每个变体看哪个变体需要最多空间。Rust 认为Message::Quit不需要任何空间Message::Move需要足够存放两个i32值的空间依此类推。因为同一时刻只有一种变体存在所以Message值所需的空间就是其最大变体所需的空间——在常见的 64 位平台上通常是Write(String)因为String比三个i32更大。15.1.8 使用Box来获得确定大小的递归类型正如前面所说Rust 需要知道类型的大小但无法计算递归类型的大小。所以我们可以改用大小已知的类型而BoxT正好满足需求它不存储数据本身而是存储指向数据的指针指针的大小是固定的usize。Rust 知道BoxT的大小因为BoxT本质上是一个指针。指针不直接存储值所以不论它指向的数据如何变化指针本身的大小都不会变。也就是说指针的大小不会随它所指向数据的大小变化而变化。基于这一点就可以修改原来的代码。具体来说把大小不确定的部分——也就是嵌套的List类型——改成BoxListenum List { Cons(i32, BoxList), Nil, }这仍旧是递归但不再直接存储List而是间接指向堆上的List值属于曲线救国。15.1.9Box类型总结只提供间接存储和堆分配。没有额外功能。没有性能开销。适用于需要间接存储的场景例如Cons List。实现了Deref和Droptrait。