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

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

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:使用

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

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

使用框启用递归类型

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

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

有关缺点列表的更多信息

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

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

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

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

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

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

文件名: src/main.rs

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

fn main() {}

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

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

使用该类型来存储列表类似于 示例 15-3:List1, 2, 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

第一个值成立,另一个值成立。该值为 另一个 value 和 another value。此值 是另一个 value (保存) 和一个 value (value),它是 finally ,表示列表结束的非递归变体。Cons1ListListCons2ListListCons3ListNil

如果我们尝试编译示例 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:尝试定义 递归枚举

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

计算非递归类型的大小

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

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

fn main() {}

为了确定为一个值分配多少空间,Rust 会选择 查看哪个变体需要最多的空间。锈 看到不需要任何空间,需要足够的 space 来存储两个值,依此类推。因为只有一个变体将是 used,则值需要的最大空间是它所需的空间 存储其最大的变体。MessageMessage::QuitMessage::Movei32Message

将此与 Rust 尝试确定 递归类型,如示例 15-2 中的枚举需要。编译器启动 通过查看 Variant,它包含一个 type 的值和一个 value 的类型 。因此,需要的空间量等于 an 加上 .为了弄清楚类型需要多少内存,编译器会查看变体,从变体开始。该 variant 包含一个 type 值和一个 type 值,并且此过程无限持续,如图 15-1 所示。ListConsi32ListConsi32ListListConsConsi32List

无限的缺点列表

图 15-1:由无限 Cons 变体组成的无限 List

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

因为 Rust 无法确定要递归分配多少空间 定义的类型,编译器会给出一个错误,并提供以下有用的建议:

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

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

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

我们可以更改示例 15-2 中 enum 的定义和用法 到示例 15-5 中的代码,它将编译:ListList

文件名: 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<TList 定义 order 具有已知大小

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

有限 Cons 列表

图 15-2 非无限大小的 List 因为 Cons 持有一个 Box

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

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

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