Box<T>指向堆上的数据

最直接的智能指针是一个 box,其类型是 writeBox<T>.Box 允许您将数据存储在堆上,而不是堆栈上。什么 remains on the stack 是指向堆数据的指针。请参阅第 4 章 查看堆栈和堆之间的差异。

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

  • 当您有一个类型在编译时无法知道其大小,并且您希望 在需要精确大小的上下文中使用该类型的值
  • 当您拥有大量数据,并且想要转移所有权,但 确保在执行此作时不会复制数据
  • 当您想要拥有一个值并且只关心它是一个类型 实现特定 trait 而不是特定类型

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

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

在我们讨论Box<T>,我们将介绍 语法以及如何与存储在Box<T>.

示例 15-1 展示了如何使用 box 来存储i32值:

文件名: src/main.rs

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

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

我们定义变量b的值为Box指向 价值5,该 API 在堆上分配。该程序将打印b = 5;在 在这种情况下,我们可以访问框中的数据,就像我们这样做时一样 数据在堆栈上。就像任何自有值一样,当 Box 从 范围,作为bmain,则它将被解除分配。这 释放同时发生在 Box(存储在堆栈上)和 it 的数据 指向 (存储在堆上)。

在堆上放置单个值不是很有用,因此您不会使用 boxes 他们自己经常以这种方式。具有像单个i32在 stack 中,它们默认存储在大多数 情况。让我们看一个案例,其中 box 允许我们定义类型 如果我们没有盒子,就不会被允许。

使用框启用递归类型

递归类型的值可以具有另一个相同类型的值作为 本身。递归类型会带来一个问题,因为在编译时 Rust 需要 了解类型占用多少空间。但是,的 递归类型理论上可以无限持续,所以 Rust 不知道怎么做 价值需要的很多空间。因为盒子的大小是已知的,所以我们可以启用 递归类型。

作为递归类型的一个示例,让我们探索 cons 列表。这是一个数据 类型常见于函数式编程语言中。缺点列表类型 我们将定义 is straightforward except the recursion;因此, 我们将使用的示例中的 concepts 在您进入 涉及递归类型的更复杂的情况。

有关缺点列表的更多信息

缺点列表是来自 Lisp 编程语言的一种数据结构 及其方言 和 它由嵌套对组成,并且是 链表。它的名字来源于consfunction(“construct ”的缩写 function“),它从其两个参数构造一个新的对。由 叫cons对于由一个值和另一个值组成的对,我们可以 构造由递归对组成的 cons 列表。

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

(1, (2, (3, Nil)))

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

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

示例 15-2 包含一个 cons 列表的枚举定义。请注意,此代码 不会编译,因为Listtype 没有已知的大小,因此 我们会演示的。

文件名: src/main.rs

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

fn main() {}

示例 15-2:第一次尝试将枚举定义为 表示i32

注意:我们正在实施一个仅保留i32的值 本示例的用途。我们可以使用泛型来实现它,因为我们 在第 10 章中讨论,定义一个可以存储 任何类型。

使用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:使用Listenum 来存储列表1, 2, 3

第一个Cons值保留1和另一个List价值。这Listvalue 为 另一个Cons值,则2和另一个List价值。这List价值 再来一个Cons值,则3以及Listvalue,最后是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 无法计算出需要多少空间来存储List价值。让我们分析一下为什么会收到这个错误。首先,我们要看看 Rust 决定需要多少空间来存储非递归类型的值。

计算非递归类型的大小

回想一下Messageenum 我们在示例 6-2 中讨论 enum 时定义的 第 6 章中的定义:

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

fn main() {}

确定要为Messagevalue 时,Rust 会去 查看哪个变体需要最多的空间。锈 看到那个Message::Quit不需要任何空间,Message::Move需要足够的 存放 2 个空间i32值,依此类推。因为只有一个变体将是 使用,最空间 aMessagevalue 将需要的是它需要的空间 存储其最大的变体。

将此与 Rust 尝试确定 递归类型(如List示例 15-2 中的 enum 需要。编译器启动 通过查看Consvariant 的 ID 中,它保存 type 为i32和一个值 的类型List.因此Cons需要的空间量等于 一i32加上List.要确定有多少内存,Listtype 需要,编译器会查看变体,从Cons变体。这Consvariant 保存 type 为i32和类型List,并且这个过程无限持续,如图 15-1 所示。

无限的缺点列表

图 15-1:无限List由无限Cons变种

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>将指向下一个List值,该值将位于堆上,而不是位于Cons变体。 从概念上讲,我们仍然有一个列表,创建时包含其他列表的列表,但是 此实现现在更像是将项彼此相邻放置 而不是彼此内部。

我们可以更改Listenum 和 的List在示例 15-3 中,将代码转换为示例 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:的定义List使用Box<T>在 order 具有已知大小

Consvariant 需要i32加上用于存储 box 的指针数据。这Nilvariant 不存储任何值,因此它需要更少的空间 比Cons变体。我们现在知道,任何Listvalue 将占用 的大小i32加上 Box 的指针数据的大小。通过使用 Box,我们 打破了无限的递归链,因此编译器可以计算出大小 它需要存储一个List价值。图 15-2 显示了Cons变体 现在看起来。

有限 Cons 列表

图 15-2:AList不是无限大小的 因为Cons持有Box

Box 仅提供间接寻址和堆分配;他们没有 其他特殊功能,例如我们将使用另一个智能指针看到的功能 类型。它们也没有这些特殊的 功能,因此它们在 cons 列表等情况下非常有用,其中 间接性是我们唯一需要的功能。我们将研究 box 的更多用例 在第 17 章中也是如此。

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

本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准