mirror of
https://github.com/KaiserY/trpl-zh-cn
synced 2025-05-25 19:39:03 +08:00
wip: 2024 edition
This commit is contained in:
parent
79dac70d39
commit
a136417b78
@ -74,7 +74,7 @@
|
|||||||
|
|
||||||
## Rust 编程思想
|
## Rust 编程思想
|
||||||
|
|
||||||
- [Rust 中的函数式语言功能:迭代器与闭包](ch13-00-functional-features.md)
|
- [函数式语言特性:迭代器与闭包](ch13-00-functional-features.md)
|
||||||
- [闭包:可以捕获其环境的匿名函数](ch13-01-closures.md)
|
- [闭包:可以捕获其环境的匿名函数](ch13-01-closures.md)
|
||||||
- [使用迭代器处理元素序列](ch13-02-iterators.md)
|
- [使用迭代器处理元素序列](ch13-02-iterators.md)
|
||||||
- [改进之前的 I/O 项目](ch13-03-improving-our-io-project.md)
|
- [改进之前的 I/O 项目](ch13-03-improving-our-io-project.md)
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
## 读取文件
|
## 读取文件
|
||||||
|
|
||||||
> [ch12-02-reading-a-file.md](https://github.com/rust-lang/book/blob/main/src/ch12-02-reading-a-file.md)
|
<!-- https://github.com/rust-lang/book/blob/main/src/ch12-02-reading-a-file.md -->
|
||||||
> <br>
|
<!-- commit 3a30e4c1fbe641afc066b3af9eb01dcdf5ed8b24 -->
|
||||||
> commit 02a168ed346042f07010f8b65b4eeed623dd31d1
|
|
||||||
|
|
||||||
现在我们要增加读取由 `file_path` 命令行参数指定的文件的功能。首先,需要一个用来测试的示例文件:我们会用一个拥有多行少量文本且有一些重复单词的文件。示例 12-3 是一首艾米莉·狄金森(Emily Dickinson)的诗,它正适合这个工作!在项目根目录创建一个文件 `poem.txt`,并输入诗 "I'm nobody! Who are you?":
|
现在我们要增加读取由 `file_path` 命令行参数指定的文件的功能。首先,需要一个用来测试的示例文件:我们会用一个拥有多行少量文本且有一些重复单词的文件。示例 12-3 是一首艾米莉·狄金森(Emily Dickinson)的诗,它正适合这个工作!在项目根目录创建一个文件 *poem.txt*,并输入诗 "I'm nobody! Who are you?":
|
||||||
|
|
||||||
<span class="filename">文件名:poem.txt</span>
|
<span class="filename">文件名:poem.txt</span>
|
||||||
|
|
||||||
@ -14,7 +13,7 @@
|
|||||||
|
|
||||||
<span class="caption">示例 12-3:艾米莉·狄金森的诗 “I’m nobody! Who are you?”,一个好的测试用例</span>
|
<span class="caption">示例 12-3:艾米莉·狄金森的诗 “I’m nobody! Who are you?”,一个好的测试用例</span>
|
||||||
|
|
||||||
创建完这个文件之后,修改 *src/main.rs* 并增加如示例 12-4 所示的打开文件的代码:
|
有了文本后,修改 *src/main.rs* 并增加如示例 12-4 所示的打开文件的代码:
|
||||||
|
|
||||||
<span class="filename">文件名:src/main.rs</span>
|
<span class="filename">文件名:src/main.rs</span>
|
||||||
|
|
||||||
@ -36,4 +35,4 @@
|
|||||||
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-04/output.txt}}
|
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-04/output.txt}}
|
||||||
```
|
```
|
||||||
|
|
||||||
好的!代码读取并打印出了文件的内容。虽然它还有一些瑕疵:此时 `main` 函数有着多个职能,通常函数只负责一个功能的话会更简洁并易于维护。另一个问题是没有尽可能的处理错误。虽然我们的程序还很小,这些瑕疵并不是什么大问题,不过随着程序功能的丰富,将会越来越难以用简单的方法修复它们。在开发程序时,及早开始重构是一个最佳实践,因为重构少量代码时要容易的多,所以让我们现在就开始吧。
|
好的!代码读取并打印出了文件的内容。虽然它还有一些瑕疵:此时 `main` 函数有着多个职能,通常函数只负责一个功能的话会更简洁并易于维护。另一个问题是没有尽可能的处理错误。虽然我们的程序还很小,这些瑕疵并不是什么大问题,不过随着程序功能的丰富,要干净地修复它们就会越来越困难。在开发程序时,及早开始重构是一个良好实践,因为重构少量代码时要容易的多。接下来我们就来进行重构。
|
||||||
|
@ -1,24 +1,23 @@
|
|||||||
## 重构改进模块性和错误处理
|
## 重构改进模块性和错误处理
|
||||||
|
|
||||||
> [ch12-03-improving-error-handling-and-modularity.md](https://github.com/rust-lang/book/blob/main/src/ch12-03-improving-error-handling-and-modularity.md)
|
<!-- https://github.com/rust-lang/book/blob/main/src/ch12-03-improving-error-handling-and-modularity.md -->
|
||||||
> <br>
|
<!-- commit 3a30e4c1fbe641afc066b3af9eb01dcdf5ed8b24 -->
|
||||||
> commit 83788ff212a3281328e2f8f223ce9e0f69220b97
|
|
||||||
|
|
||||||
为了改善我们的程序这里有四个问题需要修复,而且它们都与程序的组织方式和如何处理潜在错误有关。第一,`main` 现在进行了两个任务:它解析了参数并打开了文件。对于一个这样的小函数,这并不是一个大问题。然而如果 `main` 中的功能持续增加,`main` 函数处理的独立任务也会增加。当函数承担了更多责任,它就更难以推导,更难以测试,并且更难以在不破坏其他部分的情况下做出修改。最好能分离出功能以便每个函数就负责一个任务。
|
为了改善我们的程序这里有四个问题需要修复,而且它们都与程序的组织方式和如何处理潜在错误有关。第一,`main` 现在进行了两个任务:它解析了参数并打开了文件。然而随着 `main` 中的功能持续增加,`main` 函数处理的独立任务也会增加。当函数承担了更多责任,它就更难以推导,更难以测试,并且更难以在不破坏其他部分的情况下做出修改。最好能分离出功能以便每个函数各司其职。
|
||||||
|
|
||||||
这同时也关系到第二个问题:`query` 和 `file_path` 是程序中的配置变量,而像 `contents` 则用来执行程序逻辑。随着 `main` 函数的增长,就需要引入更多的变量到作用域中,而当作用域中有更多的变量时,将更难以追踪每个变量的目的。最好能将配置变量组织进一个结构,这样就能使它们的目的更明确了。
|
这同时也关系到第二个问题:`query` 和 `file_path` 是程序中的配置变量,而像 `contents` 则用来执行程序逻辑。随着 `main` 函数的增长,就需要引入更多的变量到作用域中,而当作用域中有更多的变量时,将更难以追踪每个变量的目的。最好能将配置变量组织进一个结构,这样就能使它们的目的更明确了。
|
||||||
|
|
||||||
第三个问题是如果打开文件失败我们使用 `expect` 来打印出错误信息,不过这个错误信息只是说 `Should have been able to read the file`。读取文件失败的原因有多种:例如文件不存在,或者没有打开此文件的权限。目前,无论处于何种情况,我们只是打印出“文件读取出现错误”的信息,这并没有给予使用者具体的信息!
|
第三个问题是如果打开文件失败时我们使用 `expect` 来打印出错误信息,不过这个错误信息只是说 `Should have been able to read the file`。读取文件失败的原因有多种:例如文件可能不存在,或者没有打开此文件的权限。目前,无论哪种情况,都会显示相同的错误信息,无法为用户提供任何有用的线索!
|
||||||
|
|
||||||
第四,我们不停地使用 `expect` 来处理不同的错误,如果用户没有指定足够的参数来运行程序,他们会从 Rust 得到 `index out of bounds` 错误,而这并不能明确地解释问题。如果所有的错误处理都位于一处,这样将来的维护者在需要修改错误处理逻辑时就只需要考虑这一处代码。将所有的错误处理都放在一处也有助于确保我们打印的错误信息对终端用户来说是有意义的。
|
第四,我们也使用 `expect` 来处理参数错误,如果用户没有指定足够的参数来运行程序,他们会从 Rust 得到 `index out of bounds` 错误,而这并不能明确地解释问题。如果所有的错误处理都位于一处,这样将来的维护者在需要修改错误处理逻辑时就只需要考虑这一处代码。将所有的错误处理都放在一处也有助于确保我们打印的错误信息对终端用户来说是有意义的。
|
||||||
|
|
||||||
让我们通过重构项目来解决这些问题。
|
让我们通过重构项目来解决这四个问题。
|
||||||
|
|
||||||
### 二进制项目的关注分离
|
### 二进制项目的关注分离
|
||||||
|
|
||||||
`main` 函数负责多个任务的组织问题在许多二进制项目中很常见。所以 Rust 社区开发出一类在 `main` 函数开始变得庞大时进行二进制程序的关注分离的指导。这些过程有如下步骤:
|
`main` 函数负责多个任务的组织问题在许多二进制项目中很常见。所以 Rust 社区开发出一类在 `main` 函数开始变得庞大时进行二进制程序的关注分离的指南。这些过程包括如下步骤:
|
||||||
|
|
||||||
* 将程序拆分成 *main.rs* 和 *lib.rs* 并将程序的逻辑放入 *lib.rs* 中。
|
- 将程序拆分成 *main.rs* 和 *lib.rs* 并将程序的逻辑放入 *lib.rs* 中。
|
||||||
* 当命令行解析逻辑比较小时,可以保留在 *main.rs* 中。
|
* 当命令行解析逻辑比较小时,可以保留在 *main.rs* 中。
|
||||||
* 当命令行解析开始变得复杂时,也同样将其从 *main.rs* 提取到 *lib.rs* 中。
|
* 当命令行解析开始变得复杂时,也同样将其从 *main.rs* 提取到 *lib.rs* 中。
|
||||||
|
|
||||||
@ -27,11 +26,11 @@
|
|||||||
* 使用参数值调用命令行解析逻辑
|
* 使用参数值调用命令行解析逻辑
|
||||||
* 设置任何其他的配置
|
* 设置任何其他的配置
|
||||||
* 调用 *lib.rs* 中的 `run` 函数
|
* 调用 *lib.rs* 中的 `run` 函数
|
||||||
* 如果 `run` 返回错误,则处理这个错误
|
* 如果 `run` 返回错误,则进行错误处理
|
||||||
|
|
||||||
这个模式的一切就是为了关注分离:*main.rs* 处理程序运行,而 *lib.rs* 处理所有的真正的任务逻辑。因为不能直接测试 `main` 函数,这个结构通过将所有的程序逻辑移动到 *lib.rs* 的函数中使得我们可以测试它们。仅仅保留在 *main.rs* 中的代码将足够小以便阅读就可以验证其正确性。让我们遵循这些步骤来重构程序。
|
这个模式的一切就是为了关注分离:*main.rs* 处理程序运行,而 *lib.rs* 处理所有的真正的任务逻辑。因为不能直接测试 `main` 函数,这个结构通过将所有的程序逻辑移动到 *lib.rs* 的函数中使得我们可以测试它们。仅仅保留在 *main.rs* 中的代码将足够小以便阅读就可以验证其正确性。让我们遵循这些步骤来重构程序。
|
||||||
|
|
||||||
### 提取参数解析器
|
#### 提取参数解析器
|
||||||
|
|
||||||
首先,我们将解析参数的功能提取到一个 `main` 将会调用的函数中,为将命令行解析逻辑移动到 *src/lib.rs* 中做准备。示例 12-5 中展示了新 `main` 函数的开头,它调用了新函数 `parse_config`。目前它仍将定义在 *src/main.rs* 中:
|
首先,我们将解析参数的功能提取到一个 `main` 将会调用的函数中,为将命令行解析逻辑移动到 *src/lib.rs* 中做准备。示例 12-5 中展示了新 `main` 函数的开头,它调用了新函数 `parse_config`。目前它仍将定义在 *src/main.rs* 中:
|
||||||
|
|
||||||
@ -43,17 +42,15 @@
|
|||||||
|
|
||||||
<span class="caption">示例 12-5:从 `main` 中提取出 `parse_config` 函数</span>
|
<span class="caption">示例 12-5:从 `main` 中提取出 `parse_config` 函数</span>
|
||||||
|
|
||||||
我们仍然将命令行参数收集进一个 vector,不过不同于在 `main` 函数中将索引 1 的参数值赋值给变量 `query` 和将索引 2 的值赋值给变量 `file_path`,我们将整个 vector 传递给 `parse_config` 函数。接着 `parse_config` 函数将包含决定哪个参数该放入哪个变量的逻辑,并将这些值返回到 `main`。仍然在 `main` 中创建变量 `query` 和 `file_path`,不过 `main` 不再负责处理命令行参数与变量如何对应。
|
我们仍然将命令行参数收集进一个 vector,不过不同于在 `main` 函数中将索引 1 的参数值赋值给变量 `query` 和将索引 2 的值赋值给变量 `file_path`,我们将整个 vector 传递给 `parse_config` 函数。接着 `parse_config` 函数将包含决定哪个参数该放入哪个变量的逻辑,并将这些值返回到 `main`。我们仍然在 `main` 中创建变量 `query` 和 `file_path`,不过 `main` 不再负责处理命令行参数与变量如何对应。
|
||||||
|
|
||||||
这对重构我们这小程序可能有点大材小用,不过我们将采用小的、增量的步骤进行重构。在做出这些改变之后,再次运行程序并验证参数解析是否仍然正常。经常验证你的进展是一个好习惯,这样在遇到问题时能帮助你定位问题的成因。
|
这对重构我们这小程序可能有点大材小用,不过我们将采用小的、增量的步骤进行重构。在做出这些改变之后,再次运行程序并验证参数解析是否仍然正常。经常验证你的进展是一个好习惯,这样在遇到问题时能帮助你定位问题的成因。
|
||||||
|
|
||||||
### 组合配置值
|
### 组合配置值
|
||||||
|
|
||||||
我们可以采取另一个小的步骤来进一步改善这个函数。现在函数返回一个元组,不过立刻又将元组拆成了独立的部分。这是一个我们可能没有进行正确抽象的信号。
|
我们可以采取另一个小的步骤来进一步改善 `parse_config` 函数。现在函数返回一个元组,不过立刻又将元组拆成了独立的部分。这是一个我们可能没有进行正确抽象的信号。
|
||||||
|
|
||||||
另一个表明还有改进空间的迹象是 `parse_config` 名称的 `config` 部分,它暗示了我们返回的两个值是相关的并都是一个配置值的一部分。目前除了将这两个值组合进元组之外并没有表达这个数据结构的意义:我们可以将这两个值放入一个结构体并给每个字段一个有意义的名字。这会让未来的维护者更容易理解不同的值如何相互关联以及它们的目的。
|
另一个表明还有改进空间的迹象是 `parse_config` 名称的 `config` 部分,它暗示了我们返回的两个值是相关的并都是一个配置值的一部分。目前除了将这两个值组合进元组之外并没有表达这个数据结构的意义;相反我们可以将这两个值放入一个结构体并给每个字段一个有意义的名字。这会让未来的维护者更容易理解不同的值如何相互关联以及它们的目的。
|
||||||
|
|
||||||
> 注意:一些同学将这种在复杂类型更为合适的场景下使用基本类型的反模式称为 **基本类型偏执**(*primitive obsession*)。
|
|
||||||
|
|
||||||
示例 12-6 展示了 `parse_config` 函数的改进。
|
示例 12-6 展示了 `parse_config` 函数的改进。
|
||||||
|
|
||||||
@ -65,8 +62,7 @@
|
|||||||
|
|
||||||
<span class="caption">示例 12-6:重构 `parse_config` 返回一个 `Config` 结构体实例</span>
|
<span class="caption">示例 12-6:重构 `parse_config` 返回一个 `Config` 结构体实例</span>
|
||||||
|
|
||||||
新定义的结构体 `Config` 中包含字段 `query` 和 `file_path`。
|
我们添加了一个名为 `Config` 的结构体,其中包含 `query` 和 `file_path` 字段。`parse_config` 的签名表明它现在返回一个 `Config` 值。在之前的 `parse_config` 函数体中,我们返回了引用 `args` 中 `String` 值的字符串 slice,现在我们定义 `Config` 来包含拥有所有权的 `String` 值。`main` 中的 `args` 变量是参数值的所有者并只允许 `parse_config` 函数借用它们,这意味着如果 `Config` 尝试获取 `args` 中值的所有权将违反 Rust 的借用规则。
|
||||||
`parse_config` 的签名表明它现在返回一个 `Config` 值。在之前的 `parse_config` 函数体中,我们返回了引用 `args` 中 `String` 值的字符串 slice,现在我们定义 `Config` 来包含拥有所有权的 `String` 值。`main` 中的 `args` 变量是参数值的所有者并只允许 `parse_config` 函数借用它们,这意味着如果 `Config` 尝试获取 `args` 中值的所有权将违反 Rust 的借用规则。
|
|
||||||
|
|
||||||
还有许多不同的方式可以处理 `String` 的数据,而最简单但有些不太高效的方式是调用这些值的 `clone` 方法。这会生成 `Config` 实例可以拥有的数据的完整拷贝,不过会比储存字符串数据的引用消耗更多的时间和内存。不过拷贝数据使得代码显得更加直白因为无需管理引用的生命周期,所以在这种情况下牺牲一小部分性能来换取简洁性的取舍是值得的。
|
还有许多不同的方式可以处理 `String` 的数据,而最简单但有些不太高效的方式是调用这些值的 `clone` 方法。这会生成 `Config` 实例可以拥有的数据的完整拷贝,不过会比储存字符串数据的引用消耗更多的时间和内存。不过拷贝数据使得代码显得更加直白因为无需管理引用的生命周期,所以在这种情况下牺牲一小部分性能来换取简洁性的取舍是值得的。
|
||||||
|
|
||||||
@ -78,7 +74,7 @@
|
|||||||
|
|
||||||
现在代码更明确的表现了我们的意图,`query` 和 `file_path` 是相关联的并且它们的目的是配置程序如何工作。任何使用这些值的代码就知道在 `config` 实例中对应目的的字段名中寻找它们。
|
现在代码更明确的表现了我们的意图,`query` 和 `file_path` 是相关联的并且它们的目的是配置程序如何工作。任何使用这些值的代码就知道在 `config` 实例中对应目的的字段名中寻找它们。
|
||||||
|
|
||||||
### 创建一个 `Config` 的构造函数
|
### 创建 `Config` 的构造函数
|
||||||
|
|
||||||
目前为止,我们将负责解析命令行参数的逻辑从 `main` 提取到了 `parse_config` 函数中,这有助于我们看清值 `query` 和 `file_path` 是相互关联的并应该在代码中表现这种关系。接着我们增加了 `Config` 结构体来描述 `query` 和 `file_path` 的相关性,并能够从 `parse_config` 函数中将这些值的名称作为结构体字段名称返回。
|
目前为止,我们将负责解析命令行参数的逻辑从 `main` 提取到了 `parse_config` 函数中,这有助于我们看清值 `query` 和 `file_path` 是相互关联的并应该在代码中表现这种关系。接着我们增加了 `Config` 结构体来描述 `query` 和 `file_path` 的相关性,并能够从 `parse_config` 函数中将这些值的名称作为结构体字段名称返回。
|
||||||
|
|
||||||
@ -102,7 +98,7 @@
|
|||||||
{{#include ../listings/ch12-an-io-project/listing-12-07/output.txt}}
|
{{#include ../listings/ch12-an-io-project/listing-12-07/output.txt}}
|
||||||
```
|
```
|
||||||
|
|
||||||
`index out of bounds: the len is 1 but the index is 1` 是一个针对程序员的错误信息,然而这并不能真正帮助终端用户理解发生了什么和他们应该做什么。现在就让我们修复它吧。
|
`index out of bounds: the len is 1 but the index is 1` 是一个面向程序员的错误信息,然而这并不能真正帮助终端用户理解发生了什么和他们应该做什么。现在就让我们修复它吧。
|
||||||
|
|
||||||
#### 改善错误信息
|
#### 改善错误信息
|
||||||
|
|
||||||
@ -116,7 +112,7 @@
|
|||||||
|
|
||||||
<span class="caption">示例 12-8:增加一个参数数量检查</span>
|
<span class="caption">示例 12-8:增加一个参数数量检查</span>
|
||||||
|
|
||||||
这类似于 [示例 9-13 中的 `Guess::new` 函数][ch9-custom-types],那里如果 `value` 参数超出了有效值的范围就调用 `panic!`。不同于检查值的范围,这里检查 `args` 的长度至少是 `3`,而函数的剩余部分则可以在假设这个条件成立的基础上运行。如果 `args` 少于 3 个项,则这个条件将为真,并调用 `panic!` 立即终止程序。
|
这类似于[示例 9-13 中的 `Guess::new` 函数][ch9-custom-types],那里如果 `value` 参数超出了有效值的范围就调用 `panic!`。不同于检查值的范围,这里检查 `args` 的长度至少是 `3`,而函数的剩余部分则可以在假设这个条件成立的基础上运行。如果 `args` 少于 3 个项,则这个条件将为真,并调用 `panic!` 立即终止程序。
|
||||||
|
|
||||||
有了 `new` 中这几行额外的代码,再次不带任何参数运行程序并看看现在错误看起来像什么:
|
有了 `new` 中这几行额外的代码,再次不带任何参数运行程序并看看现在错误看起来像什么:
|
||||||
|
|
||||||
@ -124,13 +120,13 @@
|
|||||||
{{#include ../listings/ch12-an-io-project/listing-12-08/output.txt}}
|
{{#include ../listings/ch12-an-io-project/listing-12-08/output.txt}}
|
||||||
```
|
```
|
||||||
|
|
||||||
这个输出就好多了,现在有了一个合理的错误信息。然而,还是有一堆额外的信息我们不希望提供给用户。所以在这里使用示例 9-9 中的技术可能不是最好的;正如 [第九章][ch9-error-guidelines] 所讲到的一样,`panic!` 的调用更趋向于程序上的问题而不是使用上的问题。相反我们可以使用第九章学习的另一个技术 —— 返回一个可以表明成功或错误的 [`Result`][ch9-result]。
|
这个输出就更好了:现在有了一个合理的错误信息。然而,还是有一堆额外的信息我们不希望提供给用户。所以在这里使用示例 9-13 中的技术可能不是最好的;正如[第九章][ch9-error-guidelines]所讲到的一样,`panic!` 的调用更趋向于程序上的问题而不是使用上的问题。相反我们可以使用第九章学习的另一个技术 —— 返回一个可以表明成功或错误的 [`Result`][ch9-result]。
|
||||||
|
|
||||||
#### 从 `new` 中返回 `Result` 而不是调用 `panic!`
|
#### 返回 `Result` 而不是调用 `panic!`
|
||||||
|
|
||||||
我们可以选择返回一个 `Result` 值,它在成功时会包含一个 `Config` 的实例,而在错误时会描述问题。我们还将把函数名从`new`改为`build`,因为许多程序员希望 `new` 函数永远不会失败。当 `Config::new` 与 `main` 交流时,可以使用 `Result` 类型来表明这里存在问题。接着修改 `main` 将 `Err` 成员转换为对用户更友好的错误,而不是 `panic!` 调用产生的关于 `thread 'main'` 和 `RUST_BACKTRACE` 的文本。
|
我们可以选择返回一个 `Result` 值,它在成功时会包含一个 `Config` 的实例,而在错误时会描述问题。我们还将把函数名从 `new` 改为 `build`,因为许多程序员希望 `new` 函数永远不会失败。当 `Config::new` 与 `main` 交流时,可以使用 `Result` 类型来表明这里存在问题。接着修改 `main` 将 `Err` 成员转换为对用户更友好的错误,而不是 `panic!` 调用产生的关于 `thread 'main'` 和 `RUST_BACKTRACE` 的文本。
|
||||||
|
|
||||||
示例 12-9 展示了为了返回 `Result` 在 `Config::new` 的返回值和函数体中所需的改变。注意这还不能编译,直到下一个示例同时也更新了 `main` 之后。
|
示例 12-9 展示了为了返回 `Result` 在 `Config::new` 的返回值和函数体中所需的改变。注意这还不能编译,直到下一个示例更新了 `main` 之后。
|
||||||
|
|
||||||
<span class="filename">文件名:src/main.rs</span>
|
<span class="filename">文件名:src/main.rs</span>
|
||||||
|
|
||||||
@ -140,7 +136,7 @@
|
|||||||
|
|
||||||
<span class="caption">示例 12-9:从 `Config::build` 中返回 `Result`</span>
|
<span class="caption">示例 12-9:从 `Config::build` 中返回 `Result`</span>
|
||||||
|
|
||||||
现在 `build` 函数返回一个 `Result`,在成功时带有一个 `Config` 实例而在出现错误时带有一个 `&'static str`。回忆一下第十章 “静态生命周期” 中讲到 `&'static str` 是字符串字面值的类型,也是目前的错误信息。
|
现在 `build` 函数返回一个 `Result`,在成功时带有一个 `Config` 实例而在出现错误时总是一个有着 `'static` 生命周期的字符串字面值。
|
||||||
|
|
||||||
`build` 函数体中有两处修改:当没有足够参数时不再调用 `panic!`,而是返回 `Err` 值。同时我们将 `Config` 返回值包装进 `Ok` 成员中。这些修改使得函数符合其新的类型签名。
|
`build` 函数体中有两处修改:当没有足够参数时不再调用 `panic!`,而是返回 `Err` 值。同时我们将 `Config` 返回值包装进 `Ok` 成员中。这些修改使得函数符合其新的类型签名。
|
||||||
|
|
||||||
@ -148,7 +144,7 @@
|
|||||||
|
|
||||||
#### 调用 `Config::build` 并处理错误
|
#### 调用 `Config::build` 并处理错误
|
||||||
|
|
||||||
为了处理错误情况并打印一个对用户友好的信息,我们需要像示例 12-10 那样更新 `main` 函数来处理现在 `Config::build` 返回的 `Result`。另外还需要手动实现原先由 `panic!`负责的工作,即以非零错误码退出命令行工具的工作。非零的退出状态是一个惯例信号,用来告诉调用程序的进程:该程序以错误状态退出了。
|
为了处理错误情况并打印一个对用户友好的信息,我们需要像示例 12-10 那样更新 `main` 函数来处理现在 `Config::build` 返回的 `Result`。另外还需要手动实现原先由 `panic!` 负责的工作,即以非零错误码退出命令行工具的工作。非零的退出状态是一个惯例信号,用来告诉调用程序的进程:该程序以错误状态退出了。
|
||||||
|
|
||||||
<span class="filename">文件名:src/main.rs</span>
|
<span class="filename">文件名:src/main.rs</span>
|
||||||
|
|
||||||
@ -158,7 +154,7 @@
|
|||||||
|
|
||||||
<span class="caption">示例 12-10:如果新建 `Config` 失败则使用错误码退出</span>
|
<span class="caption">示例 12-10:如果新建 `Config` 失败则使用错误码退出</span>
|
||||||
|
|
||||||
在上面的示例中,使用了一个之前没有详细说明的方法:`unwrap_or_else`,它定义于标准库的 `Result<T, E>` 上。使用 `unwrap_or_else` 可以进行一些自定义的非 `panic!` 的错误处理。当 `Result` 是 `Ok` 时,这个方法的行为类似于 `unwrap`:它返回 `Ok` 内部封装的值。然而,当其值是 `Err` 时,该方法会调用一个 **闭包**(*closure*),也就是一个我们定义的作为参数传递给 `unwrap_or_else` 的匿名函数。[第十三章][ch13] 会更详细的介绍闭包。现在你需要理解的是 `unwrap_or_else` 会将 `Err` 的内部值,也就是示例 12-9 中增加的 `not enough arguments` 静态字符串的情况,传递给闭包中位于两道竖线间的参数 `err`。闭包中的代码在其运行时可以使用这个 `err` 值。
|
在上面的示例中,使用了一个之前没有详细说明的方法:`unwrap_or_else`,它定义于标准库的 `Result<T, E>` 上。使用 `unwrap_or_else` 可以进行一些自定义的非 `panic!` 的错误处理。当 `Result` 是 `Ok` 时,这个方法的行为类似于 `unwrap`:它返回 `Ok` 内部封装的值。然而,当其值是 `Err` 时,该方法会调用一个**闭包**(*closure*),也就是一个我们定义的作为参数传递给 `unwrap_or_else` 的匿名函数。[第十三章][ch13]会更详细地介绍闭包。现在你需要理解的是 `unwrap_or_else` 会将 `Err` 的内部值,也就是示例 12-9 中增加的 `not enough arguments` 静态字符串的情况,传递给闭包中位于两道竖线间的参数 `err`。闭包中的代码在其运行时可以使用这个 `err` 值。
|
||||||
|
|
||||||
我们新增了一个 `use` 行来从标准库中导入 `process`。在错误的情况闭包中将被运行的代码只有两行:我们打印出了 `err` 值,接着调用了 `std::process::exit`。`process::exit` 会立即停止程序并将传递给它的数字作为退出状态码。这类似于示例 12-8 中使用的基于 `panic!` 的错误处理,除了不会再得到所有的额外输出了。让我们试试:
|
我们新增了一个 `use` 行来从标准库中导入 `process`。在错误的情况闭包中将被运行的代码只有两行:我们打印出了 `err` 值,接着调用了 `std::process::exit`。`process::exit` 会立即停止程序并将传递给它的数字作为退出状态码。这类似于示例 12-8 中使用的基于 `panic!` 的错误处理,除了不会再得到所有的额外输出了。让我们试试:
|
||||||
|
|
||||||
@ -170,7 +166,7 @@
|
|||||||
|
|
||||||
### 从 `main` 提取逻辑
|
### 从 `main` 提取逻辑
|
||||||
|
|
||||||
现在我们完成了配置解析的重构:让我们转向程序的逻辑。正如 [“二进制项目的关注分离”](#二进制项目的关注分离) 部分所展开的讨论,我们将提取一个叫做 `run` 的函数来存放目前 `main` 函数中不属于设置配置或处理错误的所有逻辑。一旦完成这些,`main` 函数将简明得足以通过观察来验证,而我们将能够为所有其他逻辑编写测试。
|
现在我们完成了配置解析的重构,让我们转向程序的逻辑。正如[“二进制项目的关注分离”](#二进制项目的关注分离)部分所展开的讨论,我们将提取一个叫做 `run` 的函数来存放目前 `main` 函数中不属于设置配置或处理错误的所有逻辑。一旦完成这些,`main` 函数将简明得足以通过观察来验证,而我们将能够为所有其他逻辑编写测试。
|
||||||
|
|
||||||
示例 12-11 展示了提取出来的 `run` 函数。目前我们只进行小的增量式的提取函数的改进。我们仍将在 *src/main.rs* 中定义这个函数:
|
示例 12-11 展示了提取出来的 `run` 函数。目前我们只进行小的增量式的提取函数的改进。我们仍将在 *src/main.rs* 中定义这个函数:
|
||||||
|
|
||||||
@ -184,9 +180,9 @@
|
|||||||
|
|
||||||
现在 `run` 函数包含了 `main` 中从读取文件开始的剩余的所有逻辑。`run` 函数获取一个 `Config` 实例作为参数。
|
现在 `run` 函数包含了 `main` 中从读取文件开始的剩余的所有逻辑。`run` 函数获取一个 `Config` 实例作为参数。
|
||||||
|
|
||||||
#### 从 `run` 函数中返回错误
|
#### 从 `run` 函数返回错误
|
||||||
|
|
||||||
通过将剩余的逻辑分离进 `run` 函数而不是留在 `main` 中,就可以像示例 12-9 中的 `Config::build` 那样改进错误处理。不再通过 `expect` 允许程序 panic,`run` 函数将会在出错时返回一个 `Result<T, E>`。这让我们进一步以一种对用户友好的方式统一 `main` 中的错误处理。示例 12-12 展示了 `run` 签名和函数体中的改变:
|
通过将剩余的逻辑分离进 `run` 函数中,就可以像示例 12-9 中的 `Config::build` 那样改进错误处理。不再通过 `expect` 允许程序 panic,`run` 函数将会在出错时返回一个 `Result<T, E>`。这让我们进一步以一种对用户友好的方式将处理错误的逻辑统一到 `main` 中。示例 12-12 展示了 `run` 签名和函数体中的改变:
|
||||||
|
|
||||||
<span class="filename">文件名:src/main.rs</span>
|
<span class="filename">文件名:src/main.rs</span>
|
||||||
|
|
||||||
@ -198,13 +194,13 @@
|
|||||||
|
|
||||||
这里我们做出了三个明显的修改。首先,将 `run` 函数的返回类型变为 `Result<(), Box<dyn Error>>`。之前这个函数返回 unit 类型 `()`,现在它仍然保持作为 `Ok` 时的返回值。
|
这里我们做出了三个明显的修改。首先,将 `run` 函数的返回类型变为 `Result<(), Box<dyn Error>>`。之前这个函数返回 unit 类型 `()`,现在它仍然保持作为 `Ok` 时的返回值。
|
||||||
|
|
||||||
对于错误类型,使用了 **trait 对象** `Box<dyn Error>`(在开头使用了 `use` 语句将 `std::error::Error` 引入作用域)。[第十八章][ch18] 会涉及 trait 对象。目前只需知道 `Box<dyn Error>` 意味着函数会返回实现了 `Error` trait 的类型,不过无需指定具体将会返回的值的类型。这提供了在不同的错误场景可能有不同类型的错误返回值的灵活性。这也就是 `dyn`,它是 “动态的”(“dynamic”)的缩写。
|
对于错误类型,使用 **trait 对象** `Box<dyn Error>`(在开头使用了 `use` 语句将 `std::error::Error` 引入作用域)。[第十八章][ch18]会涉及 trait 对象。目前只需知道 `Box<dyn Error>` 意味着函数会返回实现了 `Error` trait 的类型,不过无需指定具体将会返回的值的类型。这提供了在不同的错误场景可能有不同类型的错误返回值的灵活性。这也就是 `dyn`,它是“动态的”(“dynamic”)的缩写。
|
||||||
|
|
||||||
第二个改变是去掉了 `expect` 调用并替换为 [第九章][ch9-question-mark] 讲到的 `?`。不同于遇到错误就 `panic!`,`?` 会从函数中返回错误值并让调用者来处理它。
|
第二个改变是去掉了 `expect` 调用并替换为[第九章][ch9-question-mark]讲到的 `?` 运算符。不同于遇到错误就 `panic!`,`?` 会从函数中返回错误值并让调用者来处理它。
|
||||||
|
|
||||||
第三个修改是现在成功时这个函数会返回一个 `Ok` 值。因为 `run` 函数签名中声明成功类型返回值是 `()`,这意味着需要将 unit 类型值包装进 `Ok` 值中。`Ok(())` 一开始看起来有点奇怪,不过这样使用 `()` 是惯用的做法,表明调用 `run` 函数只是为了它的副作用;函数并没有返回什么有意义的值。
|
第三个修改是现在成功时这个函数会返回一个 `Ok` 值。因为 `run` 函数签名中声明成功类型返回值是 `()`,这意味着需要将 unit 类型值包装进 `Ok` 值中。`Ok(())` 一开始看起来有点奇怪,不过这样使用 `()` 是惯用的做法,表明调用 `run` 函数只是为了它的副作用;函数并没有返回什么有意义的值。
|
||||||
|
|
||||||
上述代码能够编译,不过会有一个警告:
|
运行上述代码时,它能够编译通过,但会显示一条警告:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
{{#include ../listings/ch12-an-io-project/listing-12-12/output.txt}}
|
{{#include ../listings/ch12-an-io-project/listing-12-12/output.txt}}
|
||||||
@ -212,7 +208,7 @@
|
|||||||
|
|
||||||
Rust 提示我们的代码忽略了 `Result` 值,它可能表明这里存在一个错误。但我们却没有检查这里是否有一个错误,而编译器提醒我们这里应该有一些错误处理代码!现在就让我们修正这个问题。
|
Rust 提示我们的代码忽略了 `Result` 值,它可能表明这里存在一个错误。但我们却没有检查这里是否有一个错误,而编译器提醒我们这里应该有一些错误处理代码!现在就让我们修正这个问题。
|
||||||
|
|
||||||
#### 处理 `main` 中 `run` 返回的错误
|
#### 在 `main` 中处理 `run` 返回的错误
|
||||||
|
|
||||||
我们将检查错误并使用类似示例 12-10 中 `Config::build` 处理错误的技术来处理它们,不过有一些细微的不同:
|
我们将检查错误并使用类似示例 12-10 中 `Config::build` 处理错误的技术来处理它们,不过有一些细微的不同:
|
||||||
|
|
||||||
@ -237,7 +233,7 @@ Rust 提示我们的代码忽略了 `Result` 值,它可能表明这里存在
|
|||||||
- `Config` 的定义
|
- `Config` 的定义
|
||||||
- `Config::build` 函数定义
|
- `Config::build` 函数定义
|
||||||
|
|
||||||
现在 *src/lib.rs* 的内容应该看起来像示例 12-13(为了简洁省略了函数体)。注意直到下一个示例修改完 *src/main.rs* 之后,代码还不能编译:
|
现在 *src/lib.rs* 的内容应该看起来像示例 12-13(为了简洁省略了函数体)。注意直到下一个示例修改完 *src/main.rs* 之后,代码还不能编译。
|
||||||
|
|
||||||
<span class="filename">文件名:src/lib.rs</span>
|
<span class="filename">文件名:src/lib.rs</span>
|
||||||
|
|
||||||
@ -247,7 +243,7 @@ Rust 提示我们的代码忽略了 `Result` 值,它可能表明这里存在
|
|||||||
|
|
||||||
<span class="caption">示例 12-13:将 `Config` 和 `run` 移动到 *src/lib.rs*</span>
|
<span class="caption">示例 12-13:将 `Config` 和 `run` 移动到 *src/lib.rs*</span>
|
||||||
|
|
||||||
这里使用了公有的 `pub` 关键字:在 `Config`、其字段和其 `build` 方法,以及 `run` 函数上。现在我们有了一个拥有可以测试的公有 API 的库 crate 了。
|
这里大量使用了 `pub` 关键字:在 `Config`、其字段和其 `build` 方法,以及 `run` 函数上。现在我们有了一个拥有可以测试的公有 API 的库 crate 了。
|
||||||
|
|
||||||
现在需要在 *src/main.rs* 中将移动到 *src/lib.rs* 的代码引入二进制 crate 的作用域中,如示例 12-14 所示:
|
现在需要在 *src/main.rs* 中将移动到 *src/lib.rs* 的代码引入二进制 crate 的作用域中,如示例 12-14 所示:
|
||||||
|
|
||||||
@ -261,9 +257,9 @@ Rust 提示我们的代码忽略了 `Result` 值,它可能表明这里存在
|
|||||||
|
|
||||||
我们添加了一行 `use minigrep::Config`,它将 `Config` 类型引入作用域,并使用 crate 名称作为 `run` 函数的前缀。通过这些重构,所有功能应该能够联系在一起并运行了。运行 `cargo run` 来确保一切都正确的衔接在一起。
|
我们添加了一行 `use minigrep::Config`,它将 `Config` 类型引入作用域,并使用 crate 名称作为 `run` 函数的前缀。通过这些重构,所有功能应该能够联系在一起并运行了。运行 `cargo run` 来确保一切都正确的衔接在一起。
|
||||||
|
|
||||||
哇哦!我们做了大量的工作,不过我们为将来的成功打下了基础。现在处理错误将更容易,同时代码也更加模块化。从现在开始几乎所有的工作都将在 *src/lib.rs* 中进行。
|
呼!我们做了大量的工作,不过我们为将来的成功打下了基础。现在处理错误将更容易,同时代码也更加模块化。从现在开始几乎所有的工作都将在 *src/lib.rs* 中进行。
|
||||||
|
|
||||||
让我们利用这些新创建的模块的优势来进行一些在旧代码中难以展开的工作,这些工作在新代码中非常容易实现,那就是:编写测试!
|
让我们利用这些新创建的模块的优势来进行一些在旧代码中难以展开的工作,这些工作在新代码中非常容易实现:那就是编写测试!
|
||||||
|
|
||||||
[ch13]: ch13-00-functional-features.html
|
[ch13]: ch13-00-functional-features.html
|
||||||
[ch9-custom-types]: ch09-03-to-panic-or-not-to-panic.html#创建自定义类型进行有效性验证
|
[ch9-custom-types]: ch09-03-to-panic-or-not-to-panic.html#创建自定义类型进行有效性验证
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
## 采用测试驱动开发完善库的功能
|
## 采用测试驱动开发完善库的功能
|
||||||
|
|
||||||
> [ch12-04-testing-the-librarys-functionality.md](https://github.com/rust-lang/book/blob/main/src/ch12-04-testing-the-librarys-functionality.md)
|
<!-- https://github.com/rust-lang/book/blob/main/src/ch12-04-testing-the-librarys-functionality.md -->
|
||||||
> <br>
|
<!-- commit 3a30e4c1fbe641afc066b3af9eb01dcdf5ed8b24 -->
|
||||||
> commit 8fd2327e4135876b368cc2793eb4a7e455b691f0
|
|
||||||
|
|
||||||
现在我们将逻辑提取到了 *src/lib.rs* 并将所有的参数解析和错误处理留在了 *src/main.rs* 中,为代码的核心功能编写测试将更加容易。我们可以直接使用多种参数调用函数并检查返回值而无需从命令行运行二进制文件了。
|
现在我们将逻辑提取到了 *src/lib.rs* 并将所有的参数解析和错误处理留在了 *src/main.rs* 中,为代码的核心功能编写测试将更加容易。我们可以直接使用多种参数调用函数并检查返回值而无需从命令行运行二进制文件了。
|
||||||
|
|
||||||
@ -19,7 +18,7 @@
|
|||||||
|
|
||||||
### 编写失败测试
|
### 编写失败测试
|
||||||
|
|
||||||
去掉 *src/lib.rs* 和 *src/main.rs* 中用于检查程序行为的 `println!` 语句,因为不再真正需要它们了。接着我们会像 [第十一章][ch11-anatomy] 那样增加一个 `test` 模块和一个测试函数。测试函数指定了 `search` 函数期望拥有的行为:它会获取一个需要查询的字符串和用来查询的文本,并只会返回包含请求的文本行。示例 12-15 展示了这个测试,它还不能编译:
|
去掉 *src/lib.rs* 和 *src/main.rs* 中用于检查程序行为的 `println!` 语句,因为不再真正需要它们了。接着我们会像[第十一章][ch11-anatomy]那样增加一个 `test` 模块和一个测试函数。测试函数指定了 `search` 函数期望拥有的行为:它会获取一个需要查询的字符串和用来查询的文本,并只会返回包含请求的文本行。示例 12-15 展示了这个测试,它还不能编译:
|
||||||
|
|
||||||
<span class="filename">文件名:src/lib.rs</span>
|
<span class="filename">文件名:src/lib.rs</span>
|
||||||
|
|
||||||
@ -41,19 +40,19 @@
|
|||||||
|
|
||||||
<span class="caption">示例 12-16:刚好足够使测试通过编译的 `search` 函数定义</span>
|
<span class="caption">示例 12-16:刚好足够使测试通过编译的 `search` 函数定义</span>
|
||||||
|
|
||||||
注意需要在 `search` 的签名中定义一个显式生命周期 `'a` 并用于 `contents` 参数和返回值。回忆一下 [第十章][ch10-lifetimes] 中讲到生命周期参数指定哪个参数的生命周期与返回值的生命周期相关联。在这个例子中,我们表明返回的 vector 中应该包含引用参数 `contents`(而不是参数`query`)slice 的字符串 slice。
|
注意需要在 `search` 的签名中定义一个显式生命周期 `'a` 并用于 `contents` 参数和返回值。回忆一下[第十章][ch10-lifetimes]中讲到生命周期参数指定哪个参数的生命周期与返回值的生命周期相关联。在这个例子中,我们表明返回的 vector 中应该包含引用参数 `contents`(而不是参数`query`)slice 的字符串 slice。
|
||||||
|
|
||||||
换句话说,我们告诉 Rust 函数 `search` 返回的数据将与 `search` 函数中的参数 `contents` 的数据存在的一样久。这是非常重要的!为了使这个引用有效那么 **被** slice 引用的数据也需要保持有效;如果编译器认为我们是在创建 `query` 而不是 `contents` 的字符串 slice,那么安全检查将是不正确的。
|
换句话说,我们告诉 Rust 函数 `search` 返回的数据将与 `search` 函数中的参数 `contents` 的数据存在的一样久。这是非常重要的!为了使这个引用有效那么**被** slice 引用的数据也需要保持有效;如果编译器认为我们是在创建 `query` 而不是 `contents` 的字符串 slice,那么安全检查将是不正确的。
|
||||||
|
|
||||||
如果尝试不用生命周期编译的话,我们将得到如下错误:
|
如果我们忘记添加生命周期注解而尝试编译此函数,就会得到如下错误:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
{{#include ../listings/ch12-an-io-project/output-only-02-missing-lifetimes/output.txt}}
|
{{#include ../listings/ch12-an-io-project/output-only-02-missing-lifetimes/output.txt}}
|
||||||
```
|
```
|
||||||
|
|
||||||
Rust 不可能知道我们需要的是哪一个参数,所以需要告诉它。因为参数 `contents` 包含了所有的文本而且我们希望返回匹配的那部分文本,所以我们知道 `contents` 是应该要使用生命周期语法来与返回值相关联的参数。
|
Rust 不可能知道我们需要的是哪一个参数,所以需要显式地告诉它。因为参数 `contents` 包含了所有的文本而且我们希望返回匹配的那部分文本,所以我们知道 `contents` 是应该要使用生命周期语法来与返回值相关联的参数。
|
||||||
|
|
||||||
其他语言中并不需要你在函数签名中将参数与返回值相关联。所以这么做可能仍然感觉有些陌生,随着时间的推移这将会变得越来越容易。你可能想要将这个例子与第十章中 [“生命周期确保引用有效”][validating-references-with-lifetimes] 部分做对比。
|
其他语言中并不需要你在函数签名中将参数与返回值相关联。所以这么做可能仍然感觉有些陌生,随着时间的推移这将会变得越来越容易。你可能想要将这个例子与第十章中[“生命周期确保引用有效”][validating-references-with-lifetimes]部分做对比。
|
||||||
|
|
||||||
现在运行测试:
|
现在运行测试:
|
||||||
|
|
||||||
@ -61,21 +60,21 @@ Rust 不可能知道我们需要的是哪一个参数,所以需要告诉它。
|
|||||||
{{#include ../listings/ch12-an-io-project/listing-12-16/output.txt}}
|
{{#include ../listings/ch12-an-io-project/listing-12-16/output.txt}}
|
||||||
```
|
```
|
||||||
|
|
||||||
好的,测试失败了,这正是我们所期望的。修改代码来让测试通过吧!
|
很好,测试失败了,这正是我们所期望的。修改代码来让测试通过吧!
|
||||||
|
|
||||||
### 编写使测试通过的代码
|
### 编写使测试通过的代码
|
||||||
|
|
||||||
目前测试之所以会失败是因为我们总是返回一个空的 vector。为了修复并实现 `search`,我们的程序需要遵循如下步骤:
|
目前测试之所以会失败是因为我们总是返回一个空的 vector。为了修复并实现 `search`,我们的程序需要遵循如下步骤:
|
||||||
|
|
||||||
* 遍历内容的每一行文本。
|
1. 遍历内容的每一行文本。
|
||||||
* 查看这一行是否包含要搜索的字符串。
|
2. 查看这一行是否包含要搜索的字符串。
|
||||||
* 如果有,将这一行加入列表返回值中。
|
3. 如果有,将这一行加入列表返回值中。
|
||||||
* 如果没有,什么也不做。
|
4. 如果没有,什么也不做。
|
||||||
* 返回匹配到的结果列表
|
5. 返回匹配到的结果列表。
|
||||||
|
|
||||||
让我们一步一步的来,从遍历每行开始。
|
让我们一步一步的来,从遍历每行开始。
|
||||||
|
|
||||||
#### 使用 `lines` 方法遍历每一行
|
#### 使用 `lines` 方法逐行遍历
|
||||||
|
|
||||||
Rust 有一个有助于一行一行遍历字符串的方法,出于方便它被命名为 `lines`,它如示例 12-17 这样工作。注意这还不能编译:
|
Rust 有一个有助于一行一行遍历字符串的方法,出于方便它被命名为 `lines`,它如示例 12-17 这样工作。注意这还不能编译:
|
||||||
|
|
||||||
@ -87,7 +86,7 @@ Rust 有一个有助于一行一行遍历字符串的方法,出于方便它被
|
|||||||
|
|
||||||
<span class="caption">示例 12-17:遍历 `contents` 的每一行</span>
|
<span class="caption">示例 12-17:遍历 `contents` 的每一行</span>
|
||||||
|
|
||||||
`lines` 方法返回一个迭代器。[第十三章][ch13-iterators] 会深入了解迭代器,不过我们已经在 [示例 3-5][ch3-iter] 中见过使用迭代器的方法了,在那里使用了一个 `for` 循环和迭代器在一个集合的每一项上运行了一些代码。
|
`lines` 方法返回一个迭代器。[第十三章][ch13-iterators]会深入了解迭代器,不过我们已经在[示例 3-5][ch3-iter] 中见过使用迭代器的方法了,在那里使用了一个 `for` 循环和迭代器在一个集合的每一项上运行了一些代码。
|
||||||
|
|
||||||
#### 用查询字符串搜索每一行
|
#### 用查询字符串搜索每一行
|
||||||
|
|
||||||
@ -101,9 +100,11 @@ Rust 有一个有助于一行一行遍历字符串的方法,出于方便它被
|
|||||||
|
|
||||||
<span class="caption">示例 12-18:增加检查文本行是否包含 `query` 中字符串的功能</span>
|
<span class="caption">示例 12-18:增加检查文本行是否包含 `query` 中字符串的功能</span>
|
||||||
|
|
||||||
|
目前,我们正在构建功能。为了让代码能够编译,需要从函数体返回一个我们在函数签名中所声明的值。
|
||||||
|
|
||||||
#### 存储匹配的行
|
#### 存储匹配的行
|
||||||
|
|
||||||
为了完成这个函数,我们还需要一个方法来存储包含查询字符串的行。为此可以在 `for` 循环之前创建一个可变的 vector 并调用 `push` 方法在 vector 中存放一个 `line`。在 `for` 循环之后,返回这个 vector,如示例 12-19 所示:
|
为了完成这个函数,我们需要一种方法来存储要返回的匹配行。为此可以在 `for` 循环之前创建一个可变的 vector 并调用 `push` 方法在 vector 中存放一个 `line`。在 `for` 循环之后,返回这个 vector,如示例 12-19 所示:
|
||||||
|
|
||||||
<span class="filename">文件名:src/lib.rs</span>
|
<span class="filename">文件名:src/lib.rs</span>
|
||||||
|
|
||||||
@ -119,7 +120,7 @@ Rust 有一个有助于一行一行遍历字符串的方法,出于方便它被
|
|||||||
{{#include ../listings/ch12-an-io-project/listing-12-19/output.txt}}
|
{{#include ../listings/ch12-an-io-project/listing-12-19/output.txt}}
|
||||||
```
|
```
|
||||||
|
|
||||||
测试通过了,它可以工作了!
|
测试通过了,我们知道它可以工作了!
|
||||||
|
|
||||||
现在正是可以考虑重构的时机,在保证测试通过,保持功能不变的前提下重构 `search` 函数。`search` 函数中的代码并不坏,不过并没有利用迭代器的一些实用功能。第十三章将回到这个例子并深入探索迭代器并看看如何改进代码。
|
现在正是可以考虑重构的时机,在保证测试通过,保持功能不变的前提下重构 `search` 函数。`search` 函数中的代码并不坏,不过并没有利用迭代器的一些实用功能。第十三章将回到这个例子并深入探索迭代器并看看如何改进代码。
|
||||||
|
|
||||||
@ -141,13 +142,13 @@ Rust 有一个有助于一行一行遍历字符串的方法,出于方便它被
|
|||||||
{{#include ../listings/ch12-an-io-project/no-listing-02-using-search-in-run/output.txt}}
|
{{#include ../listings/ch12-an-io-project/no-listing-02-using-search-in-run/output.txt}}
|
||||||
```
|
```
|
||||||
|
|
||||||
好的!现在试试一个会匹配多行的单词,比如 “body”:
|
不错!现在试试一个会匹配多行的单词,比如 “body”:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
{{#include ../listings/ch12-an-io-project/output-only-03-multiple-matches/output.txt}}
|
{{#include ../listings/ch12-an-io-project/output-only-03-multiple-matches/output.txt}}
|
||||||
```
|
```
|
||||||
|
|
||||||
最后,让我们确保搜索一个在诗中哪里都没有的单词时不会得到任何行,比如 "monomorphization":
|
最后,让我们确保搜索一个在诗中哪里都没有的单词时不会得到任何行,比如 *monomorphization*:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
{{#include ../listings/ch12-an-io-project/output-only-04-no-matches/output.txt}}
|
{{#include ../listings/ch12-an-io-project/output-only-04-no-matches/output.txt}}
|
||||||
@ -158,6 +159,9 @@ Rust 有一个有助于一行一行遍历字符串的方法,出于方便它被
|
|||||||
|
|
||||||
为了使这个项目更丰满,我们将简要的展示如何处理环境变量和打印到标准错误,这两者在编写命令行程序时都很有用。
|
为了使这个项目更丰满,我们将简要的展示如何处理环境变量和打印到标准错误,这两者在编写命令行程序时都很有用。
|
||||||
|
|
||||||
|
|
||||||
|
最后,让我们确保当搜索一个在诗中不存在的单词时,不会返回任何行,例如 _monomorphization_:
|
||||||
|
|
||||||
[validating-references-with-lifetimes]:
|
[validating-references-with-lifetimes]:
|
||||||
ch10-03-lifetime-syntax.html#生命周期确保引用有效
|
ch10-03-lifetime-syntax.html#生命周期确保引用有效
|
||||||
[ch11-anatomy]: ch11-01-writing-tests.html#测试函数剖析
|
[ch11-anatomy]: ch11-01-writing-tests.html#测试函数剖析
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
## 处理环境变量
|
## 处理环境变量
|
||||||
|
|
||||||
> [ch12-05-working-with-environment-variables.md](https://github.com/rust-lang/book/blob/main/src/ch12-05-working-with-environment-variables.md)
|
<!-- https://github.com/rust-lang/book/blob/main/src/ch12-05-working-with-environment-variables.md -->
|
||||||
> <br>
|
<!-- commit 56ec353290429e6547109e88afea4de027b0f1a9 -->
|
||||||
> commit 9c0fa2714859738ff73cbbb829592e4c037d7e46
|
|
||||||
|
|
||||||
我们将增加一个额外的功能来改进 `minigrep`:用户可以通过设置环境变量来设置搜索是否是大小写敏感的。当然,我们也可以将其设计为一个命令行参数并要求用户每次需要时都加上它,不过在这里我们将使用环境变量。这允许用户设置环境变量一次之后在整个终端会话中所有的搜索都将是大小写不敏感的。
|
我们将增加一个额外的功能来改进 `minigrep`:用户可以通过设置环境变量来设置搜索是否是大小写敏感的选项。当然,我们也可以将其设计为一个命令行参数并要求用户每次需要时都加上它,不过在这里我们将使用环境变量。这允许用户设置环境变量一次之后在整个终端会话中所有的搜索都将是大小写不敏感的。
|
||||||
|
|
||||||
### 编写一个大小写不敏感 `search` 函数的失败测试
|
### 编写一个大小写不敏感 `search` 函数的失败测试
|
||||||
|
|
||||||
@ -18,9 +17,9 @@
|
|||||||
|
|
||||||
<span class="caption">示例 12-20:为准备添加的大小写不敏感函数新增失败测试</span>
|
<span class="caption">示例 12-20:为准备添加的大小写不敏感函数新增失败测试</span>
|
||||||
|
|
||||||
注意我们也改变了老测试中 `contents` 的值。还新增了一个含有文本 `"Duct tape."` 的行,它有一个大写的 D,这在大小写敏感搜索时不应该匹配 "duct"。我们修改这个测试以确保不会意外破坏已经实现的大小写敏感搜索功能;这个测试现在应该能通过并在处理大小写不敏感搜索时应该能一直通过。
|
注意我们也改变了老测试中 `contents` 的值。还新增了一个含有文本 `"Duct tape."` 的行,它有一个大写的 D,这在大小写敏感搜索时不应该匹配 `"duct"`。我们修改这个测试以确保不会意外破坏已经实现的大小写敏感搜索功能;这个测试现在应该能通过并在处理大小写不敏感搜索时应该能一直通过。
|
||||||
|
|
||||||
大小写 **不敏感** 搜索的新测试使用 `"rUsT"` 作为其查询字符串。在我们将要增加的 `search_case_insensitive` 函数中,`"rUsT"` 查询应该包含带有一个大写 R 的 `"Rust:"` 还有 `"Trust me."` 这两行,即便它们与查询的大小写都不同。这个测试现在不能编译,因为还没有定义 `search_case_insensitive` 函数。请随意增加一个总是返回空 vector 的骨架实现,正如示例 12-16 中 `search` 函数为了使测试通过编译并失败时所做的那样。
|
大小写**不敏感**搜索的新测试使用 `"rUsT"` 作为其查询字符串。在我们将要增加的 `search_case_insensitive` 函数中,`"rUsT"` 查询应该包含带有一个大写 R 的 `"Rust:"` 还有 `"Trust me."` 这两行,即便它们与查询的大小写都不同。这个测试现在不能编译,因为还没有定义 `search_case_insensitive` 函数。请随意增加一个总是返回空 vector 的骨架实现,正如示例 12-16 中 `search` 函数为了使测试通过编译并失败时所做的那样。
|
||||||
|
|
||||||
### 实现 `search_case_insensitive` 函数
|
### 实现 `search_case_insensitive` 函数
|
||||||
|
|
||||||
@ -34,7 +33,7 @@
|
|||||||
|
|
||||||
<span class="caption">示例 12-21:定义 `search_case_insensitive` 函数,它在比较查询和每一行之前将它们都转换为小写</span>
|
<span class="caption">示例 12-21:定义 `search_case_insensitive` 函数,它在比较查询和每一行之前将它们都转换为小写</span>
|
||||||
|
|
||||||
首先我们将 `query` 字符串转换为小写,并将其覆盖到同名的变量中。对查询字符串调用 `to_lowercase` 是必需的,这样不管用户的查询是 `"rust"`、`"RUST"`、`"Rust"` 或者 `"rUsT"`,我们都将其当作 `"rust"` 处理并对大小写不敏感。虽然 `to_lowercase` 可以处理基本的 Unicode,但它不是 100% 准确。如果编写真实的程序的话,我们还需多做一些工作,不过这一部分是关于环境变量而不是 Unicode 的,所以这样就够了。
|
首先我们将 `query` 字符串转换为小写,并将其覆盖到同名的变量中,遮蔽原来的 `query`。对查询字符串调用 `to_lowercase` 是必需的,这样不管用户的查询是 `"rust"`、`"RUST"`、`"Rust"` 或者 `"rUsT"`,我们都将其当作 `"rust"` 处理并对大小写不敏感。虽然 `to_lowercase` 可以处理基本的 Unicode,但它不是 100% 准确。如果编写真实的程序的话,我们还需多做一些工作,不过这一部分是关于环境变量而不是 Unicode 的,所以这样就够了。
|
||||||
|
|
||||||
注意 `query` 现在是一个 `String` 而不是字符串 slice,因为调用 `to_lowercase` 是在创建新数据,而不是引用现有数据。如果查询字符串是 `"rUsT"`,这个字符串 slice 并不包含可供我们使用的小写的 `u` 或 `t`,所以必需分配一个包含 `"rust"` 的新 `String`。现在当我们将 `query` 作为一个参数传递给 `contains` 方法时,需要增加一个 & 因为 `contains` 的签名被定义为获取一个字符串 slice。
|
注意 `query` 现在是一个 `String` 而不是字符串 slice,因为调用 `to_lowercase` 是在创建新数据,而不是引用现有数据。如果查询字符串是 `"rUsT"`,这个字符串 slice 并不包含可供我们使用的小写的 `u` 或 `t`,所以必需分配一个包含 `"rust"` 的新 `String`。现在当我们将 `query` 作为一个参数传递给 `contains` 方法时,需要增加一个 & 因为 `contains` 的签名被定义为获取一个字符串 slice。
|
||||||
|
|
||||||
@ -46,7 +45,7 @@
|
|||||||
{{#include ../listings/ch12-an-io-project/listing-12-21/output.txt}}
|
{{#include ../listings/ch12-an-io-project/listing-12-21/output.txt}}
|
||||||
```
|
```
|
||||||
|
|
||||||
好的!现在,让我们在 `run` 函数中实际调用新 `search_case_insensitive` 函数。首先,我们将在 `Config` 结构体中增加一个配置项来切换大小写敏感和大小写不敏感搜索。增加这些字段会导致编译错误,因为我们还没有在任何地方初始化这些字段:
|
太好了!测试都通过了。现在,让我们在 `run` 函数中实际调用新 `search_case_insensitive` 函数。首先,我们将在 `Config` 结构体中增加一个配置项来切换大小写敏感和大小写不敏感搜索。增加这些字段会导致编译错误,因为我们还没有在任何地方初始化这些字段:
|
||||||
|
|
||||||
<span class="filename">文件名:src/lib.rs</span>
|
<span class="filename">文件名:src/lib.rs</span>
|
||||||
|
|
||||||
@ -54,7 +53,7 @@
|
|||||||
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-22/src/lib.rs:here}}
|
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-22/src/lib.rs:here}}
|
||||||
```
|
```
|
||||||
|
|
||||||
这里增加了 `ignore_case` 字符来存放一个布尔值。接着我们需要 `run` 函数检查 `case_sensitive` 字段的值并使用它来决定是否调用 `search` 函数或 `search_case_insensitive` 函数,如示例 12-22 所示。注意这还不能编译:
|
这里增加了 `ignore_case` 字段来存放一个布尔值。接着我们需要 `run` 函数检查 `case_sensitive` 字段的值并使用它来决定是否调用 `search` 函数或 `search_case_insensitive` 函数,如示例 12-22 所示。注意这还不能编译:
|
||||||
|
|
||||||
<span class="filename">文件名:src/lib.rs</span>
|
<span class="filename">文件名:src/lib.rs</span>
|
||||||
|
|
||||||
@ -74,11 +73,11 @@
|
|||||||
|
|
||||||
<span class="caption">示例 12-23:检查叫做 `IGNORE_CASE` 的环境变量</span>
|
<span class="caption">示例 12-23:检查叫做 `IGNORE_CASE` 的环境变量</span>
|
||||||
|
|
||||||
这里创建了一个新变量 `ignore_case`。为了设置它的值,需要调用 `env::var` 函数并传递我们需要寻找的环境变量名称,`IGNORE_CASE`。`env::var` 返回一个 `Result`,它在环境变量被设置时返回包含其值的 `Ok` 成员,并在环境变量未被设置时返回 `Err` 成员。
|
这里创建了一个新变量 `ignore_case`。为了设置它的值,需要调用 `env::var` 函数并传递我们需要寻找的环境变量名称,`IGNORE_CASE`。`env::var` 返回一个 `Result`,它在环境变量被设置时返回包含其值的 `Ok` 变体,并在环境变量未被设置时返回 `Err` 变体。
|
||||||
|
|
||||||
我们使用 `Result` 的 `is_ok` 方法来检查环境变量是否被设置,这也就意味着我们 **需要** 进行一个大小写不敏感的搜索。如果`IGNORE_CASE` 环境变量没有被设置为任何值,`is_ok` 会返回 false 并将进行大小写敏感的搜索。我们并不关心环境变量所设置的 **值**,只关心它是否被设置了,所以检查 `is_ok` 而不是 `unwrap`、`expect` 或任何我们已经见过的 `Result` 的方法。
|
我们使用 `Result` 的 `is_ok` 方法来检查环境变量是否被设置,这也就意味着程序应该进行一个大小写不敏感的搜索。如果 `IGNORE_CASE` 环境变量没有被设置为任何值,`is_ok` 会返回 `false` 并将进行大小写敏感的搜索。我们并不关心环境变量所设置的**值**,只关心它是否被设置了,所以检查 `is_ok` 而不是 `unwrap`、`expect` 或任何我们已经见过的 `Result` 的方法。
|
||||||
|
|
||||||
我们将变量 `ignore_case` 的值传递给 `Config` 实例,这样 `run` 函数可以读取其值并决定是否调用 `search` 或者示例 12-22 中实现的 `search_case_insensitive`。
|
我们将变量 `ignore_case` 的值传递给 `Config` 实例,这样 `run` 函数可以读取其值并决定是否调用示例 12-22 中实现的 `search_case_insensitive` 或者 `search`。
|
||||||
|
|
||||||
让我们试一试吧!首先不设置环境变量并使用查询 `to` 运行程序,这应该会匹配任何全小写的单词 “to” 的行:
|
让我们试一试吧!首先不设置环境变量并使用查询 `to` 运行程序,这应该会匹配任何全小写的单词 “to” 的行:
|
||||||
|
|
||||||
@ -86,25 +85,25 @@
|
|||||||
{{#include ../listings/ch12-an-io-project/listing-12-23/output.txt}}
|
{{#include ../listings/ch12-an-io-project/listing-12-23/output.txt}}
|
||||||
```
|
```
|
||||||
|
|
||||||
看起来程序仍然能够工作!现在将 `IGNORE_CASE` 设置为 `1` 并仍使用相同的查询 `to`。
|
看起来程序仍然能够工作!现在将 `IGNORE_CASE` 设置为 `1` 并仍使用相同的查询 *to* 来运行程序:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ IGNORE_CASE=1 cargo run to poem.txt
|
$ IGNORE_CASE=1 cargo run -- to poem.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
如果你使用 PowerShell,则需要用两个命令来分别设置环境变量并运行程序:
|
如果你使用 PowerShell,则需要用两个命令来分别设置环境变量并运行程序:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
PS> $Env:IGNORE_CASE=1; cargo run to poem.txt
|
PS> $Env:IGNORE_CASE=1; cargo run -- to poem.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
而这会让 `IGNORE_CASE` 的效果在当前 shell 会话中持续生效。可以通过 `Remove-Item` 命令来取消设置:
|
而这会让 `IGNORE_CASE` 的效果在当前 shell 会话中持续生效。可以通过 `Remove-Item` cmdlet 来取消设置:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
PS> Remove-Item Env:IGNORE_CASE
|
PS> Remove-Item Env:IGNORE_CASE
|
||||||
```
|
```
|
||||||
|
|
||||||
这回应该得到包含可能有大写字母的 “to” 的行:
|
这回应该得到包含可能有大写字母的 *to* 的行:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
Are you nobody, too?
|
Are you nobody, too?
|
||||||
@ -113,8 +112,8 @@ To tell your name the livelong day
|
|||||||
To an admiring bog!
|
To an admiring bog!
|
||||||
```
|
```
|
||||||
|
|
||||||
好极了,我们也得到了包含 “To” 的行!现在 `minigrep` 程序可以通过环境变量控制进行大小写不敏感搜索了。现在你知道了如何管理由命令行参数或环境变量设置的选项了!
|
好极了,我们也得到了包含 *to* 的行!现在 `minigrep` 程序可以通过环境变量控制进行大小写不敏感搜索了。现在你知道了如何管理由命令行参数或环境变量设置的选项了!
|
||||||
|
|
||||||
一些程序允许对相同配置同时使用参数 **和** 环境变量。在这种情况下,程序来决定参数和环境变量的优先级。作为一个留给你的测试,尝试通过一个命令行参数或一个环境变量来控制大小写敏感搜索。并在运行程序时遇到矛盾值时决定命令行参数和环境变量的优先级。
|
一些程序允许对相同配置同时使用参数**和**环境变量。在这种情况下,程序来决定参数和环境变量的优先级。作为一个留给你的测试,尝试通过一个命令行参数或一个环境变量来控制大小写敏感搜索。并在运行程序时遇到矛盾值时决定命令行参数和环境变量的优先级。
|
||||||
|
|
||||||
`std::env` 模块还包含了更多处理环境变量的实用功能;请查看官方文档来了解其可用的功能。
|
`std::env` 模块还包含了更多处理环境变量的实用功能;请查看其文档来了解其可用的功能。
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
## 将错误信息输出到标准错误而不是标准输出
|
## 将错误信息输出到标准错误而不是标准输出
|
||||||
|
|
||||||
> [ch12-06-writing-to-stderr-instead-of-stdout.md](https://github.com/rust-lang/book/blob/main/src/ch12-06-writing-to-stderr-instead-of-stdout.md)
|
<!-- https://github.com/rust-lang/book/blob/main/src/ch12-06-writing-to-stderr-instead-of-stdout.md -->
|
||||||
> <br>
|
<!-- commit 3a30e4c1fbe641afc066b3af9eb01dcdf5ed8b24 -->
|
||||||
> commit 02a168ed346042f07010f8b65b4eeed623dd31d1
|
|
||||||
|
|
||||||
目前为止,我们将所有的输出都通过 `println!` 写到了终端。大部分终端都提供了两种输出:**标准输出**(*standard output*,`stdout`)对应一般信息,**标准错误**(*standard error*,`stderr`)则用于错误信息。这种区别允许用户选择将程序正常输出定向到一个文件中并仍将错误信息打印到屏幕上。
|
目前为止,我们将所有的输出都通过 `println!` 写到了终端。大部分终端都提供了两种输出:**标准输出**(*standard output*,`stdout`)对应一般信息,**标准错误**(*standard error*,`stderr`)则用于错误信息。这种区别允许用户选择将程序正常输出定向到一个文件中并仍将错误信息打印到屏幕上。
|
||||||
|
|
||||||
但是 `println!` 宏只能够打印到标准输出,所以我们必须使用其他方法来打印到标准错误。
|
但是 `println!` 宏只能够打印到标准输出,所以我们必须使用其他方法来打印到标准错误。
|
||||||
|
|
||||||
### 检查错误应该写入何处
|
### 检查错误写入何处
|
||||||
|
|
||||||
首先,让我们观察一下目前 `minigrep` 打印的所有内容是如何被写入标准输出的,包括那些应该被写入标准错误的错误信息。可以通过将标准输出流重定向到一个文件同时有意产生一个错误来做到这一点。我们没有重定向标准错误流,所以任何发送到标准错误的内容将会继续显示在屏幕上。
|
首先,让我们观察一下目前 `minigrep` 打印的所有内容是如何被写入标准输出的,包括那些应该被写入标准错误的错误信息。可以通过将标准输出流重定向到一个文件同时有意产生一个错误来做到这一点。我们没有重定向标准错误流,所以任何发送到标准错误的内容将会继续显示在屏幕上。
|
||||||
|
|
||||||
@ -26,7 +25,7 @@ $ cargo run > output.txt
|
|||||||
Problem parsing arguments: not enough arguments
|
Problem parsing arguments: not enough arguments
|
||||||
```
|
```
|
||||||
|
|
||||||
是的,错误信息被打印到了标准输出中。像这样的错误信息被打印到标准错误中将会有用得多,将使得只有成功运行所产生的输出才会写入文件。我们接下来就修改。
|
是的,错误信息被打印到了标准输出中。像这样的错误信息被打印到标准错误中将会有用得多,这将使得只有成功运行所产生的输出才会写入文件。我们接下来就修改。
|
||||||
|
|
||||||
### 将错误打印到标准错误
|
### 将错误打印到标准错误
|
||||||
|
|
||||||
@ -64,7 +63,7 @@ Are you nobody, too?
|
|||||||
How dreary to be somebody!
|
How dreary to be somebody!
|
||||||
```
|
```
|
||||||
|
|
||||||
这一部分展示了现在我们适当的使用了成功时产生的标准输出和错误时产生的标准错误。
|
这一部分展示了现在我们适当地使用了成功时产生的标准输出和错误时产生的标准错误。
|
||||||
|
|
||||||
## 总结
|
## 总结
|
||||||
|
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
# Rust 中的函数式语言功能:迭代器与闭包
|
# 函数式语言特性:迭代器与闭包
|
||||||
|
|
||||||
> [ch13-00-functional-features.md](https://github.com/rust-lang/book/blob/main/src/ch13-00-functional-features.md)
|
<!-- https://github.com/rust-lang/book/blob/main/src/ch13-00-functional-features.md -->
|
||||||
> <br>
|
<!-- commit 56ec353290429e6547109e88afea4de027b0f1a9 -->
|
||||||
> commit daa268a0cd04ef76a8067a26ed7d28ec2a9336d3
|
|
||||||
|
|
||||||
Rust 的设计灵感来源于很多现存的语言和技术。其中一个显著的影响就是 **函数式编程**(*functional programming*)。函数式编程风格通常包含将函数作为参数值或其他函数的返回值、将函数赋值给变量以供之后执行等等。
|
Rust 的设计灵感来源于很多现存的语言和技术。其中一个显著的影响就是**函数式编程**(*functional programming*)。函数式编程风格通常包含将函数作为参数值或其他函数的返回值、将函数赋值给变量以供之后执行等等。
|
||||||
|
|
||||||
本章我们不会讨论函数式编程是或不是什么的问题,而是展示 Rust 的一些在功能上与其他被认为是函数式语言类似的特性。
|
本章我们不会讨论函数式编程是或不是什么的问题,而是展示 Rust 的一些在功能上与其他被认为是函数式语言类似的特性。
|
||||||
|
|
||||||
更具体的,我们将要涉及:
|
更具体地,我们将要涉及:
|
||||||
|
|
||||||
* **闭包**(*Closures*),一个可以储存在变量里的类似函数的结构
|
- **闭包**(*Closures*),一个可以储存在变量里的类似函数的结构
|
||||||
* **迭代器**(*Iterators*),一种处理元素序列的方式
|
- **迭代器**(*Iterators*),一种处理元素序列的方式
|
||||||
* 如何使用闭包和迭代器来改进第十二章的 I/O 项目。
|
- 如何使用闭包和迭代器来改进第十二章的 I/O 项目。
|
||||||
* 闭包和迭代器的性能。(**剧透警告:** 它们的速度超乎你的想象!)
|
- 闭包和迭代器的性能。(**剧透警告:** 它们的速度超乎你的想象!)
|
||||||
|
|
||||||
我们已经介绍了其它受函数式风格影响的 Rust 功能,比如模式匹配和枚举,这些已经在其他章节中讲到过了。因为掌握闭包和迭代器是编写符合语言风格的高性能 Rust 代码的重要一环,所以我们将专门用一整章来讲解它们。
|
我们已经介绍了其它受函数式风格影响的 Rust 功能,比如模式匹配和枚举,这些已经在其他章节中讲到过了。因为掌握闭包和迭代器是编写符合语言风格的高性能 Rust 代码的重要一环,所以我们将专门用一整章来讲解它们。
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
## 闭包:可以捕获环境的匿名函数
|
## 闭包:可以捕获环境的匿名函数
|
||||||
|
|
||||||
> [ch13-01-closures.md](https://github.com/rust-lang/book/blob/main/src/ch13-01-closures.md)
|
<!-- https://github.com/rust-lang/book/blob/main/src/ch13-01-closures.md -->
|
||||||
> <br>
|
<!-- commit 56ec353290429e6547109e88afea4de027b0f1a9 -->
|
||||||
> commit a2cb72d3ad7584cc1ae3b85f715c877872f5e3ab
|
|
||||||
|
|
||||||
Rust 的 **闭包**(*closures*)是可以保存在变量中或作为参数传递给其他函数的匿名函数。你可以在一个地方创建闭包,然后在不同的上下文中执行闭包运算。不同于函数,闭包允许捕获其被定义时所在作用域中的值。我们将展示这些闭包特性如何支持代码复用和行为定制。
|
Rust 的 **闭包**(*closures*)是可以保存在变量中或作为参数传递给其他函数的匿名函数。你可以在一个地方创建闭包,然后在不同的上下文中执行闭包运算。不同于函数,闭包允许捕获其被定义时所在作用域中的值。我们将展示这些闭包特性如何支持代码复用和行为定制。
|
||||||
|
|
||||||
### 闭包会捕获其环境
|
### 使用闭包捕获环境
|
||||||
|
|
||||||
我们首先了解如何通过闭包捕获定义它的环境中的值以便之后使用。考虑如下场景:我们的 T 恤公司偶尔会向邮件列表中的某位成员赠送一件限量版的独家 T 恤作为促销。邮件列表中的成员可以选择将他们的喜爱的颜色添加到个人信息中。如果被选中的成员设置了喜爱的颜色,他们将获得那个颜色的 T 恤。如果他没有设置喜爱的颜色,他们会获赠公司当前库存最多的颜色的款式。
|
我们首先了解如何通过闭包捕获定义它的环境中的值以便之后使用。考虑如下场景:我们的 T 恤公司偶尔会向邮件列表中的某位成员赠送一件限量版的独家 T 恤作为促销。邮件列表中的成员可以选择将他们的喜爱的颜色添加到个人信息中。如果被选中的成员设置了喜爱的颜色,他们将获得那个颜色的 T 恤。如果他没有设置喜爱的颜色,他们会获赠公司当前库存最多的颜色的款式。
|
||||||
|
|
||||||
有很多种方式来实现这一点。例如,使用有 `Red` 和 `Blue` 两个成员的 `ShirtColor` 枚举(出于简单考虑限定为两种颜色)。我们使用 `Inventory` 结构体来代表公司的库存,它有一个类型为 `Vec<ShirtColor>` 的 `shirts` 字段表示库存中的衬衫的颜色。`Inventory` 上定义的 `giveaway` 方法获取免费衬衫得主所喜爱的颜色(如有),并返回其获得的衬衫的颜色。初始代码如示例 13-1 所示:
|
有很多种方式来实现这一点。例如,使用有 `Red` 和 `Blue` 两个变体的 `ShirtColor` 枚举(出于简单考虑限定为两种颜色)。我们使用 `Inventory` 结构体来代表公司的库存,它有一个类型为 `Vec<ShirtColor>` 的 `shirts` 字段表示库存中的衬衫的颜色。`Inventory` 上定义的 `giveaway` 方法获取免费衬衫得主所喜爱的颜色(如有),并返回其获得的衬衫的颜色。初始代码如示例 13-1 所示:
|
||||||
|
|
||||||
<span class="filename">文件名:src/main.rs</span>
|
<span class="filename">文件名:src/main.rs</span>
|
||||||
|
|
||||||
@ -22,7 +21,7 @@ Rust 的 **闭包**(*closures*)是可以保存在变量中或作为参数传
|
|||||||
|
|
||||||
`main` 函数中定义的 `store` 还剩下两件蓝衬衫和一件红衬衫,可以在限量版促销活动中赠送。我们通过调用 `giveaway` 方法,为一个期望红衬衫的用户和一个没有特定偏好的用户进行赠送。
|
`main` 函数中定义的 `store` 还剩下两件蓝衬衫和一件红衬衫,可以在限量版促销活动中赠送。我们通过调用 `giveaway` 方法,为一个期望红衬衫的用户和一个没有特定偏好的用户进行赠送。
|
||||||
|
|
||||||
再次强调,这段代码有多种实现方式。这里为了专注于闭包,我们继续使用已经学习过的概念,除了 `giveaway` 方法体中使用了闭包。在 `giveaway` 方法中,我们将用户偏好作为 `Option<ShirtColor>` 类型的参数获取,并在 `user_preference` 上调用 `unwrap_or_else` 方法。[`Option<T>` 上的 `unwrap_or_else` 方法][unwrap-or-else] 由标准库定义。它接受一个无参闭包作为参数,该闭包返回一个 `T` 类型的值(与 `Option<T>` 的 `Some` 变体中存储的值类型相同,这里是 `ShirtColor`)。如果 `Option<T>` 是 `Some` 成员,则 `unwrap_or_else` 返回 `Some` 中的值。如果 `Option<T>` 是 `None` 成员,则 `unwrap_or_else` 调用闭包并返回闭包的返回值。
|
再次强调,这段代码有多种实现方式。这里为了专注于闭包,我们继续使用已经学习过的概念,除了 `giveaway` 方法体中使用了闭包。在 `giveaway` 方法中,我们将用户偏好作为 `Option<ShirtColor>` 类型的参数获取,并在 `user_preference` 上调用 `unwrap_or_else` 方法。[`Option<T>` 上的 `unwrap_or_else` 方法][unwrap-or-else]由标准库定义。它接受一个无参闭包作为参数,该闭包返回一个 `T` 类型的值(与 `Option<T>` 的 `Some` 变体中存储的值类型相同,这里是 `ShirtColor`)。如果 `Option<T>` 是 `Some` 变体,则 `unwrap_or_else` 返回 `Some` 中的值。如果 `Option<T>` 是 `None` 变体,则 `unwrap_or_else` 调用闭包并返回闭包的返回值。
|
||||||
|
|
||||||
我们将闭包表达式 `|| self.most_stocked()` 作为 `unwrap_or_else` 的参数。这是一个本身不获取参数的闭包(如果闭包有参数,它们会出现在两道竖杠之间)。闭包体调用了 `self.most_stocked()`。我们在这里定义了闭包,而 `unwrap_or_else` 的实现会在之后需要其结果的时候执行闭包。
|
我们将闭包表达式 `|| self.most_stocked()` 作为 `unwrap_or_else` 的参数。这是一个本身不获取参数的闭包(如果闭包有参数,它们会出现在两道竖杠之间)。闭包体调用了 `self.most_stocked()`。我们在这里定义了闭包,而 `unwrap_or_else` 的实现会在之后需要其结果的时候执行闭包。
|
||||||
|
|
||||||
@ -50,7 +49,7 @@ Rust 的 **闭包**(*closures*)是可以保存在变量中或作为参数传
|
|||||||
|
|
||||||
<span class="caption">示例 13-2:为闭包的参数和返回值增加可选的类型注解</span>
|
<span class="caption">示例 13-2:为闭包的参数和返回值增加可选的类型注解</span>
|
||||||
|
|
||||||
有了类型注解,闭包的语法看起来就更像函数的语法了。如下是一个对其参数加一的函数的定义与拥有相同行为闭包语法的纵向对比。这里增加了一些空格来对齐相应部分。这展示了除了使用竖线以及一些可选语法外,闭包语法与函数语法有多么地相似:
|
有了类型注解,闭包的语法看起来就更像函数的语法了。如下是一个对其参数加一的函数的定义与拥有相同行为闭包语法的纵向对比。这里增加了一些空格来对齐相应部分。这展示了除了使用管道符(|,pipes)以及一些可选语法外,闭包语法与函数语法有多么地相似:
|
||||||
|
|
||||||
```rust,ignore
|
```rust,ignore
|
||||||
fn add_one_v1 (x: u32) -> u32 { x + 1 }
|
fn add_one_v1 (x: u32) -> u32 { x + 1 }
|
||||||
@ -77,9 +76,9 @@ let add_one_v4 = |x| x + 1 ;
|
|||||||
{{#include ../listings/ch13-functional-features/listing-13-03/output.txt}}
|
{{#include ../listings/ch13-functional-features/listing-13-03/output.txt}}
|
||||||
```
|
```
|
||||||
|
|
||||||
第一次使用 `String` 值调用 `example_closure` 时,编译器推断出 `x` 的类型以及闭包的返回类型为 `String`。接着这些类型被锁定进闭包 `example_closure` 中,如果尝试对同一闭包使用不同类型则就会得到类型错误。
|
第一次使用 `String` 值调用 `example_closure` 时,编译器推断出 `x` 的类型以及闭包的返回类型为 `String`。接着这些类型被锁定进闭包 `example_closure` 闭包中,如果尝试对同一闭包使用不同类型则就会得到类型错误。
|
||||||
|
|
||||||
### 捕获引用或者移动所有权
|
### 捕获引用或移动所有权
|
||||||
|
|
||||||
闭包可以通过三种方式捕获其环境中的值,它们直接对应到函数获取参数的三种方式:不可变借用、可变借用和获取所有权。闭包将根据函数体中对捕获值的操作来决定使用哪种方式。
|
闭包可以通过三种方式捕获其环境中的值,它们直接对应到函数获取参数的三种方式:不可变借用、可变借用和获取所有权。闭包将根据函数体中对捕获值的操作来决定使用哪种方式。
|
||||||
|
|
||||||
@ -163,7 +162,7 @@ impl<T> Option<T> {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
回忆一下,`T` 是表示 `Option` 中 `Some` 成员中的值的类型的泛型。类型 `T` 也是 `unwrap_or_else` 函数的返回值类型:举例来说,在 `Option<String>` 上调用 `unwrap_or_else` 会得到一个 `String`。
|
回忆一下,`T` 是表示 `Option` 中 `Some` 变体中的值的类型的泛型。类型 `T` 也是 `unwrap_or_else` 函数的返回值类型:举例来说,在 `Option<String>` 上调用 `unwrap_or_else` 会得到一个 `String`。
|
||||||
|
|
||||||
接着注意到 `unwrap_or_else` 函数有额外的泛型参数 `F`。`F` 是参数 `f` 的类型,`f` 是调用 `unwrap_or_else` 时提供的闭包。
|
接着注意到 `unwrap_or_else` 函数有额外的泛型参数 `F`。`F` 是参数 `f` 的类型,`f` 是调用 `unwrap_or_else` 时提供的闭包。
|
||||||
|
|
||||||
|
@ -110,7 +110,7 @@ hi number 4 from the main thread!
|
|||||||
|
|
||||||
### 将 `move` 闭包与线程一同使用
|
### 将 `move` 闭包与线程一同使用
|
||||||
|
|
||||||
`move` 关键字经常用于传递给 `thread::spawn` 的闭包,因为闭包会获取从环境中取得的值的所有权,因此会将这些值的所有权从一个线程传送到另一个线程。在第十三章 [“闭包会捕获其环境”][capture] 部分讨论了闭包上下文中的 `move`。现在我们会更专注于 `move` 和 `thread::spawn` 之间的交互。
|
`move` 关键字经常用于传递给 `thread::spawn` 的闭包,因为闭包会获取从环境中取得的值的所有权,因此会将这些值的所有权从一个线程传送到另一个线程。在第十三章 [“使用闭包捕获环境”][capture] 部分讨论了闭包上下文中的 `move`。现在我们会更专注于 `move` 和 `thread::spawn` 之间的交互。
|
||||||
|
|
||||||
在第十三章中,我们讲到可以在参数列表前使用 `move` 关键字强制闭包获取其使用的环境值的所有权。这个技巧在创建新线程将值的所有权从一个线程移动到另一个线程时最为实用。
|
在第十三章中,我们讲到可以在参数列表前使用 `move` 关键字强制闭包获取其使用的环境值的所有权。这个技巧在创建新线程将值的所有权从一个线程移动到另一个线程时最为实用。
|
||||||
|
|
||||||
@ -173,4 +173,4 @@ Rust 的所有权规则又一次帮助了我们!示例 16-3 中的错误是因
|
|||||||
|
|
||||||
现在我们对线程和线程 API 有了基本的了解,让我们讨论一下使用线程实际可以 **做** 什么吧。
|
现在我们对线程和线程 API 有了基本的了解,让我们讨论一下使用线程实际可以 **做** 什么吧。
|
||||||
|
|
||||||
[capture]: ch13-01-closures.html#闭包会捕获其环境
|
[capture]: ch13-01-closures.html#使用闭包捕获环境
|
||||||
|
Loading…
Reference in New Issue
Block a user