From edb730368901aae5f313c7f1fb95fe109832e3f6 Mon Sep 17 00:00:00 2001 From: KaiserY Date: Mon, 6 Mar 2017 22:56:55 +0800 Subject: [PATCH] wip ch12-04 --- ...proving-error-handling-and-modularity.html | 317 ++++++++++ ...04-testing-the-librarys-functionality.html | 207 +++++- ...05-working-with-environment-variables.html | 70 ++- docs/print.html | 590 ++++++++++++++++++ ...improving-error-handling-and-modularity.md | 383 ++++++++++++ ...2-04-testing-the-librarys-functionality.md | 250 ++++++++ ...2-05-working-with-environment-variables.md | 83 +++ 7 files changed, 1898 insertions(+), 2 deletions(-) diff --git a/docs/ch12-03-improving-error-handling-and-modularity.html b/docs/ch12-03-improving-error-handling-and-modularity.html index 530fbc7..54401ee 100644 --- a/docs/ch12-03-improving-error-handling-and-modularity.html +++ b/docs/ch12-03-improving-error-handling-and-modularity.html @@ -353,6 +353,323 @@ impl Config { +

现在new函数返回一个Result,在成功时带有一个Config实例而在出现错误时带有一个&'static str。回忆一下第十章“静态声明周期”中讲到&'static str是一个字符串字面值,他也是现在我们的错误信息。

+

new函数体中有两处修改:当没有足够参数时不再调用panic!,而是返回Err值。同时我们将Config返回值包装进Ok成员中。这些修改使得函数符合其新的类型签名。

+

Config::new调用和错误处理

+

现在我们需要对main做一些修改,如列表 12-9 所示:

+
+Filename: src/main.rs +
# use std::env;
+# use std::fs::File;
+# use std::io::prelude::*;
+// ...snip...
+use std::process;
+
+fn main() {
+    let args: Vec<String> = env::args().collect();
+
+    let config = Config::new(&args).unwrap_or_else(|err| {
+        println!("Problem parsing arguments: {}", err);
+        process::exit(1);
+    });
+
+    println!("Searching for {}", config.search);
+    println!("In file {}", config.filename);
+
+    // ...snip...
+#
+#     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");
+#
+#     println!("With text:\n{}", contents);
+# }
+#
+# struct Config {
+#     search: String,
+#     filename: String,
+# }
+#
+# impl Config {
+#     fn new(args: &[String]) -> Result<Config, &'static str> {
+#         if args.len() < 3 {
+#             return Err("not enough arguments");
+#         }
+#
+#         let search = args[1].clone();
+#         let filename = args[2].clone();
+#
+#         Ok(Config {
+#             search: search,
+#             filename: filename,
+#         })
+#     }
+# }
+
+
+

Listing 12-9: Exiting with an error code if creating a new Config fails

+
+
+ +

新增了一个use行来从标准库中导入process。在main函数中我们将处理new函数返回的Result值,并在其返回Config::new时以一种更加清楚的方式结束进程。

+

这里使用了一个之前没有讲到的标准库中定义的Result<T, E>的方法:unwrap_or_else。当ResultOk时其行为类似于unwrap:它返回Ok内部封装的值。与unwrap不同的是,当ResultErr时,它调用一个闭包closure),也就是一个我们定义的作为参数传递给unwrap_or_else的匿名函数。第XX章会更详细的介绍闭包;这里需要理解的重要部分是unwrap_or_else会将Err的内部值传递给闭包中位于两道竖线间的参数err。使用unwrap_or_else允许我们进行一些自定义的非panic!的错误处理。

+

上述的错误处理其实只有两行:我们打印出了错误,接着调用了std::process::exit。这个函数立刻停止程序的执行并将传递给它的数组作为返回码。依照惯例,零代表成功而任何其他数字表示失败。就结果来说这依然类似于列表 12-7 中的基于panic!的错误处理,但是不再会有额外的输出了,让我们试一试:

+
$ cargo run
+   Compiling greprs v0.1.0 (file:///projects/greprs)
+    Finished debug [unoptimized + debuginfo] target(s) in 0.48 secs
+     Running `target\debug\greprs.exe`
+Problem parsing arguments: not enough arguments
+
+

非常好!现在输出就友好多了。

+

run函数中的错误处理

+

现在重构完了参数解析部分,让我们再改进一下程序的逻辑。列表 12-10 中展示了在main函数中调用提取出函数run之后的代码。run函数包含之前位于main中的部分代码:

+
+Filename: src/main.rs +
# use std::env;
+# use std::fs::File;
+# use std::io::prelude::*;
+# use std::process;
+#
+fn main() {
+#     let args: Vec<String> = env::args().collect();
+#
+#     let config = Config::new(&args).unwrap_or_else(|err| {
+#         println!("Problem parsing arguments: {}", err);
+#         process::exit(1);
+#     });
+    // ...snip...
+
+    println!("Searching for {}", config.search);
+    println!("In file {}", config.filename);
+
+    run(config);
+}
+
+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");
+
+    println!("With text:\n{}", contents);
+}
+
+// ...snip...
+#
+# struct Config {
+#     search: String,
+#     filename: String,
+# }
+#
+# impl Config {
+#     fn new(args: &[String]) -> Result<Config, &'static str> {
+#         if args.len() < 3 {
+#             return Err("not enough arguments");
+#         }
+#
+#         let search = args[1].clone();
+#         let filename = args[2].clone();
+#
+#         Ok(Config {
+#             search: search,
+#             filename: filename,
+#         })
+#     }
+# }
+
+
+

Listing 12-10: Extracting a run functionality for the rest of the program logic

+
+
+ +

run函数的内容是之前位于main中的几行,而且run函数获取一个Config作为参数。现在有了一个单独的函数了,我们就可以像列表 12-8 中的Config::new那样进行类似的改进了。列表 12-11 展示了另一个use语句将std::error::Error结构引入了作用域,还有使run函数返回Result的修改:

+
+Filename: src/main.rs +
use std::error::Error;
+# use std::env;
+# use std::fs::File;
+# use std::io::prelude::*;
+# use std::process;
+
+// ...snip...
+# fn main() {
+#     let args: Vec<String> = env::args().collect();
+#
+#     let config = Config::new(&args).unwrap_or_else(|err| {
+#         println!("Problem parsing arguments: {}", err);
+#         process::exit(1);
+#     });
+#
+#     println!("Searching for {}", config.search);
+#     println!("In file {}", config.filename);
+#
+#     run(config);
+#
+# }
+
+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)?;
+
+    println!("With text:\n{}", contents);
+
+    Ok(())
+}
+#
+# struct Config {
+#     search: String,
+#     filename: String,
+# }
+#
+# impl Config {
+#     fn new(args: &[String]) -> Result<Config, &'static str> {
+#         if args.len() < 3 {
+#             return Err("not enough arguments");
+#         }
+#
+#         let search = args[1].clone();
+#         let filename = args[2].clone();
+#
+#         Ok(Config {
+#             search: search,
+#             filename: filename,
+#         })
+#     }
+# }
+
+
+

Listing 12-11: Changing the run function to return Result

+
+
+ +

这里有三个大的修改。第一个是现在run函数的返回值是Result<(), Box<Error>>类型的。之前,函数返回 unit 类型(),现在它仍然是Ok时的返回值。对于错误类型,我们将使用Box<Error>。这是一个trait 对象trait object),第XX章会讲到。现在可以这样理解它:Box<Error>意味着函数返回了某个实现了Error trait 的类型,不过并没有指定具体的返回值类型。这样就比较灵活,因为在不同的错误场景可能有不同类型的错误返回值。Box是一个堆数据的智能指针,第YY章将会详细介绍Box

+

第二个改变是我们去掉了expect调用并替换为第9章讲到的?。不同于遇到错误就panic!,这会从函数中返回错误值并让调用者来处理它。

+

第三个修改是现在成功时这个函数会返回一个Ok值。因为run函数签名中声明成功类型返回值是(),所以需要将 unit 类型值包装进Ok值中。Ok(())一开始看起来有点奇怪,不过这样使用()是表明我们调用run只是为了它的副作用的惯用方式;它并没有返回什么有意义的值。

+

上述代码能够编译,不过会有一个警告:

+
warning: unused result which must be used, #[warn(unused_must_use)] on by default
+  --> src\main.rs:39:5
+   |
+39 |     run(config);
+   |     ^^^^^^^^^^^^
+
+

Rust 尝试告诉我们忽略Result,它有可能是一个错误值。让我们现在来处理它。我们将采用类似于列表 12-9 中处理Config::new错误的技巧,不过还有少许不同:

+

Filename: src/main.rs

+
fn main() {
+    // ...snip...
+
+    println!("Searching for {}", config.search);
+    println!("In file {}", config.filename);
+
+    if let Err(e) = run(config) {
+        println!("Application error: {}", e);
+
+        process::exit(1);
+    }
+}
+
+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)?;
+
+    println!("With text:\n{}", contents);
+
+    Ok(())
+}
+
+ +

不同于unwrap_or_else,我们使用if let来检查run是否返回Err,如果是则调用process::exit(1)。为什么呢?这个例子和Config::new的区别有些微妙。对于Config::new我们关心两件事:

+
    +
  1. 检测出任何可能发生的错误
  2. +
  3. 如果没有出现错误创建一个Config
  4. +
+

而在这个情况下,因为run在成功的时候返回一个(),唯一需要担心的就是第一件事:检测错误。如果我们使用了unwrap_or_else,则会得到()的返回值。它并没有什么用处。

+

虽然两种情况下if letunwrap_or_else的内容都是一样的:打印出错误并退出。

+

将代码拆分到库 crate

+

现在项目看起来好多了!还有一件我们尚未开始的工作:拆分 src/main.rs 并将一些代码放入 src/lib.rs 中。让我们现在就开始吧:将 src/main.rs 中的run函数移动到新建的 src/lib.rs 中。还需要移动相关的use语句和Config的定义,以及其new方法。现在 src/lib.rs 应该如列表 12-12 所示:

+
+Filename: src/lib.rs +
use std::error::Error;
+use std::fs::File;
+use std::io::prelude::*;
+
+pub struct Config {
+    pub search: String,
+    pub filename: String,
+}
+
+impl Config {
+    pub fn new(args: &[String]) -> Result<Config, &'static str> {
+        if args.len() < 3 {
+            return Err("not enough arguments");
+        }
+
+        let search = args[1].clone();
+        let filename = args[2].clone();
+
+        Ok(Config {
+            search: search,
+            filename: filename,
+        })
+    }
+}
+
+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)?;
+
+    println!("With text:\n{}", contents);
+
+    Ok(())
+}
+
+
+

Listing 12-12: Moving Config and run into src/lib.rs

+
+
+ +

注意我们还需要使用公有的pub:在Config和其字段、它的new方法和run函数上。

+

现在在 src/main.rs 中,我们需要通过extern crate greprs来引入现在位于 src/lib.rs 的代码。接着需要增加一行use greprs::Config来引入Config到作用域,并对run函数加上 crate 名称前缀,如列表 12-13 所示:

+
+Filename: src/main.rs +
extern crate greprs;
+
+use std::env;
+use std::process;
+
+use greprs::Config;
+
+fn main() {
+    let args: Vec<String> = env::args().collect();
+
+    let config = Config::new(&args).unwrap_or_else(|err| {
+        println!("Problem parsing arguments: {}", err);
+        process::exit(1);
+    });
+
+    println!("Searching for {}", config.search);
+    println!("In file {}", config.filename);
+
+    if let Err(e) = greprs::run(config) {
+        println!("Application error: {}", e);
+
+        process::exit(1);
+    }
+}
+
+
+

Listing 12-13: Bringing the greprs crate into the scope of src/main.rs

+
+
+ +

通过这些重构,所有代码应该都能运行了。运行几次cargo run来确保你没有破坏什么内容。好的!确实有很多的内容,不过已经为将来的成功奠定了基础。我们采用了一种更加优秀的方式来处理错误,并使得代码更模块化了一些。从现在开始几乎所有的工作都将在 src/lib.rs 中进行。

+

让我们利用这新创建的模块的优势来进行一些在旧代码中难以开开展的工作,他们在新代码中却很简单:编写测试!

diff --git a/docs/ch12-04-testing-the-librarys-functionality.html b/docs/ch12-04-testing-the-librarys-functionality.html index f6559c9..640603d 100644 --- a/docs/ch12-04-testing-the-librarys-functionality.html +++ b/docs/ch12-04-testing-the-librarys-functionality.html @@ -67,7 +67,212 @@
- +

测试库的功能

+
+

ch12-04-testing-the-librarys-functionality.md +
+commit 4f2dc564851dc04b271a2260c834643dfd86c724

+
+

现在为项目的核心功能编写测试将更加容易,因为我们将逻辑提取到了 src/lib.rs 中并将参数解析和错误处理都留在了 src/main.rs 里。现在我们可以直接使用多种参数调用代码并检查返回值而不用从命令行运行二进制文件了。

+

我们将要编写的是一个叫做grep的函数,它获取要搜索的项以及文本并产生一个搜索结果列表。让我们从run中去掉那行println!(也去掉 src/main.rs 中的,因为再也不需要他们了),并使用之前收集的选项来调用新的grep函数。眼下我们只增加一个空的实现,和指定grep期望行为的测试。当然,这个测试对于空的实现来说是会失败的,不过可以确保代码是可以编译的并得到期望的错误信息。列表 12-14 展示了这些修改:

+
+Filename: src/lib.rs +
# use std::error::Error;
+# use std::fs::File;
+# 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)]
+mod test {
+    use grep;
+
+    #[test]
+    fn one_result() {
+        let search = "duct";
+        let contents = "\
+Rust:
+safe, fast, productive.
+Pick three.";
+
+        assert_eq!(
+            vec!["safe, fast, productive."],
+            grep(search, contents)
+        );
+    }
+}
+
+
+

Listing 12-14: Creating a function where our logic will go and a failing test +for that function

+
+
+ +

注意需要在grep的签名中显式声明声明周期'a并用于contents参数和返回值。记住,生命周期参数用于指定函数参数于返回值的生命周期的关系。在这个例子中,我们表明返回的 vector 将包含引用参数contents的字符串 slice,而不是引用参数search的字符串 slice。换一种说法就是我们告诉 Rust 函数grep返回的数据将和传递给它的参数contents的数据存活的同样久。这是非常重要的!考虑为了使引用有效则 slice 引用的数据也需要保持有效,如果编译器认为我们是在创建search而不是contents的 slice,那么安全检查将是不正确的。如果尝试不用生命周期编译的话,我们将得到如下错误:

+
error[E0106]: missing lifetime specifier
+  --> src\lib.rs:37:46
+   |
+37 | fn grep(search: &str, contents: &str) -> Vec<&str> {
+   |                                              ^ expected lifetime parameter
+   |
+   = help: this function's return type contains a borrowed value, but the
+       signature does not say whether it is borrowed from `search` or
+           `contents`
+
+

Rust 不可能知道我们需要的是哪一个参数,所以需要告诉它。因为参数contents包含了所有的文本而且我们希望返回匹配的那部分文本,而我们知道contents是应该要使用生命周期语法来与返回值相关联的参数。

+

在函数签名中将参数与返回值相关联是其他语言不会让你做的工作,所以不用担心这感觉很奇怪!掌握如何指定生命周期会随着时间的推移越来越容易,熟能生巧。你可能想要重新阅读上一部分或返回与第十章中生命周期语法部分的例子做对比。

+

现在试试运行测试:

+
$ cargo test
+...warnings...
+    Finished debug [unoptimized + debuginfo] target(s) in 0.43 secs
+     Running target/debug/deps/greprs-abcabcabc
+
+running 1 test
+test test::one_result ... FAILED
+
+failures:
+
+---- test::one_result stdout ----
+    thread 'test::one_result' panicked at 'assertion failed: `(left == right)`
+(left: `["safe, fast, productive."]`, right: `[]`)', src/lib.rs:16
+note: Run with `RUST_BACKTRACE=1` for a backtrace.
+
+
+failures:
+    test::one_result
+
+test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
+
+error: test failed
+
+

好的,测试失败了,这正是我们所期望的。修改代码来让测试通过吧!之所以会失败是因为我们总是返回一个空的 vector。如下是如何实现grep的步骤:

+
    +
  1. 遍历每一行文本。
  2. +
  3. 查看这一行是否包含要搜索的字符串。 +
      +
    • 如果有,将这一行加入返回列表中
    • +
    • 如果没有,什么也不做
    • +
    +
  4. +
  5. 返回匹配到的列表
  6. +
+

让我们一步一步的来,从遍历每行开始。字符串类型有一个有用的方法来处理这种情况,它刚好叫做lines

+

Filename: src/lib.rs

+
fn grep<'a>(search: &str, contents: &'a str) -> Vec<&'a str> {
+    for line in contents.lines() {
+        // do something with line
+    }
+}
+
+ +

我们使用了一个for循环和lines方法来依次获得每一行。接下来,让我们看看这些行是否包含要搜索的字符串。幸运的是,字符串类型为此也有一个有用的方法containscontains的用法看起来像这样:

+

Filename: src/lib.rs

+
fn grep<'a>(search: &str, contents: &'a str) -> Vec<&'a str> {
+    for line in contents.lines() {
+        if line.contains(search) {
+            // do something with line
+        }
+    }
+}
+
+ +

最终,我们需要一个方法来存储包含要搜索字符串的行。为此可以在for循环之前创建一个可变的 vector 并调用push方法来存放一个line。在for循环之后,返回这个 vector。列表 12-15 中为完整的实现:

+
+Filename: src/lib.rs +
fn grep<'a>(search: &str, contents: &'a str) -> Vec<&'a str> {
+    let mut results = Vec::new();
+
+    for line in contents.lines() {
+        if line.contains(search) {
+            results.push(line);
+        }
+    }
+
+    results
+}
+
+
+

Listing 12-15: Fully functioning implementation of the grep function

+
+
+ +

尝试运行一下:

+
$ 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
+
+

非常好!它可以工作了。现在测试通过了,我们可以考虑一下重构grep的实现并时刻保持其功能不变。这些代码并不坏,不过并没有利用迭代器的一些实用功能。第十三章将回到这个例子并探索迭代器和如何改进代码。

+

现在grep函数是可以工作的,我们还需在在run函数中做最后一件事:还没有打印出结果呢!增加一个for循环来打印出grep函数返回的每一行:

+

Filename: src/lib.rs

+
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)?;
+
+    for line in grep(&config.search, &contents) {
+        println!("{}", line);
+    }
+
+    Ok(())
+}
+
+ +

现在程序应该能正常运行了!试试吧:

+
$ 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
+     Running `target\debug\greprs.exe a poem.txt`
+I'm nobody! Who are you?
+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 an admiring bog!
+
+

好极了!我们创建了一个属于自己的经典工具,并学习了很多如何组织程序的知识。我们还学习了一些文件输入输出、生命周期、测试和命令行解析的内容。

+
diff --git a/docs/ch12-05-working-with-environment-variables.html b/docs/ch12-05-working-with-environment-variables.html index f735425..66ef5d0 100644 --- a/docs/ch12-05-working-with-environment-variables.html +++ b/docs/ch12-05-working-with-environment-variables.html @@ -67,7 +67,75 @@
- +

处理环境变量

+
+

ch12-05-working-with-environment-variables.md +
+commit 4f2dc564851dc04b271a2260c834643dfd86c724

+
+

让我们再增加一个功能:大小写不敏感搜索。另外,这个设定将不是一个命令行参数:相反它将是一个环境变量。当然可以选择创建一个大小写不敏感的命令行参数,不过用户要求提供一个环境变量这样设置一次之后在整个终端会话中所有的搜索都将是大小写不敏感的了。

+

实现并测试一个大小写不敏感grep函数

+

首先,让我们增加一个新函数,当设置了环境变量时会调用它。增加一个新测试并重命名已经存在的那个:

+
#[cfg(test)]
+mod test {
+    use {grep, grep_case_insensitive};
+
+    #[test]
+    fn case_sensitive() {
+        let search = "duct";
+        let contents = "\
+Rust:
+safe, fast, productive.
+Pick three.
+Duct tape.";
+
+        assert_eq!(
+            vec!["safe, fast, productive."],
+            grep(search, contents)
+        );
+    }
+
+    #[test]
+    fn case_insensitive() {
+        let search = "rust";
+        let contents = "\
+Rust:
+safe, fast, productive.
+Pick three.
+Trust me.";
+
+        assert_eq!(
+            vec!["Rust:", "Trust me."],
+            grep_case_insensitive(search, contents)
+        );
+    }
+}
+
+ +

我们将定义一个叫做grep_case_insensitive的新函数。它的实现与grep函数大体上相似,不过列表 12-16 展示了一些小的区别:

+
+Filename: src/lib.rs +
fn grep_case_insensitive<'a>(search: &str, contents: &'a str) -> Vec<&'a str> {
+    let search = search.to_lowercase();
+    let mut results = Vec::new();
+
+    for line in contents.lines() {
+        if line.to_lowercase().contains(&search) {
+            results.push(line);
+        }
+    }
+
+    results
+}
+
+
+

Listing 12-16: Implementing a grep_case_insensitive function by changing the +search string and the lines of the contents to lowercase before comparing them

+
+
+ +

首先,将search字符串转换为小写,并存放于一个同名的覆盖变量中。注意现在search是一个String而不是字符串 slice,所以在将search传递给contains时需要加上 &,因为contains获取一个字符串 slice。

+
diff --git a/docs/print.html b/docs/print.html index 147dce0..5d6c995 100644 --- a/docs/print.html +++ b/docs/print.html @@ -6561,6 +6561,596 @@ impl Config { +

现在new函数返回一个Result,在成功时带有一个Config实例而在出现错误时带有一个&'static str。回忆一下第十章“静态声明周期”中讲到&'static str是一个字符串字面值,他也是现在我们的错误信息。

+

new函数体中有两处修改:当没有足够参数时不再调用panic!,而是返回Err值。同时我们将Config返回值包装进Ok成员中。这些修改使得函数符合其新的类型签名。

+

Config::new调用和错误处理

+

现在我们需要对main做一些修改,如列表 12-9 所示:

+
+Filename: src/main.rs +
# use std::env;
+# use std::fs::File;
+# use std::io::prelude::*;
+// ...snip...
+use std::process;
+
+fn main() {
+    let args: Vec<String> = env::args().collect();
+
+    let config = Config::new(&args).unwrap_or_else(|err| {
+        println!("Problem parsing arguments: {}", err);
+        process::exit(1);
+    });
+
+    println!("Searching for {}", config.search);
+    println!("In file {}", config.filename);
+
+    // ...snip...
+#
+#     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");
+#
+#     println!("With text:\n{}", contents);
+# }
+#
+# struct Config {
+#     search: String,
+#     filename: String,
+# }
+#
+# impl Config {
+#     fn new(args: &[String]) -> Result<Config, &'static str> {
+#         if args.len() < 3 {
+#             return Err("not enough arguments");
+#         }
+#
+#         let search = args[1].clone();
+#         let filename = args[2].clone();
+#
+#         Ok(Config {
+#             search: search,
+#             filename: filename,
+#         })
+#     }
+# }
+
+
+

Listing 12-9: Exiting with an error code if creating a new Config fails

+
+
+ +

新增了一个use行来从标准库中导入process。在main函数中我们将处理new函数返回的Result值,并在其返回Config::new时以一种更加清楚的方式结束进程。

+

这里使用了一个之前没有讲到的标准库中定义的Result<T, E>的方法:unwrap_or_else。当ResultOk时其行为类似于unwrap:它返回Ok内部封装的值。与unwrap不同的是,当ResultErr时,它调用一个闭包closure),也就是一个我们定义的作为参数传递给unwrap_or_else的匿名函数。第XX章会更详细的介绍闭包;这里需要理解的重要部分是unwrap_or_else会将Err的内部值传递给闭包中位于两道竖线间的参数err。使用unwrap_or_else允许我们进行一些自定义的非panic!的错误处理。

+

上述的错误处理其实只有两行:我们打印出了错误,接着调用了std::process::exit。这个函数立刻停止程序的执行并将传递给它的数组作为返回码。依照惯例,零代表成功而任何其他数字表示失败。就结果来说这依然类似于列表 12-7 中的基于panic!的错误处理,但是不再会有额外的输出了,让我们试一试:

+
$ cargo run
+   Compiling greprs v0.1.0 (file:///projects/greprs)
+    Finished debug [unoptimized + debuginfo] target(s) in 0.48 secs
+     Running `target\debug\greprs.exe`
+Problem parsing arguments: not enough arguments
+
+

非常好!现在输出就友好多了。

+

run函数中的错误处理

+

现在重构完了参数解析部分,让我们再改进一下程序的逻辑。列表 12-10 中展示了在main函数中调用提取出函数run之后的代码。run函数包含之前位于main中的部分代码:

+
+Filename: src/main.rs +
# use std::env;
+# use std::fs::File;
+# use std::io::prelude::*;
+# use std::process;
+#
+fn main() {
+#     let args: Vec<String> = env::args().collect();
+#
+#     let config = Config::new(&args).unwrap_or_else(|err| {
+#         println!("Problem parsing arguments: {}", err);
+#         process::exit(1);
+#     });
+    // ...snip...
+
+    println!("Searching for {}", config.search);
+    println!("In file {}", config.filename);
+
+    run(config);
+}
+
+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");
+
+    println!("With text:\n{}", contents);
+}
+
+// ...snip...
+#
+# struct Config {
+#     search: String,
+#     filename: String,
+# }
+#
+# impl Config {
+#     fn new(args: &[String]) -> Result<Config, &'static str> {
+#         if args.len() < 3 {
+#             return Err("not enough arguments");
+#         }
+#
+#         let search = args[1].clone();
+#         let filename = args[2].clone();
+#
+#         Ok(Config {
+#             search: search,
+#             filename: filename,
+#         })
+#     }
+# }
+
+
+

Listing 12-10: Extracting a run functionality for the rest of the program logic

+
+
+ +

run函数的内容是之前位于main中的几行,而且run函数获取一个Config作为参数。现在有了一个单独的函数了,我们就可以像列表 12-8 中的Config::new那样进行类似的改进了。列表 12-11 展示了另一个use语句将std::error::Error结构引入了作用域,还有使run函数返回Result的修改:

+
+Filename: src/main.rs +
use std::error::Error;
+# use std::env;
+# use std::fs::File;
+# use std::io::prelude::*;
+# use std::process;
+
+// ...snip...
+# fn main() {
+#     let args: Vec<String> = env::args().collect();
+#
+#     let config = Config::new(&args).unwrap_or_else(|err| {
+#         println!("Problem parsing arguments: {}", err);
+#         process::exit(1);
+#     });
+#
+#     println!("Searching for {}", config.search);
+#     println!("In file {}", config.filename);
+#
+#     run(config);
+#
+# }
+
+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)?;
+
+    println!("With text:\n{}", contents);
+
+    Ok(())
+}
+#
+# struct Config {
+#     search: String,
+#     filename: String,
+# }
+#
+# impl Config {
+#     fn new(args: &[String]) -> Result<Config, &'static str> {
+#         if args.len() < 3 {
+#             return Err("not enough arguments");
+#         }
+#
+#         let search = args[1].clone();
+#         let filename = args[2].clone();
+#
+#         Ok(Config {
+#             search: search,
+#             filename: filename,
+#         })
+#     }
+# }
+
+
+

Listing 12-11: Changing the run function to return Result

+
+
+ +

这里有三个大的修改。第一个是现在run函数的返回值是Result<(), Box<Error>>类型的。之前,函数返回 unit 类型(),现在它仍然是Ok时的返回值。对于错误类型,我们将使用Box<Error>。这是一个trait 对象trait object),第XX章会讲到。现在可以这样理解它:Box<Error>意味着函数返回了某个实现了Error trait 的类型,不过并没有指定具体的返回值类型。这样就比较灵活,因为在不同的错误场景可能有不同类型的错误返回值。Box是一个堆数据的智能指针,第YY章将会详细介绍Box

+

第二个改变是我们去掉了expect调用并替换为第9章讲到的?。不同于遇到错误就panic!,这会从函数中返回错误值并让调用者来处理它。

+

第三个修改是现在成功时这个函数会返回一个Ok值。因为run函数签名中声明成功类型返回值是(),所以需要将 unit 类型值包装进Ok值中。Ok(())一开始看起来有点奇怪,不过这样使用()是表明我们调用run只是为了它的副作用的惯用方式;它并没有返回什么有意义的值。

+

上述代码能够编译,不过会有一个警告:

+
warning: unused result which must be used, #[warn(unused_must_use)] on by default
+  --> src\main.rs:39:5
+   |
+39 |     run(config);
+   |     ^^^^^^^^^^^^
+
+

Rust 尝试告诉我们忽略Result,它有可能是一个错误值。让我们现在来处理它。我们将采用类似于列表 12-9 中处理Config::new错误的技巧,不过还有少许不同:

+

Filename: src/main.rs

+
fn main() {
+    // ...snip...
+
+    println!("Searching for {}", config.search);
+    println!("In file {}", config.filename);
+
+    if let Err(e) = run(config) {
+        println!("Application error: {}", e);
+
+        process::exit(1);
+    }
+}
+
+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)?;
+
+    println!("With text:\n{}", contents);
+
+    Ok(())
+}
+
+ +

不同于unwrap_or_else,我们使用if let来检查run是否返回Err,如果是则调用process::exit(1)。为什么呢?这个例子和Config::new的区别有些微妙。对于Config::new我们关心两件事:

+
    +
  1. 检测出任何可能发生的错误
  2. +
  3. 如果没有出现错误创建一个Config
  4. +
+

而在这个情况下,因为run在成功的时候返回一个(),唯一需要担心的就是第一件事:检测错误。如果我们使用了unwrap_or_else,则会得到()的返回值。它并没有什么用处。

+

虽然两种情况下if letunwrap_or_else的内容都是一样的:打印出错误并退出。

+

将代码拆分到库 crate

+

现在项目看起来好多了!还有一件我们尚未开始的工作:拆分 src/main.rs 并将一些代码放入 src/lib.rs 中。让我们现在就开始吧:将 src/main.rs 中的run函数移动到新建的 src/lib.rs 中。还需要移动相关的use语句和Config的定义,以及其new方法。现在 src/lib.rs 应该如列表 12-12 所示:

+
+Filename: src/lib.rs +
use std::error::Error;
+use std::fs::File;
+use std::io::prelude::*;
+
+pub struct Config {
+    pub search: String,
+    pub filename: String,
+}
+
+impl Config {
+    pub fn new(args: &[String]) -> Result<Config, &'static str> {
+        if args.len() < 3 {
+            return Err("not enough arguments");
+        }
+
+        let search = args[1].clone();
+        let filename = args[2].clone();
+
+        Ok(Config {
+            search: search,
+            filename: filename,
+        })
+    }
+}
+
+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)?;
+
+    println!("With text:\n{}", contents);
+
+    Ok(())
+}
+
+
+

Listing 12-12: Moving Config and run into src/lib.rs

+
+
+ +

注意我们还需要使用公有的pub:在Config和其字段、它的new方法和run函数上。

+

现在在 src/main.rs 中,我们需要通过extern crate greprs来引入现在位于 src/lib.rs 的代码。接着需要增加一行use greprs::Config来引入Config到作用域,并对run函数加上 crate 名称前缀,如列表 12-13 所示:

+
+Filename: src/main.rs +
extern crate greprs;
+
+use std::env;
+use std::process;
+
+use greprs::Config;
+
+fn main() {
+    let args: Vec<String> = env::args().collect();
+
+    let config = Config::new(&args).unwrap_or_else(|err| {
+        println!("Problem parsing arguments: {}", err);
+        process::exit(1);
+    });
+
+    println!("Searching for {}", config.search);
+    println!("In file {}", config.filename);
+
+    if let Err(e) = greprs::run(config) {
+        println!("Application error: {}", e);
+
+        process::exit(1);
+    }
+}
+
+
+

Listing 12-13: Bringing the greprs crate into the scope of src/main.rs

+
+
+ +

通过这些重构,所有代码应该都能运行了。运行几次cargo run来确保你没有破坏什么内容。好的!确实有很多的内容,不过已经为将来的成功奠定了基础。我们采用了一种更加优秀的方式来处理错误,并使得代码更模块化了一些。从现在开始几乎所有的工作都将在 src/lib.rs 中进行。

+

让我们利用这新创建的模块的优势来进行一些在旧代码中难以开开展的工作,他们在新代码中却很简单:编写测试!

+

测试库的功能

+
+

ch12-04-testing-the-librarys-functionality.md +
+commit 4f2dc564851dc04b271a2260c834643dfd86c724

+
+

现在为项目的核心功能编写测试将更加容易,因为我们将逻辑提取到了 src/lib.rs 中并将参数解析和错误处理都留在了 src/main.rs 里。现在我们可以直接使用多种参数调用代码并检查返回值而不用从命令行运行二进制文件了。

+

我们将要编写的是一个叫做grep的函数,它获取要搜索的项以及文本并产生一个搜索结果列表。让我们从run中去掉那行println!(也去掉 src/main.rs 中的,因为再也不需要他们了),并使用之前收集的选项来调用新的grep函数。眼下我们只增加一个空的实现,和指定grep期望行为的测试。当然,这个测试对于空的实现来说是会失败的,不过可以确保代码是可以编译的并得到期望的错误信息。列表 12-14 展示了这些修改:

+
+Filename: src/lib.rs +
# use std::error::Error;
+# use std::fs::File;
+# 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)]
+mod test {
+    use grep;
+
+    #[test]
+    fn one_result() {
+        let search = "duct";
+        let contents = "\
+Rust:
+safe, fast, productive.
+Pick three.";
+
+        assert_eq!(
+            vec!["safe, fast, productive."],
+            grep(search, contents)
+        );
+    }
+}
+
+
+

Listing 12-14: Creating a function where our logic will go and a failing test +for that function

+
+
+ +

注意需要在grep的签名中显式声明声明周期'a并用于contents参数和返回值。记住,生命周期参数用于指定函数参数于返回值的生命周期的关系。在这个例子中,我们表明返回的 vector 将包含引用参数contents的字符串 slice,而不是引用参数search的字符串 slice。换一种说法就是我们告诉 Rust 函数grep返回的数据将和传递给它的参数contents的数据存活的同样久。这是非常重要的!考虑为了使引用有效则 slice 引用的数据也需要保持有效,如果编译器认为我们是在创建search而不是contents的 slice,那么安全检查将是不正确的。如果尝试不用生命周期编译的话,我们将得到如下错误:

+
error[E0106]: missing lifetime specifier
+  --> src\lib.rs:37:46
+   |
+37 | fn grep(search: &str, contents: &str) -> Vec<&str> {
+   |                                              ^ expected lifetime parameter
+   |
+   = help: this function's return type contains a borrowed value, but the
+       signature does not say whether it is borrowed from `search` or
+           `contents`
+
+

Rust 不可能知道我们需要的是哪一个参数,所以需要告诉它。因为参数contents包含了所有的文本而且我们希望返回匹配的那部分文本,而我们知道contents是应该要使用生命周期语法来与返回值相关联的参数。

+

在函数签名中将参数与返回值相关联是其他语言不会让你做的工作,所以不用担心这感觉很奇怪!掌握如何指定生命周期会随着时间的推移越来越容易,熟能生巧。你可能想要重新阅读上一部分或返回与第十章中生命周期语法部分的例子做对比。

+

现在试试运行测试:

+
$ cargo test
+...warnings...
+    Finished debug [unoptimized + debuginfo] target(s) in 0.43 secs
+     Running target/debug/deps/greprs-abcabcabc
+
+running 1 test
+test test::one_result ... FAILED
+
+failures:
+
+---- test::one_result stdout ----
+    thread 'test::one_result' panicked at 'assertion failed: `(left == right)`
+(left: `["safe, fast, productive."]`, right: `[]`)', src/lib.rs:16
+note: Run with `RUST_BACKTRACE=1` for a backtrace.
+
+
+failures:
+    test::one_result
+
+test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
+
+error: test failed
+
+

好的,测试失败了,这正是我们所期望的。修改代码来让测试通过吧!之所以会失败是因为我们总是返回一个空的 vector。如下是如何实现grep的步骤:

+
    +
  1. 遍历每一行文本。
  2. +
  3. 查看这一行是否包含要搜索的字符串。 +
      +
    • 如果有,将这一行加入返回列表中
    • +
    • 如果没有,什么也不做
    • +
    +
  4. +
  5. 返回匹配到的列表
  6. +
+

让我们一步一步的来,从遍历每行开始。字符串类型有一个有用的方法来处理这种情况,它刚好叫做lines

+

Filename: src/lib.rs

+
fn grep<'a>(search: &str, contents: &'a str) -> Vec<&'a str> {
+    for line in contents.lines() {
+        // do something with line
+    }
+}
+
+ +

我们使用了一个for循环和lines方法来依次获得每一行。接下来,让我们看看这些行是否包含要搜索的字符串。幸运的是,字符串类型为此也有一个有用的方法containscontains的用法看起来像这样:

+

Filename: src/lib.rs

+
fn grep<'a>(search: &str, contents: &'a str) -> Vec<&'a str> {
+    for line in contents.lines() {
+        if line.contains(search) {
+            // do something with line
+        }
+    }
+}
+
+ +

最终,我们需要一个方法来存储包含要搜索字符串的行。为此可以在for循环之前创建一个可变的 vector 并调用push方法来存放一个line。在for循环之后,返回这个 vector。列表 12-15 中为完整的实现:

+
+Filename: src/lib.rs +
fn grep<'a>(search: &str, contents: &'a str) -> Vec<&'a str> {
+    let mut results = Vec::new();
+
+    for line in contents.lines() {
+        if line.contains(search) {
+            results.push(line);
+        }
+    }
+
+    results
+}
+
+
+

Listing 12-15: Fully functioning implementation of the grep function

+
+
+ +

尝试运行一下:

+
$ 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
+
+

非常好!它可以工作了。现在测试通过了,我们可以考虑一下重构grep的实现并时刻保持其功能不变。这些代码并不坏,不过并没有利用迭代器的一些实用功能。第十三章将回到这个例子并探索迭代器和如何改进代码。

+

现在grep函数是可以工作的,我们还需在在run函数中做最后一件事:还没有打印出结果呢!增加一个for循环来打印出grep函数返回的每一行:

+

Filename: src/lib.rs

+
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)?;
+
+    for line in grep(&config.search, &contents) {
+        println!("{}", line);
+    }
+
+    Ok(())
+}
+
+ +

现在程序应该能正常运行了!试试吧:

+
$ 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
+     Running `target\debug\greprs.exe a poem.txt`
+I'm nobody! Who are you?
+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 an admiring bog!
+
+

好极了!我们创建了一个属于自己的经典工具,并学习了很多如何组织程序的知识。我们还学习了一些文件输入输出、生命周期、测试和命令行解析的内容。

+

处理环境变量

+
+

ch12-05-working-with-environment-variables.md +
+commit 4f2dc564851dc04b271a2260c834643dfd86c724

+
+

让我们再增加一个功能:大小写不敏感搜索。另外,这个设定将不是一个命令行参数:相反它将是一个环境变量。当然可以选择创建一个大小写不敏感的命令行参数,不过用户要求提供一个环境变量这样设置一次之后在整个终端会话中所有的搜索都将是大小写不敏感的了。

+

实现并测试一个大小写不敏感grep函数

+

首先,让我们增加一个新函数,当设置了环境变量时会调用它。增加一个新测试并重命名已经存在的那个:

+
#[cfg(test)]
+mod test {
+    use {grep, grep_case_insensitive};
+
+    #[test]
+    fn case_sensitive() {
+        let search = "duct";
+        let contents = "\
+Rust:
+safe, fast, productive.
+Pick three.
+Duct tape.";
+
+        assert_eq!(
+            vec!["safe, fast, productive."],
+            grep(search, contents)
+        );
+    }
+
+    #[test]
+    fn case_insensitive() {
+        let search = "rust";
+        let contents = "\
+Rust:
+safe, fast, productive.
+Pick three.
+Trust me.";
+
+        assert_eq!(
+            vec!["Rust:", "Trust me."],
+            grep_case_insensitive(search, contents)
+        );
+    }
+}
+
+ +

我们将定义一个叫做grep_case_insensitive的新函数。它的实现与grep函数大体上相似,不过列表 12-16 展示了一些小的区别:

+
+Filename: src/lib.rs +
fn grep_case_insensitive<'a>(search: &str, contents: &'a str) -> Vec<&'a str> {
+    let search = search.to_lowercase();
+    let mut results = Vec::new();
+
+    for line in contents.lines() {
+        if line.to_lowercase().contains(&search) {
+            results.push(line);
+        }
+    }
+
+    results
+}
+
+
+

Listing 12-16: Implementing a grep_case_insensitive function by changing the +search string and the lines of the contents to lowercase before comparing them

+
+
+ +

首先,将search字符串转换为小写,并存放于一个同名的覆盖变量中。注意现在search是一个String而不是字符串 slice,所以在将search传递给contains时需要加上 &,因为contains获取一个字符串 slice。

diff --git a/src/ch12-03-improving-error-handling-and-modularity.md b/src/ch12-03-improving-error-handling-and-modularity.md index bdcf8af..2d65ec4 100644 --- a/src/ch12-03-improving-error-handling-and-modularity.md +++ b/src/ch12-03-improving-error-handling-and-modularity.md @@ -340,3 +340,386 @@ Listing 12-8: Return a `Result` from `Config::new` +现在`new`函数返回一个`Result`,在成功时带有一个`Config`实例而在出现错误时带有一个`&'static str`。回忆一下第十章“静态声明周期”中讲到`&'static str`是一个字符串字面值,他也是现在我们的错误信息。 + +`new`函数体中有两处修改:当没有足够参数时不再调用`panic!`,而是返回`Err`值。同时我们将`Config`返回值包装进`Ok`成员中。这些修改使得函数符合其新的类型签名。 + +### `Config::new`调用和错误处理 + +现在我们需要对`main`做一些修改,如列表 12-9 所示: + +
+Filename: src/main.rs + +```rust +# use std::env; +# use std::fs::File; +# use std::io::prelude::*; +// ...snip... +use std::process; + +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.search); + println!("In file {}", config.filename); + + // ...snip... +# +# 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"); +# +# println!("With text:\n{}", contents); +# } +# +# struct Config { +# search: String, +# filename: String, +# } +# +# impl Config { +# fn new(args: &[String]) -> Result { +# if args.len() < 3 { +# return Err("not enough arguments"); +# } +# +# let search = args[1].clone(); +# let filename = args[2].clone(); +# +# Ok(Config { +# search: search, +# filename: filename, +# }) +# } +# } +``` + +
+ +Listing 12-9: Exiting with an error code if creating a new `Config` fails + +
+
+ + + +新增了一个`use`行来从标准库中导入`process`。在`main`函数中我们将处理`new`函数返回的`Result`值,并在其返回`Config::new`时以一种更加清楚的方式结束进程。 + +这里使用了一个之前没有讲到的标准库中定义的`Result`的方法:`unwrap_or_else`。当`Result`是`Ok`时其行为类似于`unwrap`:它返回`Ok`内部封装的值。与`unwrap`不同的是,当`Result`是`Err`时,它调用一个**闭包**(*closure*),也就是一个我们定义的作为参数传递给`unwrap_or_else`的匿名函数。第XX章会更详细的介绍闭包;这里需要理解的重要部分是`unwrap_or_else`会将`Err`的内部值传递给闭包中位于两道竖线间的参数`err`。使用`unwrap_or_else`允许我们进行一些自定义的非`panic!`的错误处理。 + +上述的错误处理其实只有两行:我们打印出了错误,接着调用了`std::process::exit`。这个函数立刻停止程序的执行并将传递给它的数组作为返回码。依照惯例,零代表成功而任何其他数字表示失败。就结果来说这依然类似于列表 12-7 中的基于`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.exe` +Problem parsing arguments: not enough arguments +``` + +非常好!现在输出就友好多了。 + +### `run`函数中的错误处理 + +现在重构完了参数解析部分,让我们再改进一下程序的逻辑。列表 12-10 中展示了在`main`函数中调用提取出函数`run`之后的代码。`run`函数包含之前位于`main`中的部分代码: + +
+Filename: src/main.rs + +```rust +# use std::env; +# use std::fs::File; +# use std::io::prelude::*; +# use std::process; +# +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); +# }); + // ...snip... + + println!("Searching for {}", config.search); + println!("In file {}", config.filename); + + run(config); +} + +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"); + + println!("With text:\n{}", contents); +} + +// ...snip... +# +# struct Config { +# search: String, +# filename: String, +# } +# +# impl Config { +# fn new(args: &[String]) -> Result { +# if args.len() < 3 { +# return Err("not enough arguments"); +# } +# +# let search = args[1].clone(); +# let filename = args[2].clone(); +# +# Ok(Config { +# search: search, +# filename: filename, +# }) +# } +# } +``` + +
+ +Listing 12-10: Extracting a `run` functionality for the rest of the program logic + +
+
+ + + +`run`函数的内容是之前位于`main`中的几行,而且`run`函数获取一个`Config`作为参数。现在有了一个单独的函数了,我们就可以像列表 12-8 中的`Config::new`那样进行类似的改进了。列表 12-11 展示了另一个`use`语句将`std::error::Error`结构引入了作用域,还有使`run`函数返回`Result`的修改: + +
+Filename: src/main.rs + +```rust +use std::error::Error; +# use std::env; +# use std::fs::File; +# use std::io::prelude::*; +# use std::process; + +// ...snip... +# 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.search); +# println!("In file {}", config.filename); +# +# run(config); +# +# } + +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(()) +} +# +# struct Config { +# search: String, +# filename: String, +# } +# +# impl Config { +# fn new(args: &[String]) -> Result { +# if args.len() < 3 { +# return Err("not enough arguments"); +# } +# +# let search = args[1].clone(); +# let filename = args[2].clone(); +# +# Ok(Config { +# search: search, +# filename: filename, +# }) +# } +# } +``` + +
+ +Listing 12-11: Changing the `run` function to return `Result` + +
+
+ + + +这里有三个大的修改。第一个是现在`run`函数的返回值是`Result<(), Box>`类型的。之前,函数返回 unit 类型`()`,现在它仍然是`Ok`时的返回值。对于错误类型,我们将使用`Box`。这是一个**trait 对象**(*trait object*),第XX章会讲到。现在可以这样理解它:`Box`意味着函数返回了某个实现了`Error` trait 的类型,不过并没有指定具体的返回值类型。这样就比较灵活,因为在不同的错误场景可能有不同类型的错误返回值。`Box`是一个堆数据的智能指针,第YY章将会详细介绍`Box`。 + +第二个改变是我们去掉了`expect`调用并替换为第9章讲到的`?`。不同于遇到错误就`panic!`,这会从函数中返回错误值并让调用者来处理它。 + +第三个修改是现在成功时这个函数会返回一个`Ok`值。因为`run`函数签名中声明成功类型返回值是`()`,所以需要将 unit 类型值包装进`Ok`值中。`Ok(())`一开始看起来有点奇怪,不过这样使用`()`是表明我们调用`run`只是为了它的副作用的惯用方式;它并没有返回什么有意义的值。 + +上述代码能够编译,不过会有一个警告: + +``` +warning: unused result which must be used, #[warn(unused_must_use)] on by default + --> src\main.rs:39:5 + | +39 | run(config); + | ^^^^^^^^^^^^ +``` + +Rust 尝试告诉我们忽略`Result`,它有可能是一个错误值。让我们现在来处理它。我们将采用类似于列表 12-9 中处理`Config::new`错误的技巧,不过还有少许不同: + +Filename: src/main.rs + +```rust,ignore +fn main() { + // ...snip... + + println!("Searching for {}", config.search); + println!("In file {}", config.filename); + + if let Err(e) = run(config) { + println!("Application error: {}", e); + + process::exit(1); + } +} + +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(()) +} +``` + + + +不同于`unwrap_or_else`,我们使用`if let`来检查`run`是否返回`Err`,如果是则调用`process::exit(1)`。为什么呢?这个例子和`Config::new`的区别有些微妙。对于`Config::new`我们关心两件事: + +1. 检测出任何可能发生的错误 +2. 如果没有出现错误创建一个`Config` + +而在这个情况下,因为`run`在成功的时候返回一个`()`,唯一需要担心的就是第一件事:检测错误。如果我们使用了`unwrap_or_else`,则会得到`()`的返回值。它并没有什么用处。 + +虽然两种情况下`if let`和`unwrap_or_else`的内容都是一样的:打印出错误并退出。 + +### 将代码拆分到库 crate + +现在项目看起来好多了!还有一件我们尚未开始的工作:拆分 *src/main.rs* 并将一些代码放入 *src/lib.rs* 中。让我们现在就开始吧:将 *src/main.rs* 中的`run`函数移动到新建的 *src/lib.rs* 中。还需要移动相关的`use`语句和`Config`的定义,以及其`new`方法。现在 *src/lib.rs* 应该如列表 12-12 所示: + +
+Filename: src/lib.rs + +```rust +use std::error::Error; +use std::fs::File; +use std::io::prelude::*; + +pub struct Config { + pub search: String, + pub filename: String, +} + +impl Config { + pub fn new(args: &[String]) -> Result { + if args.len() < 3 { + return Err("not enough arguments"); + } + + let search = args[1].clone(); + let filename = args[2].clone(); + + Ok(Config { + search: search, + filename: filename, + }) + } +} + +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(()) +} +``` + +
+ +Listing 12-12: Moving `Config` and `run` into *src/lib.rs* + +
+
+ + + +注意我们还需要使用公有的`pub`:在`Config`和其字段、它的`new`方法和`run`函数上。 + +现在在 *src/main.rs* 中,我们需要通过`extern crate greprs`来引入现在位于 *src/lib.rs* 的代码。接着需要增加一行`use greprs::Config`来引入`Config`到作用域,并对`run`函数加上 crate 名称前缀,如列表 12-13 所示: + +
+Filename: src/main.rs + +```rust,ignore +extern crate greprs; + +use std::env; +use std::process; + +use greprs::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.search); + println!("In file {}", config.filename); + + if let Err(e) = greprs::run(config) { + println!("Application error: {}", e); + + process::exit(1); + } +} +``` + +
+ +Listing 12-13: Bringing the `greprs` crate into the scope of *src/main.rs* + +
+
+ + + +通过这些重构,所有代码应该都能运行了。运行几次`cargo run`来确保你没有破坏什么内容。好的!确实有很多的内容,不过已经为将来的成功奠定了基础。我们采用了一种更加优秀的方式来处理错误,并使得代码更模块化了一些。从现在开始几乎所有的工作都将在 *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 e69de29..03d9f58 100644 --- a/src/ch12-04-testing-the-librarys-functionality.md +++ b/src/ch12-04-testing-the-librarys-functionality.md @@ -0,0 +1,250 @@ +## 测试库的功能 + +> [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 4f2dc564851dc04b271a2260c834643dfd86c724 + +现在为项目的核心功能编写测试将更加容易,因为我们将逻辑提取到了 *src/lib.rs* 中并将参数解析和错误处理都留在了 *src/main.rs* 里。现在我们可以直接使用多种参数调用代码并检查返回值而不用从命令行运行二进制文件了。 + +我们将要编写的是一个叫做`grep`的函数,它获取要搜索的项以及文本并产生一个搜索结果列表。让我们从`run`中去掉那行`println!`(也去掉 *src/main.rs* 中的,因为再也不需要他们了),并使用之前收集的选项来调用新的`grep`函数。眼下我们只增加一个空的实现,和指定`grep`期望行为的测试。当然,这个测试对于空的实现来说是会失败的,不过可以确保代码是可以编译的并得到期望的错误信息。列表 12-14 展示了这些修改: + +
+Filename: src/lib.rs + +```rust +# use std::error::Error; +# use std::fs::File; +# 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>{ + 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)] +mod test { + use grep; + + #[test] + fn one_result() { + let search = "duct"; + let contents = "\ +Rust: +safe, fast, productive. +Pick three."; + + assert_eq!( + vec!["safe, fast, productive."], + grep(search, contents) + ); + } +} +``` + +
+ +Listing 12-14: Creating a function where our logic will go and a failing test +for that function + +
+
+ + + +注意需要在`grep`的签名中显式声明声明周期`'a`并用于`contents`参数和返回值。记住,生命周期参数用于指定函数参数于返回值的生命周期的关系。在这个例子中,我们表明返回的 vector 将包含引用参数`contents`的字符串 slice,而不是引用参数`search`的字符串 slice。换一种说法就是我们告诉 Rust 函数`grep`返回的数据将和传递给它的参数`contents`的数据存活的同样久。这是非常重要的!考虑为了使引用有效则 slice 引用的数据也需要保持有效,如果编译器认为我们是在创建`search`而不是`contents`的 slice,那么安全检查将是不正确的。如果尝试不用生命周期编译的话,我们将得到如下错误: + +``` +error[E0106]: missing lifetime specifier + --> src\lib.rs:37:46 + | +37 | fn grep(search: &str, contents: &str) -> Vec<&str> { + | ^ expected lifetime parameter + | + = help: this function's return type contains a borrowed value, but the + signature does not say whether it is borrowed from `search` or + `contents` +``` + +Rust 不可能知道我们需要的是哪一个参数,所以需要告诉它。因为参数`contents`包含了所有的文本而且我们希望返回匹配的那部分文本,而我们知道`contents`是应该要使用生命周期语法来与返回值相关联的参数。 + +在函数签名中将参数与返回值相关联是其他语言不会让你做的工作,所以不用担心这感觉很奇怪!掌握如何指定生命周期会随着时间的推移越来越容易,熟能生巧。你可能想要重新阅读上一部分或返回与第十章中生命周期语法部分的例子做对比。 + +现在试试运行测试: + +``` +$ cargo test +...warnings... + Finished debug [unoptimized + debuginfo] target(s) in 0.43 secs + Running target/debug/deps/greprs-abcabcabc + +running 1 test +test test::one_result ... FAILED + +failures: + +---- test::one_result stdout ---- + thread 'test::one_result' panicked at 'assertion failed: `(left == right)` +(left: `["safe, fast, productive."]`, right: `[]`)', src/lib.rs:16 +note: Run with `RUST_BACKTRACE=1` for a backtrace. + + +failures: + test::one_result + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured + +error: test failed +``` + +好的,测试失败了,这正是我们所期望的。修改代码来让测试通过吧!之所以会失败是因为我们总是返回一个空的 vector。如下是如何实现`grep`的步骤: + +1. 遍历每一行文本。 +2. 查看这一行是否包含要搜索的字符串。 + * 如果有,将这一行加入返回列表中 + * 如果没有,什么也不做 +3. 返回匹配到的列表 + +让我们一步一步的来,从遍历每行开始。字符串类型有一个有用的方法来处理这种情况,它刚好叫做`lines`: + +Filename: src/lib.rs + +```rust,ignore +fn grep<'a>(search: &str, contents: &'a str) -> Vec<&'a str> { + for line in contents.lines() { + // do something with line + } +} +``` + + + +我们使用了一个`for`循环和`lines`方法来依次获得每一行。接下来,让我们看看这些行是否包含要搜索的字符串。幸运的是,字符串类型为此也有一个有用的方法`contains`!`contains`的用法看起来像这样: + +Filename: src/lib.rs + +```rust,ignore +fn grep<'a>(search: &str, contents: &'a str) -> Vec<&'a str> { + for line in contents.lines() { + if line.contains(search) { + // do something with line + } + } +} +``` + + + +最终,我们需要一个方法来存储包含要搜索字符串的行。为此可以在`for`循环之前创建一个可变的 vector 并调用`push`方法来存放一个`line`。在`for`循环之后,返回这个 vector。列表 12-15 中为完整的实现: + +
+Filename: src/lib.rs + +```rust +fn grep<'a>(search: &str, contents: &'a str) -> Vec<&'a str> { + let mut results = Vec::new(); + + for line in contents.lines() { + if line.contains(search) { + results.push(line); + } + } + + results +} +``` + +
+ +Listing 12-15: Fully functioning implementation of the `grep` function + +
+
+ + + +尝试运行一下: + + +``` +$ 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 +``` + +非常好!它可以工作了。现在测试通过了,我们可以考虑一下重构`grep`的实现并时刻保持其功能不变。这些代码并不坏,不过并没有利用迭代器的一些实用功能。第十三章将回到这个例子并探索迭代器和如何改进代码。 + +现在`grep`函数是可以工作的,我们还需在在`run`函数中做最后一件事:还没有打印出结果呢!增加一个`for`循环来打印出`grep`函数返回的每一行: + +Filename: src/lib.rs + +```rust,ignore +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)?; + + for line in grep(&config.search, &contents) { + println!("{}", line); + } + + Ok(()) +} +``` + + + +现在程序应该能正常运行了!试试吧: + +``` +$ 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 + Running `target\debug\greprs.exe a poem.txt` +I'm nobody! Who are you? +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 an admiring bog! +``` + +好极了!我们创建了一个属于自己的经典工具,并学习了很多如何组织程序的知识。我们还学习了一些文件输入输出、生命周期、测试和命令行解析的内容。 \ No newline at end of file diff --git a/src/ch12-05-working-with-environment-variables.md b/src/ch12-05-working-with-environment-variables.md index e69de29..f713992 100644 --- a/src/ch12-05-working-with-environment-variables.md +++ b/src/ch12-05-working-with-environment-variables.md @@ -0,0 +1,83 @@ +## 处理环境变量 + +> [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 4f2dc564851dc04b271a2260c834643dfd86c724 + +让我们再增加一个功能:大小写不敏感搜索。另外,这个设定将不是一个命令行参数:相反它将是一个环境变量。当然可以选择创建一个大小写不敏感的命令行参数,不过用户要求提供一个环境变量这样设置一次之后在整个终端会话中所有的搜索都将是大小写不敏感的了。 + +### 实现并测试一个大小写不敏感`grep`函数 + +首先,让我们增加一个新函数,当设置了环境变量时会调用它。增加一个新测试并重命名已经存在的那个: + +```rust,ignore +#[cfg(test)] +mod test { + use {grep, grep_case_insensitive}; + + #[test] + fn case_sensitive() { + let search = "duct"; + let contents = "\ +Rust: +safe, fast, productive. +Pick three. +Duct tape."; + + assert_eq!( + vec!["safe, fast, productive."], + grep(search, contents) + ); + } + + #[test] + fn case_insensitive() { + let search = "rust"; + let contents = "\ +Rust: +safe, fast, productive. +Pick three. +Trust me."; + + assert_eq!( + vec!["Rust:", "Trust me."], + grep_case_insensitive(search, contents) + ); + } +} +``` + + + +我们将定义一个叫做`grep_case_insensitive`的新函数。它的实现与`grep`函数大体上相似,不过列表 12-16 展示了一些小的区别: + +
+Filename: src/lib.rs + +```rust +fn grep_case_insensitive<'a>(search: &str, contents: &'a str) -> Vec<&'a str> { + let search = search.to_lowercase(); + let mut results = Vec::new(); + + for line in contents.lines() { + if line.to_lowercase().contains(&search) { + results.push(line); + } + } + + results +} +``` + +
+ +Listing 12-16: Implementing a `grep_case_insensitive` function by changing the +search string and the lines of the contents to lowercase before comparing them + +
+
+ + + +首先,将`search`字符串转换为小写,并存放于一个同名的覆盖变量中。注意现在`search`是一个`String`而不是字符串 slice,所以在将`search`传递给`contains`时需要加上 &,因为`contains`获取一个字符串 slice。 +