Updated 'src/smart_pointers/box-t'.

This commit is contained in:
Hector PENG
2026-04-03 11:20:04 +08:00
parent 9bde0e4ea2
commit 5ded3d4242
7 changed files with 297 additions and 102 deletions

204
\ Normal file
View File

@@ -0,0 +1,204 @@
# 使用 `Box<T>` 指向堆上的数据
最简单的灵巧指针是 *匣子* ,其类型写为 `Box<T>`。匣子允许咱们存储数据于堆上,而非栈上。留在栈上的指向堆数据的指针。请参考 [第 4 章](../ownership/about_ownership.md#diff_between_stack_n_heap) 回顾栈和堆之间的区别。
除了在堆上而不是栈上存储他们的数据外,匣子数据结构并无性能开销。但他们也没有太多额外的能力。咱们将在以下这些情况下经常使用他们:
- 当咱们有个在编译时无法得知其大小的类型,而打算在需要精确大小的上下文中,使用这种类型的值时;
- 当咱们有着大量数据,而打算转移所有权,但要确保转移时数据不会被拷贝时;
- 当咱们打算拥有某个值,且只关心他是否实现特定特质,而非属于某种特定类型时。
我们将在 [“以匣子数据结构实现递归类型”](#以匣子数据结构实现递归类型) 中演示第一种情况。在第二种情形下,转移大量数据的所有权会耗时很长,因为数据会在栈上来回拷贝。为了提升这种情况下的性能,我们可以存储大量数据于堆上的一个匣子中。然后,只有少量的指针数据在栈上来回拷贝,而他引用的数据保留在堆上的一处。第三种情况称为 *特质对象*,第 18 中的 [使用特质对象抽象共用行为](../oop/trait_objects.md) 专门讨论这一主题。因此,咱们在这里学到的内容将在那个小节中再次应用!
## 在堆上存储数据
在讨论 `Box<T>` 的堆存储用例前,我们将介绍这一语法,以及怎样与 `Box<T>` 内存储的值交互。
下面清单 15-1 展示了怎样使用匣子在堆上存储 `i32` 的值:
<a name="listing_15-1"></a>
文件名:`src/main.rs`
```rust
fn main() {
let b = Box::new(5);
println! ("b = {b}");
}
```
**清单 15-1**:使用匣子在堆上存储 `i32` 值
我们定义变量 `b` 为有着指向值 `5` 的一个 `Box` 值,而值 `5` 分配在堆上。这个程序将打印 `b = 5`;在这种情况下,我们可以访问匣子中的数据,就像我们访问栈上的这一数据一样。与任何自有值一样,当匣子超出作用域时,正如 `b` 在 `main` 结束处所做的那样,他将被解除内存分配(释放)。解除内存分配会针对匣子(存储在栈上)和他指向的数据(存储在堆上)同时发生。
在堆上放置单个值并不是很有用,因此咱们不会经常以这种方式单独使用匣子。在大多数情况下,让 i32 这样的值处于他们默认存储所在的栈上更为合适。我们来看看一种情形,其中匣子允许我们定义一些,在没有匣子数据结构时我们不被允许定义的类型。
## 通过匣子数据结构得到递归类型
*递归类型* 的值,可以将同一类型的另一个值作为自身的一部分。递归类型会带来一个问题,因为 Rust 需要在编译时知道类型占用了多少空间。然而,递归类型的值的嵌套理论上可以无限地延续,因此 Rust 无法知道该值需要多少空间。由于匣子有着已知大小,我们可以通过在递归类型定义中插入匣子来得到递归类型。
作为递归类型的示例,我们来探讨一下 *构造列表cons list*the *cons* tructs *list*)。这是一种常见于函数式编程语言中的数据类型。除了递归外,我们将定义的构造列表类型很简单;因此,当咱们遇到涉及递归类型的更复杂的情况时,我们将使用的示例中的概念将很有用。
### 理解构造列表
所谓 *构造列表*,是 Lisp 编程语言及其方言中的数据结构,由嵌套对构成,是 Lisp 版本的链表。他的名称来自 [Lisp 中的 `cons` 函数](https://zh.wikipedia.org/zh-cn/%E5%88%97%E8%A1%A8%E6%A7%8B%E9%80%A0%E5%87%BD%E6%95%B8)*construct function* 的缩写),该函数会根据他的两个参数构造一个新的对。通过对由一个值和另一个对组成的对调用 `cons`,我们可以构造由递归对组成的构造列表。
例如,下面是个构造列表的伪代码表示,包含列表 `1, 2, 3`,其中每个位于括号中:
```lisp
(1, (2, (3, Nil)))
```
构造列表中的每个项目都包含两个元素:当前项目的值和下一个项目的值。列表中的最后一个项目仅包含一个名为 `Nil` 的值,且没有下一项目。构造列表是通过递归调用 `cons` 函数产生的。表示递归基础情形的规范名称是 `Nil`。请注意,这与第 6 章中讨论的 “null” 或 “nil” 概念不同,后者属于无效或不存在的值。
构造列表不是 Rust 中的常用数据结构。大多数时候,当咱们在 Rust 中有个项目列表时,使用 `Vec<T>` 是种更好的选择。在其他时间,更复杂的递归数据类型在各种情况下 *都* 很有用,但从这一章中的构造列表开始,我们可以探讨匣子数据结构,怎样让我们不受干扰地定义递归数据类型。
下面清单 15-2 包含用于构造列表的枚举定义。请注意,这段代码还不会编译,因为其中的 `List` 类型没有已知的大小,我们将演示这点。
<a name="listing_15-2"></a>
文件名:`src/main.rs`
```rust
enum List {
cons(i32, List),
Nil,
}
```
**清单 15-2**:首次尝试定义一个枚举,来表示 `i32` 值的构造列表数据结构
> **注意**:我们正在出于这个示例的目的,实现一个仅包含 `i32` 值的构造列表。正如我们在第 10 章中讨论的那样,我们本可以使用泛型来实现,定义一种可以存储任何类型值的构造列表。
使用这个 `List` 类型来存储列表 `1, 2, 3`,将看起来像下面清单 15-3 中的代码:
<a name="listing_15-3"></a>
文件名:`src/main.rs`
```rust
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` 值是另一个 `Cons` 值,其中包含 `2` 与另一个 `List`。这个 `List` 值又是另一个 `Cons` 值,其中包含 `3` 和一个 `List` 值,该值最终为 `Nil`,表示列表结束的非地归变种。
当我们尝试编译清单 15-3 中的代码时,我们会得到下面清单 15-4 中所示的报错:
<a name="listing_15-4"></a>
```console
$ cargo run
Compiling cons-list v0.1.0 (/home/hector/rust-lang-zh_CN/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**:尝试定义递归枚举时,我们得到的报错
报错显示这种类型“有着无限大小has infinite size”。原因是咱们以递归的变种定义了 `List`他直接包含本身的另一个值。因此Rust 无法计算出存储 `List` 值需要多少空间。我们来分析一下为什么我们会得到这个报错。首先我们将看看Rust 如何决定存储非递归类型需要多少空间。
### 计算非递归类型的大小
回顾咱们在第 6 章中讨论枚举定义时,在 [清单 6-2](../enums_and_pattern_matching/defining_an_enum.md#listing_6-2) 中定义的 `Message` 枚举:
```rust
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
```
为了确定为 `Message` 值分配多少空间Rust 会遍历每个变种以发现哪个变种需要最多的空间。Rust 发现 `Message::Quit` 不需要任何空间,`Message::Move` 需要足够的空间来存储两个 `i32` 值,以此类推。由于只有一个变种会被用到,因此 `Message` 值所需的最大内存空间,便是存储最大变种将占用的空间。
与 Rust 尝试确定像是清单 15-2 中的 `List` 枚举,这样的递归类型需要多少空间时的情况对比这一过程。编译器首先查看 `Cons` 变种,他包含一个 `i32` 类型的值和一个 `List` 类型的值。因此,`Cons` 需要的空间量等于 `i32` 的大小加上 `List` 的大小。为了计算出 `List` 类型需要多少内存,编译器会从 `Cons` 变种开始查看变种。 `Cons` 变种包含一个 `i32` 类型的值和一个 `List` 类型的值,而这个过程会无限地继续下去,如图 15-1 中所示。
<a name="f_15-1"></a>
![由无限个 `Cons` 变种构成的无限 `List`](../images/15-01.svg)
**图 15-1**:由无限个 `Cons` 变种构成的无限 `List`
### 使用 `Box<T>` 获得已知大小的递归类型
因为 Rust 无法计算出要为以递归方式定义出的类型分配多少空间,所以编译器会给出带有下面这个有用的建议的报错:
```console
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable
|
2 | Cons(i32, Box<List>),
| ++++ +
```
在此建议中“间接indirection” 意味着我们不应直接存储一个值,而应该改变数据结构,通过存储一个指向该值的指针,间接存储该值。
由于 `Box<T>` 是个指针Rust 总是知道 `Box<T>` 需要多少内存空间:指针的大小不会根据他指向的数据量而变化。这意味着咱们可以在 `Cons` 变种里放入一个 `Box<T>`,而不是直接放入另一个 `List` 值。`Box<T>` 将指向下一个 `List` 值,他将在内存堆上而不是在 `Cons` 变种内。从概念上讲,咱们仍然有一个列表,用持有其他列表的列表来创建,但现在这种实现更像是把列表项目放在彼此的旁边,而不是放在彼此的里面。
咱们可以把清单 15-2 中 `List` 枚举的定义和清单 15-3 中 `List` 的用法,改为下面清单 15-5 中的代码,这样就可以编译了:
文件名:`src/main.rs`
```rust
#[derive(Debug)]
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))))));
println! ("list: {:?}", list);
}
```
*清单 15-5使用 `Box<T>` 的 `List` 的定义,以便有已知的大小*
`Cons` 变种需要一个 `i32` 的大小,加上存储匣子指针数据的内存空间。`Nil` 变种不存储存储任何值,所以他需要的空间比 `Cons` 变种少。咱们现在知道,任何 `List` 值都会占用一个 `i32` 的大小,加上一个匣子的指针数据的大小。通过使用匣子,咱们已经破解了无限的递归链,因此编译器可以计算出存储 `List` 值所需的内存大小。下图 15-2 显示了 `Cons` 变种现在的样子:
![由于 `Cons` 保存了一个 `Box` 而不在是无限大小的 `List`](../images/15-02.svg)
*图 15-02不在是无限大小的 `List`,因为 `Cons` 持有着一个 `Box`*
匣子仅提供这种间接与内存堆的内存分配;他们不具备任何像咱们在其他灵巧指针类型中,将看到的其他特别能力。他们也没有这些特殊能力所带来的性能开销,所以在像构造列表这样的情况下,他们就能很有用,因为间接性是我们唯一需要的功能。在第 17 章,咱们还会看一下匣子的更多用例。
`Box<T>` 类型是一个灵巧指针,因为他实现了 `Deref` 特质,他允许 `Box<T>` 值被当作引用。当 `Box<T>` 值超出作用域时,由于 `Drop` 特质的实现,匣子所指向的内存堆数据也会被清理。这两个特质对于咱们在本章后面将讨论的其他灵巧指针所提供的功能,将更加重要。咱们来更深入地探讨这两个特质。
End

View File

@@ -0,0 +1,6 @@
[package]
name = "cons-list"
version = "0.1.0"
edition = "2024"
[dependencies]

View File

@@ -0,0 +1,11 @@
#[derive(Debug)]
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))))));
}

View File

@@ -1,8 +0,0 @@
[package]
name = "cons_list_demo"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@@ -1,35 +0,0 @@
#[derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let value = Rc::new(RefCell::new(5));
let a = Rc::new(
Cons(
Rc::clone(&value),
Rc::new(Nil)
)
);
let b = Cons(
Rc::new(RefCell::new(3)),
Rc::clone(&a)
);
let c = Cons(
Rc::new(RefCell::new(4)),
Rc::clone(&a)
);
*value.borrow_mut() += 10;
println! ("之后的 a = {:?}", a);
println! ("之后的 b = {:?}", b);
println! ("之后的 c = {:?}", c);
}

View File

@@ -7,6 +7,7 @@
当咱们掌握所有权后,咱们将为掌握那些令到 Rust 独特的功能打下了坚实基础。在这一章中,咱们将通过一些示例了解所有权,这些示例重点关注极为常见的数据结构:字符串。
<a name="diff_between_stack_n_heap"></a>
> **内存栈与内存堆**
>
> 许多编程语言都不要求咱们经常考虑栈与堆。但在像 Rust 这样的系统编程语言中,某个值是在栈上还是在堆上会影响语言的行为方式,以及为何咱们必须做出某些决定的原因。这一章稍后的所有权的一些部分,将与栈和堆结合讲解,因此这里是个预备简要说明。

View File

@@ -1,76 +1,79 @@
# 使用 `Box<T>` 指向内存堆上的数据
# 使用 `Box<T>` 指向堆上的数据
直接了当的灵巧指针,便*匣子box* ,其类型写为 `Box<T>`。匣子允许咱们将数据存储在堆上,而非栈上。留在栈上的指向堆数据的指针。请参考第 4 章,回顾一下栈与堆的区别。
简单的灵巧指针是 *匣子* ,其类型写为 `Box<T>`。匣子允许咱们存储数据于堆上,而非栈上。留在栈上的指向堆数据的指针。请参考 [第 4 章](../ownership/about_ownership.md#diff_between_stack_n_heap) 回顾栈和堆之间的区别。
除了在内存堆而不是栈上存储数据外,匣子数据结构并无性能方面的开销。但他们也没有多额外能力。在下面这些情况下,咱们将经常用到他们:
除了在堆而不是栈上存储他们的数据外,匣子数据结构并无性能开销。但他们也没有多额外能力。咱们将在以下这些情况下经常使用他们:
- 当咱们有个在编译时无法知其大小的类型,并且咱们想在需要精确大小的上下文中使用类型的值时when you have a type whose size can't be known at compile time and you want to use a value of that type in a context that requires an exact size;
- 当咱们有着大量数据,而转移所有权,但要确保转移时数据不会被拷贝时when you have a large amount of data and you want to transfer ownership but ensure the data won't be copied when you do so
- 当咱们拥有某个值,只关心他是实现特定特质的类型而非某个特定类型时when you want to own a value and you care only that it's a type that implements a particular trait rather than being of a specific type
- 当咱们有个在编译时无法知其大小的类型,而打算在需要精确大小的上下文中使用这种类型的值时
- 当咱们有着大量数据,而打算转移所有权,但要确保转移时数据不会被拷贝时;
- 当咱们打算拥有某个值,只关心他是实现特定特质,而非属于某种特定类型时
我们将在 [匣子数据结构得到递归类型”](#匣子数据结构得到递归类型) 小节演示第一种情况。在第二种情形下,转移大量数据的所有权可能需要很长的时间,因为数据在堆栈上被拷过来拷过去。为改进这种情况下的性能,们可以将内存堆上的大量数据,存储在一个匣子中。后,在栈上便只有少量的指针数据被拷来拷去了,同时其所引用的数据,则会保持在堆上的一处。第三种情况称为 *特质对象trait object*,第 17 章用了一整节,[使用允许不同类型值的特质对象”](Ch17_Object_Oriented_Programming_Features_of_Rust.md#使用允许不同类型值的特质对象),来讨论这个话题。因此,咱们在这里学到的东西,将在第 17 章中再度得到应用!
我们将在 [匣子数据结构实现递归类型”](#匣子数据结构实现递归类型) 演示第一种情况。在第二种情形下,转移大量数据的所有权会耗时很长,因为数据会在栈上来回拷贝。为了提升这种情况下的性能,们可以存储大量数据于堆上的一个匣子中。后,只有少量的指针数据在栈上来回拷贝,而他引用的数据保留在堆上的一处。第三种情况称为 *特质对象*,第 18 中的 [使用特质对象抽象共用行为](../oop/trait_objects.md) 专门讨论这一主题。因此,咱们在这里学到的内容将在那个小节中再次应用!
### 使用 `Box<T>` 在内存堆上存储数据
在咱们讨论 `Box<T>` 的内存堆存储用例之前,咱们将介绍其语法与怎样与存储在 `Box<T>` 中的值交互。
## 在堆上存储数据
下面清单 15-1 展示了如何使用匣子,在内存堆上存储一个 `i32` 的值:
在讨论 `Box<T>` 的堆存储用例前,我们将介绍这一语法,以及怎样与 `Box<T>` 内存储的值交互。
下面清单 15-1 展示了怎样使用匣子在堆上存储 `i32` 的值:
<a name="listing_15-1"></a>
文件名:`src/main.rs`
```rust
fn main() {
let b = Box::new(5);
println! ("b = {}", b);
println! ("b = {b}");
}
```
*清单 15-1使用匣子在内存堆上存储一个 `i32` 值*
**清单 15-1**:使用匣子在堆上存储 `i32`
我们变量 `b` 定义为具有指向值 `5``Box` 值,该值分配在堆上。程序将打印 `b = 5`;在这种情况下,们可以访问栈上的数据一样,访问匣子中的数据。就像任何拥有的值一样,当某个匣子超出作用域时,如 `b``main`尾处那样,他将被解除内存分配。解除内存分配,同时发生在匣子(存储在栈上)和他指向的数据(存储在内存堆上)。
我们定义变量 `b` 为有着指向值 `5`一个 `Box` 值,而值 `5` 分配在堆上。这个程序将打印 `b = 5`;在这种情况下,们可以访问匣子中的数据,就像我们访问栈上的这一数据一样。与任何自有值一样,当匣子超出作用域时,`b``main`束处所做的那样,他将被解除内存分配(释放)。解除内存分配会针对匣子(存储在栈上)和他指向的数据(存储在堆上)同时发生
在堆上放置单个值并不是很有用,因此咱们不会经常以这种方式单独使用匣子。在大多数情况下,让 i32 这样的值处于他们默认存储所在的栈上更为合适。我们来看看一种情形,其中匣子允许我们定义一些,在没有匣子数据结构时我们不被允许定义的类型。
## 匣子数据结构得到递归类型
## 通过匣子数据结构得到递归类型
**Enabling Recursive Types with Boxes**
*递归类型* 的值,可以将同一类型的另一个值作为自身的一部分。递归类型会带来一个问题,因为 Rust 需要在编译时知道类型占用了多少空间。然而,递归类型的值的嵌套理论上可以无限地延续,因此 Rust 无法知道该值需要多少空间。由于匣子有着已知大小,我们可以通过在递归类型定义中插入匣子来得到递归类型。
作为递归类型的示例,我们来探讨一下 *构造列表cons list*the *cons* tructs *list*)。这是一种常见于函数式编程语言中的数据类型。除了递归外,我们将定义的构造列表类型很简单;因此,当咱们遇到涉及递归类型的更复杂的情况时,我们将使用的示例中的概念将很有用。
*递归类型recursive type* 的值可以有另一个相同类型的值作为其自身的一部分。递归类型带来了一个问题,因为在编译时 Rust 需要知道一个类型占用了多少空间。然而,理论上递归类型的值的嵌套可以无限地继续下去,所以 Rust 无法知道值需要多少空间。因为盒子有一个已知的大小,我们可以通过在递归类型定义中插入一个盒子来得到递归类型。
### 理解构造列表
作为一个递归类型的示例,咱们来探讨一下 *构造列表cons list*the *cons* tructs *list*)。这是函数式编程语言中常见的一种数据类型。除了其中的递归之外,咱们将定义的构造列表类型是简单明了的;因此,当咱们遇到涉及递归类型的更复杂情况时,我们将使用的例子中的概念会很有用
所谓 *构造列表*,是 Lisp 编程语言及其方言中的数据结构,由嵌套对构成,是 Lisp 版本的链表。他的名称来自 [Lisp 中的 `cons` 函数](https://zh.wikipedia.org/zh-cn/%E5%88%97%E8%A1%A8%E6%A7%8B%E9%80%A0%E5%87%BD%E6%95%B8)*construct function* 的缩写),该函数会根据他的两个参数构造一个新的对。通过对由一个值和另一个对组成的对调用 `cons`,我们可以构造由递归对组成的构造列表
### 构造列表的更多信息
所谓 *构造列表cons list*,是来自 Lisp 编程语言及其方言的一种数据结构,由嵌套对组成,是 Lisp 版本的链表is made up of nested pairs, and is the Lisp version of a linked list。其名称来自于 Lisp 的 `cons` 函数是构造函数construct function的简称该函数从其两个参数构造出一个新嵌套对。通过对由一个值与另一嵌套对组成的嵌套对上调用 `cons`,咱们便可构造出由递归嵌套对组成的构造链表。
例如,下面是一个包含列表 1、2、3 的构造列表的伪代码表示,每个嵌套对都在括号里:
例如,下面是个构造列表的伪代码表示,包含列表 `1, 2, 3`,其中每个位于括号中:
```lisp
(1, (2, (3, Nil)))
```
构造列表中的每个目都包含两个元素:当前目的值下一条目。列表中最后条目,只包含名为 `Nil` 的值,没有下一目。构造列表是递归调用 `cons` 函数产生的。表示递归基础的规范名称是 `Nil`the canonical name to denote the base case of the recursion is `Nil`。请注意,这与第 6 章中的 “null” 或 “nil” 概念不同,后者是一个无效或不存在的值。
构造列表中的每个目都包含两个元素:当前目的值下一个项目的值。列表中最后一个项目仅包含一个名为 `Nil` 的值,没有下一目。构造列表是通过递归调用 `cons` 函数产生的。表示递归基础情形的规范名称是 `Nil`。请注意,这与第 6 章中讨论的 “null” 或 “nil” 概念不同,后者属于无效或不存在的值。
Rust 中,构造列表并不是一种常用数据结构。大多数时候,当咱们在 Rust 中有一个条目清单时,`Vec<T>`一个更好的选择。那么别的时候的各种情况下,更复杂的递归数据类型,*则都是* 有用的,而在本章中构造列表开始,咱们便可专心探讨匣子数据结构如何让我们定义递归数据类型。
构造列表不是 Rust 中常用数据结构。大多数时候,当咱们在 Rust 中有个项目列表时,使用 `Vec<T>`更好的选择。在其他时间,更复杂的递归数据类型在各种情况下 ** 很有用,但从这一章中构造列表开始,我们可以探讨匣子数据结构,怎样让我们不受干扰地定义递归数据类型。
下面清单 15-2 包含构造列表的一种枚举定义。请注意,这段代码还不编译,因为 List 类型没有一个已知的大小,我们将证明这一点。
下面清单 15-2 包含用于构造列表的枚举定义。请注意,这段代码还不编译,因为其中的 `List` 类型没有已知的大小,我们将演示这点。
<a name="listing_15-2"></a>
文件名:`src/main.rs`
```rust
enum List {
Cons(i32, List),
cons(i32, List),
Nil,
}
```
*清单 15-2:第一次尝试定义一个枚举来表示 `i32` 值的构造列表数据结构*
**清单 15-2**:首次尝试定义一个枚举来表示 `i32` 值的构造列表数据结构
> 注意:出于这个示例的目的,咱们正在实现一个仅包含 `i32` 值的构造列表。咱们本可以使用泛型来实现他,就像咱们在第 10 章中讨论的那样,定义出一个可存储任何类型值的构造列表。
> **注意**:我们正在出于这个示例的目的,实现一个仅包含 `i32` 值的构造列表。正如我们在第 10 章中讨论的那样,我们本可以使用泛型来实现,定义一种可以存储任何类型值的构造列表。
使用 `List` 类型来存储列表 `1, 2, 3`,看起来就会像下面清单 15-3 中的代码:
使用这个 `List` 类型来存储列表 `1, 2, 3`看起来像下面清单 15-3 中的代码:
<a name="listing_15-3"></a>
文件名:`src/main.rs`
```rust
@@ -81,40 +84,52 @@ fn main() {
}
```
*清单 15-3使用 `List` 枚举来存储列表 `1, 2, 3`*
**清单 15-3**:使用 `List` 枚举来存储列表 `1, 2, 3`
其中第一个 `Cons` 保存着 `1` 另一 `List` 值。这个 `List` 值是另一个 `Cons` 值,保存了 `2` 与另一 `List`。这个 `List`则是又一个 `Cons` 值,其保存了 `3` 一个 `Nil` 的最后 `List` 值,这个非递归变种表示列表结束。
其中第一个 `Cons` 包含 `1` 另一 `List` 值。这个 `List` 值是另一个 `Cons` 值,其中包含 `2` 与另一 `List`。这个 `List` 值又是另一个 `Cons` 值,其中包含 `3` 一个 `List` 值,该值最终为 `Nil`表示列表结束的非地归变种
如果我们尝试编译清单 15-3 中的代码,我们会得到下面清单 15-4 中的报错:
我们尝试编译清单 15-3 中的代码,我们会得到下面清单 15-4 中所示的报错:
<a name="listing_15-4"></a>
```console
$ cargo run
Compiling sp_demos v0.1.0 (/home/lennyp/rust-lang/sp_demos)
Compiling cons-list v0.1.0 (/home/hector/rust-lang-zh_CN/projects/cons-list)
error[E0072]: recursive type `List` has infinite size
--> src/main.rs:1:1
|
1 | enum List {
| ^^^^^^^^^ recursive type has infinite size
| ^^^^^^^^^
2 | Cons(i32, List),
| ---- recursive without indirection
|
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
|
2 | Cons(i32, Box<List>),
| ++++ +
For more information about this error, try `rustc --explain E0072`.
error: could not compile `sp_demos` due to previous error
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尝试定义递归枚举时,们得到的报错*
**清单 15-4**:尝试定义递归枚举时,们得到的报错
报错显示类型“有无限大小has infinite size”。原因是咱们一个递归的变种定义了 `List`直接持有自己的的另一个值。因此Rust 无法计算出他需要多少空间来存储一个 `List`。咱们来分析一下为什么们会得到这个报错。首先,咱们来看看 Rust 如何确定出他需要多少内存空间来存储某个非递归类型的值
报错显示这种类型“有无限大小has infinite size”。原因是咱们递归的变种定义了 `List`直接包含本身的另一个值。因此Rust 无法计算出存储 `List`需要多少空间。我们来分析一下为什么们会得到这个报错。首先,我们将看看Rust 如何决定存储非递归类型需要多少空间
### 计算非递归类型的大小
回顾咱们在第 6 章讨论枚举定义时,在清单 6-2 中定义的 `Message` 枚举:
回顾咱们在第 6 章讨论枚举定义时,在 [清单 6-2](../enums_and_pattern_matching/defining_an_enum.md#listing_6-2) 中定义的 `Message` 枚举:
```rust
enum Message {
@@ -125,33 +140,35 @@ enum Message {
}
```
为了确定出给一个 `Message` 值分配多少内存空间Rust 会检查每个变种,找出哪个变种需要最多的空间。Rust 发现 `Message::Quit` 不需要任何空间,`Message::Move` 需要存储两个 `i32`的足够空间,以此类推。由于只会用到一个变种,因此某个 `Message` 值所需的最大内存空间,便是存储最大变种的空间。
为了确定 `Message` 值分配多少空间Rust 会遍历每个变种,以发现哪个变种需要最多的空间。Rust 发现 `Message::Quit` 不需要任何空间,`Message::Move` 需要足够的空间来存储两个 `i32` 值,以此类推。由于只一个变种会被用到,因此 `Message` 值所需的最大内存空间,便是存储最大变种将占用的空间。
将此与 Rust 尝试确定递归类型(如清单 15-2 中的 `List` 枚举需要多少空间时发生的情况进行对比。编译器查看 `Cons` 变种开始,其持有一个 `i32` 类型的值和一个 `List` 类型的值。因此,`Cons` 需要的空间量等于 `i32` 的大小加上 `List` 的大小。为了计算出 `List` 类型需要多少内存,编译器会从 `Cons`开始查看变种。 `Cons` 变种保存了一个 `i32` 类型的值和一个 `List` 类型的值,这个过程会无限继续下去,如图 15-1 所示。
与 Rust 尝试确定像是清单 15-2 中的 `List` 枚举,这样的递归类型需要多少空间时的情况对比这一过程。编译器首先查看 `Cons` 变种,他包含一个 `i32` 类型的值和一个 `List` 类型的值。因此,`Cons` 需要的空间量等于 `i32` 的大小加上 `List` 的大小。为了计算出 `List` 类型需要多少内存,编译器会从 `Cons`开始查看变种。 `Cons` 变种包含一个 `i32` 类型的值和一个 `List` 类型的值,这个过程会无限继续下去,如图 15-1 所示。
![由无限的 `Cons` 变种组成的一个无限 `List`](../images/15-01.svg)
<a name="f_15-1"></a>
![由无限个 `Cons` 变种构成的无限 `List`](../images/15-01.svg)
*图 15-01由无限`Cons` 变种成的一个无限 `List`*
**图 15-1**:由无限`Cons` 变种成的无限 `List`
### 使用 `Box<T>` 获得已知大小的递归类型
### 获得已知大小的递归类型
因为 Rust 无法计算出要为递归方式定义的类型分配多少空间,所以编译器给出带有下面这个有用的建议的报错:
因为 Rust 无法计算出要为递归定义的类型分配多少空间,所以编译器给出带有下面这个有用的建议的报错:
```console
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
|
2 | Cons(i32, Box<List>),
| ++++ +
```
建议中,“间接,indirection 意味着我们不应直接存储一个值,而应该改变数据结构通过存储一个指向值的指针间接存储值。
这个建议中,*indirection* 意味着我们不应直接存储值,而应修改数据结构通过存储指向值的指针间接存储值。
由于 `Box<T>` 是个指针Rust 总是知道 `Box<T>` 需要多少内存空间:指针的大小不会根据他指向的数据量而变。这意味着们可以`Cons` 变种里放入一个 `Box<T>`,而不是直接放另一个 `List` 值。`Box<T>` 将指向下一个 `List` 值,他将在内存堆上而不是在 `Cons` 变种内。从概念上讲,们仍然有个列表,用持有其他列表的列表创建,但现在这种实现更像是把列表项目放在彼此的旁边,而不是放在彼此的里面
因为 `Box<T>` 是个指针Rust 总是知道 `Box<T>` 需要多少空间:指针的大小不会根据他指向的数据量而变。这意味着们可以放置一个 `Box<T>``Cons` 变种内,而不是直接放另一个 `List` 值。`Box<T>` 将指向下一个 `List` 值,该值将在堆上而不是在 `Cons` 变种内。从概念上讲,们仍然有个列表,他是以包含其他列表的列表创建,但这种实现方式现在更像是彼此相邻地放置项目,而非嵌套在彼此之中
们可以把清单 15-2 中 `List` 枚举的定义清单 15-3 中 `List` 的用法,改为下面清单 15-5 中的代码,这样就可以编译了:
们可以把清单 15-2 中 `List` 枚举的定义,以及清单 15-3 中 `List` 的用法,改为下面清单 15-5 中的代码,这段代码将编译。
<a name="listing_15-5"></a>
文件名:`src/main.rs`
```rust
@@ -165,22 +182,21 @@ use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
println! ("list: {:?}", list);
}
```
*清单 15-5使用 `Box<T>` 的 `List` 的定义,以便有已知的大小*
**清单 15-5**:为了有已知大小,而使用 `Box<T>``List` 的定义
`Cons` 变种需要一个 `i32` 的大小,加上存储匣子指针数据的内存空间。`Nil` 变种不存储存储任何值,所以他需要的空间`Cons` 变种少。咱们现在知道,任何 `List` 值都会占用一个 `i32` 的大小加上一个匣子的指针数据的大小。通过使用匣子,咱们已经破解了无限的递归链,因此编译器可以计算出存储 `List` 值所需的内存大小。下图 15-2 示了 `Cons` 变种现在的样子:
`Cons` 变种需要一个 `i32` 的大小,加上存储匣子指针数据的空间。`Nil` 变种不存储值,因此他需要`Cons` 变种更少的栈上空间。现在我们知道,任何 `List` 值都会占用一个 `i32` 的大小加上一个匣子的指针数据的大小。通过使用匣子,我们打破了无限的递归链,因此编译器可以计算出存储 `List` 值所需的内存大小。下图 15-2 示了 `Cons` 变种现在的样子:
![由于 `Cons` 保存了一个 `Box` 而不在是无限大小的 `List`](../images/15-02.svg)
<a name="f_15-2"></a>
![不是无限大小的 `List`,因为 `Cons` 包含一个 `Box`](../images/15-02.svg)
*图 15-02是无限大小的 `List`,因为 `Cons` 持有着一个 `Box`*
**图 15-02**:不是无限大小的 `List`,因为 `Cons` 包含一个 `Box`
匣子仅提供这种间接与内存堆的内存分配;他们不具备任何像咱们在其他灵巧指针类型中,将看到的其他特别能力。他们也没有这些特殊能力带来的性能开销,所以在像构造列表这样的情况下,他们就能很有用,因为间接性是我们唯一需要的功能。在第 17 章,咱们还会看一下匣子的更多用例。
匣子数据结构仅提供间接性和堆分配;他们不具备任何别的特殊能力,就像我们在其他灵巧指针类型中看到的那样。他们也没有这些特殊能力带来的性能开销,因此在像构造列表这样的,其中间接性是我们唯一需要的特性的情况下会非常有用。我们将在第 18 中,讨论匣子数据结构的更多用例。
`Box<T>` 类型是一个灵巧指针,因为他实现了 `Deref` 特质,允许 `Box<T>`被当作引用。当 `Box<T>` 值超出作用域时,由于 `Drop` 特质的实现,匣子指向的内存堆数据也会被清理。这两个特质对于咱们在本章后面将讨论的其他灵巧指针提供的功能,将更加重要。们来更深入地探讨这两个特质。
`Box<T>` 类型属于灵巧指针,因为他实现了 `Deref` 特质,该特质允许 `Box<T>`可以像引用一样对待。当 `Box<T>` 值超出作用域时,由于 `Drop` 特质的实现,匣子指向的堆数据也会被清理。对于我们将在这一章其余部分中讨论的其他灵巧指针提供的功能,这两个特质将更加重要。们来更详细地探讨这两个特质。
End