使用 Box<T> 指向堆上的数据


最直接的智能指针是一个 box,其类型是 write 盒子<T>。Box 允许您将数据存储在堆上,而不是堆栈上。堆栈上保留的是指向堆数据的指针。请参阅第 4 章 来回顾堆栈和堆之间的区别。


Box 除了将其数据存储在堆上而不是堆栈上之外,没有性能开销。但它们也没有很多额外的功能。在以下情况下,您将最常使用它们:


  • 当你有一个类型在编译时无法知道其大小,并且你想在需要精确大小的上下文中使用该类型的值时

  • 当您拥有大量数据,并且想要转移所有权,但请确保在执行此作时不会复制数据

  • 当你想拥有一个值,并且你只关心它是一个实现特定 trait 的类型,而不是一个特定的类型


我们将在“使用 Boxes “部分。在第二种情况下,转移大量数据的所有权可能需要很长时间,因为数据是在堆栈上复制的。为了提高在这种情况下的性能,我们可以将堆上的大量数据存储在一个盒子中。然后,在堆栈上仅复制少量指针数据,而它引用的数据保留在堆上的一个位置。第三种情况称为 trait 对象,第 17 章用了一整节“使用 trait 对象,允许不同类型的值“,只是针对该主题。因此,您在这里学到的内容将在第 17 章中再次应用!


使用 Box<T> 在堆上存储数据


在我们讨论 Box<T> 的堆存储用例之前,我们将介绍语法以及如何与 Box<T> 中存储的值进行交互。


示例 15-1 展示了如何使用盒子在堆上存储 i32 值:


文件名: src/main.rs

fn main() {
    let b = Box::new(5);
    println!("b = {b}");
}


示例 15-1:使用 box 在堆上存储 i32


我们将变量 b 定义为具有一个 Box 的值,该值指向值 5,该值在堆上分配。该程序将打印 b = 5;在这种情况下,我们可以访问盒子中的数据,就像这些数据在堆栈上一样。就像任何拥有的值一样,当一个 box 超出范围时,就像 main 末尾的 b 一样,它将被释放。释放同时发生在盒子(存储在堆栈上)和它指向的数据(存储在堆上)。


在堆上放置单个值不是很有用,因此您不会经常以这种方式单独使用 box。在大多数情况下,在堆栈上拥有像单个 i32 这样的值,它们默认存储在堆栈上,更合适。让我们看一个情况,其中 box 允许我们定义类型,如果我们没有 box,我们将不允许这些类型。


使用框启用递归类型


递归类型的值可以具有另一个相同类型的值作为自身的一部分。递归类型会带来一个问题,因为在编译时 Rust 需要知道一个类型占用了多少空间。然而,递归类型的值的嵌套理论上可以无限持续,因此 Rust 无法知道该值需要多少空间。因为盒子具有已知的大小,所以我们可以通过在递归类型定义中插入一个盒子来启用递归类型。


作为递归类型的一个示例,让我们探索 cons 列表。这是函数式编程语言中常见的数据类型。我们将定义的 cons 列表类型很简单,除了递归;因此,我们将使用的示例中的概念在你遇到涉及递归类型的更复杂的情况时都会很有用。


有关缺点列表的更多信息


缺点列表是一种来自 Lisp 编程语言及其方言的数据结构,由嵌套对组成,是链表的 Lisp 版本。它的名字来自 Lisp 中的 cons 函数(“construct function”的缩写),该函数从其两个参数构造一个新的对。通过在由一个值和另一个对组成的对上调用 cons,我们可以构建由递归对组成的 cons 列表。


例如,下面是一个 cons 列表的伪代码表示形式,其中包含列表 1、2、3,每对都在括号中:


(1, (2, (3, 零)))


cons 列表中的每个项目都包含两个元素:当前项目的值和下一个项目的值。列表中的最后一项仅包含一个名为 Nil 的值 没有下一项。通过递归调用 cons 来生成 cons 列表 功能。表示递归基本情况的规范名称是 Nil。请注意,这与第 6 章中的“null”或“nil”概念不同,后者是无效或不存在的值。


cons list 不是 Rust 中常用的数据结构。大多数时候,当你在 Rust 中有一个项目列表时,Vec<T> 是一个更好的选择。其他更复杂的递归数据类型在各种情况下都很有用,但从本章的 cons 列表开始,我们可以探索盒子如何让我们定义递归数据类型而不会分心。


示例 15-2 包含一个 cons 列表的枚举定义。请注意,这段代码还不会编译,因为 List 类型没有已知的大小,我们将对此进行演示。


文件名: src/main.rs

enum List {
    Cons(i32, List),
    Nil,
}

fn main() {}


示例 15-2:第一次尝试定义一个枚举来表示 i32 值的 cons list 数据结构


注意: 为了这个例子,我们正在实现一个只保存 i32 值的 cons 列表。正如我们在第 10 章中讨论的那样,我们可以使用泛型来实现它,以定义一个可以存储任何类型的值的 cons 列表类型。


使用 List 类型来存储列表 1、2、3 类似于示例 15-3 中的代码:


文件名: src/main.rs

enum List {
    Cons(i32, List),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(1, Cons(2, Cons(3, Nil)));
}


示例 15-3:使用 List 枚举存储列表 1、2、3


第一个 Cons 值包含 1 和另一个 List 值。此 List 值是另一个包含 2 和另一个 List 值的 Cons 值。这个 List 值是另一个 Cons 值,其中包含 3 和一个 List 值,最后是 Nil,表示列表结束的非递归变体。


如果我们尝试编译示例 15-3 中的代码,我们会得到示例 15-4 所示的错误:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/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, Box<List>),
  |               ++++    +

error[E0391]: cycle detected when computing when `List` needs drop
 --> src/main.rs:1:1
  |
1 | enum List {
  | ^^^^^^^^^
  |
  = note: ...which immediately requires computing when `List` needs drop again
  = note: cycle used when computing whether `List` needs drop
  = note: see https://rustc-dev-guide.rust-lang.org/overview.html#queries and https://rustc-dev-guide.rust-lang.org/query.html for more information

Some errors have detailed explanations: E0072, E0391.
For more information about an error, try `rustc --explain E0072`.
error: could not compile `cons-list` (bin "cons-list") due to 2 previous errors


示例 15-4:尝试定义递归枚举时出现的错误


该错误显示此类型“具有无限大小”。原因是我们已经定义了 具有递归变体的 List:它包含自身的另一个值 径直。因此,Rust 无法计算出需要多少空间来存储 列表值。让我们分析一下为什么会收到这个错误。首先,我们将看看 Rust 如何决定它需要多少空间来存储非递归类型的值。


计算非递归类型的大小


回想一下我们在示例 6-2 中定义的 Message 枚举,当我们在第 6 章讨论枚举定义时:

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn main() {}


为了确定为 Message 值分配多少空间,Rust 会遍历每个变体以查看哪个变体需要最多的空间。Rust 看到 Message::Quit 不需要任何空间,Message::Move 需要足够的空间来存储两个 i32 值,依此类推。因为只会使用一个变体,所以 Message 值需要的最大空间是存储其最大变体所需的空间。


这与 Rust 试图确定递归类型(如示例 15-2 中的 List 枚举)需要多少空间时发生的情况形成对比。编译器首先查看 Cons 变体,该变体包含一个 i32 类型的值和一个 List 类型的值。因此,Cons 需要的空间量等于 i32 的大小加上 List 的大小。要确定 List 有多少内存 type 需要,编译器会查看变体,从 Cons 开始 变体。Cons 变体包含一个 i32 类型的值和一个 type List 中,这个过程无限地持续下去,如图 15-1 所示。

An infinite Cons list


图 15-1:由无限 缺点 variants


使用 Box<T> 获取具有已知大小的递归类型


因为 Rust 无法弄清楚为递归定义的类型分配多少空间,所以编译器给出了一个错误,并给出了这个有用的建议:

help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
  |
2 |     Cons(i32, Box<List>),
  |               ++++    +


在这个建议中,“间接”意味着我们应该通过存储指向值的指针来改变数据结构以间接存储值,而不是直接存储一个值。


因为 Box<T> 是一个指针,所以 Rust 总是知道 Box<T> 有多少空间 needs:指针的大小不会根据其数据量而变化 指向。这意味着我们可以将 Box<T> 放在 Cons 变体中,而不是直接放入另一个 List 值。Box<T> 将指向下一个列表 值,它将位于堆上,而不是在 Cons 变体中。从概念上讲,我们仍然有一个列表,使用包含其他列表的列表创建,但这种实现现在更像是将项目彼此相邻放置,而不是彼此内部。


我们可以将示例 15-2 中 List 枚举的定义和示例 15-3 中 List 的用法更改为示例 15-5 中的代码,这将编译:


文件名: src/main.rs

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))))));
}


示例 15-5:使用 Box<T> 以获得已知大小的 List 的定义


Cons 变体需要 i32 的大小加上存储框指针数据的空间。Nil 变体不存储任何值,因此它需要的空间比 Cons 变体少。我们现在知道任何 List 值都将占用 i32 的大小加上框的指针数据的大小。通过使用 box,我们打破了无限的递归链,因此编译器可以计算出存储 List 值所需的大小。图 15-2 显示了 Cons 变体现在的样子。

A finite Cons list


图 15-2:由于 Cons 包含 Box 而没有无限大小的 List


Box 仅提供间接寻址和堆分配;它们没有任何其他特殊功能,就像我们将看到的其他智能指针类型一样。它们也没有这些特殊功能产生的性能开销,因此它们在像 cons list 这样间接寻址是我们唯一需要的功能的情况下很有用。我们也将在第 17 章中介绍盒子的更多用例。


Box<T> 类型是一个智能指针,因为它实现了 Deref trait,它允许将 Box<T> 值视为引用。当一个盒子<T> 值超出范围,则 Box 指向的堆数据将被清理 up 的原因。这两个特征对于我们将在本章其余部分讨论的其他智能指针类型提供的功能更为重要。让我们更详细地探讨这两个特征。