wip: update ch12

This commit is contained in:
KaiserY 2017-04-18 14:55:09 +08:00
parent 3788259843
commit 2adb6692ec
78 changed files with 1499 additions and 519 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -2,48 +2,39 @@
> [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) > [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)
> <br> > <br>
> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56 > commit b8e4fcbf289b82c12121b282747ce05180afb1fb
现在为项目的核心功能编写测试将更加容易,因为我们将逻辑提取到了 *src/lib.rs* 中并将参数解析和错误处理都留在了 *src/main.rs* 里。现在我们可以直接使用多种参数调用代码并检查返回值而不用从命令行运行二进制文件了。 现在我们将逻辑提取到了 *src/lib.rs* 并将所有的参数解析和错误处理留在了 *src/main.rs* 中,为代码的核心功能编写测试将更加容易。我们可以直接使用多种参数调用函数并检查返回值而无需从命令行运行二进制文件了。
我们将要编写的是一个叫做`grep`的函数,它获取要搜索的项以及文本并产生一个搜索结果列表。让我们从`run`中去掉那行`println!`(也去掉 *src/main.rs* 中的,因为再也不需要他们了),并使用之前收集的选项来调用新的`grep`函数。眼下我们只增加一个空的实现,和指定`grep`期望行为的测试。当然,这个测试对于空的实现来说是会失败的,不过可以确保代码是可以编译的并得到期望的错误信息。列表 12-14 展示了这些修改: 在这一部分我们将遵循测试驱动开发Test Driven Development, TTD的模式。这是一个软件开发技术它遵循如下步骤
1. 编写一个会失败的测试,并运行它以确保其因为你期望的原因失败。
2. 编写或修改刚好足够的代码来使得新的测试通过。
3. 重构刚刚增加或修改的代码,并确保测试仍然能通过。
4. 重复上述步骤!
这只是众多编写软件的方法之一,不过 TDD 有助于驱动代码的设计。在编写能使测试通过的代码之前编写测测试有助于在开发过程中保持高测试覆盖率。
我们将测试驱动实现`greprs`实际在文件内容中搜索查询字符串并返回匹配的行列表的部分。我们将在一个叫做`search`的函数中增加这些功能。
### 编写失败测试
首先,去掉 *src/lib.rs**src/main.rs* 中的`println!`语句,因为不再真的需要他们了。接着我们会像第十一章那样增加一个`test`模块和一个测试函数。测试函数指定了我们希望`search`函数拥有的行为:它会获取一个需要查询的字符串和用来查询的文本。列表 12-15 展示了这个测试:
<span class="filename">Filename: src/lib.rs</span> <span class="filename">Filename: src/lib.rs</span>
```rust ```rust
# use std::error::Error; # fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
# use std::fs::File; # vec![]
# use std::io::prelude::*;
#
# pub struct Config {
# pub search: String,
# pub filename: String,
# } # }
# #
// ...snip...
fn grep<'a>(search: &str, contents: &'a str) -> Vec<&'a str> {
vec![]
}
pub fn run(config: Config) -> Result<(), Box<Error>>{
let mut f = File::open(config.filename)?;
let mut contents = String::new();
f.read_to_string(&mut contents)?;
grep(&config.search, &contents);
Ok(())
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use grep; use super::*;
#[test] #[test]
fn one_result() { fn one_result() {
let search = "duct"; let query = "duct";
let contents = "\ let contents = "\
Rust: Rust:
safe, fast, productive. safe, fast, productive.
@ -51,36 +42,54 @@ Pick three.";
assert_eq!( assert_eq!(
vec!["safe, fast, productive."], vec!["safe, fast, productive."],
grep(search, contents) search(query, contents)
); );
} }
} }
``` ```
<span class="caption">Listing 12-14: Creating a function where our logic will <span class="caption">Listing 12-15: Creating a failing test for the `search`
go and a failing test for that function</span> function we wish we had</span>
这里选择使用 "duct" 作为这个测试中需要搜索的字符串。用来搜索的文本有三行,其中只有一行包含 "duct"。我们断言`search`函数的返回值只包含期望的那一行。
我们还不能运行这个测试并看到它失败,因为它甚至都还不能编译!我们将增加足够的代码来使其能够编译:一个总是会返回空 vector 的`search`函数定义,如列表 12-16 所示。一旦有了它,这个测试应该能够编译并因为空 vector 并不匹配一个包含一行`"safe, fast, productive."`的 vector 而失败。
<span class="filename">Filename: src/lib.rs</span>
```rust
fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
vec![]
}
```
<span class="caption">Listing 12-16: Defining just enough of the `search`
function that our test will compile</span>
<!-- Will add ghosting and wingdings in libreoffice /Carol --> <!-- Will add ghosting and wingdings in libreoffice /Carol -->
注意需要在`grep`的签名中显式声明声明周期`'a`并用于`contents`参数和返回值。记住,生命周期参数用于指定函数参数于返回值的生命周期的关系。在这个例子中,我们表明返回的 vector 将包含引用参数`contents`的字符串 slice而不是引用参数`search`的字符串 slice。换一种说法就是我们告诉 Rust 函数`grep`返回的数据将和传递给它的参数`contents`的数据存活的同样久。这是非常重要的!考虑为了使引用有效则 slice 引用的数据也需要保持有效,如果编译器认为我们是在创建`search`而不是`contents`的 slice那么安全检查将是不正确的。如果尝试不用生命周期编译的话我们将得到如下错误 注意需要在`search`的签名中显式定义一个显式生命周期`'a`并用于`contents`参数和返回值。回忆一下第十章中生命周期参数指定哪个参数的生命周期与返回值的生命周期相关联。在这个例子中,我们表明返回的 vector 中应该包含引用参数`contents`(而不是参数`query` slice 的字符串 slice。
换句话说,我们告诉 Rust 函数`search`返回的数据将与`search`函数中的参数`contents`的数据存在的一样久。这是非常重要的!为了使这个引用有效那么**被**slice 引用的数据也需要保持有效;如果编译器认为我们是在创建`query`而不是`contents`的字符串 slice那么安全检查将是不正确的。
如果尝试不用生命周期编译的话,我们将得到如下错误:
``` ```
error[E0106]: missing lifetime specifier error[E0106]: missing lifetime specifier
--> src\lib.rs:37:46 --> src/lib.rs:5:47
| |
37 | fn grep(search: &str, contents: &str) -> Vec<&str> { 5 | fn search(query: &str, contents: &str) -> Vec<&str> {
| ^ expected lifetime parameter | ^ expected lifetime parameter
| |
= help: this function's return type contains a borrowed value, but the = help: this function's return type contains a borrowed value, but the
signature does not say whether it is borrowed from `search` or signature does not say whether it is borrowed from `query` or `contents`
`contents`
``` ```
Rust 不可能知道我们需要的是哪一个参数,所以需要告诉它。因为参数`contents`包含了所有的文本而且我们希望返回匹配的那部分文本,而我们知道`contents`是应该要使用生命周期语法来与返回值相关联的参数。 Rust 不可能知道我们需要的是哪一个参数,所以需要告诉它。因为参数`contents`包含了所有的文本而且我们希望返回匹配的那部分文本,而我们知道`contents`是应该要使用生命周期语法来与返回值相关联的参数。
在函数签名中将参数与返回值相关联是其他语言不会让你做的工作,所以不用担心这感觉很奇怪!掌握如何指定生命周期会随着时间的推移越来越容易,熟能生巧。你可能想要重新阅读上一部分或返回与第十章中生命周期语法部分的例子做对比。 其他语言中并不需要你在函数签名中将参数与返回值相关联,所以这么做可能仍然感觉有些陌生,随着时间的推移会越来越容易。你可能想要将这个例子与第十章中生命周期语法部分做对比。
现在试试运行测试: 现在试试运行测试:
``` ```
$ cargo test $ cargo test
@ -107,54 +116,81 @@ test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
error: test failed error: test failed
``` ```
好的,测试失败了,这正是我们所期望的。修改代码来让测试通过吧!之所以会失败是因为我们总是返回一个空的 vector。如下是如何实现`grep`的步骤: 好的,测试失败了,这正是我们所期望的。修改代码来让测试通过吧!
### 编写使测试通过的代码
目前测试之所以会失败是因为我们总是返回一个空的 vector。为了修复并实现`search`,我们的程序需要遵循如下步骤:
1. 遍历每一行文本。 1. 遍历每一行文本。
2. 查看这一行是否包含要搜索的字符串。 2. 查看这一行是否包含要搜索的字符串。
* 如果有,将这一行加入返回列表中 * 如果有,将这一行加入返回列表中
* 如果没有,什么也不做 * 如果没有,什么也不做
3. 返回匹配到的列表 3. 返回匹配到的列表
让我们一步一步的来,从遍历每行开始。字符串类型有一个有用的方法来处理这种情况,它刚好叫做`lines` 让我们一步一步的来,从遍历每行开始。
#### 使用`lines`方法遍历每一行
Rust 有一个有助于一行一行遍历字符串的方法,出于方便它被成为`lines`,它如列表 12-17 这样工作:
<span class="filename">Filename: src/lib.rs</span> <span class="filename">Filename: src/lib.rs</span>
```rust,ignore ```rust,ignore
fn grep<'a>(search: &str, contents: &'a str) -> Vec<&'a str> { fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
for line in contents.lines() { for line in contents.lines() {
// do something with line // do something with line
} }
} }
``` ```
<span class="caption">Listing 12-17: Iterating through each line in
`contents`</span>
<!-- Will add wingdings in libreoffice /Carol --> <!-- Will add wingdings in libreoffice /Carol -->
我们使用了一个`for`循环和`lines`方法来依次获得每一行。接下来,让我们看看这些行是否包含要搜索的字符串。幸运的是,字符串类型为此也有一个有用的方法`contains``contains`的用法看起来像这样: `lines`方法返回一个迭代器。第十三张会深入了解迭代器,不过我们已经在列表 3-6 中见过使用迭代器的方法,在那里使用了一个`for`循环和迭代器在一个集合的每一项上运行一些代码。
<!-- so what does `lines` do on its own, if we need to use it in a for loop to
work? -->
<!-- It does nothing on its own, it returns an iterator for you to do something
with. Here, the thing we're doing with it is using it with a `for` loop. I'm
not sure exactly what you're asking or how to make the text clearer, but I
added a reference to where we've done this in the book previously. /Carol -->
#### 用查询字符串搜索每一行
接下来将会增加检查当前行是否包含查询字符串的功能。幸运的是,字符串类型为此也有一个有用的方法叫做`contains`!如列表 12-18 所示在`search`函数中加入`contains`方法:
<span class="filename">Filename: src/lib.rs</span> <span class="filename">Filename: src/lib.rs</span>
```rust,ignore ```rust,ignore
fn grep<'a>(search: &str, contents: &'a str) -> Vec<&'a str> { fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
for line in contents.lines() { for line in contents.lines() {
if line.contains(search) { if line.contains(query) {
// do something with line // do something with line
} }
} }
} }
``` ```
<span class="caption">Listing 12-18: Adding functionality to see if the line
contains the string in `query`</span>
<!-- Will add ghosting and wingdings in libreoffice /Carol --> <!-- Will add ghosting and wingdings in libreoffice /Carol -->
最终,我们需要一个方法来存储包含要搜索字符串的行。为此可以在`for`循环之前创建一个可变的 vector 并调用`push`方法来存放一个`line`。在`for`循环之后,返回这个 vector。列表 12-15 中为完整的实现: #### 存储匹配的行
最后我们需要一个方法来存储包含查询字符串的行。为此可以在`for`循环之前创建一个可变的 vector 并调用`push`方法在 vector 中存放一个`line`。在`for`循环之后,返回这个 vector如列表 12-19 所示:
<span class="filename">Filename: src/lib.rs</span> <span class="filename">Filename: src/lib.rs</span>
```rust ```rust,ignore
fn grep<'a>(search: &str, contents: &'a str) -> Vec<&'a str> { fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new(); let mut results = Vec::new();
for line in contents.lines() { for line in contents.lines() {
if line.contains(search) { if line.contains(query) {
results.push(line); results.push(line);
} }
} }
@ -163,12 +199,12 @@ fn grep<'a>(search: &str, contents: &'a str) -> Vec<&'a str> {
} }
``` ```
<span class="caption">Listing 12-15: Fully functioning implementation of the <span class="caption">Listing 12-19: Storing the lines that match so that we
`grep` function</span> can return them</span>
<!-- Will add ghosting and wingdings in libreoffice /Carol --> <!-- Will add ghosting and wingdings in libreoffice /Carol -->
尝试运行一下 现在`search`函数应该返回只包含`query`的那些行,而测试应该会通过。让我们运行测试
``` ```
$ cargo test $ cargo test
@ -190,9 +226,24 @@ running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
``` ```
非常好!它可以工作了。现在测试通过了,我们可以考虑一下重构`grep`的实现并时刻保持其功能不变。这些代码并不坏,不过并没有利用迭代器的一些实用功能。第十三章将回到这个例子并探索迭代器和如何改进代码。 测试通过了,很好,它可以工作了!
现在`grep`函数是可以工作的,我们还需在在`run`函数中做最后一件事:还没有打印出结果呢!增加一个`for`循环来打印出`grep`函数返回的每一行: 现在测试通过了,我们可以考虑一下重构`search`的实现并时刻保持测试通过来保持其功能不变的机会了。这些代码并不坏,不过并没有利用迭代器的一些实用功能。第十三章将回到这个例子并深入探索迭代器并看看如何改进代码。
<!-- If we aren't going into this here, maybe just keep it focused, there's a
lot going on here as is -->
<!-- The reason we mention refactoring here is that it's a key step in the TDD
method that we were implicitly using before. Now that we've added text to the
beginning of this section to explicitly mention that we're doing TDD and what
the steps are, we want to address the "refactor" step. People who have some
experience with Rust might also look at this example and wonder why we're not
doing this in a different way, and be concerned that we're not teaching the
best way possible. This paragraph reassures them that we know what we're doing
and we're getting to the better way in Chapter 13. /Carol -->
#### 在`run`函数中使用`search`函数
现在`search`函数是可以工作并测试通过了的,我们需要实际在`run`函数中调用`search`。需要将`config.query`值和`run`从文件中读取的`contents`传递给`search`函数。接着`run`会打印出`search`返回的每一行:
<span class="filename">Filename: src/lib.rs</span> <span class="filename">Filename: src/lib.rs</span>
@ -203,7 +254,7 @@ pub fn run(config: Config) -> Result<(), Box<Error>> {
let mut contents = String::new(); let mut contents = String::new();
f.read_to_string(&mut contents)?; f.read_to_string(&mut contents)?;
for line in grep(&config.search, &contents) { for line in search(&config.query, &contents) {
println!("{}", line); println!("{}", line);
} }
@ -213,26 +264,36 @@ pub fn run(config: Config) -> Result<(), Box<Error>> {
<!-- Will add ghosting and wingdings in libreoffice /Carol --> <!-- Will add ghosting and wingdings in libreoffice /Carol -->
现在程序应该能正常运行了!试试吧: 这里再一次使用了`for`循环获取了`search`返回的每一行,而对每一行运行的代码将他们打印了出来。
现在整个程序应该可以工作了!让我们试一试,首先使用一个只会在艾米莉·狄金森的诗中返回一行的单词 "frog"
```
$ 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`
How public, like a frog
```
好的!接下来,像 "the" 这样会匹配多行的单词会怎么样呢:
``` ```
$ cargo run the poem.txt $ cargo run the poem.txt
Compiling greprs v0.1.0 (file:///projects/greprs)
Finished debug [unoptimized + debuginfo] target(s) in 0.38 secs
Running `target\debug\greprs.exe the poem.txt`
Then there's a pair of us - don't tell!
To tell your name the livelong day
$ cargo run a poem.txt
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target\debug\greprs.exe a poem.txt` Running `target/debug/greprs the poem.txt`
I'm nobody! Who are you? Then there's a pair of us — don't tell!
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
To tell your name the livelong day To tell your name the livelong day
To an admiring bog!
``` ```
好极了!我们创建了一个属于自己的经典工具,并学习了很多如何组织程序的知识。我们还学习了一些文件输入输出、生命周期、测试和命令行解析的内容。 最后,让我们确保搜索一个在诗中哪里都没有的单词时不会得到任何行,比如 "monomorphization"
```
$ cargo run monomorphization poem.txt
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/greprs monomorphization poem.txt`
```
非常好!我们创建了一个属于自己的经典工具,并学习了很多如何组织程序的知识。我们还学习了一些文件输入输出、生命周期、测试和命令行解析的内容。
现在如果你希望的话请随意移动到第十三章。为了使这个项目章节更丰满,我们将简要的展示如何处理环境变量和打印到标准错误,这两者在编写命令行程序时都很有用。

View File

@ -2,22 +2,34 @@
> [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) > [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)
> <br> > <br>
> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56 > commit 0db6a0a34886bf02feabcab8b430b5d332a8bdf5
让我们再增加一个功能:大小写不敏感搜索。另外,这个设定将不是一个命令行参数:相反它将是一个环境变量。当然可以选择创建一个大小写不敏感的命令行参数,不过用户要求提供一个环境变量这样设置一次之后在整个终端会话中所有的搜索都将是大小写不敏感的了。 我们将用一个额外的功能来改进我们的工具:一个通过环境变量启用的大小写不敏感搜索的选项。我们将其设计为一个命令行参数并要求用户每次需要时都加上它,不过相反我们将使用环境变量。这允许用户设置环境变脸一次之后在整个终端会话中所有的搜索都将是大小写不敏感的了。
### 实现并测试一个大小写不敏感`grep`函数 ### 编写一个大小写不敏感`search`函数的失败测试
首先,让我们增加一个新函数,当设置了环境变量时会调用它。增加一个新测试并重命名已经存在的那个: 首先,增加一个新函数,当设置了环境变量时会调用它。
```rust,ignore <!-- You mean, to turn the environment variable on? I'm not sure what we're
doing here-->
<!-- No, I'm not sure how this is unclear. We're adding a new function. We will
call the new function when the user turns on the environment variable. Can you
elaborate on what part of the above statement leads to the conclusion that the
new function is going to turn the environment variable on? Can you suggest a
rewording that makes the causality direction clearer? /Carol -->
这里将继续遵循上一部分开始使用的 TDD 过程,其第一步是再次编写一个失败测试。我们将为新的大小写不敏感搜索函数新增一个测试函数,并将老的测试函数从`one_result`改名为`case_sensitive`来更清除的表明这两个测试的区别,如列表 12-20 所示:
<span class="filename">Filename: src/lib.rs</span>
```rust
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use {grep, grep_case_insensitive}; use super::*;
#[test] #[test]
fn case_sensitive() { fn case_sensitive() {
let search = "duct"; let query = "duct";
let contents = "\ let contents = "\
Rust: Rust:
safe, fast, productive. safe, fast, productive.
@ -26,13 +38,13 @@ Duct tape.";
assert_eq!( assert_eq!(
vec!["safe, fast, productive."], vec!["safe, fast, productive."],
grep(search, contents) search(query, contents)
); );
} }
#[test] #[test]
fn case_insensitive() { fn case_insensitive() {
let search = "rust"; let query = "rUsT";
let contents = "\ let contents = "\
Rust: Rust:
safe, fast, productive. safe, fast, productive.
@ -41,7 +53,7 @@ Trust me.";
assert_eq!( assert_eq!(
vec!["Rust:", "Trust me."], vec!["Rust:", "Trust me."],
grep_case_insensitive(search, contents) search_case_insensitive(query, contents)
); );
} }
} }
@ -49,17 +61,26 @@ Trust me.";
<!-- Will add ghosting and wingdings in libreoffice /Carol --> <!-- Will add ghosting and wingdings in libreoffice /Carol -->
我们将定义一个叫做`grep_case_insensitive`的新函数。它的实现与`grep`函数大体上相似,不过列表 12-16 展示了一些小的区别: <span class="caption">Listing 12-20: Adding a new failing test for the case
insensitive function we're about to add</span>
注意我们也改变了老测试中`query`和`contents`的值:将查询字符串改变为 "duct",它将会匹配带有单词 productive" 的行。还新增了一个含有文本 "Duct tape" 的行,它有一个大写的 D这在大小写敏感搜索时不应该匹配 "duct"。我们修改这个测试以确保不会意外破坏已经实现的大小写敏感搜索功能;这个测试现在应该能通过并在处理大小写不敏感搜索时应该能一直通过。
大小写不敏感搜索的新测试使用带有一些大写字母的 "rUsT" 作为其查询字符串。我们将要增加的`search_case_insensitive`的期望返回值是包含查询字符串 "rust" 的两行,"Rust:" 包含一个大写的 R 还有"Trust me."包含一个小写的 r。这个测试现在会编译失败因为还没有定义`search_case_insensitive`函数;请随意增加一个总是返回空 vector 的骨架实现,正如列表 12-16 中`search`函数那样为了使测试编译并失败时所做的那样。
### 实现`search_case_insensitive`函数
`search_case_insensitive`函数,如列表 12-21 所示,将与`search`函数基本相同。区别是它会将`query`变量和每一`line`都变为小写,这样不管输入参数是大写还是小写,在检查该行是否包含查询字符串时都会是小写。
<span class="filename">Filename: src/lib.rs</span> <span class="filename">Filename: src/lib.rs</span>
```rust ```rust
fn grep_case_insensitive<'a>(search: &str, contents: &'a str) -> Vec<&'a str> { fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let search = search.to_lowercase(); let query = query.to_lowercase();
let mut results = Vec::new(); let mut results = Vec::new();
for line in contents.lines() { for line in contents.lines() {
if line.to_lowercase().contains(&search) { if line.to_lowercase().contains(&query) {
results.push(line); results.push(line);
} }
} }
@ -68,19 +89,32 @@ fn grep_case_insensitive<'a>(search: &str, contents: &'a str) -> Vec<&'a str> {
} }
``` ```
<span class="caption">Listing 12-16: Implementing a `grep_case_insensitive` <span class="caption">Listing 12-21: Defining the `search_case_insensitive`
function by changing the search string and the lines of the contents to function to lowercase both the query and the line before comparing them</span>
lowercase before comparing them</span>
<!-- Will add ghosting and wingdings in libreoffice /Carol --> <!-- Will add ghosting and wingdings in libreoffice /Carol -->
首先,将`search`字符串转换为小写,并存放于一个同名的覆盖变量中。注意现在`search`是一个`String`而不是字符串 slice所以在将`search`传递给`contains`时需要加上 &,因为`contains`获取一个字符串 slice。 <!-- why do we lowercase the search string? and why does it need to be a string
rather than a slice? -->
<!-- We explained this above, that in order to make the search case
insensitive, we need to lowercase everything so that searches will always match
no matter what case either the query or each line uses. It needs to be a
`String` because we're creating new data, not referencing existing data, when
we call `to_lowercase`. I've tried to make both of these points clearer, but
I'm not sure exactly what was unclear about it before, so I'm not sure if I've
helped. /Carol -->
接着在检查每个`line`是否包含`search`之前增加了一个`to_lowercase`调用。因为将`line`和`search`都转换为小写,我们就可以无视大小写的匹配文件和命令行参数了。看看测试是否通过了: 首先我们将`query`字符串转换为小写,并将其储存(覆盖)到同名的变量中。对查询字符串调用`to_lowercase`是必需的这样不管用户的查询是"rust"、"RUST"、"Rust"或者"rUsT",我们都将其当作"rust"处理并对大小写不敏感。
注意`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`都转换成了小写,这样就可以不管大小写的匹配文件中的文本和用户输入的查询了。
让我们看看这个实现能否通过测试:
``` ```
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
Running target\debug\deps\greprs-e58e9b12d35dc861.exe Running target/debug/deps/greprs-e58e9b12d35dc861
running 2 tests running 2 tests
test test::case_insensitive ... ok test test::case_insensitive ... ok
@ -88,7 +122,7 @@ test test::case_sensitive ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured
Running target\debug\greprs-8a7faa2662b5030a.exe Running target/debug/greprs-8a7faa2662b5030a
running 0 tests running 0 tests
@ -101,13 +135,13 @@ running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
``` ```
好的!现在,我们必须真正的使用新的`grep_case_insensitive`函数。首先,在`Config`结构体中增加一个配置项: 好的!现在,让我们在`run`函数中调用真正的新`search_case_insensitive`函数。首先,我们将在`Config`结构体中增加一个配置项来切换大小写敏感和大小写不敏感搜索。
<span class="filename">Filename: src/lib.rs</span> <span class="filename">Filename: src/lib.rs</span>
```rust ```rust
pub struct Config { pub struct Config {
pub search: String, pub query: String,
pub filename: String, pub filename: String,
pub case_sensitive: bool, pub case_sensitive: bool,
} }
@ -115,11 +149,29 @@ pub struct Config {
<!-- Will add ghosting in libreoffice /Carol --> <!-- Will add ghosting in libreoffice /Carol -->
接着在`run`函数中检查这个选项,并根据`case_sensitive`函数的值来决定调用哪个函数 这里增加了`case_sensitive`字符来存放一个布尔值。接着我们需要`run`函数检查`case_sensitive`字段的值并使用它来决定是否调用`search`函数或`search_case_insensitive`函数,如列表 12-22所示
<span class="filename">Filename: src/lib.rs</span> <span class="filename">Filename: src/lib.rs</span>
```rust,ignore ```rust
# use std::error::Error;
# use std::fs::File;
# use std::io::prelude::*;
#
# fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
# vec![]
# }
#
# fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
# vec![]
# }
#
# struct Config {
# query: String,
# filename: String,
# case_sensitive: bool,
# }
#
pub fn run(config: Config) -> Result<(), Box<Error>>{ pub fn run(config: Config) -> Result<(), Box<Error>>{
let mut f = File::open(config.filename)?; let mut f = File::open(config.filename)?;
@ -127,9 +179,9 @@ pub fn run(config: Config) -> Result<(), Box<Error>>{
f.read_to_string(&mut contents)?; f.read_to_string(&mut contents)?;
let results = if config.case_sensitive { let results = if config.case_sensitive {
grep(&config.search, &contents) search(&config.query, &contents)
} else { } else {
grep_case_insensitive(&config.search, &contents) search_case_insensitive(&config.query, &contents)
}; };
for line in results { for line in results {
@ -140,48 +192,38 @@ pub fn run(config: Config) -> Result<(), Box<Error>>{
} }
``` ```
<span class="caption">Listing 12-22: Calling either `search` or
`search_case_insensitive` based on the value in `config.case_sensitive`</span>
<!-- Will add ghosting in libreoffice /Carol --> <!-- Will add ghosting in libreoffice /Carol -->
最后需要真正的检查环境变量。为了将标准库中的`env`模块引入作用域,在 *src/lib.rs* 开头增加一个`use`行: 最后需要实际检查环境变量。处理环境变量的函数位于标准库的`env`模块中,所以我们需要在 *src/lib.rs* 的开头增加一个`use std::env;`行将这个模块引入作用域中。接着在`Config::new`中使用`env`模块的`var`方法检查一个叫做`CASE_INSENSITIVE`的环境变量,如列表 12-23 所示
<span class="filename">Filename: src/lib.rs</span> <span class="filename">Filename: src/lib.rs</span>
```rust ```rust
use std::env; use std::env;
```
并接着在`Config::new`中使用`env`模块的`vars`方法:
<span class="filename">Filename: src/lib.rs</span>
```rust
# use std::env;
#
# struct Config { # struct Config {
# search: String, # query: String,
# filename: String, # filename: String,
# case_sensitive: bool, # case_sensitive: bool,
# } # }
#
// ...snip...
impl Config { impl Config {
pub fn new(args: &[String]) -> Result<Config, &'static str> { pub fn new(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 { if args.len() < 3 {
return Err("not enough arguments"); return Err("not enough arguments");
} }
let search = args[1].clone(); let query = args[1].clone();
let filename = args[2].clone(); let filename = args[2].clone();
let mut case_sensitive = true; let case_sensitive = env::var("CASE_INSENSITIVE").is_err();
for (name, _) in env::vars() {
if name == "CASE_INSENSITIVE" {
case_sensitive = false;
}
}
Ok(Config { Ok(Config {
search: search, query: query,
filename: filename, filename: filename,
case_sensitive: case_sensitive, case_sensitive: case_sensitive,
}) })
@ -189,32 +231,37 @@ impl Config {
} }
``` ```
<span class="caption">Listing 12-23: Checking for an environment variable named
`CASE_INSENSITIVE`</span>
<!-- Will add ghosting and wingdings in libreoffice /Carol --> <!-- Will add ghosting and wingdings in libreoffice /Carol -->
这里我们调用了`env::vars`,它与`env::args`的工作方式类似。区别是`env::vars`返回一个环境变量而不是命令行参数的迭代器。不同于使用`collect`来创建一个所有环境变量的 vector我们使用`for`循环。`env::vars`返回一系列元组:环境变量的名称和其值。我们从来也不关心它的值,只关心它是否被设置了,所以可以使用`_`占位符来取代变量名来让 Rust 知道它不应该警告一个未使用的变量。最后,有一个默认为真的变量`case_sensitive`。如果我们找到了一个`CASE_INSENSITIVE`环境变量,就将`case_sensitive`设置为假。接着将其作为`Config`的一部分返回。 这里创建了一个新变量`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`
尝试运行几次吧! 让我们试一试吧!首先不设置环境变量并使用查询"to"运行程序,这应该会匹配任何全小写的单词"to"的行:
``` ```text
$ cargo run to poem.txt $ cargo run to poem.txt
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target\debug\greprs.exe to poem.txt` Running `target/debug/greprs to poem.txt`
Are you nobody, too? Are you nobody, too?
How dreary to be somebody! How dreary to be somebody!
``` ```
看起来程序仍然能够工作!现在将`CASE_INSENSITIVE`设置为 1 并仍使用相同的查询"to",这回应该得到包含可能有大写字母的"to"的行:
``` ```
$ CASE_INSENSITIVE=1 cargo run to poem.txt $ CASE_INSENSITIVE=1 cargo run to poem.txt
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target\debug\greprs.exe to poem.txt` Running `target/debug/greprs 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
To an admiring bog! To an admiring bog!
``` ```
好极了`greprs`现在可以通过环境变量的控制来进行大小写不敏感搜索了。现在你已经知道如何处理命令行参数或环境变量了! 好极了,我们也得到了包含"To"的行!现在`greprs`程序可以通过环境变量控制进行大小写不敏感搜索了。现在你知道了如何管理由命令行参数或环境变量设置的选项了!
一些程序允许对相同配置同时使用参数_和_环境变量。在这种情况下,程序来决定参数和环境变量的优先级。作为一个留给你的测试,尝试同时通过一个命令行参数来控制大小写不敏感搜索,并在程序遇到矛盾值时决定其优先级。 一些程序允许对相同配置同时使用参数**和**环境变量。在这种情况下,程序来决定参数和环境变量的优先级。作为一个留给你的测试,尝试同时通过一个命令行参数来控制大小写不敏感搜索,并在程序遇到矛盾值时决定其优先级。
`std::env`模块还包含了更多处理环境变量的实用功能;请查看官方文档来了解其可用的功能。 `std::env`模块还包含了更多处理环境变量的实用功能;请查看官方文档来了解其可用的功能。

View File

@ -2,23 +2,39 @@
> [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) > [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)
> <br> > <br>
> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56 > commit d09cfb51a239c0ebfc056a64df48fe5f1f96b207
目前为止,我们将所有的输出都`println!`到了终端。这是可以的,不过大部分终端都提供了两种输出:“标准输出”对应大部分信息,而“标准错误”则用于错误信息。这使得处理类似于“将错误打印到终端而将其他信息输出到文件”的情况变得更容易 目前为止,我们将所有的输出都`println!`到了终端。大部分终端都提供了两种输出:“标准输出”对应大部分信息,而“标准错误”则用于错误信息。这种区别是命令行程序所期望拥有的行为:例如它允许用户选择将程序正常输出定向到一个文件中并仍将错误信息打印到屏幕上。但是`println!`只能够打印到标准输出,所以我们必需使用其他方法来打印到标准错误
可以通过在命令行使用`>`来将输出重定向到文件中,同时不使用任何参数运行来造成一个错误,就会发现我们的程序只能打印到`stdout` 我们可以验证,目前所编写的`greprs`,所有内容都被打印到了标准输出,包括应该被写入标准错误的错误信息。可以通过故意造成错误来做到这一点,一个发生这种情况的方法是不使用任何参数运行程序。我们准备将标准输出重定向到一个文件中,不过不是标准错误。命令行程序期望以这种方式工作,因为如果输出是错误信息,它应该显示在屏幕上而不是被重定向到文件中。可以看出我们的程序目前并没有满足这个期望,通过使用`>`并指定一个文件名,*output.txt*,这是期望将标注输出重定向的文件
``` ```
$ cargo run > output.txt $ cargo run > output.txt
``` ```
`>`语法告诉 shell 将标准输出的内容写入到 *output.txt* 文件中而不是打印到屏幕上。然而,如果运行命令后打开 *output.txt* 就会发现错误: <!-- why do we get an error here? Was that intentional? Does that mean it can't
print stdout to a file? -->
<!-- Yes, we're intentionally causing an error here to show that errors are
currently going to the wrong place. It's showing that `println!` only prints
to standard out, even when we're printing error messages that should go
to standard error. /Carol-->
`>`语法告诉 shell 将标准输出的内容写入到 *output.txt* 文件中而不是打印到屏幕上。我们并没有看到期望的错误信息打印到屏幕上,所以这意味着它一定被写入了文件中。让我们看看 *output.txt* 包含什么:
``` ```
Problem parsing arguments: not enough arguments Application error: No search string or filename found
``` ```
我们希望这个信息被打印到屏幕上,而只有成功运行产生的输出写入到文件中。让我们如列表 12-17 中所示改变如何打印错误信息的方法: <!-- I don't understand why we send this output to a file to then just say we
want it to the screen, won't it do that by default? And what has this got to do
with our use of println? I'm finding the motives here hard to follow -->
<!-- The point of showing this is to demonstrate that our program is NOT doing
the correct thing by default, we need to change the places we're calling
`println!` with error messages to print to standard error instead. When to use
stdout vs. stderr, and why you might want to redirect stdout but not stderr,
is something our readers will be familiar with. /Carol -->
是的,这就是错误信息,这意味着它被打印到了标准输出。这并不是命令行程序所期望拥有的。像这样的错误信息被打印到标准错误,并当以这种方式重定向标注输出时只将运行成功时的数据打印到文件中。让我们像列表 12-23 所示改变错误信息如何被打印的。因为本章早些时候的进行的重构,所有打印错误信息的代码都在一个位置,在`main`中:
<span class="filename">Filename: src/main.rs</span> <span class="filename">Filename: src/main.rs</span>
@ -32,8 +48,8 @@ use std::io::prelude::*;
use greprs::Config; use greprs::Config;
fn main() { fn main() {
let mut stderr = std::io::stderr();
let args: Vec<String> = env::args().collect(); let args: Vec<String> = env::args().collect();
let mut stderr = std::io::stderr();
let config = Config::new(&args).unwrap_or_else(|err| { let config = Config::new(&args).unwrap_or_else(|err| {
writeln!( writeln!(
@ -41,12 +57,10 @@ fn main() {
"Problem parsing arguments: {}", "Problem parsing arguments: {}",
err err
).expect("Could not write to stderr"); ).expect("Could not write to stderr");
process::exit(1); process::exit(1);
}); });
if let Err(e) = greprs::run(config) { if let Err(e) = greprs::run(config) {
writeln!( writeln!(
&mut stderr, &mut stderr,
"Application error: {}", "Application error: {}",
@ -58,27 +72,29 @@ fn main() {
} }
``` ```
<span class="caption">Listing 12-17: Writing error messages to `stderr` instead <span class="caption">Listing 12-23: Writing error messages to `stderr` instead
of `stdout`</span> of `stdout` using `writeln!`</span>
<!-- Will add ghosting and wingdings in libreoffice /Carol --> <!-- Will add ghosting and wingdings in libreoffice /Carol -->
Rust 并没有类似`println!`这样的方便写入标准错误的函数。相反,我们使用`writeln!`宏,它有点像`println!`,不过它获取一个额外的参数。第一个参数是希望写入内容的位置。可以通过`std::io::stderr`函数获取一个标准错误的句柄。我们将一个`stderr`的可变引用传递给`writeln!`;它需要是可变的因为这样才能写入信息!第二个和第三个参数就像`println!`的第一个和第二参数:一个格式化字符串和任何需要插入的变量。 Rust 并没有类似`println!`这样的方便写入标准错误的函数。相反,我们使用`writeln!`宏,它有点像`println!`,不过它获取一个额外的参数。第一个参数是希望写入内容的位置。可以通过`std::io::stderr`函数获取一个标准错误的句柄。我们将一个`stderr`的可变引用传递给`writeln!`;它需要是可变的因为这样才能写入信息!第二个和第三个参数就像`println!`的第一个和第二参数:一个格式化字符串和任何需要插入的变量。
让我们再次用相同方式运行程序,不带任何参数并用 `>`重定向`stdout` 再次用相同方式运行程序,不带任何参数并用`>`重定向`stdout`
``` ```
$ cargo run > output.txt $ cargo run > output.txt
Problem parsing arguments: not enough arguments Application error: No search string or filename found
``` ```
现在我们看到了屏幕上的错误信息,不过 `output.txt` 里什么也没有。如果我们使用正确的参数再次运行: 现在我们看到了屏幕上的错误信息,不过`output.txt`里什么也没有,这也就是命令行程序所期望的行为。
如果使用不会造成错误的参数再次运行程序,不过仍然将标准输出重定向到一个文件:
``` ```
$ cargo run to poem.txt > output.txt $ cargo run to poem.txt > output.txt
``` ```
终端将没有输出,不过 `output.txt` 将会包含其结果: 我们并不会在终端看到任何输出,同时`output.txt`将会包含其结果:
<span class="filename">Filename: output.txt</span> <span class="filename">Filename: output.txt</span>
@ -87,8 +103,10 @@ Are you nobody, too?
How dreary to be somebody! How dreary to be somebody!
``` ```
这一部分展示了现在我们使用的成功时产生的标准输出和错误时产生的标准错误是恰当的。
## 总结 ## 总结
在这一章,我们涉及了如果在 Rust 中进行常规的 I/O 操作。通过使用命令行参数、文件、环境变量和写入`stderr`的功能。现在你已经准备好编写命令行程序了。结合前几章的知识,你的代码将会是组织良好的,并能有效的将数据存储到合适的数据结构中、更好的处理错误,并且还是经过良好测试的。我们也接触了一个真实情况下需要生命周期注解来保证引用一直有效的场景。 在这一章中,我们回顾了目前为止的一些主要章节并涉及了如何在 Rust 中进行常规的 I/O 操作。通过使用命令行参数、文件、环境变量和`writeln!`宏与`writeln!`现在你已经准备好编写命令行程序了。结合前几章的知识,你的代码将会是组织良好的,并能有效的将数据存储到合适的数据结构中、更好的处理错误,并且还是经过良好测试的。
接下来,让我们探索如何利用一些 Rust 中受函数式编程语言影响的功能:闭包和迭代器。 接下来,让我们探索如何利用一些 Rust 中受函数式编程语言影响的功能:闭包和迭代器。