update ch19

This commit is contained in:
KaiserY 2023-01-30 14:05:51 +08:00
parent 7ef91fa9ae
commit 89cdab40e6
6 changed files with 64 additions and 73 deletions

View File

@ -2,7 +2,7 @@
> [ch19-00-advanced-features.md](https://github.com/rust-lang/book/blob/main/src/ch19-00-advanced-features.md)
> <br>
> commit 10f89936b02dc366a2d0b34083b97cadda9e0ce4
> commit a8536189d5f1ba2f0b1187bfae787c0d89e2871c
现在我们已经学习了 Rust 编程语言中最常用的部分。在第二十章开始另一个新项目之前,让我们聊聊一些总有一天你会遇上的部分内容。你可以将本章作为不经意间遇到未知的内容时的参考。本章将要学习的功能在一些非常特定的场景下很有用处。虽然很少会碰到它们,我们希望确保你了解 Rust 提供的所有功能。

View File

@ -2,19 +2,19 @@
> [ch19-01-unsafe-rust.md](https://github.com/rust-lang/book/blob/main/src/ch19-01-unsafe-rust.md)
> <br>
> commit 1524fa89fbaa4d52c4a2095141f6eaa6c22f8bd0
> commit 057b0d338229b87d06be772346ea0b643bdecdc5
目前为止讨论过的代码都有 Rust 在编译时会强制执行的内存安全保证。然而Rust 还隐藏有第二种语言,它不会强制执行这类内存安全保证:这被称为 **不安全 Rust***unsafe Rust*)。它与常规 Rust 代码无异,但是会提供额外的超能力。
尽管代码可能没问题,但如果 Rust 编译器没有足够的信息可以确定,它将拒绝代码。
不安全 Rust 之所以存在,是因为静态分析本质上是保守的。当编译器尝试确定一段代码是否支持某个保证时,拒绝一些合法的程序比接受错误的程序要好一些。这必然意味着有时代码 **可能** 是合法的,但如果 Rust 编译器没有足够的信息来确定,它将拒绝该代码。在这种情况下,可以使用不安全代码告诉编译器,“相信我,我知道我在干什么。”这么做的缺点就是你只能靠自己了:如果不安全代码出错了,比如解引用空指针,可能会导致不安全的内存使用。
不安全 Rust 之所以存在,是因为静态分析本质上是保守的。当编译器尝试确定一段代码是否支持某个保证时,拒绝一些合法的程序比接受无效的程序要好一些。这必然意味着有时代码 **可能** 是合法的,但如果 Rust 编译器没有足够的信息来确定,它将拒绝该代码。在这种情况下,可以使用不安全代码告诉编译器,“相信我,我知道我在干什么。” 不过千万注意,使用不安全 Rust 风险自担:如果不安全代码出错了,比如解引用空指针,可能会导致不安全的内存使用。
另一个 Rust 存在不安全一面的原因是:底层计算机硬件固有的不安全性。如果 Rust 不允许进行不安全操作那么有些任务则根本完成不了。Rust 需要能够进行像直接与操作系统交互,甚至于编写你自己的操作系统这样的底层系统编程!这也是 Rust 语言的目标之一。让我们看看不安全 Rust 能做什么,和怎么做。
### 不安全的超能力
可以通过 `unsafe` 关键字来切换到不安全 Rust接着可以开启一个新的存放不安全代码的块。这里有五类可以在不安全 Rust 中进行而不能用于安全 Rust 的操作,它们称之为 “不安全的超能力。” 这些超能力是:
可以通过 `unsafe` 关键字来切换到不安全 Rust接着可以开启一个新的存放不安全代码的块。这里有五类可以在不安全 Rust 中进行而不能用于安全 Rust 的操作,它们称之为 “不安全的超能力。*unsafe superpowers*” 这些超能力是:
* 解引用裸指针
* 调用不安全的函数或方法
@ -57,7 +57,7 @@
这里使用 `as` 将不可变和可变引用强转为对应的裸指针类型。因为直接从保证安全的引用来创建他们,可以知道这些特定的裸指针是有效,但是不能对任何裸指针做出如此假设。
接下来会创建一个不能确定其有效性的裸指针,示例 19-2 展示了如何创建一个指向任意内存地址的裸指针。尝试使用任意内存是未定义行为此地址可能有数据也可能没有编译器可能会优化掉这个内存访问或者程序可能会出现段错误segmentation fault。通常没有好的理由编写这样的代码不过却是可行的
作为展示接下来会创建一个不能确定其有效性的裸指针,示例 19-2 展示了如何创建一个指向任意内存地址的裸指针。尝试使用任意内存是未定义行为此地址可能有数据也可能没有编译器可能会优化掉这个内存访问或者程序可能会出现段错误segmentation fault。通常没有好的理由编写这样的代码不过却是可行的
```rust
{{#rustdoc_include ../listings/ch19-advanced-features/listing-19-02/src/main.rs:here}}
@ -81,7 +81,7 @@
### 调用不安全函数或方法
第二类要求使用不安全块的操作是调用不安全函数。不安全函数和方法与常规函数方法十分类似,除了其开头有一个额外的 `unsafe`。在此上下文中,关键字`unsafe`表示该函数具有调用时需要满足的要求,而 Rust 不会保证满足这些要求。通过在 `unsafe` 块中调用不安全函数,表明我们已经阅读过此函数的文档并对其是否满足函数自身的契约负责。
第二类可以在进行不安全块的操作是调用不安全函数。不安全函数和方法与常规函数方法十分类似,除了其开头有一个额外的 `unsafe`。在此上下文中,关键字`unsafe`表示该函数具有调用时需要满足的要求,而 Rust 不会保证满足这些要求。通过在 `unsafe` 块中调用不安全函数,表明我们已经阅读过此函数的文档并对其是否满足函数自身的契约负责。
如下是一个没有做任何操作的不安全函数 `dangerous` 的例子:
@ -95,13 +95,13 @@
{{#include ../listings/ch19-advanced-features/output-only-01-missing-unsafe/output.txt}}
```
通过`dangerous` 调用插入 `unsafe`,我们向 Rust 保证了我们已经阅读过函数的文档,理解如何正确使用,并验证过其满足函数的契约。
通过 `unsafe` 块,我们向 Rust 保证了我们已经阅读过函数的文档,理解如何正确使用,并验证过其满足函数的契约。
不安全函数体也是有效的 `unsafe` 块,所以在不安全函数中进行另一个不安全操作时无需新增额外的 `unsafe` 块。
#### 创建不安全代码的安全抽象
仅仅因为函数包含不安全代码并不意味着整个函数都需要标记为不安全的。事实上,将不安全代码封装进安全函数是一个常见的抽象。作为一个例子,标准库中的函数,`split_at_mut`,它需要一些不安全代码,让我们探索如何可以实现它。这个安全函数定义于可变 slice 之上:它获取一个 slice 并从给定的索引参数开始将其分为两个 slice。`split_at_mut` 的用法如示例 19-4 所示:
仅仅因为函数包含不安全代码并不意味着整个函数都需要标记为不安全的。事实上,将不安全代码封装进安全函数是一个常见的抽象。作为一个例子,了解一下标准库中的函数 `split_at_mut`,它需要一些不安全代码,让我们探索如何可以实现它。这个安全函数定义于可变 slice 之上:它获取一个 slice 并从给定的索引参数开始将其分为两个 slice。`split_at_mut` 的用法如示例 19-4 所示:
```rust
{{#rustdoc_include ../listings/ch19-advanced-features/listing-19-04/src/main.rs:here}}
@ -153,7 +153,7 @@ Rust 的借用检查器不能理解我们要借用这个 slice 的两个不同
<span class="caption">示例 19-7: 通过任意内存地址创建 slice</span>
我们并不拥有这个任意地址的内存,也不能保证这段代码创建的 slice 包含有效的 `i32` 值。试图使用臆测为有效的 `slice` 会导致未定义的行为。
我们并不拥有这个任意地址的内存,也不能保证这段代码创建的 slice 包含有效的 `i32` 值。试图使用臆测为有效的 `values` 会导致未定义的行为。
#### 使用 `extern` 函数调用外部代码
@ -173,7 +173,7 @@ Rust 的借用检查器不能理解我们要借用这个 slice 的两个不同
> #### 从其它语言调用 Rust 函数
>
> 也可以使用 `extern` 来创建一个允许其他语言调用 Rust 函数的接口。不同于 `extern` 块,就在 `fn` 关键字之前增加 `extern` 关键字并指定所用到的 ABI。还需增加 `#[no_mangle]` 注解来告诉 Rust 编译器不要 mangle 此函数的名称。*Mangling* 发生于当编译器将我们指定的函数名修改为不同的名称时,这会增加用于其他编译过程的额外信息,不过会使其名称更难以阅读。每一个编程语言的编译器都会以稍微不同的方式 mangle 函数名,所以为了使 Rust 函数能在其他语言中指定,必须禁用 Rust 编译器的 name mangling。
> 也可以使用 `extern` 来创建一个允许其他语言调用 Rust 函数的接口。不同于创建整个 `extern` 块,就在 `fn` 关键字之前增加 `extern` 关键字并为相关函数指定所用到的 ABI。还需增加 `#[no_mangle]` 注解来告诉 Rust 编译器不要 mangle 此函数的名称。*Mangling* 发生于当编译器将我们指定的函数名修改为不同的名称时,这会增加用于其他编译过程的额外信息,不过会使其名称更难以阅读。每一个编程语言的编译器都会以稍微不同的方式 mangle 函数名,所以为了使 Rust 函数能在其他语言中指定,必须禁用 Rust 编译器的 name mangling。
>
> 在如下的例子中,一旦其编译为动态库并从 C 语言中链接,`call_from_c` 函数就能够在 C 代码中访问:
>
@ -202,9 +202,7 @@ Rust 的借用检查器不能理解我们要借用这个 slice 的两个不同
静态(`static`)变量类似于第三章 [“变量和常量的区别”][differences-between-variables-and-constants] 部分讨论的常量。通常静态变量的名称采用 `SCREAMING_SNAKE_CASE` 写法。静态变量只能储存拥有 `'static` 生命周期的引用,这意味着 Rust 编译器可以自己计算出其生命周期而无需显式标注。访问不可变静态变量是安全的。
常量与不可变静态变量可能看起来很类似,不过一个微妙的区别是静态变量中的值有一个固定的内存地址。使用这个值总是会访问相同的地址。另一方面,常量则允许在任何被用到的时候复制其数据。
常量与静态变量的另一个区别在于静态变量可以是可变的。访问和修改可变静态变量都是 **不安全** 的。示例 19-10 展示了如何声明、访问和修改名为 `COUNTER` 的可变静态变量:
常量与不可变静态变量的一个微妙的区别是静态变量中的值有一个固定的内存地址。使用这个值总是会访问相同的地址。另一方面,常量则允许在任何被用到的时候复制其数据。另一个区别在于静态变量可以是可变的。访问和修改可变静态变量都是 **不安全** 的。示例 19-10 展示了如何声明、访问和修改名为 `COUNTER` 的可变静态变量:
<span class="filename">文件名src/main.rs</span>
@ -234,7 +232,7 @@ Rust 的借用检查器不能理解我们要借用这个 slice 的两个不同
### 访问联合体中的字段
仅适用于 `unsafe` 的最后一个操作是访问 **联合体** 中的字段,`union` 和 `struct` 类似,但是在一个实例中同时只能使用一个声明的字段。联合体主要用于和 C 代码中的联合体交互。访问联合体的字段是不安全的,因为 Rust 无法保证当前存储在联合体实例中数据的类型。可以查看 [参考文档][reference] 了解有关联合体的更多信息。
仅适用于 `unsafe` 的最后一个操作是访问 **联合体** 中的字段,`union` 和 `struct` 类似,但是在一个实例中同时只能使用一个声明的字段。联合体主要用于和 C 代码中的联合体交互。访问联合体的字段是不安全的,因为 Rust 无法保证当前存储在联合体实例中数据的类型。可以查看 [参考 Rust 文档][reference] 了解有关联合体的更多信息。
### 何时使用不安全代码

View File

@ -2,17 +2,17 @@
> [ch19-03-advanced-traits.md](https://github.com/rust-lang/book/blob/main/src/ch19-03-advanced-traits.md)
> <br>
> commit 81d05c9a6d06d79f2a85c8ea184f41dc82532d98
> commit 95e931170404cb98d476b19017cbbdbc00d0834d
第十章 [“trait定义共享的行为”][traits-defining-shared-behavior] 部分,我们第一次涉及到了 trait不过就像生命周期一样,我们并没有覆盖一些较为高级的细节。现在我们更加了解 Rust 了,可以深入理解其本质了。
第十章 [“trait定义共享的行为”][traits-defining-shared-behavior] 部分,我们第一次涉及到了 trait不过我们并没有覆盖一些较为高级的细节。现在我们更加了解 Rust 了,可以深入理解其本质了。
### 关联类型在 trait 定义中指定占位符类型
**关联类型***associated types*)是一个将类型占位符与 trait 相关联的方式,这样 trait 的方法签名中就可以使用这些占位符类型。trait 的实现者会针对特定的实现在这个类型的位置指定相应的具体类型。如此可以定义一个使用多种类型的 trait直到实现此 trait 时都无需知道这些类型具体是什么。
**关联类型***associated types*)是一个将类型占位符与 trait 相关联的方式,这样 trait 的方法签名中就可以使用这些占位符类型。trait 的实现者会针对特定的实现在这个占位符类型指定相应的具体类型。如此可以定义一个使用多种类型的 trait直到实现此 trait 时都无需知道这些类型具体是什么。
本章所描述的大部分内容都非常少见。关联类型则比较适中;它们比本书其他的内容要少见,不过比本章中的很多内容要更常见。
一个带有关联类型的 trait 的例子是标准库提供的 `Iterator` trait。它有一个叫做 `Item` 的关联类型来替代遍历的值的类型。第十三章的 [“`Iterator` trait 和 `next` 方法”][the-iterator-trait-and-the-next-method] 部分曾提到过 `Iterator` trait 的定义如示例 19-12 所示:
一个带有关联类型的 trait 的例子是标准库提供的 `Iterator` trait。它有一个叫做 `Item` 的关联类型来替代遍历的值的类型。`Iterator` trait 的定义如示例 19-12 所示:
```rust,noplayground
{{#rustdoc_include ../listings/ch19-advanced-features/listing-19-12/src/lib.rs}}
@ -20,19 +20,17 @@
<span class="caption">示例 19-12: `Iterator` trait 的定义中带有关联类型 `Item`</span>
`Item` 是一个占位类型,同时 `next` 方法定义表明它返回 `Option<Self::Item>` 类型的值。这个 trait 的实现者会指定 `Item` 的具体类型,然而不管实现者指定何种类型,`next` 方法都会返回一个包含了此具体类型值的 `Option`
`Item` 是一个占位类型,同时 `next` 方法定义表明它返回 `Option<Self::Item>` 类型的值。这个 trait 的实现者会指定 `Item` 的具体类型,然而不管实现者指定何种类型,`next` 方法都会返回一个包含了此具体类型值的 `Option`
关联类型看起来像一个类似泛型的概念,因为它允许定义一个函数而不指定其可以处理的类型。那么为什么要使用关联类型呢?
让我们通过一个在第十三章中出现的 `Counter` 结构体上实现 `Iterator` trait 的例子来检视其中的区别。在示例 13-21 中,指定了 `Item` 的类型为 `u32`
关联类型看起来像一个类似泛型的概念,因为它允许定义一个函数而不指定其可以处理的类型。让我们通过在一个 `Counter` 结构体上实现 `Iterator` trait 的例子来检视其中的区别。这个实现中指定了 `Item` 的类型为 `u32`
<span class="filename">文件名src/lib.rs</span>
```rust,ignore
{{#rustdoc_include ../listings/ch19-advanced-features/listing-13-21-reproduced/src/lib.rs:ch19}}
{{#rustdoc_include ../listings/ch19-advanced-features/no-listing-22-iterator-on-counter/src/lib.rs:ch19}}
```
这类似于泛型。那么为什么 `Iterator` trait 不像示例 19-13 那样定义呢?
个语法类似于泛型。那么为什么 `Iterator` trait 不像示例 19-13 那样定义呢?
```rust,noplayground
{{#rustdoc_include ../listings/ch19-advanced-features/listing-19-13/src/lib.rs}}
@ -44,11 +42,13 @@
通过关联类型,则无需标注类型,因为不能多次实现这个 trait。对于示例 19-12 使用关联类型的定义,我们只能选择一次 `Item` 会是什么类型,因为只能有一个 `impl Iterator for Counter`。当调用 `Counter``next` 时不必每次指定我们需要 `u32` 值的迭代器。
关联类型也会成为 trait 契约的一部分trait 的实现必须提供一个类型来替代关联类型占位符。关联类型通常有一个描述类型用途的名字,并且在 API 文档中为关联类型编写文档是一个最佳实践。
### 默认泛型类型参数和运算符重载
当使用泛型类型参数时,可以为泛型指定一个默认的具体类型。如果默认类型就足够的话,这消除了为具体类型实现 trait 的需要。为泛型类型指定默认类型的语法是在声明泛型类型时使用 `<PlaceholderType=ConcreteType>`
这种情况的一个非常好的例子是用于运算符重载。**运算符重载***Operator overloading*是指在特定情况下自定义运算符(比如 `+`)行为的操作。
这种情况的一个非常好的例子是使用 **运算符重载***Operator overloading*),这是指在特定情况下自定义运算符(比如 `+`)行为的操作。
Rust 并不允许创建自定义运算符或重载任意运算符,不过 `std::ops` 中所列出的运算符和相应的 trait 可以通过实现运算符相关 trait 来重载。例如,示例 19-14 中展示了如何在 `Point` 结构体上实现 `Add` trait 来重载 `+` 运算符,这样就可以将两个 `Point` 实例相加了:
@ -143,7 +143,7 @@ Rust 既不能避免一个 trait 与另一个 trait 拥有相同名称的方法
因为 `fly` 方法获取一个 `self` 参数,如果有两个 **类型** 都实现了同一 **trait**Rust 可以根据 `self` 的类型计算出应该使用哪一个 trait 实现。
然而,关联函数是 trait 的一部分,但没有 `self` 参数。当同一作用域的两个类型实现了同一 traitRust 就不能计算出我们期望的是哪一个类型,除非使用 **完全限定语法***fully qualified syntax*)。例如,拿示例 19-19 中的 `Animal` trait 来说,它有关联函数 `baby_name`,结构体 `Dog` 实现了 `Animal`,同时有关联函数 `baby_name` 直接定义于 `Dog` 之上:
然而,不是方法的关联函数没有 `self` 参数。当存在多个类型或者 trait 定义了相同函数名的非方法函数时Rust 就不总是能计算出我们期望的是哪一个类型,除非使用 **完全限定语法***fully qualified syntax*)。例如示例 19-19 中的创建了一个希望将所有小狗叫做 *Spot* 的动物收容所的 trait。`Animal` trait 有一个关联非方法函数 `baby_name`。结构体 `Dog` 实现了 `Animal`,同时又直接提供了关联非方法函数 `baby_name`
<span class="filename">文件名src/main.rs</span>
@ -153,7 +153,7 @@ Rust 既不能避免一个 trait 与另一个 trait 拥有相同名称的方法
<span class="caption">示例 19-19: 一个带有关联函数的 trait 和一个带有同名关联函数并实现了此 trait 的类型</span>
这段代码用于一个动物收容所,他们将所有的小狗起名为 Spot这实现为定义于 `Dog` 上的关联函数 `baby_name`。`Dog` 类型还实现了 `Animal` trait它描述了所有动物的共有的特征。小狗被称为 puppy这表现为 `Dog``Animal` trait 实现中与 `Animal` trait 相关联的函数 `baby_name`
`Dog`定义的关联函数 `baby_name` 的实现代码将所有的小狗起名为 Spot。`Dog` 类型还实现了 `Animal` trait它描述了所有动物的共有的特征。小狗被称为 puppy这表现为 `Dog``Animal` trait 实现中与 `Animal` trait 相关联的函数 `baby_name`
`main` 调用了 `Dog::baby_name` 函数,它直接调用了定义于 `Dog` 之上的关联函数。这段代码会打印出:
@ -171,13 +171,13 @@ Rust 既不能避免一个 trait 与另一个 trait 拥有相同名称的方法
<span class="caption">示例 19-20: 尝试调用 `Animal` trait 的 `baby_name` 函数,不过 Rust 并不知道该使用哪一个实现</span>
因为 `Animal::baby_name` 是关联函数而不是方法,因此它没有 `self` 参数Rust 无法计算出所需的是哪一个 `Animal::baby_name` 实现。我们会得到这个编译错误:
因为 `Animal::baby_name` 没有 `self` 参数,同时这可能会有其它类型实现了 `Animal` traitRust 无法计算出所需的是哪一个 `Animal::baby_name` 实现。我们会得到这个编译错误:
```console
{{#include ../listings/ch19-advanced-features/listing-19-20/output.txt}}
```
为了消歧义并告诉 Rust 我们希望使用的是 `Dog``Animal` 实现,需要使用 **完全限定语法**,这是调用函数时最为明确的方式。示例 19-21 展示了如何使用完全限定语法:
为了消歧义并告诉 Rust 我们希望使用的是 `Dog``Animal` 实现而不是其它类型的 `Animal` 实现,需要使用 **完全限定语法**,这是调用函数时最为明确的方式。示例 19-21 展示了如何使用完全限定语法:
<span class="filename">文件名src/main.rs</span>
@ -199,13 +199,13 @@ Rust 既不能避免一个 trait 与另一个 trait 拥有相同名称的方法
<Type as Trait>::function(receiver_if_method, next_arg, ...);
```
对于关联函数,其没有一个 `receiver`,故只会有其他参数的列表。可以选择在任何函数或方法调用处使用完全限定语法。然而,允许省略任何 Rust 能够从程序中的其他信息中计算出的部分。只有当存在多个同名实现而 Rust 需要帮助以便知道我们希望调用哪个实现时,才需要使用这个较为冗长的语法。
对于不是方法的关联函数,其没有一个 `receiver`,故只会有其他参数的列表。可以选择在任何函数或方法调用处使用完全限定语法。然而,允许省略任何 Rust 能够从程序中的其他信息中计算出的部分。只有当存在多个同名实现而 Rust 需要帮助以便知道我们希望调用哪个实现时,才需要使用这个较为冗长的语法。
### 父 trait 用于在另一个 trait 中使用某 trait 的功能
有时我们可能会需要某个 trait 使用另一个 trait 的功能。在这种情况下,需要能够依赖相关的 trait 也被实现。这个所需的 trait 是我们实现的 trait 的 **父trait***supertrait*)。
有时我们可能会需要编写一个依赖另一个 trait 的 trait 定义:对于一个实现了第一个 trait 的类型,你希望要求这个类型也实现了第二个 trait。如此就可使 trait 定义使用第二个 trait 的关联项。这个所需的 trait 是我们实现的 trait 的 **父trait***supertrait*)。
例如我们希望创建一个带有 `outline_print` 方法的 trait `OutlinePrint`,它会打印出带有星号框的值。也就是说,如果 `Point` 实现了 `Display` 并返回 `(x, y)`调用以 `1` 作为 `x``3` 作为 `y``Point` 实例的 `outline_print` 会显示如下:
例如我们希望创建一个带有 `outline_print` 方法的 trait `OutlinePrint`,它会将给定的值格式化为带有星号框。也就是说,给定一个实现了标准库 `Display` trait 的并返回 `(x, y)``Point`,当调用以 `1` 作为 `x``3` 作为 `y``Point` 实例的 `outline_print` 会显示如下:
```text
**********
@ -269,11 +269,10 @@ Rust 既不能避免一个 trait 与另一个 trait 拥有相同名称的方法
此方法的缺点是,因为 `Wrapper` 是一个新类型,它没有定义于其值之上的方法;必须直接在 `Wrapper` 上实现 `Vec<T>` 的所有方法,这样就可以代理到`self.0` 上 —— 这就允许我们完全像 `Vec<T>` 那样对待 `Wrapper`。如果希望新类型拥有其内部类型的每一个方法,为封装类型实现 `Deref` trait第十五章 [“通过 `Deref` trait 将智能指针当作常规引用处理”][smart-pointer-deref] 部分讨论过)并返回其内部类型是一种解决方案。如果不希望封装类型拥有所有内部类型的方法 —— 比如为了限制封装类型的行为 —— 则必须只自行实现所需的方法。
上面便是 newtype 模式如何与 trait 结合使用的;还有一个不涉及 trait 的实用模式。现在让我们将话题的焦点转移到一些与 Rust 类型系统交互的高级方法上来吧。
甚至当不涉及 trait 时 newtype 模式也很有用。现在让我们将话题的焦点转移到一些与 Rust 类型系统交互的高级方法上来吧。
[newtype]: ch19-04-advanced-types.html#为了类型安全和抽象而使用-newtype-模式
[newtype]: ch19-03-advanced-traits.html#newtype-模式用以在外部类型上实现外部-trait
[implementing-a-trait-on-a-type]: ch10-02-traits.html#为类型实现-trait
[the-iterator-trait-and-the-next-method]: ch13-02-iterators.html#iterator-trait-和-next-方法
[traits-defining-shared-behavior]: ch10-02-traits.html#trait定义共享的行为
[smart-pointer-deref]: ch15-02-deref.html#通过实现-deref-trait-将某类型像引用一样处理
[tuple-structs]: ch05-01-defining-structs.html#使用没有命名字段的元组结构体来创建不同的类型

View File

@ -2,7 +2,7 @@
> [ch19-04-advanced-types.md](https://github.com/rust-lang/book/blob/main/src/ch19-04-advanced-types.md)
> <br>
> commit a90f07f1e9a7fc75dc9105a6c6f16d5c13edceb0
> commit 95e931170404cb98d476b19017cbbdbc00d0834d
Rust 的类型系统有一些我们曾经提到但没有讨论过的功能。首先我们从一个关于为什么 newtype 与类型一样有用的更宽泛的讨论开始。接着会转向类型别名type aliases一个类似于 newtype 但有着稍微不同的语义的功能。我们还会讨论 `!` 类型和动态大小类型。
@ -10,15 +10,15 @@ Rust 的类型系统有一些我们曾经提到但没有讨论过的功能。首
### 为了类型安全和抽象而使用 newtype 模式
newtype 模式可以用于一些其他我们还未讨论的功能,包括静态的确保某值不被混淆,和用来表示一个值的单位。实际上示例 19-15 中已经有一个这样的例子:`Millimeters` 和 `Meters` 结构体都在 newtype 中封装了 `u32` 值。如果编写了一个有 `Millimeters` 类型参数的函数,不小心使用 `Meters` 或普通的 `u32` 值来调用该函数的程序是不能编译的。
newtype 模式可以用于一些其他我们还未讨论的功能,包括静态的确保某值不被混淆,和用来表示一个值的单位。实际上示例 19-15 中已经有一个这样的例子:`Millimeters` 和 `Meters` 结构体都在 newtype 中封装了 `u32` 值。如果编写了一个有 `Millimeters` 类型参数的函数,不小心使用 `Meters` 或普通的 `u32` 值来调用该函数的程序是不能编译的。
另一个 newtype 模式的应用在于抽象掉一些类型的实现细节:例如,封装类型可以暴露出与直接使用其内部私有类型时所不同的公有 API,以便限制其功能
newtype 模式也可以用于抽象掉一些类型的实现细节:例如,封装类型可以暴露出与直接使用其内部私有类型时所不同的公有 API。
newtype 也可以隐藏其内部的泛型类型。例如,可以提供一个封装了 `HashMap<i32, String>``People` 类型,用来储存人名以及相应的 ID。使用 `People` 的代码只需与提供的公有 API 交互即可,比如向 `People` 集合增加名字字符串的方法,这样这些代码就无需知道在内部我们将一个 `i32` ID 赋予了这个名字了。newtype 模式是一种实现第十七章 [“封装隐藏了实现细节”][encapsulation-that-hides-implementation-details] 部分所讨论的隐藏实现细节的封装的轻量级方法。
### 类型别名用来创建类型同义词
连同 newtype 模式,Rust 提供了声明 **类型别名***type alias*)的能力,使用 `type` 关键字来给予现有类型另一个名字。例如,可以像这样创建 `i32` 的别名 `Kilometers`
Rust 提供了声明 **类型别名***type alias*)的能力,使用 `type` 关键字来给予现有类型另一个名字。例如,可以像这样创建 `i32` 的别名 `Kilometers`
```rust
{{#rustdoc_include ../listings/ch19-advanced-features/no-listing-04-kilometers-alias/src/main.rs:here}}
@ -30,7 +30,7 @@ newtype 也可以隐藏其内部的泛型类型。例如,可以提供一个封
{{#rustdoc_include ../listings/ch19-advanced-features/no-listing-04-kilometers-alias/src/main.rs:there}}
```
因为 `Kilometers``i32` 的别名,他们是同一类型,可以将 `i32``Kilometers` 相加,也可以将 `Kilometers` 传递给获取 `i32` 参数的函数。但通过这种手段无法获得上一部分讨论的 newtype 模式所提供的类型检查的好处。
因为 `Kilometers``i32` 的别名,他们是同一类型,可以将 `i32``Kilometers` 相加,也可以将 `Kilometers` 传递给获取 `i32` 参数的函数。但通过这种手段无法获得上一部分讨论的 newtype 模式所提供的类型检查的好处。换句话说,如果在哪里混用 `Kilometers``i32` 的值,编译器也不会给出一个错误。
类型别名的主要用途是减少重复。例如,可能会有这样很长的类型:
@ -68,7 +68,7 @@ Box<dyn Fn() + Send + 'static>
{{#rustdoc_include ../listings/ch19-advanced-features/no-listing-06-result-alias/src/lib.rs:here}}
```
因为这位于 `std::io` 中,可用的完全限定的别名是 `std::io::Result<T>` —— 也就是说,`Result<T, E>` 中 `E` 放入了 `std::io::Error`。`Write` trait 中的函数最终看起来像这样:
因为这位于 `std::io` 中,可用的完全限定的别名是 `std::io::Result<T>`也就是说,`Result<T, E>` 中 `E` 放入了 `std::io::Error`。`Write` trait 中的函数最终看起来像这样:
```rust,noplayground
{{#rustdoc_include ../listings/ch19-advanced-features/no-listing-06-result-alias/src/lib.rs:there}}
@ -124,7 +124,7 @@ never type 的另一个用途是 `panic!`。还记得 `Option<T>` 上的 `unwrap
### 动态大小类型和 `Sized` trait
Rust 需要知道有关类型的某些细节,例如为特定类型的值需要分配多少空间。这使得类型系统的一个概念有些令人迷惑:即 **动态大小类型***dynamically sized types*)。这有时被称为 “DST” 或 “unsized types”这些类型允许我们处理只有在运行时才知道大小的类型。
Rust 需要知道有关类型的某些细节,例如为特定类型的值需要分配多少空间。这便是起初留下的一个类型系统中令人迷惑的角落:即 **动态大小类型***dynamically sized types*)。这有时被称为 “DST” 或 “unsized types”这些类型允许我们处理只有在运行时才知道大小的类型。
让我们深入研究一个贯穿本书都在使用的动态大小类型的细节:`str`。没错,不是 `&str`,而是 `str` 本身。`str` 是一个 DST直到运行时我们都不知道字符串有多长。因为直到运行时都不能知道其大小也就意味着不能创建 `str` 类型的变量,也不能获取 `str` 类型的参数。考虑一下这些代码,他们不能工作:
@ -134,13 +134,11 @@ Rust 需要知道有关类型的某些细节,例如为特定类型的值需要
Rust 需要知道应该为特定类型的值分配多少内存,同时所有同一类型的值必须使用相同数量的内存。如果允许编写这样的代码,也就意味着这两个 `str` 需要占用完全相同大小的空间,不过它们有着不同的长度。这也就是为什么不可能创建一个存放动态大小类型的变量的原因。
那么该怎么办呢?你已经知道了这种问题的答案:`s1` 和 `s2` 的类型是 `&str` 而不是 `str`。如果你回想第四章 [“字符串 slice”][string-slices] 部分slice 数据结构储存了开始位置和 slice 的长度。
所以虽然 `&T` 是一个储存了 `T` 所在的内存位置的单个值,`&str` 则是 **两个** 值:`str` 的地址和其长度。这样,`&str` 就有了一个在编译时可以知道的大小:它是 `usize` 长度的两倍。也就是说,我们总是知道 `&str` 的大小,而无论其引用的字符串是多长。这里是 Rust 中动态大小类型的常规用法:他们有一些额外的元信息来储存动态信息的大小。这引出了动态大小类型的黄金规则:必须将动态大小类型的值置于某种指针之后。
那么该怎么办呢?你已经知道了这种问题的答案:`s1` 和 `s2` 的类型是 `&str` 而不是 `str`。如果你回想第四章 [“字符串 slice”][string-slices] 部分slice 数据结构仅仅储存了开始位置和 slice 的长度。所以虽然 `&T` 是一个储存了 `T` 所在的内存位置的单个值,`&str` 则是 **两个** 值:`str` 的地址和其长度。这样,`&str` 就有了一个在编译时可以知道的大小:它是 `usize` 长度的两倍。也就是说,我们总是知道 `&str` 的大小,而无论其引用的字符串是多长。这里是 Rust 中动态大小类型的常规用法:他们有一些额外的元信息来储存动态信息的大小。这引出了动态大小类型的黄金规则:必须将动态大小类型的值置于某种指针之后。
可以将 `str` 与所有类型的指针结合:比如 `Box<str>``Rc<str>`。事实上之前我们已经见过了不过是另一个动态大小类型trait。每一个 trait 都是一个可以通过 trait 名称来引用的动态大小类型。在第十七章 [“为使用不同类型的值而设计的 trait 对象”][using-trait-objects-that-allow-for-values-of-different-types] 部分,我们提到了为了将 trait 用于 trait 对象,必须将他们放入指针之后,比如 `&dyn Trait``Box<dyn Trait>``Rc<dyn Trait>` 也可以)。
为了处理 DSTRust 有一个特定的 trait 来决定一个类型的大小是否在编译时可知:这就是 `Sized` trait。这个 trait 自动为编译器在编译时就知道大小的类型实现。另外Rust 隐式的为每一个泛型函数增加了 `Sized` bound。也就是说对于如下泛型函数定义
为了处理 DSTRust 提供了 `Sized` trait 来决定一个类型的大小是否在编译时可知。这个 trait 自动为编译器在编译时就知道大小的类型实现。另外Rust 隐式的为每一个泛型函数增加了 `Sized` bound。也就是说对于如下泛型函数定义
```rust,ignore
{{#rustdoc_include ../listings/ch19-advanced-features/no-listing-12-generic-fn-definition/src/lib.rs}}

View File

@ -2,13 +2,15 @@
> [ch19-05-advanced-functions-and-closures.md](https://github.com/rust-lang/book/blob/main/src/ch19-05-advanced-functions-and-closures.md)
> <br>
> commit 9e30688e0ac4a1ad86fc60aa380bebfb1c34b8a7
> commit a8536189d5f1ba2f0b1187bfae787c0d89e2871c
接下来我们将探索一些有关函数和闭包的高级功能:函数指针以及返回值闭包。
本部分将探索一些有关函数和闭包的高级功能,这包括函数指针以及返回值闭包。
### 函数指针
我们讨论过了如何向函数传递闭包;也可以向函数传递常规函数!这在我们希望传递已经定义的函数而不是重新定义闭包作为参数时很有用。通过函数指针允许我们使用函数作为另一个函数的参数。函数的类型是 `fn` (使用小写的 “f” )以免与 `Fn` 闭包 trait 相混淆。`fn` 被称为 **函数指针***function pointer*)。指定参数为函数指针的语法类似于闭包,如示例 19-27 所示:
我们讨论过了如何向函数传递闭包;也可以向函数传递常规函数!这个技术在我们希望传递已经定义的函数而不是重新定义闭包作为参数时很有用。函数满足类型 `fn`(小写的 f不要与闭包 trait 的 `Fn` 相混淆。`fn` 被称为 **函数指针***function pointer*)。通过函数指针允许我们使用函数作为另一个函数的参数。
指定参数为函数指针的语法类似于闭包,如示例 19-27 所示,这里定义了一个 `add_one` 函数将其参数加一。`do_twice` 函数获取两个参数:一个指向任何获取一个 `i32` 参数并返回一个 `i32` 的函数指针,和一个 `i32` 值。`do_twice` 函数传递 `arg` 参数调用 `f` 函数两次,接着将两次函数调用的结果相加。`main` 函数使用 `add_one``5` 作为参数调用 `do_twice`
<span class="filename">文件名src/main.rs</span>
@ -40,7 +42,7 @@
注意这里必须使用 [“高级 trait”][advanced-traits] 部分讲到的完全限定语法,因为存在多个叫做 `to_string` 的函数;这里使用了定义于 `ToString` trait 的 `to_string` 函数,标准库为所有实现了 `Display` 的类型实现了这个 trait。
另一个实用的模式暴露了元组结构体和元组结构体枚举成员的实现细节。这些项使用 `()` 作为初始化语法,这看起来就像函数调用,同时它们确实被实现为返回由参数构造的实例的函数。它们也被称为实现了闭包 trait 的函数指针,并可以采用类似如下的方式调用
回忆一下第六章 [“枚举值”][enum-values] 部分中定义的每一个枚举成员也变成了一个构造函数。我们可以使用这些构造函数作为实现了闭包 trait 的函数指针,这意味着可以指定构造函数作为接受闭包的方法的参数,如下
```rust
{{#rustdoc_include ../listings/ch19-advanced-features/no-listing-17-map-initializer/src/main.rs:here}}
@ -75,4 +77,5 @@
接下来让我们学习宏!
[advanced-traits]: ch19-03-advanced-traits.html#高级-trait
[enum-values]: ch06-01-defining-an-enum.html#枚举值
[using-trait-objects-that-allow-for-values-of-different-types]: ch17-02-trait-objects.html#为使用不同类型的值而设计的-trait-对象

View File

@ -2,7 +2,7 @@
> [ch19-06-macros.md](https://github.com/rust-lang/book/blob/main/src/ch19-06-macros.md)
> <br>
> commit acc806a06b5a23c7397b7218aecec0e774619512
> commit 95e931170404cb98d476b19017cbbdbc00d0834d
我们已经在本书中使用过像 `println!` 这样的宏了,不过还没完全探索什么是宏以及它是如何工作的。**宏***Macro*)指的是 Rust 中一系列的功能:使用 `macro_rules!`**声明***Declarative*)宏,和三种 **过程***Procedural*)宏:
@ -55,11 +55,9 @@ let v: Vec<u32> = vec![1, 2, 3];
`vec!` 宏的结构和 `match` 表达式的结构类似。此处有一个分支模式 `( $( $x:expr ),* )` ,后跟 `=>` 以及和模式相关的代码块。如果模式匹配,该相关代码块将被执行。这里这个宏只有一个模式,那就只有一个有效匹配方向,其他任何模式方向(译者注:不匹配这个模式)都会导致错误。更复杂的宏会有多个分支模式。
宏定义中有效模式语法和在第十八章提及的模式语法是不同的,因为宏模式所匹配的是 Rust 代码结构而不是值。回过头来检查下示例 19-28 中模式片段什么意思。对于全部的宏模式语法,请查阅[参考]。
宏定义中有效模式语法和在第十八章提及的模式语法是不同的,因为宏模式所匹配的是 Rust 代码结构而不是值。回过头来检查下示例 19-28 中模式片段什么意思。对于全部的宏模式语法,请查阅 [Rust 参考][ref]。
[参考]: https://doc.rust-lang.org/reference/macros-by-example.html
首先,一对括号包含了整个模式。接下来是美元符号( `$` ),后跟一对括号,其捕获了符合括号内模式的值用以在替代代码中使用。`$()` 内则是 `$x:expr` ,其匹配 Rust 的任意表达式,并将该表达式命名为 `$x`
首先,一对括号包含了整个模式。我们使用美元符号(`$`)在宏系统中声明一个变量来包含匹配该模式的 Rust 代码。美元符号明确表明这是一个宏变量而不是普通 Rust 变量。之后是一对括号,其捕获了符合括号内模式的值用以在替代代码中使用。`$()` 内则是 `$x:expr` ,其匹配 Rust 的任意表达式,并将该表达式命名为 `$x`
`$()` 之后的逗号说明一个可有可无的逗号分隔符可以出现在 `$()` 所匹配的代码之后。紧随逗号之后的 `*` 说明该模式匹配零个或更多个 `*` 之前的任何模式。
@ -79,17 +77,13 @@ let v: Vec<u32> = vec![1, 2, 3];
我们已经定义了一个宏,其可以接收任意数量和类型的参数,同时可以生成能够创建包含指定元素的 vector 的代码。
`macro_rules!` 中有一些奇怪的地方。在将来,会有第二种采用 `macro` 关键字的声明宏,其工作方式类似但修复了这些极端情况。在此之后,`macro_rules!` 实际上就过时deprecated了。在此基础之上同时鉴于大多数 Rust 程序员 **使用** 宏而非 **编写** 宏的事实,此处不再深入探讨 `macro_rules!`。请查阅在线文档或其他资源,如 [“The Little Book of Rust Macros”][tlborm] 来更多地了解如何写宏。
[tlborm]: https://veykril.github.io/tlborm/
请查阅在线文档或其他资源,如 [“The Little Book of Rust Macros”][tlborm] 来更多地了解如何写宏,该书由 Daniel Keep 开始编写并由 Lukas Wirth 继续维护。
### 用于从属性生成代码的过程宏
第二种形式的宏被称为 **过程宏***procedural macros*),因为它们更像函数(一种过程类型)。过程宏接收 Rust 代码作为输入,在这些代码上进行操作,然后产生另一些代码作为输出,而非像声明式宏那样匹配对应模式然后以另一部分代码替换当前代码。
第二种形式的宏被称为 **过程宏***procedural macros*),因为它们更像函数(一种过程类型)。过程宏接收 Rust 代码作为输入,在这些代码上进行操作,然后产生另一些代码作为输出,而非像声明式宏那样匹配对应模式然后以另一部分代码替换当前代码。有三种类型的过程宏自定义派生derive类属性和类函数不过它们的工作方式都类似。
有三种类型的过程宏自定义派生derive类属性和类函数不过它们的工作方式都类似。
创建过程宏时,其定义必须驻留在它们自己的具有特殊 crate 类型的 crate 中。这么做出于复杂的技术原因,将来我们希望能够消除这些限制。使用这些宏需采用类似示例 19-29 所示的代码形式,其中 `some_attribute` 是一个使用特定宏的占位符。
创建过程宏时,其定义必须驻留在它们自己的具有特殊 crate 类型的 crate 中。这么做出于复杂的技术原因,将来我们希望能够消除这些限制。在示例 19-29 中展示了如何定义过程宏,其中 `some_attribute` 是一个使用特定宏变体的占位符。
<span class="filename">文件名src/lib.rs</span>
@ -101,7 +95,7 @@ pub fn some_name(input: TokenStream) -> TokenStream {
}
```
<span class="caption">示例 19-29: 一个使用过程宏的例子</span>
<span class="caption">示例 19-29: 一个定义过程宏的例子</span>
定义过程宏的函数接收一个 TokenStream 作为输入并生成 TokenStream 作为输出。`TokenStream` 是定义于`proc_macro` crate 里代表一系列 token 的类型Rust 默认携带了`proc_macro` crate。这就是宏的核心宏所处理的源代码组成了输入 `TokenStream`,宏生成的代码是输出 `TokenStream`。函数上还有一个属性;这个属性指明了我们创建的过程宏的类型。在同一 crate 中可以有多种的过程宏。
@ -109,7 +103,7 @@ pub fn some_name(input: TokenStream) -> TokenStream {
### 如何编写自定义 `derive`
让我们创建一个 `hello_macro` crate其包含名为 `HelloMacro` 的 trait 和关联函数 `hello_macro`。不同于让 crate 的用户为其每一个类型实现 `HelloMacro` trait我们将会提供一个过程式宏以便用户可以使用 `#[derive(HelloMacro)]` 注解他们的类型来得到 `hello_macro` 函数的默认实现。该默认实现会打印 `Hello, Macro! My name is TypeName!`,其中 `TypeName` 为定义了 trait 的类型名。换言之,我们会创建一个 crate使程序员能够写类似示例 19-30 中的代码。
让我们创建一个 `hello_macro` crate其包含名为 `HelloMacro` 的 trait 和关联函数 `hello_macro`。不同于让用户为其每一个类型实现 `HelloMacro` trait我们将会提供一个过程式宏以便用户可以使用 `#[derive(HelloMacro)]` 注解他们的类型来得到 `hello_macro` 函数的默认实现。该默认实现会打印 `Hello, Macro! My name is TypeName!`,其中 `TypeName` 为定义了 trait 的类型名。换言之,我们会创建一个 crate使程序员能够写类似示例 19-30 中的代码。
<span class="filename">文件名src/main.rs</span>
@ -173,9 +167,6 @@ $ cargo new hello_macro_derive --lib
现在,我们已经引入了三个新的 crate`proc_macro` 、 [`syn`] 和 [`quote`] 。Rust 自带 `proc_macro` crate因此无需将其加到 *Cargo.toml* 文件的依赖中。`proc_macro` crate 是编译器用来读取和操作我们 Rust 代码的 API。
[`syn`]: https://crates.io/crates/syn
[`quote`]: https://crates.io/crates/quote
`syn` crate 将字符串中的 Rust 代码解析成为一个可以操作的数据结构。`quote` 则将 `syn` 解析的数据结构转换回 Rust 代码。这些 crate 让解析任何我们所要处理的 Rust 代码变得更简单:为 Rust 编写整个的解析器并不是一件简单的工作。
当用户在一个类型上指定 `#[derive(HelloMacro)]` 时,`hello_macro_derive` 函数将会被调用。因为我们已经使用 `proc_macro_derive` 及其指定名称`HelloMacro`对 `hello_macro_derive` 函数进行了注解,指定名称`HelloMacro`就是 trait 名,这是大多数过程宏遵循的习惯。
@ -206,8 +197,6 @@ DeriveInput {
该结构体的字段展示了我们解析的 Rust 代码是一个类单元结构体,其 `ident`identifier表示名字`Pancakes`。该结构体里面有更多字段描述了所有类型的 Rust 代码,查阅 [`syn` 中 `DeriveInput` 的文档][syn-docs] 以获取更多信息。
[syn-docs]: https://docs.rs/syn/0.14.4/syn/struct.DeriveInput.html
很快我们将定义 `impl_hello_macro` 函数,其用于构建所要包含在内的 Rust 新代码。但在此之前,注意其输出也是 `TokenStream`。所返回的 `TokenStream` 会被加到我们的 crate 用户所写的代码中,因此,当用户编译他们的 crate 时,他们会通过修改后的 `TokenStream` 获取到我们所提供的额外功能。
你可能也注意到了,当调用 `syn::parse` 函数失败时,我们用 `unwrap` 来使 `hello_macro_derive` 函数 panic。在错误时 panic 对过程宏来说是必须的,因为 `proc_macro_derive` 函数必须返回 `TokenStream` 而不是 `Result`,以此来符合过程宏的 API。这里选择用 `unwrap` 来简化了这个例子;在生产代码中,则应该通过 `panic!``expect` 来提供关于发生何种错误的更加明确的错误信息。
@ -228,8 +217,6 @@ DeriveInput {
这个宏也提供了一些非常酷的模板机制;我们可以写 `#name` ,然后 `quote!` 会以名为 `name` 的变量值来替换它。你甚至可以做一些类似常用宏那样的重复代码的工作。查阅 [`quote` crate 的文档][quote-docs] 来获取详尽的介绍。
[quote-docs]: https://docs.rs/quote
我们期望我们的过程式宏能够为通过 `#name` 获取到的用户注解类型生成 `HelloMacro` trait 的实现。该 trait 的实现有一个函数 `hello_macro` ,其函数体包括了我们期望提供的功能:打印 `Hello, Macro! My name is` 和注解的类型名。
此处所使用的 `stringify!` 为 Rust 内置宏。其接收一个 Rust 表达式,如 `1 + 2` ,然后在编译时将表达式转换为一个字符串常量,如 `"1 + 2"` 。这与 `format!``println!` 是不同的,它计算表达式并将结果转换为 `String` 。有一种可能的情况是,所输入的 `#name` 可能是一个需要打印的表达式,因此我们用 `stringify!` 。`stringify!` 也能通过在编译时将 `#name` 转换为字符串来节省内存分配。
@ -268,8 +255,6 @@ pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
类函数Function-like宏的定义看起来像函数调用的宏。类似于 `macro_rules!`,它们比函数更灵活;例如,可以接受未知数量的参数。然而 `macro_rules!` 宏只能使用之前 [“使用 `macro_rules!` 的声明宏用于通用元编程”][decl] 介绍的类匹配的语法定义。类函数宏获取 `TokenStream` 参数,其定义使用 Rust 代码操纵 `TokenStream`,就像另两种过程宏一样。一个类函数宏例子是可以像这样被调用的 `sql!` 宏:
[decl]: #使用-macro_rules-的声明宏用于通用元编程
```rust,ignore
let sql = sql!(SELECT * FROM posts WHERE id=1);
```
@ -288,3 +273,11 @@ pub fn sql(input: TokenStream) -> TokenStream {
好的!现在我们学习了 Rust 并不常用但在特定情况下你可能用得着的功能。我们介绍了很多复杂的主题,这样若你在错误信息提示或阅读他人代码时遇到他们,至少可以说之前已经见过这些概念和语法了。你可以使用本章作为一个解决方案的参考。
接下来,我们将再开始一个项目,将本书所学的所有内容付与实践!
[ref]: https://doc.rust-lang.org/reference/macros-by-example.html
[tlborm]: https://veykril.github.io/tlborm/
[`syn`]: https://crates.io/crates/syn
[`quote`]: https://crates.io/crates/quote
[syn-docs]: https://docs.rs/syn/1.0/syn/struct.DeriveInput.html
[quote-docs]: https://docs.rs/quote
[decl]: #使用-macro_rules-的声明宏用于通用元编程