update ch12

pull/685/head
KaiserY 2 years ago
parent a14433ba5f
commit 6d567ee104

@ -2,15 +2,15 @@
> [ch12-00-an-io-project.md](https://github.com/rust-lang/book/blob/main/src/ch12-00-an-io-project.md) > [ch12-00-an-io-project.md](https://github.com/rust-lang/book/blob/main/src/ch12-00-an-io-project.md)
> <br> > <br>
> commit db919bc6bb9071566e9c4f05053672133eaac33e > commit 02a168ed346042f07010f8b65b4eeed623dd31d1
本章既是一个目前所学的很多技能的概括,也是一个更多标准库功能的探索。我们将构建一个与文件和命令行输入/输出交互的命令行工具来练习现在一些你已经掌握的 Rust 技能。 本章既是一个目前所学的很多技能的概括,也是一个更多标准库功能的探索。我们将构建一个与文件和命令行输入/输出交互的命令行工具来练习现在一些你已经掌握的 Rust 技能。
Rust 的运行速度、安全性、单二进制文件输出和跨平台支持使其成为创建命令行程序的绝佳选择,所以我们的项目将创建一个我们自己版本的经典命令行工具:`grep`。grep 是 “**G**lobally search a **R**egular **E**xpression and **P**rint.” 的首字母缩写。`grep` 最简单的使用场景是在特定文件中搜索指定字符串。为此,`grep` 获取一个文件和一个字符串作为参数,接着读取文件并找到其中包含字符串参数的行,然后打印出这些行。 Rust 的运行速度、安全性、单二进制文件输出和跨平台支持使其成为创建命令行程序的绝佳选择,所以我们的项目将创建一个我们自己版本的经典命令行搜索工具:`grep`。grep 是 “**G**lobally search a **R**egular **E**xpression and **P**rint.” 的首字母缩写。`grep` 最简单的使用场景是在特定文件中搜索指定字符串。为此,`grep` 获取一个文件路径和一个字符串作为参数,接着读取文件并找到其中包含字符串参数的行,然后打印出这些行。
在这个过程中,我们会展示如何让我们的命令行工具利用很多命令行工具中用到的终端功能。读取环境变量来使得用户可以配置工具的行为。打印到标准错误控制流(`stderr`)而不是标准输出(`stdout`),例如这样用户可以选择将成功输出重定向到文件中的同时仍然在屏幕上显示错误信息。 在这个过程中,我们会展示如何让我们的命令行工具利用很多命令行工具中用到的终端功能。读取环境变量来使得用户可以配置工具的行为。打印到标准错误控制流(`stderr`)而不是标准输出(`stdout`),例如这样用户可以选择将成功输出重定向到文件中的同时仍然在屏幕上显示错误信息。
一位 Rust 社区的成员Andrew Gallant已经创建了一个功能完整且非常快速的 `grep` 版本,叫做 `ripgrep`。相比之下,我们的 `grep` 版本将非常简单,本章将教会你一些帮助理解像 `ripgrep` 这样真实项目的背景知识。 一位 Rust 社区的成员Andrew Gallant已经创建了一个功能完整且非常快速的 `grep` 版本,叫做 `ripgrep`。相比之下,我们的版本将非常简单,本章将教会你一些帮助理解像 `ripgrep` 这样真实项目的背景知识。
我们的 `grep` 项目将会结合之前所学的一些内容: 我们的 `grep` 项目将会结合之前所学的一些内容:

@ -2,7 +2,7 @@
> [ch12-01-accepting-command-line-arguments.md](https://github.com/rust-lang/book/blob/main/src/ch12-01-accepting-command-line-arguments.md) > [ch12-01-accepting-command-line-arguments.md](https://github.com/rust-lang/book/blob/main/src/ch12-01-accepting-command-line-arguments.md)
> <br> > <br>
> commit 0f87daf683ae3de3cb725faecb11b7e7e89f0e5a > commit 02a168ed346042f07010f8b65b4eeed623dd31d1
一如既往使用 `cargo new` 新建一个项目,我们称之为 `minigrep` 以便与可能已经安装在系统上的 `grep` 工具相区别: 一如既往使用 `cargo new` 新建一个项目,我们称之为 `minigrep` 以便与可能已经安装在系统上的 `grep` 工具相区别:
@ -12,19 +12,19 @@ $ cargo new minigrep
$ cd minigrep $ cd minigrep
``` ```
第一个任务是让 `minigrep` 能够接受两个命令行参数:文件和要搜索的字符串。也就是说我们希望能够使用 `cargo run`、要搜索的字符串和被搜索的文件的路径来运行程序,像这样: 第一个任务是让 `minigrep` 能够接受两个命令行参数:文件路径和要搜索的字符串。也就是说我们希望能够使用 `cargo run`、要搜索的字符串和被搜索的文件的路径来运行程序,像这样:
```console ```console
$ cargo run searchstring example-filename.txt $ cargo run -- searchstring example-filename.txt
``` ```
现在 `cargo new` 生成的程序忽略任何传递给它的参数。[Crates.io](https://crates.io/) 上有一些现成的库可以帮助我们接受命令行参数,不过我们正在学习这些内容,让我们自己来实现一个。 现在 `cargo new` 生成的程序忽略任何传递给它的参数。[Crates.io](https://crates.io/) 上有一些现成的库可以帮助我们接受命令行参数,不过我们正在学习这些内容,让我们自己来实现一个。
### 读取参数值 ### 读取参数值
为了确保 `minigrep` 能够获取传递给它的命令行参数的值,我们需要一个 Rust 标准库提供的函数,也就是 `std::env::args`。这个函数返回一个传递给程序的命令行参数的 **迭代器***iterator*)。我们会在 [第十三章][ch13] 全面的介绍它们。但是现在只需理解迭代器的两个细节:迭代器生成一系列的值,可以在迭代器上调用 `collect` 方法将其转换为一个集合,比如包含所有迭代器产生元素的 vector。 为了确保 `minigrep` 能够获取传递给它的命令行参数的值,我们需要一个 Rust 标准库提供的函数 `std::env::args`。这个函数返回一个传递给程序的命令行参数的 **迭代器***iterator*)。我们会在 [第十三章][ch13] 全面的介绍它们。但是现在只需理解迭代器的两个细节:迭代器生成一系列的值,可以在迭代器上调用 `collect` 方法将其转换为一个集合,比如包含所有迭代器产生元素的 vector。
使用示例 12-1 中的代码来读取任何传递给 `minigrep` 的命令行参数并将其收集到一个 vector 中。 示例 12-1 中允许 `minigrep` 程序读取任何传递给它的命令行参数并将其收集到一个 vector 中。
<span class="filename">文件名src/main.rs</span> <span class="filename">文件名src/main.rs</span>
@ -42,7 +42,7 @@ $ cargo run searchstring example-filename.txt
`main` 函数的第一行,我们调用了 `env::args`,并立即使用 `collect` 来创建了一个包含迭代器所有值的 vector。`collect` 可以被用来创建很多类型的集合,所以这里显式注明 `args` 的类型来指定我们需要一个字符串 vector。虽然在 Rust 中我们很少会需要注明类型,然而 `collect` 是一个经常需要注明类型的函数,因为 Rust 不能推断出你想要什么类型的集合。 `main` 函数的第一行,我们调用了 `env::args`,并立即使用 `collect` 来创建了一个包含迭代器所有值的 vector。`collect` 可以被用来创建很多类型的集合,所以这里显式注明 `args` 的类型来指定我们需要一个字符串 vector。虽然在 Rust 中我们很少会需要注明类型,然而 `collect` 是一个经常需要注明类型的函数,因为 Rust 不能推断出你想要什么类型的集合。
最后,我们使用调试格式 `:?` 打印出 vector。让我们尝试分别用两种方式不包含参数和包含参数运行代码 最后,我们使用调试打印出 vector。让我们尝试分别用两种方式不包含参数和包含参数运行代码
```console ```console
{{#include ../listings/ch12-an-io-project/listing-12-01/output.txt}} {{#include ../listings/ch12-an-io-project/listing-12-01/output.txt}}
@ -56,7 +56,7 @@ $ cargo run searchstring example-filename.txt
### 将参数值保存进变量 ### 将参数值保存进变量
打印出参数 vector 中的值展示了程序可以访问指定为命令行参数的值。现在需要将这两个参数的值保存进变量这样就可以在程序的余下部分使用这些值了。让我们如示例 12-2 这样做: 目前程序可以访问指定为命令行参数的值。现在需要将这两个参数的值保存进变量这样就可以在程序的余下部分使用这些值了。让我们如示例 12-2 这样做:
<span class="filename">文件名src/main.rs</span> <span class="filename">文件名src/main.rs</span>
@ -64,9 +64,9 @@ $ cargo run searchstring example-filename.txt
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-02/src/main.rs}} {{#rustdoc_include ../listings/ch12-an-io-project/listing-12-02/src/main.rs}}
``` ```
<span class="caption">示例 12-2创建变量来存放查询参数和文件参数</span> <span class="caption">示例 12-2创建变量来存放查询参数和文件路径参数</span>
正如之前打印出 vector 时所所看到的,程序的名称占据了 vector 的第一个值 `args[0]`,所以我们从索引 `1` 开始。`minigrep` 获取的第一个参数是需要搜索的字符串,所以将其将第一个参数的引用存放在变量 `query` 中。第二个参数将是文件名,所以将第二个参数的引用放入变量 `filename` 中。 正如之前打印出 vector 时所所看到的,程序的名称占据了 vector 的第一个值 `args[0]`,所以我们从索引 `1` 的参数开始。`minigrep` 获取的第一个参数是需要搜索的字符串,所以将其将第一个参数的引用存放在变量 `query` 中。第二个参数将是文件路径,所以将第二个参数的引用放入变量 `file_path` 中。
我们将临时打印出这些变量的值来证明代码如我们期望的那样工作。使用参数 `test``sample.txt` 再次运行这个程序: 我们将临时打印出这些变量的值来证明代码如我们期望的那样工作。使用参数 `test``sample.txt` 再次运行这个程序:

@ -2,9 +2,9 @@
> [ch12-02-reading-a-file.md](https://github.com/rust-lang/book/blob/main/src/ch12-02-reading-a-file.md) > [ch12-02-reading-a-file.md](https://github.com/rust-lang/book/blob/main/src/ch12-02-reading-a-file.md)
> <br> > <br>
> commit 0f87daf683ae3de3cb725faecb11b7e7e89f0e5a > commit 02a168ed346042f07010f8b65b4eeed623dd31d1
现在我们要增加读取由 `filename` 命令行参数指定的文件的功能。首先,需要一个用来测试的示例文件:用来确保 `minigrep` 正常工作的最好的文件是拥有多行少量文本且有一些重复单词的文件。示例 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>
@ -26,7 +26,7 @@
首先,我们增加了一个 `use` 语句来引入标准库中的相关部分:我们需要 `std::fs` 来处理文件。 首先,我们增加了一个 `use` 语句来引入标准库中的相关部分:我们需要 `std::fs` 来处理文件。
`main` 中新增了一行语句:`fs::read_to_string` 接受 `filename`,打开文件,接着返回包含其内容的 `Result<String>`。 `main` 中新增了一行语句:`fs::read_to_string` 接受 `file_path`,打开文件,接着返回包含其内容的 `std::io::Result<String>`。
在这些代码之后,我们再次增加了临时的 `println!` 打印出读取文件之后 `contents` 的值,这样就可以检查目前为止的程序能否工作。 在这些代码之后,我们再次增加了临时的 `println!` 打印出读取文件之后 `contents` 的值,这样就可以检查目前为止的程序能否工作。
@ -36,4 +36,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` 函数有着多个职能,通常函数只负责一个功能的话会更简洁并易于维护。另一个问题是没有尽可能的处理错误。虽然我们的程序还很小,这些瑕疵并不是什么大问题,不过随着程序功能的丰富,将会越来越难以用简单的方法修复他们。在开发程序时,及早开始重构是一个最佳实践,因为重构少量代码时要容易的多,所以让我们现在就开始吧。

@ -2,15 +2,13 @@
> [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) > [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> > <br>
> commit c8a9ac9cee7923422b2eceebf0375363440dbfc1 > commit 83788ff212a3281328e2f8f223ce9e0f69220b97
为了改善我们的程序这里有四个问题需要修复,而且他们都与程序的组织方式和如何处理潜在错误有关。 为了改善我们的程序这里有四个问题需要修复,而且他们都与程序的组织方式和如何处理潜在错误有关。第一,`main` 现在进行了两个任务:它解析了参数并打开了文件。对于一个这样的小函数,这并不是一个大问题。然而如果 `main` 中的功能持续增加,`main` 函数处理的独立任务也会增加。当函数承担了更多责任,它就更难以推导,更难以测试,并且更难以在不破坏其他部分的情况下做出修改。最好能分离出功能以便每个函数就负责一个任务。
第一,`main` 现在进行了两个任务:它解析了参数并打开了文件。对于一个这样的小函数,这并不是一个大问题。然而如果 `main` 中的功能持续增加,`main` 函数处理的独立任务也会增加。当函数承担了更多责任,它就更难以推导,更难以测试,并且更难以在不破坏其他部分的情况下做出修改。最好能分离出功能以便每个函数就负责一个任务 这同时也关系到第二个问题:`query` 和 `file_path` 是程序中的配置变量,而像 `contents` 则用来执行程序逻辑。随着 `main` 函数的增长,就需要引入更多的变量到作用域中,而当作用域中有更多的变量时,将更难以追踪每个变量的目的。最好能将配置变量组织进一个结构,这样就能使他们的目的更明确了
这同时也关系到第二个问题:`query` 和 `filename` 是程序中的配置变量,而像 `contents` 则用来执行程序逻辑。随着 `main` 函数的增长,就需要引入更多的变量到作用域中,而当作用域中有更多的变量时,将更难以追踪每个变量的目的。最好能将配置变量组织进一个结构,这样就能使他们的目的更明确了。 第三个问题是如果打开文件失败我们使用 `expect` 来打印出错误信息,不过这个错误信息只是说 `Should have been able to read the file`。读取文件失败的原因有多种:例如文件不存在,或者没有打开此文件的权限。目前,无论处于何种情况,我们只是打印出“文件读取出现错误”的信息,这并没有给予使用者具体的信息!
第三个问题是如果打开文件失败我们使用 `expect` 来打印出错误信息,不过这个错误信息只是说 `Something went wrong reading the file`。读取文件失败的原因有多种:例如文件不存在,或者没有打开此文件的权限。目前,无论处于何种情况,我们只是打印出“文件读取出现错误”的信息,这并没有给予使用者具体的信息!
第四,我们不停地使用 `expect` 来处理不同的错误,如果用户没有指定足够的参数来运行程序,他们会从 Rust 得到 `index out of bounds` 错误,而这并不能明确地解释问题。如果所有的错误处理都位于一处,这样将来的维护者在需要修改错误处理逻辑时就只需要考虑这一处代码。将所有的错误处理都放在一处也有助于确保我们打印的错误信息对终端用户来说是有意义的。 第四,我们不停地使用 `expect` 来处理不同的错误,如果用户没有指定足够的参数来运行程序,他们会从 Rust 得到 `index out of bounds` 错误,而这并不能明确地解释问题。如果所有的错误处理都位于一处,这样将来的维护者在需要修改错误处理逻辑时就只需要考虑这一处代码。将所有的错误处理都放在一处也有助于确保我们打印的错误信息对终端用户来说是有意义的。
@ -18,7 +16,7 @@
### 二进制项目的关注分离 ### 二进制项目的关注分离
`main` 函数负责多个任务的组织问题在许多二进制项目中很常见。所以 Rust 社区开发出一类在 `main` 函数开始变得庞大时进行二进制程序的关注分离的指导性过程。这些过程有如下步骤: `main` 函数负责多个任务的组织问题在许多二进制项目中很常见。所以 Rust 社区开发出一类在 `main` 函数开始变得庞大时进行二进制程序的关注分离的指导。这些过程有如下步骤:
* 将程序拆分成 *main.rs**lib.rs* 并将程序的逻辑放入 *lib.rs* 中。 * 将程序拆分成 *main.rs**lib.rs* 并将程序的逻辑放入 *lib.rs* 中。
* 当命令行解析逻辑比较小时,可以保留在 *main.rs* 中。 * 当命令行解析逻辑比较小时,可以保留在 *main.rs* 中。
@ -67,22 +65,22 @@
<span class="caption">示例 12-6重构 `parse_config` 返回一个 `Config` 结构体实例</span> <span class="caption">示例 12-6重构 `parse_config` 返回一个 `Config` 结构体实例</span>
新定义的结构体 `Config` 中包含字段 `query``filename`。 新定义的结构体 `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` 实例可以拥有的数据的完整拷贝,不过会比储存字符串数据的引用消耗更多的时间和内存。不过拷贝数据使得代码显得更加直白因为无需管理引用的生命周期,所以在这种情况下牺牲一小部分性能来换取简洁性的取舍是值得的。
> #### 使用 `clone` 的权衡取舍 > #### 使用 `clone` 的权衡取舍
> >
> 由于其运行时消耗,许多 Rustacean 之间有一个趋势是倾向于避免使用 `clone` 来解决所有权问题。在关于迭代器的第十三章中,我们将会学习如何更有效率的处理这种情况,不过现在,复制一些字符串来取得进展是没有问题的,因为只会进行一次这样的拷贝,而且文件和要搜索的字符串都比较短。在第一轮编写时拥有一个可以工作但有点低效的程序要比尝试过度优化代码更好一些。随着你对 Rust 更加熟练,将能更轻松的直奔合适的方法,不过现在调用 `clone` 是完全可以接受的。 > 由于其运行时消耗,许多 Rustacean 之间有一个趋势是倾向于避免使用 `clone` 来解决所有权问题。在关于迭代器的第十三章中,我们将会学习如何更有效率的处理这种情况,不过现在,复制一些字符串来取得进展是没有问题的,因为只会进行一次这样的拷贝,而且文件路径和要搜索的字符串都比较短。在第一轮编写时拥有一个可以工作但有点低效的程序要比尝试过度优化代码更好一些。随着你对 Rust 更加熟练,将能更轻松的直奔合适的方法,不过现在调用 `clone` 是完全可以接受的。
我们更新 `main``parse_config` 返回的 `Config` 实例放入变量 `config` 中,并将之前分别使用 `query``filename` 变量的代码更新为现在的使用 `Config` 结构体的字段的代码。 我们更新 `main``parse_config` 返回的 `Config` 实例放入变量 `config` 中,并将之前分别使用 `query``file_path` 变量的代码更新为现在的使用 `Config` 结构体的字段的代码。
现在代码更明确的表现了我们的意图,`query` 和 `filename` 是相关联的并且他们的目的是配置程序如何工作。任何使用这些值的代码就知道在 `config` 实例中对应目的的字段名中寻找他们。 现在代码更明确的表现了我们的意图,`query` 和 `file_path` 是相关联的并且他们的目的是配置程序如何工作。任何使用这些值的代码就知道在 `config` 实例中对应目的的字段名中寻找他们。
### 创建一个 `Config` 的构造函数 ### 创建一个 `Config` 的构造函数
目前为止,我们将负责解析命令行参数的逻辑从 `main` 提取到了 `parse_config` 函数中,这有助于我们看清值 `query``filename` 是相互关联的并应该在代码中表现这种关系。接着我们增加了 `Config` 结构体来描述 `query``filename` 的相关性,并能够从 `parse_config` 函数中将这些值的名称作为结构体字段名称返回。 目前为止,我们将负责解析命令行参数的逻辑从 `main` 提取到了 `parse_config` 函数中,这有助于我们看清值 `query``file_path` 是相互关联的并应该在代码中表现这种关系。接着我们增加了 `Config` 结构体来描述 `query``file_path` 的相关性,并能够从 `parse_config` 函数中将这些值的名称作为结构体字段名称返回。
所以现在 `parse_config` 函数的目的是创建一个 `Config` 实例,我们可以将 `parse_config` 从一个普通函数变为一个叫做 `new` 的与结构体关联的函数。做出这个改变使得代码更符合习惯:可以像标准库中的 `String` 调用 `String::new` 来创建一个该类型的实例那样,将 `parse_config` 变为一个与 `Config` 关联的 `new` 函数。示例 12-7 展示了需要做出的修改: 所以现在 `parse_config` 函数的目的是创建一个 `Config` 实例,我们可以将 `parse_config` 从一个普通函数变为一个叫做 `new` 的与结构体关联的函数。做出这个改变使得代码更符合习惯:可以像标准库中的 `String` 调用 `String::new` 来创建一个该类型的实例那样,将 `parse_config` 变为一个与 `Config` 关联的 `new` 函数。示例 12-7 展示了需要做出的修改:
@ -108,7 +106,7 @@
#### 改善错误信息 #### 改善错误信息
在示例 12-8 中,在 `new` 函数中增加了一个检查在访问索引 `1``2` 之前检查 slice 是否足够长。如果 slice 不够长,我们使用一个更好的错误信息 panic 而不是 `index out of bounds` 信息 在示例 12-8 中,在 `new` 函数中增加了一个检查在访问索引 `1``2` 之前检查 slice 是否足够长。如果 slice 不够长,程序会打印一个更好的错误信息并 panic
<span class="filename">文件名src/main.rs</span> <span class="filename">文件名src/main.rs</span>
@ -249,7 +247,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`、其字段和其 `new` 方法,以及 `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 所示:

@ -2,18 +2,18 @@
> [ch12-04-testing-the-librarys-functionality.md](https://github.com/rust-lang/book/blob/main/src/ch12-04-testing-the-librarys-functionality.md) > [ch12-04-testing-the-librarys-functionality.md](https://github.com/rust-lang/book/blob/main/src/ch12-04-testing-the-librarys-functionality.md)
> <br> > <br>
> commit 04170d1feee2a47525b39f1edce77ba615ca9cdf > commit 8fd2327e4135876b368cc2793eb4a7e455b691f0
现在我们将逻辑提取到了 *src/lib.rs* 并将所有的参数解析和错误处理留在了 *src/main.rs* 中,为代码的核心功能编写测试将更加容易。我们可以直接使用多种参数调用函数并检查返回值而无需从命令行运行二进制文件了。 现在我们将逻辑提取到了 *src/lib.rs* 并将所有的参数解析和错误处理留在了 *src/main.rs* 中,为代码的核心功能编写测试将更加容易。我们可以直接使用多种参数调用函数并检查返回值而无需从命令行运行二进制文件了。
在这一部分我们将遵循测试驱动开发Test Driven Development, TDD的模式来逐步增加 `minigrep` 的搜索逻辑。这是一个软件开发技术,它遵循如下步骤: 在这一部分我们将遵循测试驱动开发Test Driven Development, TDD的模式来逐步增加 `minigrep` 的搜索逻辑。它遵循如下步骤:
1. 编写一个失败的测试,并运行它以确保它失败的原因是你所期望的。 1. 编写一个失败的测试,并运行它以确保它失败的原因是你所期望的。
2. 编写或修改足够的代码来使新的测试通过。 2. 编写或修改足够的代码来使新的测试通过。
3. 重构刚刚增加或修改的代码,并确保测试仍然能通过。 3. 重构刚刚增加或修改的代码,并确保测试仍然能通过。
4. 从步骤 1 开始重复! 4. 从步骤 1 开始重复!
这只是众多编写软件的方法之一,不过 TDD 有助于驱动代码的设计。在编写能使测试通过的代码之前编写测试有助于在开发过程中保持高测试覆盖率。 虽然这只是众多编写软件的方法之一,不过 TDD 有助于驱动代码的设计。在编写能使测试通过的代码之前编写测试有助于在开发过程中保持高测试覆盖率。
我们将测试驱动实现实际在文件内容中搜索查询字符串并返回匹配的行示例的功能。我们将在一个叫做 `search` 的函数中增加这些功能。 我们将测试驱动实现实际在文件内容中搜索查询字符串并返回匹配的行示例的功能。我们将在一个叫做 `search` 的函数中增加这些功能。
@ -31,7 +31,7 @@
这里选择使用 `"duct"` 作为这个测试中需要搜索的字符串。用来搜索的文本有三行,其中只有一行包含 `"duct"`。(注意双引号之后的反斜杠,这告诉 Rust 不要在字符串字面值内容的开头加入换行符)我们断言 `search` 函数的返回值只包含期望的那一行。 这里选择使用 `"duct"` 作为这个测试中需要搜索的字符串。用来搜索的文本有三行,其中只有一行包含 `"duct"`。(注意双引号之后的反斜杠,这告诉 Rust 不要在字符串字面值内容的开头加入换行符)我们断言 `search` 函数的返回值只包含期望的那一行。
我们还不能运行这个测试并看到它失败,因为它甚至都还不能编译:`search` 函数还不存在呢!我们将增加足够的代码来使其能够编译:一个总是会返回空 vector 的 `search` 函数定义,如示例 12-16 所示。然后这个测试应该能够编译并因为空 vector 并不匹配一个包含一行 `"safe, fast, productive."` 的 vector 而失败。 我们还不能运行这个测试并看到它失败,因为它甚至都还不能编译:`search` 函数还不存在呢!根据 TDD 的原则,我们将增加足够的代码来使其能够编译:一个总是会返回空 vector 的 `search` 函数定义,如示例 12-16 所示。然后这个测试应该能够编译并因为空 vector 并不匹配一个包含一行 `"safe, fast, productive."` 的 vector 而失败。
<span class="filename">文件名src/lib.rs</span> <span class="filename">文件名src/lib.rs</span>
@ -103,7 +103,7 @@ Rust 有一个有助于一行一行遍历字符串的方法,出于方便它被
#### 存储匹配的行 #### 存储匹配的行
我们还需要一个方法来存储包含查询字符串的行。为此可以在 `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>

@ -8,7 +8,7 @@
### 编写一个大小写不敏感 `search` 函数的失败测试 ### 编写一个大小写不敏感 `search` 函数的失败测试
我们希望增加一个新函数 `search_case_insensitive`,并将会在设置了环境变量时调用它。这里将继续遵循 TDD 过程,其第一步是再次编写一个失败测试。我们将为新的大小写不敏感搜索函数新增一个测试函数,并将老的测试函数从 `one_result` 改名为 `case_sensitive` 来更清楚的表明这两个测试的区别,如示例 12-20 所示: 首先我们希望增加一个新函数 `search_case_insensitive`,并将会在环境变量有值时调用它。这里将继续遵循 TDD 过程,其第一步是再次编写一个失败测试。我们将为新的大小写不敏感搜索函数新增一个测试函数,并将老的测试函数从 `one_result` 改名为 `case_sensitive` 来更清楚的表明这两个测试的区别,如示例 12-20 所示:
<span class="filename">文件名src/lib.rs</span> <span class="filename">文件名src/lib.rs</span>
@ -38,7 +38,7 @@
注意 `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。
接下来在检查每个 `line` 是否包含 `search` 之前增加了一个 `to_lowercase` 调用将他们都变为小写。现在我们将 `line``query` 都转换成了小写,这样就可以不管查询的大小写进行匹配了。 接下来我们对每一 `line` 都调用 `to_lowercase` 将其转为小写。现在我们将 `line``query` 都转换成了小写,这样就可以不管查询的大小写进行匹配了。
让我们看看这个实现能否通过测试: 让我们看看这个实现能否通过测试:
@ -54,7 +54,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}}
``` ```
这里增加了 `case_sensitive` 字符来存放一个布尔值。接着我们需要 `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>
@ -62,9 +62,9 @@
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-22/src/lib.rs:there}} {{#rustdoc_include ../listings/ch12-an-io-project/listing-12-22/src/lib.rs:there}}
``` ```
<span class="caption">示例 12-22根据 `config.case_sensitive` 的值调用 `search``search_case_insensitive`</span> <span class="caption">示例 12-22根据 `config.ignore_case` 的值调用 `search``search_case_insensitive`</span>
最后需要实际检查环境变量。处理环境变量的函数位于标准库的 `env` 模块中,所以我们需要在 *src/lib.rs* 的开头增加一个 `use std::env;`将这个模块引入作用域中。接着`Config::new`使用 `env` 模块的 `var` 方法来检查一个叫做 `IGNORE_CASE` 的环境变量,如示例 12-23 所示: 最后需要实际检查环境变量。处理环境变量的函数位于标准库的 `env` 模块中,所以我们需要在 *src/lib.rs* 的开头将这个模块引入作用域中。接着使用 `env` 模块的 `var` 方法来检查一个叫做 `CASE_INSENSITIVE` 的环境变量,如示例 12-23 所示:
<span class="filename">文件名src/lib.rs</span> <span class="filename">文件名src/lib.rs</span>
@ -74,11 +74,11 @@
<span class="caption">示例 12-23检查叫做 `IGNORE_CASE` 的环境变量</span> <span class="caption">示例 12-23检查叫做 `IGNORE_CASE` 的环境变量</span>
这里创建了一个新变量 `case_sensitive`。为了设置它的值,需要调用 `env::var` 函数并传递我们需要寻找的环境变量名称,`IGNORE_CASE`。`env::var` 返回一个 `Result`,它在环境变量被设置时返回包含其值的 `Ok` 成员,并在环境变量未被设置时返回 `Err` 成员。 这里创建了一个新变量 `ignore_case`。为了设置它的值,需要调用 `env::var` 函数并传递我们需要寻找的环境变量名称,`IGNORE_CASE`。`env::var` 返回一个 `Result`,它在环境变量被设置时返回包含其值的 `Ok` 成员,并在环境变量未被设置时返回 `Err` 成员。
我们使用 `Result``is_err` 方法来检查其是否是一个 error也就是环境变量未被设置的情况这也就意味着我们 **需要** 进行一个大小写敏感搜索。如果`IGNORE_CASE` 环境变量被设置为任何值,`is_err` 会返回 false 并将进行大小写不敏感搜索。我们并不关心环境变量所设置的 **值**,只关心它是否被设置了,所以检查 `is_err` 而不是 `unwrap`、`expect` 或任何我们已经见过的 `Result` 的方法。 我们使用 `Result``is_err` 方法来检查其是否是一个 error也就是环境变量未被设置的情况这也就意味着我们 **需要** 进行一个大小写敏感搜索。如果`IGNORE_CASE` 环境变量被设置为任何值,`is_ok` 会返回 false 并将进行大小写不敏感搜索。我们并不关心环境变量所设置的 **值**,只关心它是否被设置了,所以检查 `is_ok` 而不是 `unwrap`、`expect` 或任何我们已经见过的 `Result` 的方法。
我们将变量 `case_sensitive` 的值传递给 `Config` 实例,这样 `run` 函数可以读取其值并决定是否调用 `search` 或者示例 12-22 中实现的 `search_case_insensitive` 我们将变量 `ignore_case` 的值传递给 `Config` 实例,这样 `run` 函数可以读取其值并决定是否调用 `search` 或者示例 12-22 中实现的 `search_case_insensitive`
让我们试一试吧!首先不设置环境变量并使用查询 `to` 运行程序,这应该会匹配任何全小写的单词 “to” 的行: 让我们试一试吧!首先不设置环境变量并使用查询 `to` 运行程序,这应该会匹配任何全小写的单词 “to” 的行:
@ -88,6 +88,10 @@
看起来程序仍然能够工作!现在将 `IGNORE_CASE` 设置为 `1` 并仍使用相同的查询 `to` 看起来程序仍然能够工作!现在将 `IGNORE_CASE` 设置为 `1` 并仍使用相同的查询 `to`
```console
$ IGNORE_CASE=1 cargo run to poem.txt
```
如果你使用 PowerShell则需要用两个命令来分别设置环境变量并运行程序 如果你使用 PowerShell则需要用两个命令来分别设置环境变量并运行程序
```console ```console
@ -97,9 +101,6 @@ PS> $Env:IGNORE_CASE=1; cargo run to poem.txt
这回应该得到包含可能有大写字母的 “to” 的行: 这回应该得到包含可能有大写字母的 “to” 的行:
```console ```console
$ (IGNORE_CASE=1; cargo run to poem.txt)
Finished dev [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep to poem.txt`
Are you nobody, too? Are you nobody, too?
How dreary to be somebody! How dreary to be somebody!
To tell your name the livelong day To tell your name the livelong day
@ -108,6 +109,6 @@ To an admiring bog!
好极了,我们也得到了包含 “To” 的行!现在 `minigrep` 程序可以通过环境变量控制进行大小写不敏感搜索了。现在你知道了如何管理由命令行参数或环境变量设置的选项了! 好极了,我们也得到了包含 “To” 的行!现在 `minigrep` 程序可以通过环境变量控制进行大小写不敏感搜索了。现在你知道了如何管理由命令行参数或环境变量设置的选项了!
一些程序允许对相同配置同时使用参数 **和** 环境变量。在这种情况下,程序来决定参数和环境变量的优先级。作为一个留给你的测试,尝试通过一个命令行参数或一个环境变量来控制大小写敏感搜索。并在运行程序时遇到矛盾值时决定命令行参数和环境变量的优先级。 一些程序允许对相同配置同时使用参数 **和** 环境变量。在这种情况下,程序来决定参数和环境变量的优先级。作为一个留给你的测试,尝试通过一个命令行参数或一个环境变量来控制大小写敏感搜索。并在运行程序时遇到矛盾值时决定命令行参数和环境变量的优先级。
`std::env` 模块还包含了更多处理环境变量的实用功能;请查看官方文档来了解其可用的功能。 `std::env` 模块还包含了更多处理环境变量的实用功能;请查看官方文档来了解其可用的功能。

@ -2,7 +2,7 @@
> [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) > [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> > <br>
> commit c5c12e911b20fac20eefc511f6fe8d432a8e5ec2 > commit 02a168ed346042f07010f8b65b4eeed623dd31d1
目前为止,我们将所有的输出都通过 `println!` 写到了终端。大部分终端都提供了两种输出:**标准输出***standard output*`stdout`)对应一般信息,**标准错误***standard error*`stderr`)则用于错误信息。这种区别允许用户选择将程序正常输出定向到一个文件中并仍将错误信息打印到屏幕上。 目前为止,我们将所有的输出都通过 `println!` 写到了终端。大部分终端都提供了两种输出:**标准输出***standard output*`stdout`)对应一般信息,**标准错误***standard error*`stderr`)则用于错误信息。这种区别允许用户选择将程序正常输出定向到一个文件中并仍将错误信息打印到屏幕上。
@ -12,9 +12,9 @@
首先,让我们观察一下目前 `minigrep` 打印的所有内容是如何被写入标准输出的,包括那些应该被写入标准错误的错误信息。可以通过将标准输出流重定向到一个文件同时有意产生一个错误来做到这一点。我们没有重定向标准错误流,所以任何发送到标准错误的内容将会继续显示在屏幕上。 首先,让我们观察一下目前 `minigrep` 打印的所有内容是如何被写入标准输出的,包括那些应该被写入标准错误的错误信息。可以通过将标准输出流重定向到一个文件同时有意产生一个错误来做到这一点。我们没有重定向标准错误流,所以任何发送到标准错误的内容将会继续显示在屏幕上。
命令行程序被期望将错误信息发送到标准错误流,这样即便选择将标准输出流重定向到文件中时仍然能看到错误信息。目前我们的程序并不符合期望;相反我们将看到它将错误信息输出保存到了文件中 命令行程序被期望将错误信息发送到标准错误流,这样即便选择将标准输出流重定向到文件中时仍然能看到错误信息。目前我们的程序并不符合期望;相反我们将看到它将错误信息输出保存到了文件中
我们通过 `>` 和文件 *output.txt* 来运行程序,我们期望重定向标准输出流到该文件中。在这里,我们没有传递任何参数,所以会产生一个错误: 我们通过 `>` 和文件路径 *output.txt* 来运行程序,我们期望重定向标准输出流到该文件中。在这里,我们没有传递任何参数,所以会产生一个错误:
```console ```console
$ cargo run > output.txt $ cargo run > output.txt
@ -40,7 +40,7 @@ Problem parsing arguments: not enough arguments
<span class="caption">示例 12-24使用 `eprintln!` 将错误信息写入标准错误而不是标准输出</span> <span class="caption">示例 12-24使用 `eprintln!` 将错误信息写入标准错误而不是标准输出</span>
`println!` 改为 `eprintln!` 之后,让我们再次尝试用同样的方式运行程序,不使用任何参数并通过 `>` 重定向标准输出: 现在我们再次尝试用同样的方式运行程序,不使用任何参数并通过 `>` 重定向标准输出:
```console ```console
$ cargo run > output.txt $ cargo run > output.txt
@ -52,7 +52,7 @@ Problem parsing arguments: not enough arguments
如果使用不会造成错误的参数再次运行程序,不过仍然将标准输出重定向到一个文件,像这样: 如果使用不会造成错误的参数再次运行程序,不过仍然将标准输出重定向到一个文件,像这样:
```console ```console
$ cargo run to poem.txt > output.txt $ cargo run -- to poem.txt > output.txt
``` ```
我们并不会在终端看到任何输出,同时 `output.txt` 将会包含其结果: 我们并不会在终端看到任何输出,同时 `output.txt` 将会包含其结果:

Loading…
Cancel
Save