From 44af8683ff13217dc32aa9c6b1eceb9c2cbb9b2e Mon Sep 17 00:00:00 2001 From: KaiserY Date: Mon, 28 Aug 2017 16:20:19 +0800 Subject: [PATCH] check ch13-01 --- ...h12-01-accepting-command-line-arguments.md | 81 ++-- src/ch12-02-reading-a-file.md | 64 ++- ...improving-error-handling-and-modularity.md | 372 ++++++------------ ...2-04-testing-the-librarys-functionality.md | 165 +++----- ...2-05-working-with-environment-variables.md | 125 ++---- ...-06-writing-to-stderr-instead-of-stdout.md | 90 ++--- src/ch13-00-functional-features.md | 10 +- src/ch13-01-closures.md | 8 +- 8 files changed, 305 insertions(+), 610 deletions(-) diff --git a/src/ch12-01-accepting-command-line-arguments.md b/src/ch12-01-accepting-command-line-arguments.md index 6b651ae..4ead180 100644 --- a/src/ch12-01-accepting-command-line-arguments.md +++ b/src/ch12-01-accepting-command-line-arguments.md @@ -22,19 +22,11 @@ $ cargo run searchstring example-filename.txt ### 读取参数值 -为了能够获取传递给程序的命令行参数的值,我们需要调用一个 Rust 标准库提供的函数:`std::env::args`。这个函数返回一个传递给程序的命令行参数的**迭代器**(*iterator*)。我们还未讨论到迭代器,第十三章会全面的介绍他们。但是对于我们现在的目的来说只需要明白两点: +首先我们需要程序能够获取传递给它的命令行参数的值,为此需要一个 Rust 标准库提供的函数:`std::env::args`。这个函数返回一个传递给程序的命令行参数的 **迭代器**(*iterator*)。我们还未讨论到迭代器,第十三章会全面的介绍他们。但是对于我们现在的目的来说只需要明白两点:迭代器生成一系列的值,可以在迭代器上调用 `collect` 方法将其转换为一个 vector,比如包含所有迭代器产生元素的 vector。 -1. 迭代器生成一系列的值。 -2. 在迭代器上调用`collect`方法可以将其生成的元素转换为一个 vector。 +让我们尝试一下:使用列表 12-1 中的代码来读取任何传递给 `minigrep` 的命令行参数并将其收集到一个 vector 中。 -让我们尝试一下:使用列表 12-1 中的代码来读取任何传递给`greprs`的命令行参数并将其收集到一个 vector 中。 - - - - -Filename: src/main.rs +文件名: src/main.rs ```rust use std::env; @@ -45,61 +37,34 @@ fn main() { } ``` -Listing 12-1: Collect the command line arguments into a vector and print them -out - - - -首先使用`use`语句来将`std::env`模块引入作用域以便可以使用它的`args`函数。注意`std::env::args`函数嵌套进了两层模块中。如第七章讲到的,当所需函数嵌套了多于一层模块时,通常将父模块引入作用域,而不是其自身。这便于我们利用`std::env`中的其他函数。这比增加了`use std::env::args;`后仅仅使用`args`调用函数要更明确一些;这样看起来好像一个定义于当前模块的函数。 - - - - - -> 注意:`std::env::args`在其任何参数包含无效 Unicode 字符时会 panic。如果你需要接受包含无效 Unicode 字符的参数,使用`std::env::args_os`代替。这个函数返回`OsString`值而不是`String`值。出于简单考虑这里使用`std::env::args`,因为`OsString`值每个平台都不一样而且比`String`值处理起来更复杂。 +列表 12-1:将命令行参数收集到一个 vector 中并打印出来 - +首先使用 `use` 语句来将 `std::env` 模块引入作用域以便可以使用它的 `args` 函数。注意 `std::env::args` 函数被嵌套进了两层模块中。正如第七章讲到的,当所需函数嵌套了多于一层模块时,通常将父模块引入作用域,而不是其自身。这便于我们利用 `std::env` 中的其他函数。这比增加了 `use std::env::args;` 后仅仅使用 `args` 调用函数要更明确一些;这样容易被错认成一个定义于当前模块的函数。 - - +> ### `args` 函数和无效的 Unicode +> +> 注意 `std::env::args` 在其任何参数包含无效 Unicode 字符时会 panic。如果你需要接受包含无效 Unicode 字符的参数,使用 `std::env::args_os` 代替。这个函数返回 `OsString` 值而不是 `String` 值。这里出于简单考虑使用了 `std::env::args`,因为 `OsString` 值每个平台都不一样而且比 `String` 值处理起来更复杂。 -在`main`函数的第一行,我们调用了`env::args`,并立即使用`collect`来创建了一个包含迭代器所有值的 vector。`collect`可以被用来创建很多类型的集合,所以这里显式注明的`args`类型来指定我们需要一个字符串 vector。虽然在 Rust 中我们很少会需要注明类型,`collect`就是一个经常需要注明类型的函数,因为 Rust 不能推断出你想要什么类型的集合。 +在 `main` 函数的第一行,我们调用了 `env::args`,并立即使用 `collect` 来创建了一个包含迭代器所有值的 vector。`collect` 可以被用来创建很多类型的集合,所以这里显式注明的 `args` 类型来指定我们需要一个字符串 vector。虽然在 Rust 中我们很少会需要注明类型,`collect` 就是一个经常需要注明类型的函数,因为 Rust 不能推断出你想要什么类型的集合。 -最后,我们使用调试格式`:?`打印出 vector。让我们尝试不用参数运行代码,接着用两个参数: +最后,我们使用调试格式 `:?` 打印出 vector。让我们尝试不用参数运行代码,接着用两个参数: -``` +```text $ cargo run -["target/debug/greprs"] +["target/debug/minigrep"] $ cargo run needle haystack ...snip... -["target/debug/greprs", "needle", "haystack"] +["target/debug/minigrep", "needle", "haystack"] ``` - - - -你可能注意到了 vector 的第一个值是"target/debug/greprs",它是我们二进制文件的名称。其原因超出了本章介绍的范围,不过需要记住的是我们保存了所需的两个参数。 +你可能注意到了 vector 的第一个值是 `"target/debug/minigrep"`,它是我们二进制文件的名称。这与 C 中的参数列表的行为相符合,并使得程序可以在执行过程中使用它的名字。能够访问程序名称在需要在信息中打印时,或者需要根据执行程序所使用的命令行别名来改变程序行为时显得很方便,不过考虑到本章的目的,我们将忽略它并只保存所需的两个参数。 ### 将参数值保存进变量 -打印出参数 vector 中的值仅仅展示了可以访问程序中指定为命令行参数的值。但是这并不是我们想要做的,我们希望将这两个参数的值保存进变量这样就可以在程序使用这些值。让我们如列表 12-2 这样做: - - - +打印出参数 vector 中的值展示了程序可以访问指定为命令行参数的值。现在需要将这两个参数的值保存进变量这样就可以在程序的余下部分使用这些值。让我们如列表 12-2 这样做: -Filename: src/main.rs +文件名: src/main.rs ```rust,should_panic use std::env; @@ -115,18 +80,16 @@ fn main() { } ``` -Listing 12-2: Create variables to hold the query argument and filename argument +列表 12-2:创建变量来存放查询参数和文件名参数 - +正如之前打印出 vector 时所所看到的,程序的名称占据了 vector 的第一个值 `args[0]`,所以我们从索引 `1` 开始。`minigrep` 获取的第一个参数是需要搜索的字符串,所以将其将第一个参数的引用存放在变量 `query` 中。第二个参数将是文件名,所以将第二个参数的引用放入变量 `filename` 中。 -正如我们在打印出 vector 时所看到的,程序的名称占据了 vector 的第一个值`args[0]`,所以我们从索引`1`开始。第一个参数`greprs`是需要搜索的字符串,所以将其将第一个参数的引用存放在变量`query`中。第二个参数将是文件名,所以将第二个参数的引用放入变量`filename`中。 +我们将临时打印出出这些变量的值,再一次证明代码如我们期望的那样工作。让我们使用参数 `test` 和 `sample.txt` 再次运行这个程序: -我们将临时打印出出这些变量的值,再一次证明代码如我们期望的那样工作。让我们使用参数`test`和`sample.txt`再次运行这个程序: - -``` +```text $ cargo run test sample.txt - Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs - Running `target/debug/greprs test sample.txt` + Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs + Running `target/debug/minigrep test sample.txt` Searching for test In file sample.txt ``` diff --git a/src/ch12-02-reading-a-file.md b/src/ch12-02-reading-a-file.md index 2e10185..e7cc635 100644 --- a/src/ch12-02-reading-a-file.md +++ b/src/ch12-02-reading-a-file.md @@ -2,17 +2,17 @@ > [ch12-02-reading-a-file.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-02-reading-a-file.md) >
-> commit b8e4fcbf289b82c12121b282747ce05180afb1fb +> commit b693c8400817f1022820fd63e3529cbecc35070c -接下来我们将读取由命令行文件名参数指定的文件。首先,需要一个用来测试的示例文件——用来确保`greprs`正常工作的最好的文件是拥有少量文本和多个行且有一些重复单词的文件。列表 12-3 是一首艾米莉·狄金森(Emily Dickinson)的诗,它正适合这个工作!在项目根目录创建一个文件`poem.txt`,并输入诗 "I'm nobody! Who are you?": +接下来我们将读取由命令行文件名参数指定的文件。首先,需要一个用来测试的示例文件——用来确保 `minigrep` 正常工作的最好的文件是拥有少量文本和多个行且有一些重复单词的文件。列表 12-3 是一首艾米莉·狄金森(Emily Dickinson)的诗,它正适合这个工作!在项目根目录创建一个文件 `poem.txt`,并输入诗 "I'm nobody! Who are you?": -Filename: poem.txt +文件名: poem.txt ```text -I'm nobody! Who are you? +I’m nobody! Who are you? Are you nobody, too? -Then there's a pair of us — don't tell! -They'd banish us, you know. +Then there’s a pair of us — don’t tell! +They’d banish us, you know. How dreary to be somebody! How public, like a frog @@ -20,19 +20,11 @@ To tell your name the livelong day To an admiring bog! ``` -Listing 12-3: The poem "I'm nobody! Who are you?" by -Emily Dickinson that will make a good test case - - - - +列表 12-3:艾米莉·狄金森的诗 “I’m nobody! Who are you?”,一个好的测试用例 创建完这个文件之后,修改 *src/main.rs* 并增加如列表 12-4 所示的打开文件的代码: -Filename: src/main.rs +文件名: src/main.rs ```rust,should_panic use std::env; @@ -40,46 +32,46 @@ use std::fs::File; use std::io::prelude::*; fn main() { - let args: Vec = env::args().collect(); - - let query = &args[1]; - let filename = &args[2]; - - println!("Searching for {}", query); +# let args: Vec = env::args().collect(); +# +# let query = &args[1]; +# let filename = &args[2]; +# +# println!("Searching for {}", query); + // ...snip... println!("In file {}", filename); let mut f = File::open(filename).expect("file not found"); let mut contents = String::new(); - f.read_to_string(&mut contents).expect("something went wrong reading the file"); + f.read_to_string(&mut contents) + .expect("something went wrong reading the file"); println!("With text:\n{}", contents); } ``` -Listing 12-4: Reading the contents of the file specified by the second argument +列表 12-4:读取第二个参数所指定的文件内容 - +首先,我们增加了更多的 `use` 语句来引入标准库中的相关部分:需要 `std::fs::File` 来处理文件,而 `std::io::prelude::*` 则包含许多对于 I/O 包括文件 I/O 有帮助的 trait。类似于 Rust 有一个通用的 prelude 来自动引入特定内容,`std::io` 也有其自己的 prelude 来引入处理 I/O 时所需的通用内容。不同于默认的 prelude,必须显式 `use` 位于 `std::io` 中的 prelude。 -首先,增加了更多的`use`语句来引入标准库中的相关部分:需要`std::fs::File`来处理文件,而`std::io::prelude::*`则包含许多对于 I/O 包括文件 I/O 有帮助的 trait。类似于 Rust 有一个通用的 prelude 来自动引入特定内容,`std::io`也有其自己的 prelude 来引入处理 I/O 时所需的通用内容。不同于默认的 prelude,必须显式`use`位于`std::io`中的 prelude。 +在 `main` 中,我们增加了三点内容:第一,通过传递变量 `filename` 的值调用 `File::open` 函数来获取文件的可变句柄。创建了叫做 `contents` 的变量并将其设置为一个可变的,空的`String`。它将会存放之后读取的文件的内容。第三,对文件句柄调用 `read_to_string` 并传递 `contents` 的可变引用作为参数。 -在`main`中,我们增加了三点内容:第一,通过传递变量`filename`的值调用`File::open`函数的值来获取文件的可变句柄。创建了叫做`contents`的变量并将其设置为一个可变的,空的`String`。它将会存放之后读取的文件的内容。第三,对文件句柄调用`read_to_string`并传递`contents`的可变引用作为参数。 - -在这些代码之后,我们再次增加了临时的`println!`打印出读取文件后`contents`的值,这样就可以检查目前为止的程序能否工作。 +在这些代码之后,我们再次增加了临时的 `println!` 打印出读取文件后 `contents` 的值,这样就可以检查目前为止的程序能否工作。 尝试运行这些代码,随意指定一个字符串作为第一个命令行参数(因为还未实现搜索功能的部分)而将 *poem.txt* 文件将作为第二个参数: -``` +```text $ cargo run the poem.txt - Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs - Running `target/debug/greprs the poem.txt` + Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs + Running `target/debug/minigrep the poem.txt` Searching for the In file poem.txt With text: -I'm nobody! Who are you? +I’m nobody! Who are you? Are you nobody, too? -Then there's a pair of us — don't tell! -They'd banish us, you know. +Then there’s a pair of us — don’t tell! +They’d banish us, you know. How dreary to be somebody! How public, like a frog @@ -87,4 +79,4 @@ To tell your name the livelong day To an admiring bog! ``` -好的!代码读取并打印出了文件的内容。虽然它还有一些瑕疵:`main`函数有着多个功能,同时也没有处理可能出现的错误。虽然我们的程序还很小,这些瑕疵并不是什么大问题。不过随着程序功能的丰富,将会越来越难以用简单的方法修复他们。在开发程序时,及早开始重构是一个最佳实践,因为重构少量代码时要容易的多,所以让我们现在就开始吧。 +好的!代码读取并打印出了文件的内容。虽然它还有一些瑕疵:`main` 函数有着多个职能,通常函数只负责一个功能的话会更简洁并易于维护。另一个问题是没有尽可能的处理错误。虽然我们的程序还很小,这些瑕疵并不是什么大问题,不过随着程序功能的丰富,将会越来越难以用简单的方法修复他们。在开发程序时,及早开始重构是一个最佳实践,因为重构少量代码时要容易的多,所以让我们现在就开始吧。 diff --git a/src/ch12-03-improving-error-handling-and-modularity.md b/src/ch12-03-improving-error-handling-and-modularity.md index 023eee6..5914a22 100644 --- a/src/ch12-03-improving-error-handling-and-modularity.md +++ b/src/ch12-03-improving-error-handling-and-modularity.md @@ -2,45 +2,40 @@ > [ch12-03-improving-error-handling-and-modularity.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-03-improving-error-handling-and-modularity.md) >
-> commit b8e4fcbf289b82c12121b282747ce05180afb1fb +> commit 5908c59a5a4cc58fd863605b80b295a335c2cbdf 为了改善我们的程序这里有四个问题需要修复,而且他们都与程序的组织方式和如何处理潜在错误有关。 -第一,`main`现在进行了两个任务:它解析了参数并打开了文件。对于一个这样的小函数,这并不是一个大问题。然而如果`main`中的功能持续增加,`main`函数处理的单独的任务也会增加。当函数承担了更多责任,它就更难以推导,更难以测试,并且更难以在不破坏其他部分的情况下做出修改。最好能分离出功能这样每个函数就负责一个任务。 +第一,`main` 现在进行了两个任务:它解析了参数并打开了文件。对于一个这样的小函数,这并不是一个大问题。然而如果 `main` 中的功能持续增加,`main` 函数处理的独立的任务也会增加。当函数承担了更多责任,它就更难以推导,更难以测试,并且更难以在不破坏其他部分的情况下做出修改。最好能分离出功能以便每个函数就负责一个任务。 -这同时也关系到第二个问题:`search`和`filename`是程序中的配置变量,而像`f`和`contents`则用来执行程序逻辑。随着`main`函数的增长,就需要引入更多的变量到作用域中,而当作用域中有更多的变量时,将更难以追踪每个变量的目的。最好能将将配置变量组织进一个结构这样就能使他们的目的更明确了。 +这同时也关系到第二个问题:`search` 和 `filename` 是程序中的配置变量,而像 `f` 和 `contents` 则用来执行程序逻辑。随着 `main` 函数的增长,就需要引入更多的变量到作用域中,而当作用域中有更多的变量时,将更难以追踪每个变量的目的。最好能将将配置变量组织进一个结构这样就能使他们的目的更明确了。 -第三个问题是如果打开文件失败我们使用`expect`来打印出错误信息,不过这个错误信息只是说`file not found`。除了缺少文件之外还有很多打开文件可能失败的方式:例如,文件可能存在,不过可能没有打开它的权限。如果我们现在就出于这种情况,打印出的`file not found`错误信息就给了用户一个不符合事实的建议! +第三个问题是如果打开文件失败我们使用 `expect` 来打印出错误信息,不过这个错误信息只是说 `file not found`。除了缺少文件之外还有很多打开文件可能失败的方式:例如,文件可能存在,不过可能没有打开它的权限。如果我们现在就出于这种情况,打印出的 `file not found` 错误信息就给了用户错误的建议! -第四,我们不停的使用`expect`来处理不同的错误,如果用户没有指定足够的参数来运行程序,他们会从 Rust 得到 "index out of bounds" 错误而这并不能明确的解释问题。如果所有的错误处理都位于一处这样将来的维护者在需要修改错误处理逻辑时就只需要咨询一处代码。将所有的错误处理都放在一处也有助于确保我们打印的错误信息对终端用户来说是有意义的。 +第四,我们不停的使用 `expect` 来处理不同的错误,如果用户没有指定足够的参数来运行程序,他们会从 Rust 得到 "index out of bounds" 错误,而这并不能明确的解释问题。如果所有的错误处理都位于一处这样将来的维护者在需要修改错误处理逻辑时就只需要考虑这一处代码。将所有的错误处理都放在一处也有助于确保我们打印的错误信息对终端用户来说是有意义的。 让我们通过重构项目来解决这些问题。 ### 二进制项目的关注分离 -`main`函数负责多个任务的组织问题在许多二进制项目中很常见。所以 Rust 社区开发出一个类在`main`函数开始变得庞大时进行二进制程序的关注分离的指导性过程。这些过程有如下步骤: +`main` 函数负责多个任务的组织问题在许多二进制项目中很常见。所以 Rust 社区开发出一类在 `main` 函数开始变得庞大时进行二进制程序的关注分离的指导性过程。这些过程有如下步骤: 1. 将程序拆分成 *main.rs* 和 *lib.rs* 并将程序的逻辑放入 *lib.rs* 中。 2. 当命令行解析逻辑比较小时,可以保留在 *main.rs* 中。 3. 当命令行解析开始变得复杂时,也同样将其从 *main.rs* 提取到 *lib.rs*中。 -4. 经过这些过程之后保留在`main`函数中的责任是: +4. 经过这些过程之后保留在 `main` 函数中的责任应该被限制为: * 使用参数值调用命令行解析逻辑 * 设置任何其他的配置 - * 调用 *lib.rs* 中的`run`函数 - * 如果`run`返回错误,则处理这个错误 + * 调用 *lib.rs* 中的 `run` 函数 + * 如果 `run` 返回错误,则处理这个错误 -这个模式的一切就是为了关注分离:*main.rs* 处理程序运行,而 *lib.rs* 处理所有的真正的任务逻辑。因为不能直接测试`main`函数,这个结构通过将所有的程序逻辑移动到 *lib.rs* 的函数中使得我们可以测试他们。仅仅保留在 *main.rs* 中的代码将足够小以便阅读就可以验证其正确性。 - - - - +这个模式的一切就是为了关注分离:*main.rs* 处理程序运行,而 *lib.rs* 处理所有的真正的任务逻辑。因为不能直接测试 `main` 函数,这个结构通过将所有的程序逻辑移动到 *lib.rs* 的函数中使得我们可以测试他们。仅仅保留在 *main.rs* 中的代码将足够小以便阅读就可以验证其正确性。 ### 提取参数解析器 -首先,我们将提取解析参数的功能。列表 12-5 中展示了新`main`函数的开头,它调用了新函数`parse_config`。目前它仍将定义在 *src/main.rs* 中: +首先,我们将解析参数的功能提取到一个 `main` 将会调用的函数中,为将命令行解析逻辑移动到 *src/lib.rs* 做准备。列表 12-5 中展示了新 `main` 函数的开头,它调用了新函数 `parse_config`。目前它仍将定义在 *src/main.rs* 中: -Filename: src/main.rs +文件名: src/main.rs ```rust,ignore fn main() { @@ -59,12 +54,9 @@ fn parse_config(args: &[String]) -> (&str, &str) { } ``` -Listing 12-5: Extract a `parse_config` function from -`main` +列表 12-5:从 `main` 中提取出 `parse_config` 函数 - - -我们仍然将命令行参数收集进一个 vector,不过不同于在`main`函数中将索引 1 的参数值赋值给变量`query`和将索引 2 的值赋值给变量`filename`,我们将整个 vector 传递给`parse_config`函数。接着`parse_config`函数将包含知道哪个参数该放入哪个变量的逻辑,并将这些值返回到`main`。仍然在`main`中创建变量`query`和`filename`,不过`main`不再负责处理命令行参数与变量如何对应。 +我们仍然将命令行参数收集进一个 vector,不过不同于在`main`函数中将索引 1 的参数值赋值给变量 `query` 和将索引 2 的值赋值给变量 `filename`,我们将整个 vector 传递给 `parse_config` 函数。接着 `parse_config` 函数将包含决定哪个参数该放入哪个变量的逻辑,并将这些值返回到 `main`。仍然在 `main` 中创建变量 `query` 和 `filename`,不过 `main` 不再负责处理命令行参数与变量如何对应。 这对我们这小程序可能有点大材小用,不过我们将采用小的、增量的步骤进行重构。在做出这些改变之后,再次运行程序并验证参数解析是否仍然正常。经常验证你的进展是一个好习惯,这样在遇到问题时能帮助你定位问题的成因。 @@ -72,26 +64,13 @@ fn parse_config(args: &[String]) -> (&str, &str) { 我们可以采取另一个小的步骤来进一步改善这个函数。现在函数返回一个元组,不过立刻又就将元组拆成了独立的部分。这是一个我们可能没有进行正确抽象的信号。 -另一个表明还有改进空间的迹象是`parse_config`的`config`部分,它暗示了我们返回的两个值是相关的并都是一个配置值的一部分。目前除了将这两个值组合进元组之外并没有表达这个数据结构的意义:我们可以将这两个值放入一个结构体并给每个字段一个有意义的名字。这会让未来的维护者更容易理解不同的值如何相互关联以及他们的目的。 - - +另一个表明还有改进空间的迹象是 `parse_config` 名称的 `config` 部分,它暗示了我们返回的两个值是相关的并都是一个配置值的一部分。目前除了将这两个值组合进元组之外并没有表达这个数据结构的意义:我们可以将这两个值放入一个结构体并给每个字段一个有意义的名字。这会让未来的维护者更容易理解不同的值如何相互关联以及他们的目的。 +> 注意:一些同学将这种拒绝使用相对而言更为合适的复合类型而使用基本类型的模式称为 **基本类型偏执**(*primitive obsession*)。 +列表 12-6 展示了新定义的结构体 `Config`,它有字段 `query` 和 `filename`。我们也改变了 `parse_config` 函数来返回一个 `Config` 结构体的实例,并更新 `main` 来使用结构体字段而不是单独的变量: -> 注意:一些同学将这种拒绝使用相对而言更为合适的复合类型而使用基本类型的模式称为**基本类型偏执**(*primitive obsession*)。 - - - - -列表 12-6 展示了新定义的结构体`Config`,它有字段`query`和`filename`。我们也改变了`parse_config`函数来返回一个`Config`结构体的实例,并更新`main`来使用结构体字段而不是单独的变量: - -Filename: src/main.rs +文件名: src/main.rs ```rust,should_panic # use std::env; @@ -119,54 +98,32 @@ fn parse_config(args: &[String]) -> Config { let query = args[1].clone(); let filename = args[2].clone(); - Config { - query: query, - filename: filename, - } + Config { query, filename } } ``` -Listing 12-6: Refactoring `parse_config` to return an instance of a `Config` -struct +列表 12-6:重构 `parse_config` 返回一个 `Config` 结构体实例 - +`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` 中,并将之前分别使用 `search` 和 `filename` 变量的代码更新为现在的使用 `Config` 结构体的字段的代码。 -我们更新`main`将`parse_config`返回的`Config`实例放入变量`config`中,并更新之前分别使用`search`和`filename`变量的代码为现在的使用`Config`结构体的字段。 +现在代码更明确的表现了我们的意图,`query` 和 `filename` 是相关联的并且他们的目的是配置程序如何工作的。任何使用这些值的代码就知道在 `config` 实例中对应目的的字段名中寻找他们。 -现在代码更明确的表现了我们的意图,`query`和`filename`是相关联的并且他们的目的是配置程序如何工作的。任何使用这些值的代码就知道在`config`实例中对应目的的字段名中寻找他们。 +### 创建一个 `Config` 构造函数 -### 创建一个`Config`构造函数 +目前为止,我们将负责解析命令行参数的逻辑从 `main` 提取到了 `parse_config` 函数中,这有助于我们看清值 `query` 和 `filename` 是相互关联的并应该在代码中表现这种关系。接着我们增加了 `Config` 结构体来描述 `query` 和 `filename` 的相关性,并能够从 `parse_config` 函数中将这些值的名称作为结构体字段名称返回。 - - +所以现在 `parse_config` 函数的目的是创建一个 `Config` 实例,我们可以将 `parse_config` 从一个普通函数变为一个叫做 `new` 的与结构体关联的函数。做出这个改变使得代码更符合习惯:可以像标准库中的 `String` 调用 `String::new` 来创建一个该类型的实例那样,将 `parse_config` 变为一个与 `Config` 关联的 `new` 函数。列表 12-7 展示了需要做出的修改: -目前为止,我们将负责解析命令行参数的逻辑从`main`提取到了`parse_config`函数中,这帮助我们看清值`query`和`filename`是相互关联的并应该在代码中表现这种关系。接着我们增加了`Config`结构体来命名`query`和`filename`的相关目的,并能够从`parse_config`函数中将这些值的名称作为结构体字段名称返回。 -所以现在`parse_config`函数的目的是创建一个`Config`实例,我们可以将`parse_config`从一个普通函数变为一个叫做`new`的与结构体关联的函数。做出这个改变使得代码更符合习惯:可以像标准库中的`String`调用`String::new`来创建一个该类型的实例那样,将`parse_config`变为一个与`Config`关联的`new`函数。列表 12-7 展示了需要做出的修改: - -Filename: src/main.rs +文件名: src/main.rs ```rust,should_panic # use std::env; @@ -191,41 +148,35 @@ impl Config { let query = args[1].clone(); let filename = args[2].clone(); - Config { - query: query, - filename: filename, - } + Config { query, filename } } } ``` -Listing 12-7: Changing `parse_config` into -`Config::new` +列表 12-7:将 `parse_config` 变为 `Config::new` - - -这里将`main`中调用`parse_config`的地方更新为调用`Config::new`。我们将`parse_config`的名字改为`new`并将其移动到`impl`块中,这使得`new`函数与`Config`相关联。再次尝试编译并确保它可以工作。 +这里将 `main` 中调用 `parse_config` 的地方更新为调用 `Config::new`。我们将 `parse_config` 的名字改为 `new` 并将其移动到 `impl` 块中,这使得 `new` 函数与 `Config` 相关联。再次尝试编译并确保它可以工作。 ### 修复错误处理 -现在我们开始修复错误处理。回忆一下之前提到过如果`args` vector 包含少于 3 个项并尝试访问 vector 中索引 1 或 索引 2 的值会造成程序 panic。尝试不带任何参数运行程序;这将看起来像这样: +现在我们开始修复错误处理。回忆一下之前提到过如果 `args` vector 包含少于 3 个项并尝试访问 vector 中索引 1 或 索引 2 的值会造成程序 panic。尝试不带任何参数运行程序;这将看起来像这样: -``` +```text $ cargo run - Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs - Running `target/debug/greprs` + Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs + Running `target/debug/minigrep` thread 'main' panicked at 'index out of bounds: the len is 1 -but the index is 1', /stable-dist-rustc/build/src/libcollections/vec.rs:1307 +but the index is 1', /stable-dist-rustc/build/src/libcollections/vec.rs:1307 note: Run with `RUST_BACKTRACE=1` for a backtrace. ``` -`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` 是一个针对程序员的错误信息,然而这并不能真正帮助终端用户理解发生了什么和他们应该做什么。现在就让我们修复它吧。 ### 改善错误信息 -在列表 12-8 中,在`new`函数中增加了一个检查在访问索引 1 和 2 之前检查 slice 是否足够长。如果 slice 不够长,我们使用一个更好的错误信息 panic 而不是`index out of bounds`信息: +在列表 12-8 中,在 `new` 函数中增加了一个检查在访问索引 1 和 2 之前检查 slice 是否足够长。如果 slice 不够长,我们使用一个更好的错误信息 panic 而不是 `index out of bounds` 信息: -Filename: src/main.rs +文件名: src/main.rs ```rust,ignore // ...snip... @@ -236,37 +187,30 @@ fn new(args: &[String]) -> Config { // ...snip... ``` -Listing 12-8: Adding a check for the number of -arguments - - +列表 12-8:增加一个参数数量检查 -这类似于列表 9-8 中的`Guess::new`函数,那里如果`value`参数超出了有效值的范围就调用`panic!`。不同于检查值的范围,这里检查`args`的长度至少是 3,而函数的剩余部分则可以假设这个条件成立的基础上运行。如果 -`args`少于 3 个项,这个条件将为真,并调用`panic!`立即终止程序。 +这类似于列表 9-8 中的 `Guess::new` 函数,那里如果 `value` 参数超出了有效值的范围就调用 `panic!`。不同于检查值的范围,这里检查 `args` 的长度至少是 3,而函数的剩余部分则可以在假设这个条件成立的基础上运行。如果 +`args` 少于 3 个项,则这个条件将为真,并调用 `panic!` 立即终止程序。 -有了`new`中这几行额外的代码,再次不带任何参数运行程序并看看现在错误看起来像什么: +有了 `new` 中这几行额外的代码,再次不带任何参数运行程序并看看现在错误看起来像什么: -``` +```bash $ cargo run - Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs - Running `target/debug/greprs` + Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs + Running `target/debug/minigrep` thread 'main' panicked at 'not enough arguments', src/main.rs:29 note: Run with `RUST_BACKTRACE=1` for a backtrace. ``` -这个输出就好多了,现在有了一个合理的错误信息。然而,我们还有一堆额外的信息不希望提供给用户。所以在这里使用列表 9-8 中的技术可能不是最好的;无论如何`panic!`调用更适合程序问题而不是使用问题,正如第九章所讲到的。相反我们可以使用那一章学习的另一个技术:返回一个可以表明成功或错误的`Result`。 - - - +这个输出就好多了,现在有了一个合理的错误信息。然而,我们还有一堆额外的信息不希望提供给用户。所以在这里使用列表 9-8 中的技术可能不是最好的;无论如何 `panic!` 调用更适合程序上的问题而不是使用上的问题,正如第九章所讲到的。相反我们可以使用那一章学习的另一个技术:返回一个可以表明成功或错误的 `Result`。 -#### 从`new`中返回`Result`而不是调用`panic!` +#### 从 `new` 中返回 `Result` 而不是调用 `panic!` -我们可以选择返回一个`Result`值,它在成功时会包含一个`Config`的实例,而在错误时会描述问题。当`Config::new`与`main`交流时,在使用`Result`类型存在问题时可以使用 Rust 的信号方式。接着修改`main`将`Err`成员转换为对用户更友好的错误,而不是`panic!`调用产生的关于`thread 'main'`和`RUST_BACKTRACE`的文本。 +我们可以选择返回一个 `Result` 值,它在成功时会包含一个 `Config` 的实例,而在错误时会描述问题。当 `Config::new` 与 `main` 交流时,可以使用 `Result` 类型来表明这里存在问题。接着修改 `main` 将 `Err` 成员转换为对用户更友好的错误,而不是 `panic!` 调用产生的关于 `thread 'main'` 和 `RUST_BACKTRACE` 的文本。 -列表 12-9 展示了`Config::new`返回值和函数体中返回`Result`所需的改变: +列表 12-9 展示了为了返回 `Result` 在 `Config::new` 的返回值和函数体中所需的改变: -Filename: src/main.rs +文件名: src/main.rs ```rust,ignore impl Config { @@ -278,33 +222,24 @@ impl Config { let query = args[1].clone(); let filename = args[2].clone(); - Ok(Config { - query: query, - filename: filename, - }) + Ok(Config { query, filename }) } } ``` -Listing 12-9: Return a `Result` from `Config::new` - - - - - +列表 12-9:从 `Config::new` 中返回 `Result` -现在`new`函数返回一个`Result`,在成功时带有一个`Config`实例而在出现错误时带有一个`&'static str`。回忆一下第十章“静态声明周期”中讲到`&'static str`是一个字符串字面值,也是目前的错误信息。 +现在 `new` 函数返回一个 `Result`,在成功时带有一个 `Config` 实例而在出现错误时带有一个 `&'static str`。回忆一下第十章 “静态生命周期” 中讲到 `&'static str` 是一个字符串字面值,也是目前的错误信息。 -`new`函数体中有两处修改:当没有足够参数时不再调用`panic!`,而是返回`Err`值。同时我们将`Config`返回值包装进`Ok`成员中。这些修改使得函数符合其新的类型签名。 +`new` 函数体中有两处修改:当没有足够参数时不再调用 `panic!`,而是返回 `Err` 值。同时我们将 `Config` 返回值包装进 `Ok` 成员中。这些修改使得函数符合其新的类型签名。 -通过让`Config::new`返回一个`Err`值,这就允许`main`函数处理`new`函数返回的`Result`值并在出现错误的情况更明确的结束进程。 +通过让 `Config::new` 返回一个 `Err` 值,这就允许 `main` 函数处理 `new` 函数返回的 `Result` 值并在出现错误的情况更明确的结束进程。 -### `Config::new`调用并处理错误 +### `Config::new` 调用并处理错误 -为了处理错误情况并打印一个对用户友好的信息,我们需要像列表 12-10 那样更新`main`函数来处理现在`Config::new`返回的`Result`。另外还需要实现一些`panic!`替我们处理的问题:使用错误码 1 退出命令行工具。非零的退出状态是一个告诉调用程序的进程我们的程序以错误状态退出的惯例信号。 +为了处理错误情况并打印一个对用户友好的信息,我们需要像列表 12-10 那样更新 `main` 函数来处理现在 `Config::new` 返回的 `Result`。另外还需要负责手动实现 `panic!` 的使用非零错误码退出命令行工具的工作。非零的退出状态是一个告诉调用程序的进程我们的程序以错误状态退出的惯例信号。 -Filename: src/main.rs +文件名: src/main.rs ```rust,ignore use std::process; @@ -320,53 +255,29 @@ fn main() { // ...snip... ``` -Listing 12-10: Exiting with an error code if creating a -new `Config` fails - - - - - - +列表 12-10:如果新建 `Config` 失败则使用错误码退出 -在上面的列表中,使用了一个之前没有涉及到的方法:`unwrap_or_else`,它定义于标准库的`Result`上。使用`unwrap_or_else`可以进行一些自定义的非`panic!`的错误处理。当`Result`是`Ok`时,这个方法的行为类似于`unwrap`:它返回`Ok`内部封装的值。然而,当`Result`是`Err`时,它调用一个**闭包**(*closure*),也就是一个我们定义的作为参数传递给`unwrap_or_else`的匿名函数。第十三章会更详细的介绍闭包。现在你需要理解的是`unwrap_or_else`会将`Err`的内部值,也就是列表 12-9 中增加的`not enough arguments`静态字符串的情况,传递给闭包中位于两道竖线间的参数`err`。闭包中的代码在其运行时可以使用这个`err`值。 +在上面的列表中,使用了一个之前没有涉及到的方法:`unwrap_or_else`,它定义于标准库的 `Result` 上。使用 `unwrap_or_else` 可以进行一些自定义的非 `panic!` 的错误处理。当 `Result` 是 `Ok` 时,这个方法的行为类似于 `unwrap`:它返回 `Ok` 内部封装的值。然而,当其值是 `Err` 时,该方法会调用一个 **闭包**(*closure*),也就是一个我们定义的作为参数传递给 `unwrap_or_else` 的匿名函数。第十三章会更详细的介绍闭包。现在你需要理解的是 `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`(在开头增加了新的`use`行从标准库中导入了`process`)。`process::exit`会立即停止程序并将传递给它的数字作为返回状态码。这类似于列表 12-8 中使用的基于`panic!`的错误处理,除了不会在得到所有的额外输出了。让我们试试: - -``` +```text $ cargo run - Compiling greprs v0.1.0 (file:///projects/greprs) - Finished debug [unoptimized + debuginfo] target(s) in 0.48 secs - Running `target/debug/greprs` + Compiling minigrep v0.1.0 (file:///projects/minigrep) + Finished dev [unoptimized + debuginfo] target(s) in 0.48 secs + Running `target/debug/minigrep` Problem parsing arguments: not enough arguments ``` 非常好!现在输出对于用户来说就友好多了。 -### 提取`run`函数 +### 从 `main` 提取逻辑 -现在我们完成了配置解析的重构:让我们转向程序的逻辑。正如“二进制项目的关注分离”部分的讨论所留下的过程,我们将提取一个叫做`run`的函数来存放目前`main`函数中不属于设置配置或处理错误的所有逻辑。一旦完成这些,`main`函数将简明的足以通过观察来验证,而我们将能够为所有其他逻辑编写测试。 +现在我们完成了配置解析的重构:让我们转向程序的逻辑。正如 “二进制项目的关注分离” 部分所展开的讨论,我们将提取一个叫做 `run` 的函数来存放目前 `main `函数中不属于设置配置或处理错误的所有逻辑。一旦完成这些,`main` 函数将简明的足以通过观察来验证,而我们将能够为所有其他逻辑编写测试。 - - +列表 12-11 展示了提取出来的 `run` 函数。目前我们只进行小的增量式的提取函数的改进。我们仍将在 *src/main.rs* 中定义这个函数: -列表 12-11 展示了提取出来的`run`函数。目前我们只进行小的增量式的提取函数的改进并仍将在 *src/main.rs* 中定义这个函数: - -Filename: src/main.rs +文件名: src/main.rs ```rust,ignore fn main() { @@ -382,7 +293,8 @@ fn run(config: Config) { let mut f = File::open(config.filename).expect("file not found"); let mut contents = String::new(); - f.read_to_string(&mut contents).expect("something went wrong reading the file"); + f.read_to_string(&mut contents) + .expect("something went wrong reading the file"); println!("With text:\n{}", contents); } @@ -390,18 +302,15 @@ fn run(config: Config) { // ...snip... ``` -Listing 12-11: Extracting a `run` function containing the -rest of the program logic +列表 12-11:提取 `run` 函数来包含剩余的程序逻辑 - +现在 `run` 函数包含了 `main` 中从读取文件开始的剩余的所有逻辑。`run` 函数获取一个 `Config` 实例作为参数。 -现在`run`函数包含了`main`中从读取文件开始的剩余的所有逻辑。`run`函数获取一个`Config`实例作为参数。 +#### 从 `run` 函数中返回错误 -#### 从`run`函数中返回错误 +通过将剩余的逻辑分离进 `run` 函数而不是留在 `main` 中,就可以像列表 12-9 中的 `Config::new` 那样改进错误处理。不再通过 `expect` 允许程序 panic,`run` 函数将会在出错时返回一个 `Result`。这让我们进一步以一种对用户友好的方式统一 `main` 中的错误处理。列表 12-12 展示了 `run` 签名和函数体中的改变: -通过将剩余的逻辑分离进`run`函数而不是留在`main`中,就可以像列表 12-9 中的`Config::new`那样改进错误处理。不再通过通过`expect`允许程序 panic,`run`函数将会在出错时返回一个`Result`。这让我们进一步以一种对用户友好的方式统一`main`中的错误处理。列表 12-12 展示了`run`签名和函数体中的变化: - -Filename: src/main.rs +文件名: src/main.rs ```rust,ignore use std::error::Error; @@ -420,40 +329,33 @@ fn run(config: Config) -> Result<(), Box> { } ``` -Listing 12-12: Changing the `run` function to return -`Result` - - +列表 12-12:修改 `run` 函数返回 `Result` + `Result<(), Box>`。之前这个函数返回 unit 类型 `()`,现在它仍然保持作为 `Ok` 时的返回值。 -这里做出了三个大的改变。第一,改变了`run`函数的返回值为`Result<(), Box>`。之前这个函数返回 unit 类型`()`,现在它仍然保持作为`Ok`时的返回值。 +对于错误类型,使用了 **trait 对象** `Box`(在开头使用了 `use` 语句将 `std::error::Error` 引入作用域)。第十七章会涉及 trait 对象。目前只需知道 `Box` 意味着函数会返回实现了 `Error` trait 的类型,不过无需指定具体将会返回的值的类型。这提供了在不同的错误场景可能有不同类型的错误返回值的灵活性。 - - +第二个改变是去掉了 `expect` 调用并替换为第九章讲到的 `?`。不同于遇到错误就 `panic!`,这会从函数中返回错误值并让调用者来处理它。 -对于错误类型,使用了**trait 对象**`Box`(在开头使用了`use`语句将`std::error::Error`引入作用域)。第十七章会涉及 trait 对象。目前只需知道`Box`意味着函数会返回实现了`Error` trait 的类型,不过无需指定具体将会返回的值的类型。这提供了在不同的错误场景可能有不同类型的错误返回值的灵活性。 - -第二个改变是去掉了`expect`调用并替换为第九章讲到的`?`。不同于遇到错误就`panic!`,这会从函数中返回错误值并让调用者来处理它。 - -第三个修改是现在成功时这个函数会返回一个`Ok`值。因为`run`函数签名中声明成功类型返回值是`()`,这意味着需要将 unit 类型值包装进`Ok`值中。`Ok(())`一开始看起来有点奇怪,不过这样使用`()`是表明我们调用`run`只是为了它的副作用的惯用方式;它并没有返回什么有意义的值。 +第三个修改是现在成功时这个函数会返回一个 `Ok` 值。因为 `run` 函数签名中声明成功类型返回值是 `()`,这意味着需要将 unit 类型值包装进 `Ok` 值中。`Ok(())` 一开始看起来有点奇怪,不过这样使用 `()` 是表明我们调用 `run` 只是为了它的副作用的惯用方式;它并没有返回什么有意义的值。 上述代码能够编译,不过会有一个警告: -``` -warning: unused result which must be used, #[warn(unused_must_use)] on by default +```text +warning: unused result which must be used, #[warn(unused_must_use)] on by +default --> src/main.rs:39:5 | 39 | run(config); | ^^^^^^^^^^^^ ``` -Rust 提示我们的代码忽略了`Result`值,它可能表明这里存在一个错误。虽然我们没有检查这里是否有一个错误,而编译器提醒我们这里应该有一些错误处理代码!现在就让我们修正他们。 +Rust 提示我们的代码忽略了 `Result` 值,它可能表明这里存在一个错误。虽然我们没有检查这里是否有一个错误,而编译器提醒我们这里应该有一些错误处理代码!现在就让我们修正他们。 -#### 处理`main`中`run`返回的错误 +#### 处理 `main` 中 `run` 返回的错误 -我们将检查错误并使用与列表 12-10 中处理错误类似的技术来优雅的处理他们,不过有一些细微的不同: +我们将检查错误并使用类似列表 12-10 中 `Config::new` 处理错误的技术来处理他们,不过有一些细微的不同: -Filename: src/main.rs +文件名: src/main.rs ```rust,ignore fn main() { @@ -470,26 +372,24 @@ fn main() { } ``` - +我们使用 `if let` 来检查 `run` 是否返回一个 `Err` 值,不同于 `unwrap_or_else`,并在出错时调用 `process::exit(1)`。`run` 并不返回像 `Config::new` 返回的 `Config` 实例那样需要 `unwrap` 的值。因为 `run` 在成功时返回 `()`,而我们只关心发现一个错误,所以并不需要 `unwrap_or_else` 来返回未封装的值,因为它只会是 `()`。 -我们使用`if let`来检查`run`是否返回一个`Err`值,不同于`unwrap_or_else`,并在出错时调用`process::exit(1)`。`run`并不返回像`Config::new`返回的`Config`实例那样需要`unwrap`的值。因为`run`在成功时返回`()`,而我们只关心发现一个错误,所以并不需要`unwrap_or_else`来返回未封装的值,因为它只会是`()`。 - -不过两个例子中`if let`和`unwrap_or_else`的函数体都一样:打印出错误并退出。 +不过两个例子中 `if let` 和 `unwrap_or_else` 的函数体都一样:打印出错误并退出。 ### 将代码拆分到库 crate -现在项目看起来好多了!现在我们将要拆分 *src/main.rs* 并将一些代码放入 *src/lib.rs*,这样就能测试他们并拥有一个小的`main`函数。 +现在项目看起来好多了!现在我们将要拆分 *src/main.rs* 并将一些代码放入 *src/lib.rs*,这样就能测试他们并拥有一个拥有更少功能的 `main` 函数。 让我们将如下代码片段从 *src/main.rs* 移动到新文件 *src/lib.rs* 中: -- `run`函数定义 -- 相关的`use`语句 -- `Config`的定义 -- `Config::new`函数定义 +- `run` 函数定义 +- 相关的 `use` 语句 +- `Config` 的定义 +- `Config::new` 函数定义 -现在 *src/lib.rs* 的内容应该看起来像列表 12-13: +现在 *src/lib.rs* 的内容应该看起来像列表 12-13(为了简洁省略了函数体): -Filename: src/lib.rs +文件名: src/lib.rs ```rust,ignore use std::error::Error; @@ -503,87 +403,43 @@ pub struct Config { impl Config { pub fn new(args: &[String]) -> Result { - if args.len() < 3 { - return Err("not enough arguments"); - } - - let query = args[1].clone(); - let filename = args[2].clone(); - - Ok(Config { - query: query, - filename: filename, - }) + // ...snip... } } -pub fn run(config: Config) -> Result<(), Box>{ - let mut f = File::open(config.filename)?; - - let mut contents = String::new(); - f.read_to_string(&mut contents)?; - - println!("With text:\n{}", contents); - - Ok(()) +pub fn run(config: Config) -> Result<(), Box> { + // ...snip... } ``` -Listing 12-13: Moving `Config` and `run` into -*src/lib.rs* - - - -这里使用了公有的`pub`:在`Config`、其字段和其`new`方法,以及`run`函数上。现在我们有了一个拥有可以测试的公有 API 的库 crate 了。 +列表 12-13:将 `Config` 和 `run` 移动到 *src/lib.rs* -#### 从二进制 crate 中调用库 crate +这里使用了公有的 `pub`:在 `Config`、其字段和其 `new`方法,以及 `run` 函数上。现在我们有了一个拥有可以测试的公有 API 的库 crate 了。 -现在需要在 *src/main.rs* 中使用`extern crate greprs`将移动到 *src/lib.rs* 的代码引入二进制 crate 的作用域。接着我们将增加一个`use greprs::Config`行将`Config`类型引入作用域,并使用库 crate 的名称作为`run`函数的前缀,如列表 12-14 所示: +现在需要在 *src/main.rs* 中使用 `extern crate greprs` 将移动到 *src/lib.rs* 的代码引入二进制 crate 的作用域。接着我们将增加一个 `use greprs::Config` 行将 `Config` 类型引入作用域,并使用库 crate 的名称作为 `run` 函数的前缀,如列表 12-14 所示: Filename: src/main.rs ```rust,ignore -extern crate greprs; +extern crate minigrep; use std::env; use std::process; -use greprs::Config; +use minigrep::Config; fn main() { - let args: Vec = env::args().collect(); - - let config = Config::new(&args).unwrap_or_else(|err| { - println!("Problem parsing arguments: {}", err); - process::exit(1); - }); - - println!("Searching for {}", config.query); - println!("In file {}", config.filename); - - if let Err(e) = greprs::run(config) { - println!("Application error: {}", e); - - process::exit(1); + // ...snip... + if let Err(e) = minigrep::run(config) { + // ...snip... } } ``` -Listing 12-14: Bringing the `greprs` crate into the scope -of *src/main.rs* - - - - - -通过这些重构,所有功能应该抖联系在一起并可以运行了。运行`cargo run`来确保一切都正确的衔接在一起。 +列表 12-14:将 `minigrep` crate 引入 *src/main.rs* 的作用域 - - +为了将库 crate 引入二进制 crate,我们使用 `extern crate minigrep`。接着增加 `use minigrep::Config` 将 `Config` 类型引入作用域,并使用 crate 名作为 `run` 函数的前缀。通过这些重构,所有功能应该能够联系在一起并运行了。运行 `cargo run` 来确保一切都正确的衔接在一起。 -哇哦!这可有很多的工作,不过我们为将来成功打下了基础。现在处理错误将更容易,同时代码也更模块化。从现在开始几乎所有的工作都将在 *src/lib.rs* 中进行。 +哇哦!这可有很多的工作,不过我们为将来成功打下了基础。现在处理错误将更容易,同时代码也更加模块化。从现在开始几乎所有的工作都将在 *src/lib.rs* 中进行。 让我们利用这些新创建的模块的优势来进行一些在旧代码中难以展开的工作,他们在新代码中却很简单:编写测试! \ No newline at end of file diff --git a/src/ch12-04-testing-the-librarys-functionality.md b/src/ch12-04-testing-the-librarys-functionality.md index bdcf7a5..fd6b0bd 100644 --- a/src/ch12-04-testing-the-librarys-functionality.md +++ b/src/ch12-04-testing-the-librarys-functionality.md @@ -2,11 +2,11 @@ > [ch12-04-testing-the-librarys-functionality.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-04-testing-the-librarys-functionality.md) >
-> commit b8e4fcbf289b82c12121b282747ce05180afb1fb +> commit 5908c59a5a4cc58fd863605b80b295a335c2cbdf -现在我们将逻辑提取到了 *src/lib.rs* 并将所有的参数解析和错误处理留在了 *src/main.rs* 中,为代码的核心功能编写测试将更加容易。我们可以直接使用多种参数调用函数并检查返回值而无需从命令行运行二进制文件了。 +现在我们将逻辑提取到了 *src/lib.rs* 并将所有的参数解析和错误处理留在了 *src/main.rs* 中,为代码的核心功能编写测试将更加容易。我们可以直接使用多种参数调用函数并检查返回值而无需从命令行运行二进制文件了。如果你愿意的话,请自行为 `Config::new` 和 `run` 函数的功能编写一些测试。 -在这一部分,我们将遵循测试驱动开发(Test Driven Development, TTD)的模式。这是一个软件开发技术,它遵循如下步骤: +在这一部分,我们将遵循测试驱动开发(Test Driven Development, TTD)的模式来逐步增加 `minigrep` 的搜索逻辑。这是一个软件开发技术,它遵循如下步骤: 1. 编写一个会失败的测试,并运行它以确保其因为你期望的原因失败。 2. 编写或修改刚好足够的代码来使得新的测试通过。 @@ -15,13 +15,13 @@ 这只是众多编写软件的方法之一,不过 TDD 有助于驱动代码的设计。在编写能使测试通过的代码之前编写测试有助于在开发过程中保持高测试覆盖率。 -我们将测试驱动实现`greprs`实际在文件内容中搜索查询字符串并返回匹配的行列表的部分。我们将在一个叫做`search`的函数中增加这些功能。 +我们将测试驱动实现实际在文件内容中搜索查询字符串并返回匹配的行列表的功能。我们将在一个叫做 `search` 的函数中增加这些功能。 ### 编写失败测试 -首先,去掉 *src/lib.rs* 和 *src/main.rs* 中的`println!`语句,因为不再真的需要他们了。接着我们会像第十一章那样增加一个`test`模块和一个测试函数。测试函数指定了我们希望`search`函数拥有的行为:它会获取一个需要查询的字符串和用来查询的文本。列表 12-15 展示了这个测试: +首先,去掉 *src/lib.rs* 和 *src/main.rs* 中的`println!`语句,因为不再真正需要他们了。接着我们会像第十一章那样增加一个 `test` 模块和一个测试函数。测试函数指定了 `search` 函数期望拥有的行为:它会获取一个需要查询的字符串和用来查询的文本,并只会返回包含请求的文本行。列表 12-15 展示了这个测试: -Filename: src/lib.rs +文件名: src/lib.rs ```rust # fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { @@ -48,33 +48,29 @@ Pick three."; } ``` -Listing 12-15: Creating a failing test for the `search` -function we wish we had +列表 12-15:创建一个我们期望的 `search` 函数的失败测试 -这里选择使用 "duct" 作为这个测试中需要搜索的字符串。用来搜索的文本有三行,其中只有一行包含 "duct"。我们断言`search`函数的返回值只包含期望的那一行。 +这里选择使用 "duct" 作为这个测试中需要搜索的字符串。用来搜索的文本有三行,其中只有一行包含 "duct"。我们断言 `search` 函数的返回值只包含期望的那一行。 -我们还不能运行这个测试并看到它失败,因为它甚至都还不能编译!我们将增加足够的代码来使其能够编译:一个总是会返回空 vector 的`search`函数定义,如列表 12-16 所示。一旦有了它,这个测试应该能够编译并因为空 vector 并不匹配一个包含一行`"safe, fast, productive."`的 vector 而失败。 +我们还不能运行这个测试并看到它失败,因为它甚至都还不能编译!我们将增加足够的代码来使其能够编译:一个总是会返回空 vector 的 `search` 函数定义,如列表 12-16 所示。一旦有了它,这个测试应该能够编译并因为空 vector 并不匹配一个包含一行 `"safe, fast, productive."` 的 vector 而失败。 -Filename: src/lib.rs +文件名: src/lib.rs -```rust -fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { - vec![] +``` +pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { + vec![] } ``` -Listing 12-16: Defining just enough of the `search` -function that our test will compile +列表 12-16:刚好足够使测试通过编译的 `search` 函数定义 - +注意需要在 `search` 的签名中定义一个显式生命周期 `'a` 并用于 `contents` 参数和返回值。回忆一下第十章中讲到生命周期参数指定哪个参数的生命周期与返回值的生命周期相关联。在这个例子中,我们表明返回的 vector 中应该包含引用参数 `contents`(而不是参数`query`) slice 的字符串 slice。 -注意需要在`search`的签名中显式定义一个显式生命周期`'a`并用于`contents`参数和返回值。回忆一下第十章中生命周期参数指定哪个参数的生命周期与返回值的生命周期相关联。在这个例子中,我们表明返回的 vector 中应该包含引用参数`contents`(而不是参数`query`) slice 的字符串 slice。 - -换句话说,我们告诉 Rust 函数`search`返回的数据将与`search`函数中的参数`contents`的数据存在的一样久。这是非常重要的!为了使这个引用有效那么**被**slice 引用的数据也需要保持有效;如果编译器认为我们是在创建`query`而不是`contents`的字符串 slice,那么安全检查将是不正确的。 +换句话说,我们告诉 Rust 函数 `search` 返回的数据将与 `search` 函数中的参数 `contents` 的数据存在的一样久。这是非常重要的!为了使这个引用有效那么 **被** slice 引用的数据也需要保持有效;如果编译器认为我们是在创建 `query` 而不是 `contents` 的字符串 slice,那么安全检查将是不正确的。 如果尝试不用生命周期编译的话,我们将得到如下错误: -``` +```text error[E0106]: missing lifetime specifier --> src/lib.rs:5:47 | @@ -85,17 +81,17 @@ error[E0106]: missing lifetime specifier signature does not say whether it is borrowed from `query` or `contents` ``` -Rust 不可能知道我们需要的是哪一个参数,所以需要告诉它。因为参数`contents`包含了所有的文本而且我们希望返回匹配的那部分文本,而我们知道`contents`是应该要使用生命周期语法来与返回值相关联的参数。 +Rust 不可能知道我们需要的是哪一个参数,所以需要告诉它。因为参数 `contents` 包含了所有的文本而且我们希望返回匹配的那部分文本,所以我们知道 `contents` 是应该要使用生命周期语法来与返回值相关联的参数。 其他语言中并不需要你在函数签名中将参数与返回值相关联,所以这么做可能仍然感觉有些陌生,随着时间的推移会越来越容易。你可能想要将这个例子与第十章中生命周期语法部分做对比。 现在试尝试运行测试: -``` +```text $ cargo test ...warnings... - Finished debug [unoptimized + debuginfo] target(s) in 0.43 secs - Running target/debug/deps/greprs-abcabcabc + Finished dev [unoptimized + debuginfo] target(s) in 0.43 secs + Running target/debug/deps/minigrep-abcabcabc running 1 test test test::one_result ... FAILED @@ -120,52 +116,42 @@ error: test failed ### 编写使测试通过的代码 -目前测试之所以会失败是因为我们总是返回一个空的 vector。为了修复并实现`search`,我们的程序需要遵循如下步骤: +目前测试之所以会失败是因为我们总是返回一个空的 vector。为了修复并实现 `search`,我们的程序需要遵循如下步骤: -1. 遍历每一行文本。 -2. 查看这一行是否包含要搜索的字符串。 - * 如果有,将这一行加入返回列表中。 - * 如果没有,什么也不做。 -3. 返回匹配到的列表 +* 遍历每一行文本。 +* 查看这一行是否包含要搜索的字符串。 +* 如果有,将这一行加入返回列表中。 +* 如果没有,什么也不做。 +* 返回匹配到的列表 让我们一步一步的来,从遍历每行开始。 -#### 使用`lines`方法遍历每一行 +#### 使用 `lines` 方法遍历每一行 -Rust 有一个有助于一行一行遍历字符串的方法,出于方便它被命名为`lines`,它如列表 12-17 这样工作: +Rust 有一个有助于一行一行遍历字符串的方法,出于方便它被命名为 `lines`,它如列表 12-17 这样工作: Filename: src/lib.rs ```rust,ignore -fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { +pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { for line in contents.lines() { // do something with line } } ``` -Listing 12-17: Iterating through each line in -`contents` +列表 12-17:遍历 `contents` 的每一行 - - -`lines`方法返回一个迭代器。第十三章会深入了解迭代器,不过我们已经在列表 3-6 中见过使用迭代器的方法,在那里使用了一个`for`循环和迭代器在一个集合的每一项上运行一些代码。 - - - +`lines` 方法返回一个迭代器。第十三章会深入了解迭代器,不过我们已经在列表 3-6 中见过使用迭代器的方法,在那里使用了一个 `for` 循环和迭代器在一个集合的每一项上运行了一些代码。 #### 用查询字符串搜索每一行 -接下来将会增加检查当前行是否包含查询字符串的功能。幸运的是,字符串类型为此也有一个有用的方法叫做`contains`!如列表 12-18 所示在`search`函数中加入`contains`方法: +接下来将会增加检查当前行是否包含查询字符串的功能。幸运的是,字符串类型为此也有一个叫做 `contains` 的实用方法!如列表 12-18 所示在 `search` 函数中加入 `contains` 方法调用: -Filename: src/lib.rs +文件名: src/lib.rs ```rust,ignore -fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { +pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { for line in contents.lines() { if line.contains(query) { // do something with line @@ -174,19 +160,16 @@ fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { } ``` -Listing 12-18: Adding functionality to see if the line -contains the string in `query` - - +列表 12-18:增加检查文本行是否包含 `query` 中字符串的功能 #### 存储匹配的行 -最后我们需要一个方法来存储包含查询字符串的行。为此可以在`for`循环之前创建一个可变的 vector 并调用`push`方法在 vector 中存放一个`line`。在`for`循环之后,返回这个 vector,如列表 12-19 所示: +最后我们需要一个方法来存储包含查询字符串的行。为此可以在 `for` 循环之前创建一个可变的 vector 并调用 `push` 方法在 vector 中存放一个 `line`。在 `for` 循环之后,返回这个 vector,如列表 12-19 所示: -Filename: src/lib.rs +文件名: src/lib.rs ```rust,ignore -fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { +pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { let mut results = Vec::new(); for line in contents.lines() { @@ -199,53 +182,27 @@ fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { } ``` -Listing 12-19: Storing the lines that match so that we -can return them - - +列表 12-19:储存匹配的行以便可以返回他们 -现在`search`函数应该返回只包含`query`的那些行,而测试应该会通过。让我们运行测试: +现在 `search` 函数应该返回只包含 `query` 的那些行,而测试应该会通过。让我们运行测试: -``` +```text $ cargo test running 1 test test test::one_result ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured - - Running target/debug/greprs-2f55ee8cd1721808 - -running 0 tests - -test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured - - Doc-tests greprs - -running 0 tests - -test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured ``` 测试通过了,很好,它可以工作了! -现在测试通过了,我们可以考虑一下重构`search`的实现并时刻保持测试通过来保持其功能不变的机会了。这些代码并不坏,不过并没有利用迭代器的一些实用功能。第十三章将回到这个例子并深入探索迭代器并看看如何改进代码。 +现在测试通过了,我们可以考虑一下重构 `search` 的实现并时刻保持测试通过来保持其功能不变的机会了。`search` 函数中的代码并不坏,不过并没有利用迭代器的一些实用功能。第十三章将回到这个例子并深入探索迭代器并看看如何改进代码。 - - +#### 在 `run` 函数中使用 `search` 函数 -#### 在`run`函数中使用`search`函数 +现在 `search` 函数是可以工作并测试通过了的,我们需要实际在 `run` 函数中调用 `search`。需要将 `config.query` 值和 `run` 从文件中读取的 `contents` 传递给 `search` 函数。接着 `run` 会打印出 `search` 返回的每一行: -现在`search`函数是可以工作并测试通过了的,我们需要实际在`run`函数中调用`search`。需要将`config.query`值和`run`从文件中读取的`contents`传递给`search`函数。接着`run`会打印出`search`返回的每一行: - -Filename: src/lib.rs +文件名: src/lib.rs ```rust,ignore pub fn run(config: Config) -> Result<(), Box> { @@ -262,38 +219,36 @@ pub fn run(config: Config) -> Result<(), Box> { } ``` - - -这里再一次使用了`for`循环获取了`search`返回的每一行,而对每一行运行的代码将他们打印了出来。 +这里仍然使用了 `for` 循环获取了 `search` 返回的每一行并打印出来。 现在整个程序应该可以工作了!让我们试一试,首先使用一个只会在艾米莉·狄金森的诗中返回一行的单词 "frog": -``` +```text $ cargo run frog poem.txt - Compiling greprs v0.1.0 (file:///projects/greprs) - Finished debug [unoptimized + debuginfo] target(s) in 0.38 secs - Running `target/debug/greprs frog poem.txt` + Compiling minigrep v0.1.0 (file:///projects/minigrep) + Finished dev [unoptimized + debuginfo] target(s) in 0.38 secs + Running `target/debug/minigrep frog poem.txt` How public, like a frog ``` 好的!接下来,像 "the" 这样会匹配多行的单词会怎么样呢: -``` +```text $ cargo run the poem.txt - Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs - Running `target/debug/greprs the poem.txt` -Then there's a pair of us — don't tell! + Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs + Running `target/debug/minigrep the poem.txt` +Then there’s a pair of us — don’t tell! To tell your name the livelong day ``` 最后,让我们确保搜索一个在诗中哪里都没有的单词时不会得到任何行,比如 "monomorphization": -``` +```text $ cargo run monomorphization poem.txt - Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs - Running `target/debug/greprs monomorphization poem.txt` + Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs + Running `target/debug/minigrep monomorphization poem.txt` ``` -非常好!我们创建了一个属于自己的经典工具,并学习了很多如何组织程序的知识。我们还学习了一些文件输入输出、生命周期、测试和命令行解析的内容。 +非常好!我们创建了一个属于自己的迷你版经典工具,并学习了很多如何组织程序的知识。我们还学习了一些文件输入输出、生命周期、测试和命令行解析的内容。 -现在如果你希望的话请随意移动到第十三章。为了使这个项目章节更丰满,我们将简要的展示如何处理环境变量和打印到标准错误,这两者在编写命令行程序时都很有用。 +为了使这个项目章节更丰满,我们将简要的展示如何处理环境变量和打印到标准错误,这两者在编写命令行程序时都很有用。现在如果你希望的话请随意移动到第十三章。 diff --git a/src/ch12-05-working-with-environment-variables.md b/src/ch12-05-working-with-environment-variables.md index 3e18760..40dc70a 100644 --- a/src/ch12-05-working-with-environment-variables.md +++ b/src/ch12-05-working-with-environment-variables.md @@ -2,25 +2,17 @@ > [ch12-05-working-with-environment-variables.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-05-working-with-environment-variables.md) >
-> commit 0db6a0a34886bf02feabcab8b430b5d332a8bdf5 +> commit adababc48956f4d39c97c8b6fc14a104d90e20dc -我们将用一个额外的功能来改进我们的工具:一个通过环境变量启用的大小写不敏感搜索的选项。我们将其设计为一个命令行参数并要求用户每次需要时都加上它,不过相反我们将使用环境变量。这允许用户设置环境变量一次之后在整个终端会话中所有的搜索都将是大小写不敏感的了。 +我们将用一个额外的功能来改进我们的工具:一个通过环境变量启用的大小写不敏感搜索的选项。我们可以将其设计为一个命令行参数并要求用户每次需要时都加上它,不过相反我们将使用环境变量。这允许用户设置环境变量一次之后在整个终端会话中所有的搜索都将是大小写不敏感的。 -### 编写一个大小写不敏感`search`函数的失败测试 +### 编写一个大小写不敏感 `search` 函数的失败测试 首先,增加一个新函数,当设置了环境变量时会调用它。 - - +这里将继续遵循上一部分开始使用的 TDD 过程,其第一步是再次编写一个失败测试。我们将为新的大小写不敏感搜索函数新增一个测试函数,并将老的测试函数从 `one_result` 改名为 `case_sensitive` 来更清楚的表明这两个测试的区别,如列表 12-20 所示: -这里将继续遵循上一部分开始使用的 TDD 过程,其第一步是再次编写一个失败测试。我们将为新的大小写不敏感搜索函数新增一个测试函数,并将老的测试函数从`one_result`改名为`case_sensitive`来更清除的表明这两个测试的区别,如列表 12-20 所示: - -Filename: src/lib.rs +文件名: src/lib.rs ```rust #[cfg(test)] @@ -59,20 +51,17 @@ Trust me."; } ``` - - -Listing 12-20: Adding a new failing test for the case -insensitive function we're about to add +列表 12-20:为准备添加的大小写不敏感函数新增失败测试 -注意我们也改变了老测试中`query`和`contents`的值:将查询字符串改变为 "duct",它将会匹配带有单词 productive" 的行。还新增了一个含有文本 "Duct tape" 的行,它有一个大写的 D,这在大小写敏感搜索时不应该匹配 "duct"。我们修改这个测试以确保不会意外破坏已经实现的大小写敏感搜索功能;这个测试现在应该能通过并在处理大小写不敏感搜索时应该能一直通过。 +注意我们也改变了老测试中 `contents` 的值。还新增了一个含有文本 "Duct tape" 的行,它有一个大写的 D,这在大小写敏感搜索时不应该匹配 "duct"。我们修改这个测试以确保不会意外破坏已经实现的大小写敏感搜索功能;这个测试现在应该能通过并在处理大小写不敏感搜索时应该能一直通过。 -大小写不敏感搜索的新测试使用带有一些大写字母的 "rUsT" 作为其查询字符串。我们将要增加的`search_case_insensitive`的期望返回值是包含查询字符串 "rust" 的两行,"Rust:" 包含一个大写的 R 还有"Trust me."包含一个小写的 r。这个测试现在会编译失败因为还没有定义`search_case_insensitive`函数;请随意增加一个总是返回空 vector 的骨架实现,正如列表 12-16 中`search`函数那样为了使测试编译并失败时所做的那样。 +大小写 **不敏感** 搜索的新测试使用 "rUsT" 作为其查询字符串。在我们将要增加的 `search_case_insensitive` 函数中,“rUsT” 查询应该包含 “Rust:” 包含一个大写的 R 还有 “Trust me.” 这两行,即便他们与查询的大小写都不同。这个测试现在会编译失败因为还没有定义 `search_case_insensitive` 函数。请随意增加一个总是返回空 vector 的骨架实现,正如列表 12-16 中 `search` 函数为了使测试编译并失败时所做的那样。 -### 实现`search_case_insensitive`函数 +### 实现 `search_case_insensitive` 函数 -`search_case_insensitive`函数,如列表 12-21 所示,将与`search`函数基本相同。区别是它会将`query`变量和每一`line`都变为小写,这样不管输入参数是大写还是小写,在检查该行是否包含查询字符串时都会是小写。 +`search_case_insensitive` 函数,如列表 12-21 所示,将与 `search` 函数基本相同。唯一的区别是它会将 `query` 变量和每一 `line` 都变为小写,这样不管输入参数是大写还是小写,在检查该行是否包含查询字符串时都会是小写。 -Filename: src/lib.rs +文件名: src/lib.rs ```rust fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { @@ -89,55 +78,27 @@ fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { } ``` -Listing 12-21: Defining the `search_case_insensitive` -function to lowercase both the query and the line before comparing them - - +列表 12-21:定义 `search_case_insensitive` 函数,它在比较查询和每一行之前将他们都转换为小写 - - +首先我们将 `query` 字符串转换为小写,并将其覆盖到同名的变量中。对查询字符串调用 `to_lowercase` 是必需的,这样不管用户的查询是 “rust”、“RUST”、“Rust” 或者 “rUsT”,我们都将其当作 “rust” 处理并对大小写不敏感。 -首先我们将`query`字符串转换为小写,并将其储存(覆盖)到同名的变量中。对查询字符串调用`to_lowercase`是必需的这样不管用户的查询是"rust"、"RUST"、"Rust"或者"rUsT",我们都将其当作"rust"处理并对大小写不敏感。 +注意 `query` 现在是一个 `String` 而不是字符串 slice,因为调用 `to_lowercase` 是在创建新数据,而不是引用现有数据。如果查询字符串是 “rUsT”,这个字符串 slice 并不包含可供我们使用的小写的 “u” 或 “t”,所以必需分配一个包含 “rust” 的新 `String`。现在当我们将 `query` 作为一个参数传递给 `contains` 方法时,需要增加一个 & 因为 `contains` 的签名被定义为获取一个字符串 slice。 -注意`query`现在是一个`String`而不是字符串 slice,因为调用`to_lowercase`是在创建新数据,而不是引用现有数据。如果查询字符串是"rUsT",这个字符串 slice 并不包含可供我们使用的小写的 u,所以必需分配一个包含"rust"的新`String`。因为`query`现在是一个`String`,当我们将`query`作为一个参数传递给`contains`方法时,需要增加一个 & 因为`contains`的签名被定义为获取一个字符串 slice。 - -接下来在检查每个`line`是否包含`search`之前增加了一个`to_lowercase`调用。这会将"Rust:"变为"rust:"并将"Trust me."变为"trust me."。现在我们将`line`和`query`都转换成了小写,这样就可以不管大小写的匹配文件中的文本和用户输入的查询了。 +接下来在检查每个 `line` 是否包含 `search` 之前增加了一个 `to_lowercase` 调用将他们都变为小写。现在我们将 `line` 和 `query` 都转换成了小写,这样就可以不管查询的大小写进行匹配了。 让我们看看这个实现能否通过测试: -``` - Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs - Running target/debug/deps/greprs-e58e9b12d35dc861 - +```text running 2 tests test test::case_insensitive ... ok test test::case_sensitive ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured - - Running target/debug/greprs-8a7faa2662b5030a - -running 0 tests - -test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured - - Doc-tests greprs - -running 0 tests - -test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured ``` -好的!现在,让我们在`run`函数中调用真正的新`search_case_insensitive`函数。首先,我们将在`Config`结构体中增加一个配置项来切换大小写敏感和大小写不敏感搜索。 +好的!现在,让我们在 `run` 函数中实际调用新 `search_case_insensitive` 函数。首先,我们将在 `Config` 结构体中增加一个配置项来切换大小写敏感和大小写不敏感搜索: -Filename: src/lib.rs +文件名: src/lib.rs ```rust pub struct Config { @@ -147,11 +108,9 @@ pub struct Config { } ``` - - -这里增加了`case_sensitive`字符来存放一个布尔值。接着我们需要`run`函数检查`case_sensitive`字段的值并使用它来决定是否调用`search`函数或`search_case_insensitive`函数,如列表 12-22所示: +这里增加了 `case_sensitive` 字符来存放一个布尔值。接着我们需要 `run` 函数检查 `case_sensitive` 字段的值并使用它来决定是否调用 `search` 函数或 `search_case_insensitive` 函数,如列表 12-22 所示: -Filename: src/lib.rs +文件名: src/lib.rs ```rust # use std::error::Error; @@ -192,14 +151,11 @@ pub fn run(config: Config) -> Result<(), Box>{ } ``` -Listing 12-22: Calling either `search` or -`search_case_insensitive` based on the value in `config.case_sensitive` +列表 12-22:根据 `config.case_sensitive` 的值调用 `search` 或 `search_case_insensitive` - +最后需要实际检查环境变量。处理环境变量的函数位于标准库的 `env` 模块中,所以我们需要在 *src/lib.rs* 的开头增加一个 `use std::env;` 行将这个模块引入作用域中。接着在 `Config::new` 中使用 `env` 模块的 `var` 方法来检查一个叫做 `CASE_INSENSITIVE` 的环境变量,如列表 12-23 所示: -最后需要实际检查环境变量。处理环境变量的函数位于标准库的`env`模块中,所以我们需要在 *src/lib.rs* 的开头增加一个`use std::env;`行将这个模块引入作用域中。接着在`Config::new`中使用`env`模块的`var`方法检查一个叫做`CASE_INSENSITIVE`的环境变量,如列表 12-23 所示: - -Filename: src/lib.rs +文件名: src/lib.rs ```rust use std::env; @@ -222,46 +178,43 @@ impl Config { let case_sensitive = env::var("CASE_INSENSITIVE").is_err(); - Ok(Config { - query: query, - filename: filename, - case_sensitive: case_sensitive, - }) + Ok(Config { query, filename, case_sensitive }) } } ``` -Listing 12-23: Checking for an environment variable named -`CASE_INSENSITIVE` +列表 12-23:检查叫做 `CASE_INSENSITIVE` 的环境变量 + +这里创建了一个新变量 `case_sensitive`。为了设置它的值,需要调用 `env::var` 函数并传递我们需要寻找的环境变量名称,`CASE_INSENSITIVE`。`env::var` 返回一个 `Result`,它在环境变量被设置时返回包含其值的 `Ok` 成员,并在环境变量未被设置时返回 `Err` 成员。 - +我们使用 `Result` 的 `is_err` 方法来检查其是否是一个 error(也就是环境变量未被设置的情况),这也就意味着我们 **需要** 进行一个大小写敏感搜索。如果`CASE_INSENSITIVE` 环境变量被设置为任何值,`is_err` 会返回 false 并将进行大小写不敏感搜索。我们并不关心环境变量所设置的 **值**,只关心它是否被设置了,所以检查 `is_err` 而不是 `unwrap`、`expect` 或任何我们已经见过的 `Result` 的方法。 -这里创建了一个新变量`case_sensitive`。为了设置它的值,需要调用`env::var`函数并传递我们需要寻找的环境变量名称,`CASE_INSENSITIVE`。`env::var`返回一个`Result`,它在环境变量被设置时返回包含其值的`Ok`成员,并在环境变量未被设置时返回`Err`成员。我们使用`Result`的`is_err`方法来检查其是否是一个 error(也就是环境变量未被设置的情况),这也就意味着我们**需要**进行一个大小写敏感搜索。如果`CASE_INSENSITIVE`环境变量被设置为任何值,`is_err`会返回 false 并将进行大小写不敏感搜索。我们并不关心环境变量所设置的值,只关心它是否被设置了,所以检查`is_err`而不是`unwrap`、`expect`或任何我们已经见过的`Result`的方法。我们将变量`case_sensitive`的值传递给`Config`实例这样`run`函数可以读取其值并决定是否调用`search`或者列表 12-22 中实现的`search_case_insensitive`。 +我们将变量 `case_sensitive` 的值传递给 `Config` 实例,这样 `run` 函数可以读取其值并决定是否调用 `search` 或者列表 12-22 中实现的 `search_case_insensitive`。 -让我们试一试吧!首先不设置环境变量并使用查询"to"运行程序,这应该会匹配任何全小写的单词"to"的行: +让我们试一试吧!首先不设置环境变量并使用查询 “to” 运行程序,这应该会匹配任何全小写的单词 “to” 的行: ```text $ cargo run to poem.txt - Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs - Running `target/debug/greprs to poem.txt` + Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs + Running `target/debug/minigrep to poem.txt` Are you nobody, too? How dreary to be somebody! ``` -看起来程序仍然能够工作!现在将`CASE_INSENSITIVE`设置为 1 并仍使用相同的查询"to",这回应该得到包含可能有大写字母的"to"的行: +看起来程序仍然能够工作!现在将 `CASE_INSENSITIVE` 设置为 1 并仍使用相同的查询 “to”,这回应该得到包含可能有大写字母的 “to” 的行: -``` +```text $ CASE_INSENSITIVE=1 cargo run to poem.txt - Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs - Running `target/debug/greprs to poem.txt` + Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs + Running `target/debug/minigrep to poem.txt` Are you nobody, too? How dreary to be somebody! To tell your name the livelong day To an admiring bog! ``` -好极了,我们也得到了包含"To"的行!现在`greprs`程序可以通过环境变量控制进行大小写不敏感搜索了。现在你知道了如何管理由命令行参数或环境变量设置的选项了! +好极了,我们也得到了包含 “To” 的行!现在 `minigrep` 程序可以通过环境变量控制进行大小写不敏感搜索了。现在你知道了如何管理由命令行参数或环境变量设置的选项了! -一些程序允许对相同配置同时使用参数**和**环境变量。在这种情况下,程序来决定参数和环境变量的优先级。作为一个留给你的测试,尝试同时通过一个命令行参数来控制大小写不敏感搜索,并在程序遇到矛盾值时决定其优先级。 +一些程序允许对相同配置同时使用参数 **和** 环境变量。在这种情况下,程序来决定参数和环境变量的优先级。作为一个留给你的测试,尝试通过一个命令行参数或一个环境变量来控制大小写不敏感搜索。并在运行程序时遇到矛盾值时决定命令行参数和环境变量的优先级。 -`std::env`模块还包含了更多处理环境变量的实用功能;请查看官方文档来了解其可用的功能。 \ No newline at end of file +`std::env` 模块还包含了更多处理环境变量的实用功能;请查看官方文档来了解其可用的功能。 \ No newline at end of file diff --git a/src/ch12-06-writing-to-stderr-instead-of-stdout.md b/src/ch12-06-writing-to-stderr-instead-of-stdout.md index 1ab95ce..9f1c23e 100644 --- a/src/ch12-06-writing-to-stderr-instead-of-stdout.md +++ b/src/ch12-06-writing-to-stderr-instead-of-stdout.md @@ -2,111 +2,83 @@ > [ch12-06-writing-to-stderr-instead-of-stdout.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-06-writing-to-stderr-instead-of-stdout.md) >
-> commit d09cfb51a239c0ebfc056a64df48fe5f1f96b207 +> commit 7db14aa689553706198ffcb11a8c60b478e752fe -目前为止,我们将所有的输出都`println!`到了终端。大部分终端都提供了两种输出:“标准输出”对应大部分信息,而“标准错误”则用于错误信息。这种区别是命令行程序所期望拥有的行为:例如它允许用户选择将程序正常输出定向到一个文件中并仍将错误信息打印到屏幕上。但是`println!`只能够打印到标准输出,所以我们必需使用其他方法来打印到标准错误。 +目前为止,我们将所有的输出都 `println!` 到了终端。大部分终端都提供了两种输出:**标准输出**(*standard output*)对应大部分信息(有时在代码中使用缩写 `stdout`),**标准错误**(*standard error*)则用于错误信息(`stderr`)。这种区别允许用户选择将程序正常输出定向到一个文件中并仍将错误信息打印到屏幕上。 -我们可以验证,目前所编写的`greprs`,所有内容都被打印到了标准输出,包括应该被写入标准错误的错误信息。可以通过故意造成错误来做到这一点,一个发生这种情况的方法是不使用任何参数运行程序。我们准备将标准输出重定向到一个文件中,不过不是标准错误。命令行程序期望以这种方式工作,因为如果输出是错误信息,它应该显示在屏幕上而不是被重定向到文件中。可以看出我们的程序目前并没有满足这个期望,通过使用`>`并指定一个文件名,*output.txt*,这是期望将标注输出重定向的文件: +但是 `println!` 函数只能够打印到标准输出,所以我们必需使用其他方法来打印到标准错误。 -``` -$ cargo run > output.txt -``` +### 检查错误应该写入何处 - - +首先,让我们观察一下目前 `minigrep` 打印的所有内容都被写入了标准输出,包括应该被写入标准错误的错误信息。可以通过将标准输出流重定向到一个文件同时有意产生一个错误来做到这一点。我们没有重定向标准错误流,所以任何发送到标准错误的内容将会继续显示在屏幕上。命令行程序被期望将错误信息发送到标准错误流,这样即便选择将标准输出流重定向到文件中时仍然能看到错误信息。目前我们的程序并不符合期望;我们将看到相反它将错误信息输出保存到了文件中。 -`>`语法告诉 shell 将标准输出的内容写入到 *output.txt* 文件中而不是打印到屏幕上。我们并没有看到期望的错误信息打印到屏幕上,所以这意味着它一定被写入了文件中。让我们看看 *output.txt* 包含什么: +展示这种行为的方式是通过 `>` 和文件名 *output.txt* 来与运行程序,这个文件是期望重定向标准输出流的位置。并不传递任何参数这样应该会产生一个错误: -``` -Application error: No search string or filename found +```text +$ cargo run > output.txt ``` - - +`>` 语法告诉 shell 将标准输出的内容写入到 *output.txt* 文件中而不是屏幕上。我们并没有看到期望的错误信息打印到屏幕上,所以这意味着它一定被写入了文件中。让我们看看 *output.txt* 包含什么: -是的,这就是错误信息,这意味着它被打印到了标准输出。这并不是命令行程序所期望拥有的。像这样的错误信息被打印到标准错误,并当以这种方式重定向标注输出时只将运行成功时的数据打印到文件中。让我们像列表 12-23 所示改变错误信息如何被打印的。因为本章早些时候的进行的重构,所有打印错误信息的代码都在一个位置,在`main`中: +```text +Problem parsing arguments: not enough arguments +``` -Filename: src/main.rs +是的,错误信息被打印到了标准输出中。像这样的错误信息被打印到标准错误中将有用的多,并在重定向标准输出时只将成功运行的信息写入文件。我们将改变他们。 -```rust,ignore -extern crate greprs; +### 将错误打印到标准错误 -use std::env; -use std::process; -use std::io::prelude::*; +让我们如列表 12-24 所示的代码改变错误信息是如何被打印的。得益于本章早些时候的重构,所有打印错误信息的代码都位于 `main` 一个函数中。标准库提供了 `eprintln!` 宏来打印到标准错误流,所以将两个调用 `println!` 打印错误信息的维持替换为 `eprintln!`: -use greprs::Config; +文件名: src/main.rs +```rust,ignore fn main() { let args: Vec = env::args().collect(); - let mut stderr = std::io::stderr(); let config = Config::new(&args).unwrap_or_else(|err| { - writeln!( - &mut stderr, - "Problem parsing arguments: {}", - err - ).expect("Could not write to stderr"); + eprintln!("Problem parsing arguments: {}", err); process::exit(1); }); - if let Err(e) = greprs::run(config) { - writeln!( - &mut stderr, - "Application error: {}", - e - ).expect("Could not write to stderr"); + if let Err(e) = minigrep::run(config) { + eprintln!("Application error: {}", e); process::exit(1); } } ``` -Listing 12-23: Writing error messages to `stderr` instead -of `stdout` using `writeln!` - - - -Rust 并没有类似`println!`这样的方便写入标准错误的函数。相反,我们使用`writeln!`宏,它有点像`println!`,不过它获取一个额外的参数。第一个参数是希望写入内容的位置。可以通过`std::io::stderr`函数获取一个标准错误的句柄。我们将一个`stderr`的可变引用传递给`writeln!`;它需要是可变的因为这样才能写入信息!第二个和第三个参数就像`println!`的第一个和第二参数:一个格式化字符串和任何需要插入的变量。 +列表 12-24:使用 `eprintln!` 将错误信息写入标准错误而不是标准输出 -再次用相同方式运行程序,不带任何参数并用`>`重定向`stdout`: +将 `println!` 改为 `eprintln!` 之后,让我们再次尝试用同样的方式运行程序,不使用任何参数并通过 `>` 重定向标准输出: -``` +```text $ cargo run > output.txt -Application error: No search string or filename found +Problem parsing arguments: not enough arguments ``` -现在我们看到了屏幕上的错误信息,不过`output.txt`里什么也没有,这也就是命令行程序所期望的行为。 +现在我们看到了屏幕上的错误信息,同时 `output.txt` 里什么也没有,这也就是命令行程序所期望的行为。 如果使用不会造成错误的参数再次运行程序,不过仍然将标准输出重定向到一个文件: -``` +```text $ cargo run to poem.txt > output.txt ``` -我们并不会在终端看到任何输出,同时`output.txt`将会包含其结果: +我们并不会在终端看到任何输出,同时 `output.txt` 将会包含其结果: -Filename: output.txt +文件名: output.txt -``` +```text Are you nobody, too? How dreary to be somebody! ``` -这一部分展示了现在我们使用的成功时产生的标准输出和错误时产生的标准错误是恰当的。 +这一部分展示了现在我们适当的使用成功时产生的标准输出和错误时产生的标准错误。 ## 总结 -在这一章中,我们回顾了目前为止的一些主要章节并涉及了如何在 Rust 中进行常规的 I/O 操作。通过使用命令行参数、文件、环境变量和`writeln!`宏与`stderr`,现在你已经准备好编写命令行程序了。结合前几章的知识,你的代码将会是组织良好的,并能有效的将数据存储到合适的数据结构中、更好的处理错误,并且还是经过良好测试的。 +在这一章中,我们回顾了目前为止的一些主要章节并涉及了如何在 Rust 环境中进行常规的 I/O 操作。通过使用命令行参数、文件、环境变量和打印错误的 `eprintln!` 宏,现在你已经准备好编写命令行程序了。通过结合前几章的知识,你的代码将会是组织良好的,并能有效的将数据存储到合适的数据结构中、更好的处理错误,并且还是经过良好测试的。 接下来,让我们探索如何利用一些 Rust 中受函数式编程语言影响的功能:闭包和迭代器。 \ No newline at end of file diff --git a/src/ch13-00-functional-features.md b/src/ch13-00-functional-features.md index 9bc13b5..ef032ee 100644 --- a/src/ch13-00-functional-features.md +++ b/src/ch13-00-functional-features.md @@ -2,15 +2,15 @@ > [ch13-00-functional-features.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch13-00-functional-features.md) >
-> commit 4f2dc564851dc04b271a2260c834643dfd86c724 +> commit d06a6a181fd61704cbf7feb55bc61d518c6469f9 -Rust 的设计灵感来源于很多前人的成果。影响 Rust 的其中之一就是函数式编程,在这里函数也是值并可以被用作参数或其他函数的返回值、赋值给变量等等。我们将回避解释函数式编程的具体是什么以及其优缺点,而是突出展示 Rust 中那些类似被认为是函数式的编程语言中的功能。 +Rust 的设计灵感来源于很多现存的语言和计数。其中一个显著的影响就是 **函数式编程**(*functional programming*)。函数式编程风格通常包含将函数作为参数值或其他函数的返回值、将函数赋值给变量以供之后执行等等。这里我们将不会辩论函数式编程到底是什么或不是什么,而是突出展示 Rust 中那些类似很多经常被认为是函数式的语言中功能的功能。 更具体的,我们将要涉及: * **闭包**(*Closures*),一个可以储存在变量里的类似函数的结构 * **迭代器**(*Iterators*),一种处理元素序列的方式。。 -* 如何使用这些功能来改进上一章的项目 -* 这些功能的性能。**剧透高能:**他们的速度超乎想象! +* 如何使用这些功能来改进第十二章的 I/O 项目 +* 这些功能的性能。**剧透高能:** 他们的速度超乎你的想象! -这并不是一个 Rust 受函数式风格影响的完整功能列表:还有模式匹配、枚举和很多其他功能。不过掌握闭包和迭代器则是编写符合语言风格的快速的 Rust 代码的重要一环。 \ No newline at end of file +还有其他受函数式风格影响的 Rust 功能,比如模式匹配和枚举,这些已经在其他章节中讲到过了。掌握闭包和迭代器则是编写符合语言风格的高性能 Rust 代码的重要一环,所以我们将专门用一整章来讲解他们。 \ No newline at end of file diff --git a/src/ch13-01-closures.md b/src/ch13-01-closures.md index 303ab29..a37b0d4 100644 --- a/src/ch13-01-closures.md +++ b/src/ch13-01-closures.md @@ -1,8 +1,12 @@ -## 闭包 +## 闭包:可以捕获环境的匿名函数 > [ch13-01-closures.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch13-01-closures.md) >
-> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56 +> commit 56352c28cf3fe0402fa5a7cba73890e314d720eb + +Rust 的闭包是可以保存进变量或作为参数传递给其他函数的匿名函数。 + + Rust 提供了定义**闭包**的能力,它类似于函数。让我们先不从技术上的定义开始,而是看看闭包语句结构,然后再返回他们的定义。列表 13-1 展示了一个被赋值给变量`add_one`的小的闭包定义,之后可以用这个变量来调用闭包: