diff --git a/docs/ch01-00-introduction.html b/docs/ch01-00-introduction.html index ffb2dd1..8329572 100644 --- a/docs/ch01-00-introduction.html +++ b/docs/ch01-00-introduction.html @@ -47,7 +47,7 @@
diff --git a/docs/ch01-01-installation.html b/docs/ch01-01-installation.html index f4f433c..76cd7df 100644 --- a/docs/ch01-01-installation.html +++ b/docs/ch01-01-installation.html @@ -47,7 +47,7 @@
diff --git a/docs/ch01-02-hello-world.html b/docs/ch01-02-hello-world.html index 0cd336d..f6922a0 100644 --- a/docs/ch01-02-hello-world.html +++ b/docs/ch01-02-hello-world.html @@ -47,7 +47,7 @@
@@ -160,7 +160,7 @@ main.rs $ cd hello_cargo

我们向cargo new传递了--bin因为我们的目标是生成一个可执行程序,而不是一个库。可执行文件是二进制可执行文件,通常就叫做 二进制文件binaries)。项目的名称被定为hello_cargo,同时 Cargo 在一个同名目录中创建它的文件,接着我们可以进入查看。

-

如果列出 hello_cargo 目录中的文件,将会看到 Cargo 生成了两个文件和一个目录:一个 Cargo.toml 文件和一个 src 目录,main.rs 文件位于目录中。它也在 hello_cargo 目录初始化了一个 git 仓库,以及一个 .gitignore 文件;你可以改为使用不同的版本控制系统(VCS),或者不使用 VCS,通过--vcs参数。

+

如果列出 hello_cargo 目录中的文件,将会看到 Cargo 生成了一个文件和一个目录:一个 Cargo.toml 文件和一个 src 目录,main.rs 文件位于目录中。它也在 hello_cargo 目录初始化了一个 git 仓库,以及一个 .gitignore 文件;你可以改为使用不同的版本控制系统(VCS),或者不使用 VCS,通过--vcs参数。

使用你选择的文本编辑器(IDE)打开 Cargo.toml 文件。它应该看起来像这样:

Filename: Cargo.toml

[package]
diff --git a/docs/ch02-00-guessing-game-tutorial.html b/docs/ch02-00-guessing-game-tutorial.html
index 8282c1d..cdeb3ce 100644
--- a/docs/ch02-00-guessing-game-tutorial.html
+++ b/docs/ch02-00-guessing-game-tutorial.html
@@ -47,7 +47,7 @@
         
 
         
 
         
@@ -167,7 +167,7 @@ let mut bar = 5; // mutable

我们还没有分析完这行代码。虽然这是单独一行代码,但它只是一个逻辑上代码行(虽然换行了但仍是一个语句)的第一部分。第二部分是这个方法:

.expect("Failed to read line");
 
-

当使用.foo()语法调用方法时,明智的选择是换行并留出空白(缩进)来把长的代码行拆开。我们可以把代码写成这样:

+

当使用.expect()语法调用方法时,明智的选择是换行并留出空白(缩进)来把长的代码行拆开。我们可以把代码写成这样:

io::stdin().read_line(&mut guess).expect("Failed to read line");
 

不过,过长的代码行难以阅读,所以最好拆开来写,两行代码两个方法调用。现在来看看这行代码干了什么。

diff --git a/docs/ch03-00-common-programming-concepts.html b/docs/ch03-00-common-programming-concepts.html index b1c7798..b21a412 100644 --- a/docs/ch03-00-common-programming-concepts.html +++ b/docs/ch03-00-common-programming-concepts.html @@ -47,7 +47,7 @@
diff --git a/docs/ch03-01-variables-and-mutability.html b/docs/ch03-01-variables-and-mutability.html index de2acc8..ab64150 100644 --- a/docs/ch03-01-variables-and-mutability.html +++ b/docs/ch03-01-variables-and-mutability.html @@ -47,7 +47,7 @@
diff --git a/docs/ch03-02-data-types.html b/docs/ch03-02-data-types.html index 214e29a..29a6681 100644 --- a/docs/ch03-02-data-types.html +++ b/docs/ch03-02-data-types.html @@ -47,7 +47,7 @@
diff --git a/docs/ch03-03-how-functions-work.html b/docs/ch03-03-how-functions-work.html index eb08517..f3ef078 100644 --- a/docs/ch03-03-how-functions-work.html +++ b/docs/ch03-03-how-functions-work.html @@ -47,7 +47,7 @@
diff --git a/docs/ch03-04-comments.html b/docs/ch03-04-comments.html index 9816297..483be4b 100644 --- a/docs/ch03-04-comments.html +++ b/docs/ch03-04-comments.html @@ -47,7 +47,7 @@
diff --git a/docs/ch03-05-control-flow.html b/docs/ch03-05-control-flow.html index b47c27a..5c1e39e 100644 --- a/docs/ch03-05-control-flow.html +++ b/docs/ch03-05-control-flow.html @@ -47,7 +47,7 @@
diff --git a/docs/ch04-00-understanding-ownership.html b/docs/ch04-00-understanding-ownership.html index 0d203a4..1f1ca9c 100644 --- a/docs/ch04-00-understanding-ownership.html +++ b/docs/ch04-00-understanding-ownership.html @@ -47,7 +47,7 @@
diff --git a/docs/ch04-01-what-is-ownership.html b/docs/ch04-01-what-is-ownership.html index b324089..10d7b59 100644 --- a/docs/ch04-01-what-is-ownership.html +++ b/docs/ch04-01-what-is-ownership.html @@ -47,7 +47,7 @@
diff --git a/docs/ch04-02-references-and-borrowing.html b/docs/ch04-02-references-and-borrowing.html index 5b8bac2..6320a96 100644 --- a/docs/ch04-02-references-and-borrowing.html +++ b/docs/ch04-02-references-and-borrowing.html @@ -47,7 +47,7 @@
diff --git a/docs/ch04-03-slices.html b/docs/ch04-03-slices.html index 250ce27..de61231 100644 --- a/docs/ch04-03-slices.html +++ b/docs/ch04-03-slices.html @@ -47,7 +47,7 @@
diff --git a/docs/ch05-00-structs.html b/docs/ch05-00-structs.html index 4e2b6bf..5dc74e9 100644 --- a/docs/ch05-00-structs.html +++ b/docs/ch05-00-structs.html @@ -47,7 +47,7 @@
diff --git a/docs/ch05-01-method-syntax.html b/docs/ch05-01-method-syntax.html index 9529046..38c0488 100644 --- a/docs/ch05-01-method-syntax.html +++ b/docs/ch05-01-method-syntax.html @@ -47,7 +47,7 @@
@@ -175,7 +175,7 @@ impl Rectangle {

如果结合列表 5-8 的main函数来运行,就会看到想要得到的输出!方法可以在self后增加多个参数,而且这些参数就像函数中的参数一样工作。

关联函数

impl块的另一个好用的功能是:允许在impl块中定义self作为参数的函数。这被称为关联函数associated functions),因为他们与结构体相关联。即便如此他们也是函数而不是方法,因为他们并不作用于一个结构体的实例。你已经使用过一个关联函数了:String::from

-

关联函数经常被用作返回一个结构体新实例的构造函数。例如我们可以一个关联函数,它获取一个维度参数并且用来作为长宽,这样可以更轻松的创建一个正方形Rectangle而不必指定两次同样的值:

+

关联函数经常被用作返回一个结构体新实例的构造函数。例如我们可以提供一个关联函数,它接受一个维度参数并且用来作为长和宽,这样可以更轻松的创建一个正方形Rectangle而不必指定两次同样的值:

Filename: src/main.rs

# #[derive(Debug)]
 # struct Rectangle {
diff --git a/docs/ch06-00-enums.html b/docs/ch06-00-enums.html
index 524c9d6..67661a1 100644
--- a/docs/ch06-00-enums.html
+++ b/docs/ch06-00-enums.html
@@ -47,7 +47,7 @@
         
 
         
 
         
diff --git a/docs/ch06-01-defining-an-enum.html b/docs/ch06-01-defining-an-enum.html index 0e19f5c..de0d5f3 100644 --- a/docs/ch06-01-defining-an-enum.html +++ b/docs/ch06-01-defining-an-enum.html @@ -47,7 +47,7 @@
@@ -233,7 +233,7 @@ crashes, which have probably caused a billion dollars of pain and damage in the last forty years.

我称之为我万亿美元的错误。当时,我在在一个面向对象语言设计第一个综合性的面向引用的类型系统。我的目标是通过编译器的自动检查来保证所有引用的应有都应该是绝对安全的。不过我未能抗拒引入一个空引用的诱惑,仅仅是因为它是这么的容易实现。这引发了无数错误、漏洞和系统崩溃,在之后的四十多年中造成了数以万计美元的苦痛和伤害。

-

空值的为题在于当你尝试像一个非空值那样使用一个空值,会出现某种形式的错误。因为空和非空的属性是无处不在的,非常容易出现这类错误。

+

空值的问题在于当你尝试像一个非空值那样使用一个空值,会出现某种形式的错误。因为空和非空的属性是无处不在的,非常容易出现这类错误。

然而,空值尝试表达的概念仍然是有意义的:空值是一个因为某种原因目前无效或缺失的值。

问题不在于具体的概念而在于特定的实现。为此,Rust 并没有空值,不过它确实拥有一个可以编码存在或不存在概念的枚举。这个枚举是Option<T>,而且它定义于标准库中,如下:

enum Option<T> {
diff --git a/docs/ch06-02-match.html b/docs/ch06-02-match.html
index 4ef56a3..768e0b8 100644
--- a/docs/ch06-02-match.html
+++ b/docs/ch06-02-match.html
@@ -47,7 +47,7 @@
         
 
         
 
         
@@ -195,7 +195,7 @@ an Option<i32>

None => None,
 

匹配上了!这里没有值来加一,所以程序结束并返回=>右侧的值None,因为第一个分支就匹配到了,其他的分支将不再比较。

-

match与枚举相结合在很多场景中都是有用的。你会在 Rust 代码中看到很多这样的模式:match一个枚举,绑定其中的值到一个变量,接着根据其值执行代码。这在一开有点复杂,不过一旦习惯了,你会希望所有语言都拥有它!这一直都是用户的最爱。

+

match与枚举相结合在很多场景中都是有用的。你会在 Rust 代码中看到很多这样的模式:match一个枚举,绑定其中的值到一个变量,接着根据其值执行代码。这在一开始有点复杂,不过一旦习惯了,你会希望所有语言都拥有它!这一直是用户的最爱。

匹配是穷尽的

match还有另一方面需要讨论。考虑一下plus_one函数的这个版本:

fn plus_one(x: Option<i32>) -> Option<i32> {
diff --git a/docs/ch06-03-if-let.html b/docs/ch06-03-if-let.html
index 43874eb..8a2f0e7 100644
--- a/docs/ch06-03-if-let.html
+++ b/docs/ch06-03-if-let.html
@@ -47,7 +47,7 @@
         
 
         
 
         
diff --git a/docs/ch07-00-modules.html b/docs/ch07-00-modules.html index a49fcd9..05ad154 100644 --- a/docs/ch07-00-modules.html +++ b/docs/ch07-00-modules.html @@ -47,7 +47,7 @@
diff --git a/docs/ch07-01-mod-and-the-filesystem.html b/docs/ch07-01-mod-and-the-filesystem.html index c1e14bf..1be71ca 100644 --- a/docs/ch07-01-mod-and-the-filesystem.html +++ b/docs/ch07-01-mod-and-the-filesystem.html @@ -47,7 +47,7 @@
diff --git a/docs/ch07-02-controlling-visibility-with-pub.html b/docs/ch07-02-controlling-visibility-with-pub.html index 6f70929..8ffcc2b 100644 --- a/docs/ch07-02-controlling-visibility-with-pub.html +++ b/docs/ch07-02-controlling-visibility-with-pub.html @@ -47,7 +47,7 @@
diff --git a/docs/ch07-03-importing-names-with-use.html b/docs/ch07-03-importing-names-with-use.html index a35ffb4..4176e64 100644 --- a/docs/ch07-03-importing-names-with-use.html +++ b/docs/ch07-03-importing-names-with-use.html @@ -47,7 +47,7 @@
diff --git a/docs/ch08-00-common-collections.html b/docs/ch08-00-common-collections.html index 90d5a08..a78d653 100644 --- a/docs/ch08-00-common-collections.html +++ b/docs/ch08-00-common-collections.html @@ -47,7 +47,7 @@
diff --git a/docs/ch08-01-vectors.html b/docs/ch08-01-vectors.html index b97d598..f928f84 100644 --- a/docs/ch08-01-vectors.html +++ b/docs/ch08-01-vectors.html @@ -47,7 +47,7 @@
diff --git a/docs/ch08-02-strings.html b/docs/ch08-02-strings.html index dbc8ac8..8966d8f 100644 --- a/docs/ch08-02-strings.html +++ b/docs/ch08-02-strings.html @@ -47,7 +47,7 @@
diff --git a/docs/ch08-03-hash-maps.html b/docs/ch08-03-hash-maps.html index 02acedf..0ed67a9 100644 --- a/docs/ch08-03-hash-maps.html +++ b/docs/ch08-03-hash-maps.html @@ -47,7 +47,7 @@
diff --git a/docs/ch09-00-error-handling.html b/docs/ch09-00-error-handling.html index 29bdedf..4f4cdcf 100644 --- a/docs/ch09-00-error-handling.html +++ b/docs/ch09-00-error-handling.html @@ -47,7 +47,7 @@
diff --git a/docs/ch09-01-unrecoverable-errors-with-panic.html b/docs/ch09-01-unrecoverable-errors-with-panic.html index b5f940f..9d87ca5 100644 --- a/docs/ch09-01-unrecoverable-errors-with-panic.html +++ b/docs/ch09-01-unrecoverable-errors-with-panic.html @@ -47,7 +47,7 @@
diff --git a/docs/ch09-02-recoverable-errors-with-result.html b/docs/ch09-02-recoverable-errors-with-result.html index 1b03969..db8be30 100644 --- a/docs/ch09-02-recoverable-errors-with-result.html +++ b/docs/ch09-02-recoverable-errors-with-result.html @@ -47,7 +47,7 @@
diff --git a/docs/ch09-03-to-panic-or-not-to-panic.html b/docs/ch09-03-to-panic-or-not-to-panic.html index 8a60eb1..e86fb7c 100644 --- a/docs/ch09-03-to-panic-or-not-to-panic.html +++ b/docs/ch09-03-to-panic-or-not-to-panic.html @@ -47,7 +47,7 @@
diff --git a/docs/ch10-00-generics.html b/docs/ch10-00-generics.html index b715250..87d1fbd 100644 --- a/docs/ch10-00-generics.html +++ b/docs/ch10-00-generics.html @@ -47,7 +47,7 @@
diff --git a/docs/ch10-01-syntax.html b/docs/ch10-01-syntax.html index a02c508..f844ee4 100644 --- a/docs/ch10-01-syntax.html +++ b/docs/ch10-01-syntax.html @@ -47,7 +47,7 @@
diff --git a/docs/ch10-02-traits.html b/docs/ch10-02-traits.html index 219cfe8..363e960 100644 --- a/docs/ch10-02-traits.html +++ b/docs/ch10-02-traits.html @@ -47,7 +47,7 @@
diff --git a/docs/ch10-03-lifetime-syntax.html b/docs/ch10-03-lifetime-syntax.html index 89ec0c2..4f36ba9 100644 --- a/docs/ch10-03-lifetime-syntax.html +++ b/docs/ch10-03-lifetime-syntax.html @@ -47,7 +47,7 @@
diff --git a/docs/ch11-00-testing.html b/docs/ch11-00-testing.html index 6e46f25..c1fe3f9 100644 --- a/docs/ch11-00-testing.html +++ b/docs/ch11-00-testing.html @@ -47,7 +47,7 @@
diff --git a/docs/ch11-01-writing-tests.html b/docs/ch11-01-writing-tests.html index c5284e4..9d62674 100644 --- a/docs/ch11-01-writing-tests.html +++ b/docs/ch11-01-writing-tests.html @@ -47,7 +47,7 @@
diff --git a/docs/ch11-02-running-tests.html b/docs/ch11-02-running-tests.html index e4edde1..fed6007 100644 --- a/docs/ch11-02-running-tests.html +++ b/docs/ch11-02-running-tests.html @@ -47,7 +47,7 @@
diff --git a/docs/ch11-03-test-organization.html b/docs/ch11-03-test-organization.html index 25265b7..a5391d2 100644 --- a/docs/ch11-03-test-organization.html +++ b/docs/ch11-03-test-organization.html @@ -47,7 +47,7 @@
diff --git a/docs/ch12-00-an-io-project.html b/docs/ch12-00-an-io-project.html index 27045c0..c741e23 100644 --- a/docs/ch12-00-an-io-project.html +++ b/docs/ch12-00-an-io-project.html @@ -47,7 +47,7 @@
diff --git a/docs/ch12-01-accepting-command-line-arguments.html b/docs/ch12-01-accepting-command-line-arguments.html index 373af1c..ae65a5a 100644 --- a/docs/ch12-01-accepting-command-line-arguments.html +++ b/docs/ch12-01-accepting-command-line-arguments.html @@ -47,7 +47,7 @@
diff --git a/docs/ch12-02-reading-a-file.html b/docs/ch12-02-reading-a-file.html index ac9056e..79ee643 100644 --- a/docs/ch12-02-reading-a-file.html +++ b/docs/ch12-02-reading-a-file.html @@ -47,7 +47,7 @@
diff --git a/docs/ch12-03-improving-error-handling-and-modularity.html b/docs/ch12-03-improving-error-handling-and-modularity.html index cc72436..7fca87e 100644 --- a/docs/ch12-03-improving-error-handling-and-modularity.html +++ b/docs/ch12-03-improving-error-handling-and-modularity.html @@ -47,7 +47,7 @@
diff --git a/docs/ch12-04-testing-the-librarys-functionality.html b/docs/ch12-04-testing-the-librarys-functionality.html index 965c3ac..07a346c 100644 --- a/docs/ch12-04-testing-the-librarys-functionality.html +++ b/docs/ch12-04-testing-the-librarys-functionality.html @@ -47,7 +47,7 @@
@@ -71,44 +71,32 @@

ch12-04-testing-the-librarys-functionality.md
-commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

+commit b8e4fcbf289b82c12121b282747ce05180afb1fb

-

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

-

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

+

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

+

在这一部分,我们将遵循测试驱动开发(Test Driven Development, TTD)的模式。这是一个软件开发技术,它遵循如下步骤:

+
    +
  1. 编写一个会失败的测试,并运行它以确保其因为你期望的原因失败。
  2. +
  3. 编写或修改刚好足够的代码来使得新的测试通过。
  4. +
  5. 重构刚刚增加或修改的代码,并确保测试仍然能通过。
  6. +
  7. 重复上述步骤!
  8. +
+

这只是众多编写软件的方法之一,不过 TDD 有助于驱动代码的设计。在编写能使测试通过的代码之前编写测测试有助于在开发过程中保持高测试覆盖率。

+

我们将测试驱动实现greprs实际在文件内容中搜索查询字符串并返回匹配的行列表的部分。我们将在一个叫做search的函数中增加这些功能。

+

编写失败测试

+

首先,去掉 src/lib.rssrc/main.rs 中的println!语句,因为不再真的需要他们了。接着我们会像第十一章那样增加一个test模块和一个测试函数。测试函数指定了我们希望search函数拥有的行为:它会获取一个需要查询的字符串和用来查询的文本。列表 12-15 展示了这个测试:

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,
+
# fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
+#      vec![]
 # }
 #
-// ...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;
+    use super::*;
 
     #[test]
     fn one_result() {
-        let search = "duct";
+        let query = "duct";
         let contents = "\
 Rust:
 safe, fast, productive.
@@ -116,28 +104,38 @@ Pick three.";
 
         assert_eq!(
             vec!["safe, fast, productive."],
-            grep(search, contents)
+            search(query, contents)
         );
     }
 }
 
-

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

+

Listing 12-15: Creating a failing test for the search +function we wish we had

+

这里选择使用 "duct" 作为这个测试中需要搜索的字符串。用来搜索的文本有三行,其中只有一行包含 "duct"。我们断言search函数的返回值只包含期望的那一行。

+

我们还不能运行这个测试并看到它失败,因为它甚至都还不能编译!我们将增加足够的代码来使其能够编译:一个总是会返回空 vector 的search函数定义,如列表 12-16 所示。一旦有了它,这个测试应该能够编译并因为空 vector 并不匹配一个包含一行"safe, fast, productive."的 vector 而失败。

+

Filename: src/lib.rs

+
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

-

注意需要在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
-  --> 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`
+ --> src/lib.rs:5:47
+  |
+5 | fn search(query: &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 `query` or `contents`
 

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

-

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

-

现在试试运行测试:

+

其他语言中并不需要你在函数签名中将参数与返回值相关联,所以这么做可能仍然感觉有些陌生,随着时间的推移会越来越容易。你可能想要将这个例子与第十章中生命周期语法部分做对比。

+

现在试尝试运行测试:

$ cargo test
 ...warnings...
     Finished debug [unoptimized + debuginfo] target(s) in 0.43 secs
@@ -161,44 +159,61 @@ test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
 
 error: test failed
 
-

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

+

好的,测试失败了,这正是我们所期望的。修改代码来让测试通过吧!

+

编写使测试通过的代码

+

目前测试之所以会失败是因为我们总是返回一个空的 vector。为了修复并实现search,我们的程序需要遵循如下步骤:

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

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

+

让我们一步一步的来,从遍历每行开始。

+

使用lines方法遍历每一行

+

Rust 有一个有助于一行一行遍历字符串的方法,出于方便它被成为lines,它如列表 12-17 这样工作:

Filename: src/lib.rs

-
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() {
         // do something with line
     }
 }
 
+

Listing 12-17: Iterating through each line in +contents

-

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

+

lines方法返回一个迭代器。第十三张会深入了解迭代器,不过我们已经在列表 3-6 中见过使用迭代器的方法,在那里使用了一个for循环和迭代器在一个集合的每一项上运行一些代码。

+ + +

用查询字符串搜索每一行

+

接下来将会增加检查当前行是否包含查询字符串的功能。幸运的是,字符串类型为此也有一个有用的方法叫做contains!如列表 12-18 所示在search函数中加入contains方法:

Filename: src/lib.rs

-
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() {
-        if line.contains(search) {
+        if line.contains(query) {
             // do something with line
         }
     }
 }
 
+

Listing 12-18: Adding functionality to see if the line +contains the string in query

-

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

+

存储匹配的行

+

最后我们需要一个方法来存储包含查询字符串的行。为此可以在for循环之前创建一个可变的 vector 并调用push方法在 vector 中存放一个line。在for循环之后,返回这个 vector,如列表 12-19 所示:

Filename: src/lib.rs

-
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();
 
     for line in contents.lines() {
-        if line.contains(search) {
+        if line.contains(query) {
             results.push(line);
         }
     }
@@ -206,10 +221,10 @@ error: test failed
     results
 }
 
-

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

+

Listing 12-19: Storing the lines that match so that we +can return them

-

尝试运行一下:

+

现在search函数应该返回只包含query的那些行,而测试应该会通过。让我们运行测试:

$ cargo test
 running 1 test
 test test::one_result ... ok
@@ -228,8 +243,20 @@ running 0 tests
 
 test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
 
-

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

-

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

+

测试通过了,很好,它可以工作了!

+

现在测试通过了,我们可以考虑一下重构search的实现并时刻保持测试通过来保持其功能不变的机会了。这些代码并不坏,不过并没有利用迭代器的一些实用功能。第十三章将回到这个例子并深入探索迭代器并看看如何改进代码。

+ + +

run函数中使用search函数

+

现在search函数是可以工作并测试通过了的,我们需要实际在run函数中调用search。需要将config.query值和run从文件中读取的contents传递给search函数。接着run会打印出search返回的每一行:

Filename: src/lib.rs

pub fn run(config: Config) -> Result<(), Box<Error>> {
     let mut f = File::open(config.filename)?;
@@ -237,7 +264,7 @@ test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
     let mut contents = String::new();
     f.read_to_string(&mut contents)?;
 
-    for line in grep(&config.search, &contents) {
+    for line in search(&config.query, &contents) {
         println!("{}", line);
     }
 
@@ -245,26 +272,28 @@ test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
 }
 
-

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

-
$ cargo run the poem.txt
+

这里再一次使用了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.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!
+     Running `target/debug/greprs frog poem.txt`
 How public, like a frog
-To tell your name the livelong day
-To an admiring bog!
 
-

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

+

好的!接下来,像 "the" 这样会匹配多行的单词会怎么样呢:

+
$ 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!
+To tell your name the livelong day
+
+

最后,让我们确保搜索一个在诗中哪里都没有的单词时不会得到任何行,比如 "monomorphization":

+
$ cargo run monomorphization poem.txt
+    Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
+     Running `target/debug/greprs monomorphization poem.txt`
+
+

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

+

现在如果你希望的话请随意移动到第十三章。为了使这个项目章节更丰满,我们将简要的展示如何处理环境变量和打印到标准错误,这两者在编写命令行程序时都很有用。

diff --git a/docs/ch12-05-working-with-environment-variables.html b/docs/ch12-05-working-with-environment-variables.html index 9b60371..e64e7a7 100644 --- a/docs/ch12-05-working-with-environment-variables.html +++ b/docs/ch12-05-working-with-environment-variables.html @@ -47,7 +47,7 @@
@@ -71,18 +71,27 @@

ch12-05-working-with-environment-variables.md
-commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

+commit 0db6a0a34886bf02feabcab8b430b5d332a8bdf5

-

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

-

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

-

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

-
#[cfg(test)]
+

我们将用一个额外的功能来改进我们的工具:一个通过环境变量启用的大小写不敏感搜索的选项。我们将其设计为一个命令行参数并要求用户每次需要时都加上它,不过相反我们将使用环境变量。这允许用户设置环境变脸一次之后在整个终端会话中所有的搜索都将是大小写不敏感的了。

+

编写一个大小写不敏感search函数的失败测试

+

首先,增加一个新函数,当设置了环境变量时会调用它。

+ + +

这里将继续遵循上一部分开始使用的 TDD 过程,其第一步是再次编写一个失败测试。我们将为新的大小写不敏感搜索函数新增一个测试函数,并将老的测试函数从one_result改名为case_sensitive来更清除的表明这两个测试的区别,如列表 12-20 所示:

+

Filename: src/lib.rs

+
#[cfg(test)]
 mod test {
-    use {grep, grep_case_insensitive};
+    use super::*;
 
     #[test]
     fn case_sensitive() {
-        let search = "duct";
+        let query = "duct";
         let contents = "\
 Rust:
 safe, fast, productive.
@@ -91,13 +100,13 @@ Duct tape.";
 
         assert_eq!(
             vec!["safe, fast, productive."],
-            grep(search, contents)
+            search(query, contents)
         );
     }
 
     #[test]
     fn case_insensitive() {
-        let search = "rust";
+        let query = "rUsT";
         let contents = "\
 Rust:
 safe, fast, productive.
@@ -106,20 +115,25 @@ Trust me.";
 
         assert_eq!(
             vec!["Rust:", "Trust me."],
-            grep_case_insensitive(search, contents)
+            search_case_insensitive(query, contents)
         );
     }
 }
 
-

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

+

Listing 12-20: Adding a new failing test for the case +insensitive function we're about to add

+

注意我们也改变了老测试中querycontents的值:将查询字符串改变为 "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都变为小写,这样不管输入参数是大写还是小写,在检查该行是否包含查询字符串时都会是小写。

Filename: src/lib.rs

-
fn grep_case_insensitive<'a>(search: &str, contents: &'a str) -> Vec<&'a str> {
-    let search = search.to_lowercase();
+
fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
+    let query = query.to_lowercase();
     let mut results = Vec::new();
 
     for line in contents.lines() {
-        if line.to_lowercase().contains(&search) {
+        if line.to_lowercase().contains(&query) {
             results.push(line);
         }
     }
@@ -127,14 +141,24 @@ Trust me.";
     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

+

Listing 12-21: Defining the search_case_insensitive +function to lowercase both the query and the line before comparing them

-

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

-

接着在检查每个line是否包含search之前增加了一个to_lowercase调用。因为将linesearch都转换为小写,我们就可以无视大小写的匹配文件和命令行参数了。看看测试是否通过了:

+ + +

首先我们将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."。现在我们将linequery都转换成了小写,这样就可以不管大小写的匹配文件中的文本和用户输入的查询了。

+

让我们看看这个实现能否通过测试:

    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
 test test::case_insensitive ... ok
@@ -142,7 +166,7 @@ test test::case_sensitive ... ok
 
 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
 
@@ -154,27 +178,45 @@ running 0 tests
 
 test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
 
-

好的!现在,我们必须真正的使用新的grep_case_insensitive函数。首先,在Config结构体中增加一个配置项:

+

好的!现在,让我们在run函数中调用真正的新search_case_insensitive函数。首先,我们将在Config结构体中增加一个配置项来切换大小写敏感和大小写不敏感搜索。

Filename: src/lib.rs

pub struct Config {
-    pub search: String,
+    pub query: String,
     pub filename: String,
     pub case_sensitive: bool,
 }
 
-

接着在run函数中检查这个选项,并根据case_sensitive函数的值来决定调用哪个函数:

+

这里增加了case_sensitive字符来存放一个布尔值。接着我们需要run函数检查case_sensitive字段的值并使用它来决定是否调用search函数或search_case_insensitive函数,如列表 12-22所示:

Filename: src/lib.rs

-
pub fn run(config: Config) -> Result<(), Box<Error>>{
+
# 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>>{
     let mut f = File::open(config.filename)?;
 
     let mut contents = String::new();
     f.read_to_string(&mut contents)?;
 
     let results = if config.case_sensitive {
-        grep(&config.search, &contents)
+        search(&config.query, &contents)
     } else {
-        grep_case_insensitive(&config.search, &contents)
+        search_case_insensitive(&config.query, &contents)
     };
 
     for line in results {
@@ -184,65 +226,61 @@ test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
     Ok(())
 }
 
+

Listing 12-22: Calling either search or +search_case_insensitive based on the value in config.case_sensitive

-

最后需要真正的检查环境变量。为了将标准库中的env模块引入作用域,在 src/lib.rs 开头增加一个use行:

+

最后需要实际检查环境变量。处理环境变量的函数位于标准库的env模块中,所以我们需要在 src/lib.rs 的开头增加一个use std::env;行将这个模块引入作用域中。接着在Config::new中使用env模块的var方法检查一个叫做CASE_INSENSITIVE的环境变量,如列表 12-23 所示:

Filename: src/lib.rs

use std::env;
-
-

并接着在Config::new中使用env模块的vars方法:

-

Filename: src/lib.rs

-
# use std::env;
-#
 # struct Config {
-#     search: String,
+#     query: String,
 #     filename: String,
 #     case_sensitive: bool,
 # }
-#
+
+// ...snip...
+
 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 query = args[1].clone();
         let filename = args[2].clone();
 
-        let mut case_sensitive = true;
-
-        for (name, _) in env::vars() {
-            if name == "CASE_INSENSITIVE" {
-                case_sensitive = false;
-            }
-        }
+        let case_sensitive = env::var("CASE_INSENSITIVE").is_err();
 
         Ok(Config {
-            search: search,
+            query: query,
             filename: filename,
             case_sensitive: case_sensitive,
         })
     }
 }
 
+

Listing 12-23: Checking for an environment variable named +CASE_INSENSITIVE

-

这里我们调用了env::vars,它与env::args的工作方式类似。区别是env::vars返回一个环境变量而不是命令行参数的迭代器。不同于使用collect来创建一个所有环境变量的 vector,我们使用for循环。env::vars返回一系列元组:环境变量的名称和其值。我们从来也不关心它的值,只关心它是否被设置了,所以可以使用_占位符来取代变量名来让 Rust 知道它不应该警告一个未使用的变量。最后,有一个默认为真的变量case_sensitive。如果我们找到了一个CASE_INSENSITIVE环境变量,就将case_sensitive设置为假。接着将其作为Config的一部分返回。

-

尝试运行几次吧!

-
$ cargo run to poem.txt
+

这里创建了一个新变量case_sensitive。为了设置它的值,需要调用env::var函数并传递我们需要寻找的环境变量名称,CASE_INSENSITIVEenv::var返回一个Result,它在环境变量被设置时返回包含其值的Ok成员,并在环境变量未被设置时返回Err成员。我们使用Resultis_err方法来检查其是否是一个 error(也就是环境变量未被设置的情况),这也就意味着我们需要进行一个大小写敏感搜索。如果CASE_INSENSITIVE环境变量被设置为任何值,is_err会返回 false 并将进行大小写不敏感搜索。我们并不关心环境变量所设置的值,只关心它是否被设置了,所以检查is_err而不是unwrapexpect或任何我们已经见过的Result的方法。我们将变量case_sensitive的值传递给Config实例这样run函数可以读取其值并决定是否调用search或者列表 12-22 中实现的search_case_insensitive

+

让我们试一试吧!首先不设置环境变量并使用查询"to"运行程序,这应该会匹配任何全小写的单词"to"的行:

+
$ cargo run to poem.txt
     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?
 How dreary to be somebody!
 
+

看起来程序仍然能够工作!现在将CASE_INSENSITIVE设置为 1 并仍使用相同的查询"to",这回应该得到包含可能有大写字母的"to"的行:

$ CASE_INSENSITIVE=1 cargo run to poem.txt
     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?
 How dreary to be somebody!
 To tell your name the livelong day
 To an admiring bog!
 
-

好极了!greprs现在可以通过环境变量的控制来进行大小写不敏感搜索了。现在你已经知道如何处理命令行参数或环境变量了!

-

一些程序允许对相同配置同时使用参数_和_环境变量。在这种情况下,程序来决定参数和环境变量的优先级。作为一个留给你的测试,尝试同时通过一个命令行参数来控制大小写不敏感搜索,并在程序遇到矛盾值时决定其优先级。

+

好极了,我们也得到了包含"To"的行!现在greprs程序可以通过环境变量控制进行大小写不敏感搜索了。现在你知道了如何管理由命令行参数或环境变量设置的选项了!

+

一些程序允许对相同配置同时使用参数环境变量。在这种情况下,程序来决定参数和环境变量的优先级。作为一个留给你的测试,尝试同时通过一个命令行参数来控制大小写不敏感搜索,并在程序遇到矛盾值时决定其优先级。

std::env模块还包含了更多处理环境变量的实用功能;请查看官方文档来了解其可用的功能。

diff --git a/docs/ch12-06-writing-to-stderr-instead-of-stdout.html b/docs/ch12-06-writing-to-stderr-instead-of-stdout.html index 26722a2..bfcaab3 100644 --- a/docs/ch12-06-writing-to-stderr-instead-of-stdout.html +++ b/docs/ch12-06-writing-to-stderr-instead-of-stdout.html @@ -47,7 +47,7 @@
@@ -71,16 +71,30 @@

ch12-06-writing-to-stderr-instead-of-stdout.md
-commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

+commit d09cfb51a239c0ebfc056a64df48fe5f1f96b207

-

目前为止,我们将所有的输出都println!到了终端。这是可以的,不过大部分终端都提供了两种输出:“标准输出”对应大部分信息,而“标准错误”则用于错误信息。这使得处理类似于“将错误打印到终端而将其他信息输出到文件”的情况变得更容易。

-

可以通过在命令行使用>来将输出重定向到文件中,同时不使用任何参数运行来造成一个错误,就会发现我们的程序只能打印到stdout

+

目前为止,我们将所有的输出都println!到了终端。大部分终端都提供了两种输出:“标准输出”对应大部分信息,而“标准错误”则用于错误信息。这种区别是命令行程序所期望拥有的行为:例如它允许用户选择将程序正常输出定向到一个文件中并仍将错误信息打印到屏幕上。但是println!只能够打印到标准输出,所以我们必需使用其他方法来打印到标准错误。

+

我们可以验证,目前所编写的greprs,所有内容都被打印到了标准输出,包括应该被写入标准错误的错误信息。可以通过故意造成错误来做到这一点,一个发生这种情况的方法是不使用任何参数运行程序。我们准备将标准输出重定向到一个文件中,不过不是标准错误。命令行程序期望以这种方式工作,因为如果输出是错误信息,它应该显示在屏幕上而不是被重定向到文件中。可以看出我们的程序目前并没有满足这个期望,通过使用>并指定一个文件名,output.txt,这是期望将标注输出重定向的文件:

$ cargo run > output.txt
 
-

>语法告诉 shell 将标准输出的内容写入到 output.txt 文件中而不是打印到屏幕上。然而,如果运行命令后打开 output.txt 就会发现错误:

-
Problem parsing arguments: not enough arguments
+
+
+

>语法告诉 shell 将标准输出的内容写入到 output.txt 文件中而不是打印到屏幕上。我们并没有看到期望的错误信息打印到屏幕上,所以这意味着它一定被写入了文件中。让我们看看 output.txt 包含什么:

+
Application error: No search string or filename found
 
-

我们希望这个信息被打印到屏幕上,而只有成功运行产生的输出写入到文件中。让我们如列表 12-17 中所示改变如何打印错误信息的方法:

+ + +

是的,这就是错误信息,这意味着它被打印到了标准输出。这并不是命令行程序所期望拥有的。像这样的错误信息被打印到标准错误,并当以这种方式重定向标注输出时只将运行成功时的数据打印到文件中。让我们像列表 12-23 所示改变错误信息如何被打印的。因为本章早些时候的进行的重构,所有打印错误信息的代码都在一个位置,在main中:

Filename: src/main.rs

extern crate greprs;
 
@@ -91,8 +105,8 @@ use std::io::prelude::*;
 use greprs::Config;
 
 fn main() {
-    let mut stderr = std::io::stderr();
     let args: Vec<String> = env::args().collect();
+    let mut stderr = std::io::stderr();
 
     let config = Config::new(&args).unwrap_or_else(|err| {
         writeln!(
@@ -100,12 +114,10 @@ fn main() {
             "Problem parsing arguments: {}",
             err
         ).expect("Could not write to stderr");
-
         process::exit(1);
     });
 
     if let Err(e) = greprs::run(config) {
-
         writeln!(
             &mut stderr,
             "Application error: {}",
@@ -116,24 +128,26 @@ fn main() {
     }
 }
 
-

Listing 12-17: Writing error messages to stderr instead -of stdout

+

Listing 12-23: Writing error messages to stderr instead +of stdout using writeln!

Rust 并没有类似println!这样的方便写入标准错误的函数。相反,我们使用writeln!宏,它有点像println!,不过它获取一个额外的参数。第一个参数是希望写入内容的位置。可以通过std::io::stderr函数获取一个标准错误的句柄。我们将一个stderr的可变引用传递给writeln!;它需要是可变的因为这样才能写入信息!第二个和第三个参数就像println!的第一个和第二参数:一个格式化字符串和任何需要插入的变量。

-

让我们再次用相同方式运行程序,不带任何参数并用 >重定向stdout

+

再次用相同方式运行程序,不带任何参数并用>重定向stdout

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

终端将没有输出,不过 output.txt 将会包含其结果:

+

我们并不会在终端看到任何输出,同时output.txt将会包含其结果:

Filename: output.txt

Are you nobody, too?
 How dreary to be somebody!
 
+

这一部分展示了现在我们使用的成功时产生的标准输出和错误时产生的标准错误是恰当的。

总结

-

在这一章,我们涉及了如果在 Rust 中进行常规的 I/O 操作。通过使用命令行参数、文件、环境变量和写入stderr的功能。现在你已经准备好编写命令行程序了。结合前几章的知识,你的代码将会是组织良好的,并能有效的将数据存储到合适的数据结构中、更好的处理错误,并且还是经过良好测试的。我们也接触了一个真实情况下需要生命周期注解来保证引用一直有效的场景。

+

在这一章中,我们回顾了目前为止的一些主要章节并涉及了如何在 Rust 中进行常规的 I/O 操作。通过使用命令行参数、文件、环境变量和writeln!宏与writeln!,现在你已经准备好编写命令行程序了。结合前几章的知识,你的代码将会是组织良好的,并能有效的将数据存储到合适的数据结构中、更好的处理错误,并且还是经过良好测试的。

接下来,让我们探索如何利用一些 Rust 中受函数式编程语言影响的功能:闭包和迭代器。

diff --git a/docs/ch13-00-functional-features.html b/docs/ch13-00-functional-features.html index e241f67..4c96b8e 100644 --- a/docs/ch13-00-functional-features.html +++ b/docs/ch13-00-functional-features.html @@ -47,7 +47,7 @@
diff --git a/docs/ch13-01-closures.html b/docs/ch13-01-closures.html index 3d9d013..49620a1 100644 --- a/docs/ch13-01-closures.html +++ b/docs/ch13-01-closures.html @@ -47,7 +47,7 @@
diff --git a/docs/ch13-02-iterators.html b/docs/ch13-02-iterators.html index 42b8c3e..4a53a1a 100644 --- a/docs/ch13-02-iterators.html +++ b/docs/ch13-02-iterators.html @@ -47,7 +47,7 @@
diff --git a/docs/ch13-03-improving-our-io-project.html b/docs/ch13-03-improving-our-io-project.html index 15d806e..94015e3 100644 --- a/docs/ch13-03-improving-our-io-project.html +++ b/docs/ch13-03-improving-our-io-project.html @@ -47,7 +47,7 @@
diff --git a/docs/ch13-04-performance.html b/docs/ch13-04-performance.html index 93c30f6..f84b189 100644 --- a/docs/ch13-04-performance.html +++ b/docs/ch13-04-performance.html @@ -47,7 +47,7 @@
diff --git a/docs/ch14-00-more-about-cargo.html b/docs/ch14-00-more-about-cargo.html index 2738007..0e03056 100644 --- a/docs/ch14-00-more-about-cargo.html +++ b/docs/ch14-00-more-about-cargo.html @@ -47,7 +47,7 @@
diff --git a/docs/ch14-01-release-profiles.html b/docs/ch14-01-release-profiles.html index 10b4943..bb6768b 100644 --- a/docs/ch14-01-release-profiles.html +++ b/docs/ch14-01-release-profiles.html @@ -47,7 +47,7 @@
diff --git a/docs/ch14-02-publishing-to-crates-io.html b/docs/ch14-02-publishing-to-crates-io.html index c548572..ef4d28e 100644 --- a/docs/ch14-02-publishing-to-crates-io.html +++ b/docs/ch14-02-publishing-to-crates-io.html @@ -47,7 +47,7 @@
diff --git a/docs/ch14-03-cargo-workspaces.html b/docs/ch14-03-cargo-workspaces.html index fad2cb4..36769ea 100644 --- a/docs/ch14-03-cargo-workspaces.html +++ b/docs/ch14-03-cargo-workspaces.html @@ -47,7 +47,7 @@
diff --git a/docs/ch14-04-installing-binaries.html b/docs/ch14-04-installing-binaries.html index ab563a5..612cc5d 100644 --- a/docs/ch14-04-installing-binaries.html +++ b/docs/ch14-04-installing-binaries.html @@ -47,7 +47,7 @@
diff --git a/docs/ch14-05-extending-cargo.html b/docs/ch14-05-extending-cargo.html index 090f726..3b365a4 100644 --- a/docs/ch14-05-extending-cargo.html +++ b/docs/ch14-05-extending-cargo.html @@ -47,7 +47,7 @@
diff --git a/docs/ch15-00-smart-pointers.html b/docs/ch15-00-smart-pointers.html index 88a2133..a4f0dce 100644 --- a/docs/ch15-00-smart-pointers.html +++ b/docs/ch15-00-smart-pointers.html @@ -47,7 +47,7 @@
diff --git a/docs/ch15-01-box.html b/docs/ch15-01-box.html index e9e2bd6..a15be48 100644 --- a/docs/ch15-01-box.html +++ b/docs/ch15-01-box.html @@ -47,7 +47,7 @@
diff --git a/docs/ch15-02-deref.html b/docs/ch15-02-deref.html index 942b27b..43df912 100644 --- a/docs/ch15-02-deref.html +++ b/docs/ch15-02-deref.html @@ -47,7 +47,7 @@
diff --git a/docs/ch15-03-drop.html b/docs/ch15-03-drop.html index 54975e3..ebee845 100644 --- a/docs/ch15-03-drop.html +++ b/docs/ch15-03-drop.html @@ -47,7 +47,7 @@
diff --git a/docs/ch15-04-rc.html b/docs/ch15-04-rc.html index b1de0ee..0904282 100644 --- a/docs/ch15-04-rc.html +++ b/docs/ch15-04-rc.html @@ -47,7 +47,7 @@
diff --git a/docs/ch15-05-interior-mutability.html b/docs/ch15-05-interior-mutability.html index cdb5c64..bcd2469 100644 --- a/docs/ch15-05-interior-mutability.html +++ b/docs/ch15-05-interior-mutability.html @@ -47,7 +47,7 @@
diff --git a/docs/ch15-06-reference-cycles.html b/docs/ch15-06-reference-cycles.html index 97ffebf..add2cc5 100644 --- a/docs/ch15-06-reference-cycles.html +++ b/docs/ch15-06-reference-cycles.html @@ -47,7 +47,7 @@
diff --git a/docs/ch16-00-concurrency.html b/docs/ch16-00-concurrency.html index 8d6c268..13aa771 100644 --- a/docs/ch16-00-concurrency.html +++ b/docs/ch16-00-concurrency.html @@ -47,7 +47,7 @@
diff --git a/docs/ch16-01-threads.html b/docs/ch16-01-threads.html index 9cf22aa..a3f8bea 100644 --- a/docs/ch16-01-threads.html +++ b/docs/ch16-01-threads.html @@ -47,7 +47,7 @@
diff --git a/docs/ch16-02-message-passing.html b/docs/ch16-02-message-passing.html index a05e6b7..e9c29e7 100644 --- a/docs/ch16-02-message-passing.html +++ b/docs/ch16-02-message-passing.html @@ -47,7 +47,7 @@
diff --git a/docs/ch16-03-shared-state.html b/docs/ch16-03-shared-state.html index b720818..beb6686 100644 --- a/docs/ch16-03-shared-state.html +++ b/docs/ch16-03-shared-state.html @@ -47,7 +47,7 @@
diff --git a/docs/ch16-04-extensible-concurrency-sync-and-send.html b/docs/ch16-04-extensible-concurrency-sync-and-send.html index dd90e16..5707f24 100644 --- a/docs/ch16-04-extensible-concurrency-sync-and-send.html +++ b/docs/ch16-04-extensible-concurrency-sync-and-send.html @@ -47,7 +47,7 @@
diff --git a/docs/ch17-00-oop.html b/docs/ch17-00-oop.html index b629dbf..dd94646 100644 --- a/docs/ch17-00-oop.html +++ b/docs/ch17-00-oop.html @@ -47,7 +47,7 @@
diff --git a/docs/ch17-01-what-is-oo.html b/docs/ch17-01-what-is-oo.html index 7efdbff..1fc6292 100644 --- a/docs/ch17-01-what-is-oo.html +++ b/docs/ch17-01-what-is-oo.html @@ -47,7 +47,7 @@
@@ -150,6 +150,10 @@ impl AveragedCollection { + +
@@ -160,6 +164,10 @@ impl AveragedCollection { + +
diff --git a/docs/ch17-02-trait-objects.html b/docs/ch17-02-trait-objects.html new file mode 100644 index 0000000..e5b5bd4 --- /dev/null +++ b/docs/ch17-02-trait-objects.html @@ -0,0 +1,396 @@ + + + + + trait对象 - Rust 程序设计语言 简体中文版 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+

为使用不同类型的值而设计的Trait对象

+
+

ch17-02-trait-objects.md +
+commit 872dc793f7017f815fb1e5389200fd208e12792d

+
+

在第8章,我们谈到了vector的局限是vectors只能存储同种类型的元素。我们在Listing 8-1有一个例子,其中定义了一个SpreadsheetCell 枚举类型,可以存储整形、浮点型和text,这样我们就可以在每个cell存储不同的数据类型了,同时还有一个代表一行cell的vector。当我们的代码编译的时候,如果交换地处理的各种东西是固定的类型是已知的,那么这是可行的。

+
<!-- The code example I want to reference did not have a listing number; it's
+the one with SpreadsheetCell. I will go back and add Listing 8-1 next time I
+get Chapter 8 for editing. /Carol -->
+
+

有时,我们想我们使用的类型集合是可扩展的,可以被使用我们的库的程序员扩展。比如很多图形化接口工具有一个条目列表,从这个列表迭代和调用draw方法在每个条目上。我们将要创建一个库crate,包含称为rust_gui的CUI库的结构体。我们的GUI库可以包含一些给开发者使用的类型,比如Button或者TextField。使用rust_gui的程序员会创建更多可以在屏幕绘图的类型:一个程序员可能会增加Image,另外一个可能会增加SelectBox。我们不会在本章节实现一个完善的GUI库,但是我们会展示如何把各部分组合在一起。

+

当要写一个rust_gui库时,我们不知道其他程序员要创建什么类型,所以我们无法定义一个enum来包含所有的类型。我们知道的是rust_gui需要有能力跟踪所有这些不同类型的大量的值,需要有能力在每个值上调用draw方法。我们的GUI库不需要确切地知道当调用draw方法时会发生什么,只要值有可用的方法供我们调用就可以。

+

在有继承的语言里,我们可能会定义一个名为Component的类,该类上有一个draw方法。其他的类比如ButtonImageSelectBox会从Component继承并继承draw方法。它们会各自覆写draw方法来自定义行为,但是框架会把所有的类型当作是Component的实例,并在它们上调用draw

+

定义一个带有自定义行为的Trait

+

不过,在Rust语言中,我们可以定义一个名为Draw的trait,其上有一个名为draw的方法。我们定义一个带有trait对象的vector,绑定了一种指针的trait,比如&引用或者一个Box<T>智能指针。

+

我们提到,我们不会调用结构体和枚举的对象,从而区分于其他语言的对象。在结构体的数据或者枚举的字段和impl块中的行为是分开的,而其他语言则是数据和行为被组合到一个概念里。Trait对象更像其他语言的对象,在这种场景下,他们组合了由指针组成的数据到实体对象,该对象带有在trait中定义的方法行为。但是,trait对象是和其他语言是不同的,因为我们不能向一个trait对象增加数据。trait对象不像其他语言那样有用:它们的目的是允许从公有的行为上抽象。

+

trait定义了在给定场景下我们所需要的行为。在我们会使用一个实体类型或者一个通用类型的地方,我们可以把trait当作trait对象使用。Rust的类型系统会保证我们为trait对象带入的任何值会实现trait的方法。我们不需要在编译阶段知道所有可能的类型,我们可以把所有的实例统一对待。Listing 17-03展示了如何定义一个名为Draw的带有draw方法的trait。

+

Filename: src/lib.rs

+
pub trait Draw {
+    fn draw(&self);
+}
+
+

Listing 17-3:Draw trait的定义

+ +

因为我们已经在第10章讨论过如何定义trait,你可能比较熟悉。下面是新的定义:Listing 17-4有一个名为Screen的结构体,里面有一个名为components的vector,components的类型是BoxBox<Draw>是一个trait对象:它是一个任何Box内部的实现了Drawtrait的类型的替身。

+

Filename: src/lib.rs

+
# pub trait Draw {
+#     fn draw(&self);
+# }
+#
+pub struct Screen {
+    pub components: Vec<Box<Draw>>,
+}
+
+

Listing 17-4: 定义一个Screen结构体,带有一个含有实现了Drawtrait的components vector成员

+

+

Screen结构体上,我们将要定义一个run方法,该方法会在它的components上调用draw方法,如Listing 17-5所示:

+

Filename: src/lib.rs

+
# pub trait Draw {
+#     fn draw(&self);
+# }
+#
+# pub struct Screen {
+#     pub components: Vec<Box<Draw>>,
+# }
+#
+impl Screen {
+    pub fn run(&self) {
+        for component in self.components.iter() {
+            component.draw();
+        }
+    }
+}
+
+

Listing 17-5:在Screen上实现一个run方法,该方法在每个组件上调用draw方法 +

+

这是区别于定义一个使用带有trait绑定的通用类型参数的结构体。通用类型参数一次只能被一个实体类型替代,而trait对象可以在运行时允许多种实体类型填充trait对象。比如,我们已经定义了Screen结构体使用通用类型和一个trait绑定,如Listing 17-6所示:

+

Filename: src/lib.rs

+
# pub trait Draw {
+#     fn draw(&self);
+# }
+#
+pub struct Screen<T: Draw> {
+    pub components: Vec<T>,
+}
+
+impl<T> Screen<T>
+    where T: Draw {
+    pub fn run(&self) {
+        for component in self.components.iter() {
+            component.draw();
+        }
+    }
+}
+
+

Listing 17-6: 一种Screen结构体的替代实现,它的run方法使用通用类型和trait绑定 +

+

这个例子只能使我们有一个Screen实例,这个实例有一个组件列表,所有的组件类型是Button或者TextField。如果你有同种的集合,那么可以优先使用通用和trait绑定,这是因为为了使用具体的类型,定义是在编译阶段是单一的。

+

而如果使用内部有Vec<Box<Draw>> trait对象的列表的Screen结构体,Screen实例可以同时包含Box<Button>Box<TextField>Vec。我们看它是怎么工作的,然后讨论运行时性能的实现。

+

来自我们或者库使用者的实现

+

现在,我们增加一些实现了Drawtrait的类型。我们会再次提供Button,实际上实现一个GUI库超出了本书的范围,所以draw方法的内部不会有任何有用的实现。为了想象一下实现可能的样子,Button结构体可能有 widthheightlabel`字段,如Listing 17-7所示:

+

Filename: src/lib.rs

+
# pub trait Draw {
+#     fn draw(&self);
+# }
+#
+pub struct Button {
+    pub width: u32,
+    pub height: u32,
+    pub label: String,
+}
+
+impl Draw for Button {
+    fn draw(&self) {
+        // Code to actually draw a button
+    }
+}
+
+

Listing 17-7: 实现了Draw trait的Button 结构体

+

Button上的 widthheightlabel会和其他组件不同,比如TextField可能有widthheight, +labelplaceholder字段。每个我们可以在屏幕上绘制的类型会实现Drawtrait,在draw方法中使用不同的代码,定义了如何绘制Button(GUI代码的具体实现超出了本章节的范围)。除了Draw trait,Button可能也有另一个impl块,包含了当按钮被点击的时候的响应方法。这类方法不适用于TextField这样的类型。

+

有时,使用我们的库决定了实现一个包含widthheightoptions``SelectBox结构体。它们在SelectBox类型上实现了Drawtrait,如 Listing 17-8所示:

+

Filename: src/main.rs

+
extern crate rust_gui;
+use rust_gui::Draw;
+
+struct SelectBox {
+    width: u32,
+    height: u32,
+    options: Vec<String>,
+}
+
+impl Draw for SelectBox {
+    fn draw(&self) {
+        // Code to actually draw a select box
+    }
+}
+
+

Listing 17-8: 另外一个crate中,在SelectBox结构体上使用rust_gui和实现了Draw trait +

+

我们的库的使用者现在可以写他们的main函数来创建一个Screen实例,然后通过把自身放入Box<T>变成trait对象,向screen增加SelectBoxButton。它们可以在每个Screen实例上调用run方法,这会调用每个组件的draw方法。 Listing 17-9展示了实现:

+

Filename: src/main.rs

+
use rust_gui::{Screen, Button};
+
+fn main() {
+    let screen = Screen {
+        components: vec![
+            Box::new(SelectBox {
+                width: 75,
+                height: 10,
+                options: vec![
+                    String::from("Yes"),
+                    String::from("Maybe"),
+                    String::from("No")
+                ],
+            }),
+            Box::new(Button {
+                width: 50,
+                height: 10,
+                label: String::from("OK"),
+            }),
+        ],
+    };
+
+    screen.run();
+}
+
+

Listing 17-9: 使用trait对象来存储实现了相同trait的不同类型 +

+

虽然我们不知道有些人可能有一天会增加SelectBox类型,但是我们的Screen 有能力操作SelectBox和绘制,因为SelectBox实现了Draw类型,这意味着它实现了draw方法。

+

只关心值响应的消息,而不关心值的具体类型,这类似于动态类型语言中的duck typing:如果它像鸭子一样走路,像鸭子一样叫,那么它肯定是只鸭子!在Listing 17-5的Screenrun方法的实现中,run不需要知道每个组件的具体类型。它也不检查是否一个组件是Button或者SelectBox的实例,只是调用组件的draw方法即可。通过指定Box<Draw>作为componentsvector中的值类型,我们定义了:Screen需要可以被调用其draw方法的值。

+

使用trait对象和支持duck typing的Rust类型系统的好处是,我们永远不需要在运行时检查一个值是否实现了一个特殊方法,或者担心因为调用了一个值没有实现方法而遇到错误。如果值没有实现trait对象需要的trait,Rust不会编译我们的代码。

+

比如,Listing 17-10展示了当我们创建一个把String当做其成员的Screen时发生的情况:

+

Filename: src/main.rs

+
extern crate rust_gui;
+use rust_gui::Draw;
+
+fn main() {
+    let screen = Screen {
+        components: vec![
+            Box::new(String::from("Hi")),
+        ],
+    };
+
+    screen.run();
+}
+
+

Listing 17-10: 尝试使用一种没有实现trait对象的trait的类型

+

+

我们会遇到这个错误,因为String没有实现 Drawtrait:

+
error[E0277]: the trait bound `std::string::String: Draw` is not satisfied
+  -->
+   |
+ 4 |             Box::new(String::from("Hi")),
+   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Draw` is not
+   implemented for `std::string::String`
+   |
+   = note: required for the cast to the object type `Draw`
+
+

这个报错让我们知道,或者我们传入了本来不想传给Screen的东西,我们应该传入一个不同的类型,或者是我们应该在String上实现Draw,这样,Screen才能调用它的draw方法。

+

Trait对象执行动态分发

+

回忆一下第10章,我们讨论过当我们使用通用类型的trait绑定时,编译器执行单类型的处理过程:在我们需要使用通用类型参数的地方,编译器为每个实体类型产生了非通用的函数实现和方法。由于非单类型而产生的代码是 static dispatch:当方法被调用,代码会执行在编译阶段就决定的方法,这样寻找那段代码是非常快速的。

+

当我们使用trait对象,编译器不能执行单类型的,因为我们不知道可能被代码调用的类型。而,当方法被调用的时候,Rust跟踪可能被使用的代码,然后在运行时找出为了方法被调用时该使用哪些代码。这也是我们熟知的dynamic dispatch,当运行时的查找发生时是比较耗费资源的。动态分发也防止编译器选择内联函数的代码,这样防止了一些优化。虽然我们写代码时得到了额外的代码灵活性,不过,这是一个权衡考虑。

+

Trait 对象需要对象安全

+ +

Not all traits can be made into trait objects; only object safe traits can. A +trait is object safe as long as both of the following are true:

+
    +
  • The trait does not require Self to be Sized
  • +
  • All of the trait's methods are object safe.
  • +
+

Self is a keyword that is an alias for the type that we're implementing +traits or methods on. Sized is a marker trait like the Send and Sync +traits that we talked about in Chapter 16. Sized is automatically implemented +on types that have a known size at compile time, such as i32 and references. +Types that do not have a known size include slices ([T]) and trait objects.

+

Sized is an implicit trait bound on all generic type parameters by default. +Most useful operations in Rust require a type to be Sized, so making Sized +a default requirement on trait bounds means we don't have to write T: Sized +with most every use of generics. If we want to be able to use a trait on +slices, however, we need to opt out of the Sized trait bound, and we can do +that by specifying T: ?Sized as a trait bound.

+

Traits have a default bound of Self: ?Sized, which means that they can be +implemented on types that may or may not be Sized. If we create a trait Foo +that opts out of the Self: ?Sized bound, that would look like the following:

+
trait Foo: Sized {
+    fn some_method(&self);
+}
+
+

The trait Sized is now a super trait of trait Foo, which means trait +Foo requires types that implement Foo (that is, Self) to be Sized. +We're going to talk about super traits in more detail in Chapter 19.

+

The reason a trait like Foo that requires Self to be Sized is not allowed +to be a trait object is that it would be impossible to implement the trait +Foo for the trait object Foo: trait objects aren't sized, but Foo +requires Self to be Sized. A type can't be both sized and unsized at the +same time!

+

For the second object safety requirement that says all of a trait's methods +must be object safe, a method is object safe if either:

+
    +
  • It requires Self to be Sized or
  • +
  • It meets all three of the following: +
      +
    • It must not have any generic type parameters
    • +
    • Its first argument must be of type Self or a type that dereferences to +the Self type (that is, it must be a method rather than an associated +function and have self, &self, or &mut self as the first argument)
    • +
    • It must not use Self anywhere else in the signature except for the +first argument
    • +
    +
  • +
+

Those rules are a bit formal, but think of it this way: if your method requires +the concrete Self type somewhere in its signature, but an object forgets the +exact type that it is, there's no way that the method can use the original +concrete type that it's forgotten. Same with generic type parameters that are +filled in with concrete type parameters when the trait is used: the concrete +types become part of the type that implements the trait. When the type is +erased by the use of a trait object, there's no way to know what types to fill +in the generic type parameters with.

+

An example of a trait whose methods are not object safe is the standard +library's Clone trait. The signature for the clone method in the Clone +trait looks like this:

+
pub trait Clone {
+    fn clone(&self) -> Self;
+}
+
+

String implements the Clone trait, and when we call the clone method on +an instance of String we get back an instance of String. Similarly, if we +call clone on an instance of Vec, we get back an instance of Vec. The +signature of clone needs to know what type will stand in for Self, since +that's the return type.

+

If we try to implement Clone on a trait like the Draw trait from Listing +17-3, we wouldn't know whether Self would end up being a Button, a +SelectBox, or some other type that will implement the Draw trait in the +future.

+

The compiler will tell you if you're trying to do something that violates the +rules of object safety in regards to trait objects. For example, if we had +tried to implement the Screen struct in Listing 17-4 to hold types that +implement the Clone trait instead of the Draw trait, like this:

+
pub struct Screen {
+    pub components: Vec<Box<Clone>>,
+}
+
+

We'll get this error:

+
error[E0038]: the trait `std::clone::Clone` cannot be made into an object
+ -->
+  |
+2 |     pub components: Vec<Box<Clone>>,
+  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::clone::Clone` cannot be
+  made into an object
+  |
+  = note: the trait cannot require that `Self : Sized`
+
+ + +
+ + + + + + + + +
+ + + + + + + +
+ + + + + + + + + + + + diff --git a/docs/index.html b/docs/index.html index 3ee5a0f..1c1ac38 100644 --- a/docs/index.html +++ b/docs/index.html @@ -46,7 +46,7 @@
diff --git a/docs/print.html b/docs/print.html index 2275a73..24974f6 100644 --- a/docs/print.html +++ b/docs/print.html @@ -2,7 +2,7 @@ - 什么是面向对象 - Rust 程序设计语言 简体中文版 + trait对象 - Rust 程序设计语言 简体中文版 @@ -47,7 +47,7 @@
@@ -218,7 +218,7 @@ main.rs $ cd hello_cargo

我们向cargo new传递了--bin因为我们的目标是生成一个可执行程序,而不是一个库。可执行文件是二进制可执行文件,通常就叫做 二进制文件binaries)。项目的名称被定为hello_cargo,同时 Cargo 在一个同名目录中创建它的文件,接着我们可以进入查看。

-

如果列出 hello_cargo 目录中的文件,将会看到 Cargo 生成了两个文件和一个目录:一个 Cargo.toml 文件和一个 src 目录,main.rs 文件位于目录中。它也在 hello_cargo 目录初始化了一个 git 仓库,以及一个 .gitignore 文件;你可以改为使用不同的版本控制系统(VCS),或者不使用 VCS,通过--vcs参数。

+

如果列出 hello_cargo 目录中的文件,将会看到 Cargo 生成了一个文件和一个目录:一个 Cargo.toml 文件和一个 src 目录,main.rs 文件位于目录中。它也在 hello_cargo 目录初始化了一个 git 仓库,以及一个 .gitignore 文件;你可以改为使用不同的版本控制系统(VCS),或者不使用 VCS,通过--vcs参数。

使用你选择的文本编辑器(IDE)打开 Cargo.toml 文件。它应该看起来像这样:

Filename: Cargo.toml

[package]
@@ -389,7 +389,7 @@ let mut bar = 5; // mutable
 

我们还没有分析完这行代码。虽然这是单独一行代码,但它只是一个逻辑上代码行(虽然换行了但仍是一个语句)的第一部分。第二部分是这个方法:

.expect("Failed to read line");
 
-

当使用.foo()语法调用方法时,明智的选择是换行并留出空白(缩进)来把长的代码行拆开。我们可以把代码写成这样:

+

当使用.expect()语法调用方法时,明智的选择是换行并留出空白(缩进)来把长的代码行拆开。我们可以把代码写成这样:

io::stdin().read_line(&mut guess).expect("Failed to read line");
 

不过,过长的代码行难以阅读,所以最好拆开来写,两行代码两个方法调用。现在来看看这行代码干了什么。

@@ -2542,7 +2542,7 @@ impl Rectangle {

如果结合列表 5-8 的main函数来运行,就会看到想要得到的输出!方法可以在self后增加多个参数,而且这些参数就像函数中的参数一样工作。

关联函数

impl块的另一个好用的功能是:允许在impl块中定义self作为参数的函数。这被称为关联函数associated functions),因为他们与结构体相关联。即便如此他们也是函数而不是方法,因为他们并不作用于一个结构体的实例。你已经使用过一个关联函数了:String::from

-

关联函数经常被用作返回一个结构体新实例的构造函数。例如我们可以一个关联函数,它获取一个维度参数并且用来作为长宽,这样可以更轻松的创建一个正方形Rectangle而不必指定两次同样的值:

+

关联函数经常被用作返回一个结构体新实例的构造函数。例如我们可以提供一个关联函数,它接受一个维度参数并且用来作为长和宽,这样可以更轻松的创建一个正方形Rectangle而不必指定两次同样的值:

Filename: src/main.rs

# #[derive(Debug)]
 # struct Rectangle {
@@ -2734,7 +2734,7 @@ crashes, which have probably caused a billion dollars of pain and damage in
 the last forty years.

我称之为我万亿美元的错误。当时,我在在一个面向对象语言设计第一个综合性的面向引用的类型系统。我的目标是通过编译器的自动检查来保证所有引用的应有都应该是绝对安全的。不过我未能抗拒引入一个空引用的诱惑,仅仅是因为它是这么的容易实现。这引发了无数错误、漏洞和系统崩溃,在之后的四十多年中造成了数以万计美元的苦痛和伤害。

-

空值的为题在于当你尝试像一个非空值那样使用一个空值,会出现某种形式的错误。因为空和非空的属性是无处不在的,非常容易出现这类错误。

+

空值的问题在于当你尝试像一个非空值那样使用一个空值,会出现某种形式的错误。因为空和非空的属性是无处不在的,非常容易出现这类错误。

然而,空值尝试表达的概念仍然是有意义的:空值是一个因为某种原因目前无效或缺失的值。

问题不在于具体的概念而在于特定的实现。为此,Rust 并没有空值,不过它确实拥有一个可以编码存在或不存在概念的枚举。这个枚举是Option<T>,而且它定义于标准库中,如下:

enum Option<T> {
@@ -2899,7 +2899,7 @@ an Option<i32>

None => None,
 

匹配上了!这里没有值来加一,所以程序结束并返回=>右侧的值None,因为第一个分支就匹配到了,其他的分支将不再比较。

-

match与枚举相结合在很多场景中都是有用的。你会在 Rust 代码中看到很多这样的模式:match一个枚举,绑定其中的值到一个变量,接着根据其值执行代码。这在一开有点复杂,不过一旦习惯了,你会希望所有语言都拥有它!这一直都是用户的最爱。

+

match与枚举相结合在很多场景中都是有用的。你会在 Rust 代码中看到很多这样的模式:match一个枚举,绑定其中的值到一个变量,接着根据其值执行代码。这在一开始有点复杂,不过一旦习惯了,你会希望所有语言都拥有它!这一直是用户的最爱。

匹配是穷尽的

match还有另一方面需要讨论。考虑一下plus_one函数的这个版本:

fn plus_one(x: Option<i32>) -> Option<i32> {
@@ -6910,44 +6910,32 @@ Chapter 1; the tips should apply to any example in the book /Carol -->
 

ch12-04-testing-the-librarys-functionality.md
-commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

+commit b8e4fcbf289b82c12121b282747ce05180afb1fb

-

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

-

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

+

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

+

在这一部分,我们将遵循测试驱动开发(Test Driven Development, TTD)的模式。这是一个软件开发技术,它遵循如下步骤:

+
    +
  1. 编写一个会失败的测试,并运行它以确保其因为你期望的原因失败。
  2. +
  3. 编写或修改刚好足够的代码来使得新的测试通过。
  4. +
  5. 重构刚刚增加或修改的代码,并确保测试仍然能通过。
  6. +
  7. 重复上述步骤!
  8. +
+

这只是众多编写软件的方法之一,不过 TDD 有助于驱动代码的设计。在编写能使测试通过的代码之前编写测测试有助于在开发过程中保持高测试覆盖率。

+

我们将测试驱动实现greprs实际在文件内容中搜索查询字符串并返回匹配的行列表的部分。我们将在一个叫做search的函数中增加这些功能。

+

编写失败测试

+

首先,去掉 src/lib.rssrc/main.rs 中的println!语句,因为不再真的需要他们了。接着我们会像第十一章那样增加一个test模块和一个测试函数。测试函数指定了我们希望search函数拥有的行为:它会获取一个需要查询的字符串和用来查询的文本。列表 12-15 展示了这个测试:

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,
+
# fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
+#      vec![]
 # }
 #
-// ...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;
+    use super::*;
 
     #[test]
     fn one_result() {
-        let search = "duct";
+        let query = "duct";
         let contents = "\
 Rust:
 safe, fast, productive.
@@ -6955,28 +6943,38 @@ Pick three.";
 
         assert_eq!(
             vec!["safe, fast, productive."],
-            grep(search, contents)
+            search(query, contents)
         );
     }
 }
 
-

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

+

Listing 12-15: Creating a failing test for the search +function we wish we had

+

这里选择使用 "duct" 作为这个测试中需要搜索的字符串。用来搜索的文本有三行,其中只有一行包含 "duct"。我们断言search函数的返回值只包含期望的那一行。

+

我们还不能运行这个测试并看到它失败,因为它甚至都还不能编译!我们将增加足够的代码来使其能够编译:一个总是会返回空 vector 的search函数定义,如列表 12-16 所示。一旦有了它,这个测试应该能够编译并因为空 vector 并不匹配一个包含一行"safe, fast, productive."的 vector 而失败。

+

Filename: src/lib.rs

+
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

-

注意需要在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
-  --> 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`
+ --> src/lib.rs:5:47
+  |
+5 | fn search(query: &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 `query` or `contents`
 

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

-

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

-

现在试试运行测试:

+

其他语言中并不需要你在函数签名中将参数与返回值相关联,所以这么做可能仍然感觉有些陌生,随着时间的推移会越来越容易。你可能想要将这个例子与第十章中生命周期语法部分做对比。

+

现在试尝试运行测试:

$ cargo test
 ...warnings...
     Finished debug [unoptimized + debuginfo] target(s) in 0.43 secs
@@ -7000,44 +6998,61 @@ test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
 
 error: test failed
 
-

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

+

好的,测试失败了,这正是我们所期望的。修改代码来让测试通过吧!

+

编写使测试通过的代码

+

目前测试之所以会失败是因为我们总是返回一个空的 vector。为了修复并实现search,我们的程序需要遵循如下步骤:

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

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

+

让我们一步一步的来,从遍历每行开始。

+

使用lines方法遍历每一行

+

Rust 有一个有助于一行一行遍历字符串的方法,出于方便它被成为lines,它如列表 12-17 这样工作:

Filename: src/lib.rs

-
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() {
         // do something with line
     }
 }
 
+

Listing 12-17: Iterating through each line in +contents

-

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

+

lines方法返回一个迭代器。第十三张会深入了解迭代器,不过我们已经在列表 3-6 中见过使用迭代器的方法,在那里使用了一个for循环和迭代器在一个集合的每一项上运行一些代码。

+ + +

用查询字符串搜索每一行

+

接下来将会增加检查当前行是否包含查询字符串的功能。幸运的是,字符串类型为此也有一个有用的方法叫做contains!如列表 12-18 所示在search函数中加入contains方法:

Filename: src/lib.rs

-
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() {
-        if line.contains(search) {
+        if line.contains(query) {
             // do something with line
         }
     }
 }
 
+

Listing 12-18: Adding functionality to see if the line +contains the string in query

-

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

+

存储匹配的行

+

最后我们需要一个方法来存储包含查询字符串的行。为此可以在for循环之前创建一个可变的 vector 并调用push方法在 vector 中存放一个line。在for循环之后,返回这个 vector,如列表 12-19 所示:

Filename: src/lib.rs

-
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();
 
     for line in contents.lines() {
-        if line.contains(search) {
+        if line.contains(query) {
             results.push(line);
         }
     }
@@ -7045,10 +7060,10 @@ error: test failed
     results
 }
 
-

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

+

Listing 12-19: Storing the lines that match so that we +can return them

-

尝试运行一下:

+

现在search函数应该返回只包含query的那些行,而测试应该会通过。让我们运行测试:

$ cargo test
 running 1 test
 test test::one_result ... ok
@@ -7067,8 +7082,20 @@ running 0 tests
 
 test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
 
-

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

-

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

+

测试通过了,很好,它可以工作了!

+

现在测试通过了,我们可以考虑一下重构search的实现并时刻保持测试通过来保持其功能不变的机会了。这些代码并不坏,不过并没有利用迭代器的一些实用功能。第十三章将回到这个例子并深入探索迭代器并看看如何改进代码。

+ + +

run函数中使用search函数

+

现在search函数是可以工作并测试通过了的,我们需要实际在run函数中调用search。需要将config.query值和run从文件中读取的contents传递给search函数。接着run会打印出search返回的每一行:

Filename: src/lib.rs

pub fn run(config: Config) -> Result<(), Box<Error>> {
     let mut f = File::open(config.filename)?;
@@ -7076,7 +7103,7 @@ test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
     let mut contents = String::new();
     f.read_to_string(&mut contents)?;
 
-    for line in grep(&config.search, &contents) {
+    for line in search(&config.query, &contents) {
         println!("{}", line);
     }
 
@@ -7084,42 +7111,53 @@ test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
 }
 
-

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

-
$ cargo run the poem.txt
+

这里再一次使用了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.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!
+     Running `target/debug/greprs frog poem.txt`
 How public, like a frog
-To tell your name the livelong day
-To an admiring bog!
 
-

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

+

好的!接下来,像 "the" 这样会匹配多行的单词会怎么样呢:

+
$ 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!
+To tell your name the livelong day
+
+

最后,让我们确保搜索一个在诗中哪里都没有的单词时不会得到任何行,比如 "monomorphization":

+
$ cargo run monomorphization poem.txt
+    Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
+     Running `target/debug/greprs monomorphization poem.txt`
+
+

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

+

现在如果你希望的话请随意移动到第十三章。为了使这个项目章节更丰满,我们将简要的展示如何处理环境变量和打印到标准错误,这两者在编写命令行程序时都很有用。

处理环境变量

ch12-05-working-with-environment-variables.md
-commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

+commit 0db6a0a34886bf02feabcab8b430b5d332a8bdf5

-

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

-

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

-

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

-
#[cfg(test)]
+

我们将用一个额外的功能来改进我们的工具:一个通过环境变量启用的大小写不敏感搜索的选项。我们将其设计为一个命令行参数并要求用户每次需要时都加上它,不过相反我们将使用环境变量。这允许用户设置环境变脸一次之后在整个终端会话中所有的搜索都将是大小写不敏感的了。

+

编写一个大小写不敏感search函数的失败测试

+

首先,增加一个新函数,当设置了环境变量时会调用它。

+ + +

这里将继续遵循上一部分开始使用的 TDD 过程,其第一步是再次编写一个失败测试。我们将为新的大小写不敏感搜索函数新增一个测试函数,并将老的测试函数从one_result改名为case_sensitive来更清除的表明这两个测试的区别,如列表 12-20 所示:

+

Filename: src/lib.rs

+
#[cfg(test)]
 mod test {
-    use {grep, grep_case_insensitive};
+    use super::*;
 
     #[test]
     fn case_sensitive() {
-        let search = "duct";
+        let query = "duct";
         let contents = "\
 Rust:
 safe, fast, productive.
@@ -7128,13 +7166,13 @@ Duct tape.";
 
         assert_eq!(
             vec!["safe, fast, productive."],
-            grep(search, contents)
+            search(query, contents)
         );
     }
 
     #[test]
     fn case_insensitive() {
-        let search = "rust";
+        let query = "rUsT";
         let contents = "\
 Rust:
 safe, fast, productive.
@@ -7143,20 +7181,25 @@ Trust me.";
 
         assert_eq!(
             vec!["Rust:", "Trust me."],
-            grep_case_insensitive(search, contents)
+            search_case_insensitive(query, contents)
         );
     }
 }
 
-

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

+

Listing 12-20: Adding a new failing test for the case +insensitive function we're about to add

+

注意我们也改变了老测试中querycontents的值:将查询字符串改变为 "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都变为小写,这样不管输入参数是大写还是小写,在检查该行是否包含查询字符串时都会是小写。

Filename: src/lib.rs

-
fn grep_case_insensitive<'a>(search: &str, contents: &'a str) -> Vec<&'a str> {
-    let search = search.to_lowercase();
+
fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
+    let query = query.to_lowercase();
     let mut results = Vec::new();
 
     for line in contents.lines() {
-        if line.to_lowercase().contains(&search) {
+        if line.to_lowercase().contains(&query) {
             results.push(line);
         }
     }
@@ -7164,14 +7207,24 @@ Trust me.";
     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

+

Listing 12-21: Defining the search_case_insensitive +function to lowercase both the query and the line before comparing them

-

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

-

接着在检查每个line是否包含search之前增加了一个to_lowercase调用。因为将linesearch都转换为小写,我们就可以无视大小写的匹配文件和命令行参数了。看看测试是否通过了:

+ + +

首先我们将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."。现在我们将linequery都转换成了小写,这样就可以不管大小写的匹配文件中的文本和用户输入的查询了。

+

让我们看看这个实现能否通过测试:

    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
 test test::case_insensitive ... ok
@@ -7179,7 +7232,7 @@ test test::case_sensitive ... ok
 
 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
 
@@ -7191,27 +7244,45 @@ running 0 tests
 
 test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
 
-

好的!现在,我们必须真正的使用新的grep_case_insensitive函数。首先,在Config结构体中增加一个配置项:

+

好的!现在,让我们在run函数中调用真正的新search_case_insensitive函数。首先,我们将在Config结构体中增加一个配置项来切换大小写敏感和大小写不敏感搜索。

Filename: src/lib.rs

pub struct Config {
-    pub search: String,
+    pub query: String,
     pub filename: String,
     pub case_sensitive: bool,
 }
 
-

接着在run函数中检查这个选项,并根据case_sensitive函数的值来决定调用哪个函数:

+

这里增加了case_sensitive字符来存放一个布尔值。接着我们需要run函数检查case_sensitive字段的值并使用它来决定是否调用search函数或search_case_insensitive函数,如列表 12-22所示:

Filename: src/lib.rs

-
pub fn run(config: Config) -> Result<(), Box<Error>>{
+
# 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>>{
     let mut f = File::open(config.filename)?;
 
     let mut contents = String::new();
     f.read_to_string(&mut contents)?;
 
     let results = if config.case_sensitive {
-        grep(&config.search, &contents)
+        search(&config.query, &contents)
     } else {
-        grep_case_insensitive(&config.search, &contents)
+        search_case_insensitive(&config.query, &contents)
     };
 
     for line in results {
@@ -7221,80 +7292,90 @@ test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
     Ok(())
 }
 
+

Listing 12-22: Calling either search or +search_case_insensitive based on the value in config.case_sensitive

-

最后需要真正的检查环境变量。为了将标准库中的env模块引入作用域,在 src/lib.rs 开头增加一个use行:

+

最后需要实际检查环境变量。处理环境变量的函数位于标准库的env模块中,所以我们需要在 src/lib.rs 的开头增加一个use std::env;行将这个模块引入作用域中。接着在Config::new中使用env模块的var方法检查一个叫做CASE_INSENSITIVE的环境变量,如列表 12-23 所示:

Filename: src/lib.rs

use std::env;
-
-

并接着在Config::new中使用env模块的vars方法:

-

Filename: src/lib.rs

-
# use std::env;
-#
 # struct Config {
-#     search: String,
+#     query: String,
 #     filename: String,
 #     case_sensitive: bool,
 # }
-#
+
+// ...snip...
+
 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 query = args[1].clone();
         let filename = args[2].clone();
 
-        let mut case_sensitive = true;
-
-        for (name, _) in env::vars() {
-            if name == "CASE_INSENSITIVE" {
-                case_sensitive = false;
-            }
-        }
+        let case_sensitive = env::var("CASE_INSENSITIVE").is_err();
 
         Ok(Config {
-            search: search,
+            query: query,
             filename: filename,
             case_sensitive: case_sensitive,
         })
     }
 }
 
+

Listing 12-23: Checking for an environment variable named +CASE_INSENSITIVE

-

这里我们调用了env::vars,它与env::args的工作方式类似。区别是env::vars返回一个环境变量而不是命令行参数的迭代器。不同于使用collect来创建一个所有环境变量的 vector,我们使用for循环。env::vars返回一系列元组:环境变量的名称和其值。我们从来也不关心它的值,只关心它是否被设置了,所以可以使用_占位符来取代变量名来让 Rust 知道它不应该警告一个未使用的变量。最后,有一个默认为真的变量case_sensitive。如果我们找到了一个CASE_INSENSITIVE环境变量,就将case_sensitive设置为假。接着将其作为Config的一部分返回。

-

尝试运行几次吧!

-
$ cargo run to poem.txt
+

这里创建了一个新变量case_sensitive。为了设置它的值,需要调用env::var函数并传递我们需要寻找的环境变量名称,CASE_INSENSITIVEenv::var返回一个Result,它在环境变量被设置时返回包含其值的Ok成员,并在环境变量未被设置时返回Err成员。我们使用Resultis_err方法来检查其是否是一个 error(也就是环境变量未被设置的情况),这也就意味着我们需要进行一个大小写敏感搜索。如果CASE_INSENSITIVE环境变量被设置为任何值,is_err会返回 false 并将进行大小写不敏感搜索。我们并不关心环境变量所设置的值,只关心它是否被设置了,所以检查is_err而不是unwrapexpect或任何我们已经见过的Result的方法。我们将变量case_sensitive的值传递给Config实例这样run函数可以读取其值并决定是否调用search或者列表 12-22 中实现的search_case_insensitive

+

让我们试一试吧!首先不设置环境变量并使用查询"to"运行程序,这应该会匹配任何全小写的单词"to"的行:

+
$ cargo run to poem.txt
     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?
 How dreary to be somebody!
 
+

看起来程序仍然能够工作!现在将CASE_INSENSITIVE设置为 1 并仍使用相同的查询"to",这回应该得到包含可能有大写字母的"to"的行:

$ CASE_INSENSITIVE=1 cargo run to poem.txt
     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?
 How dreary to be somebody!
 To tell your name the livelong day
 To an admiring bog!
 
-

好极了!greprs现在可以通过环境变量的控制来进行大小写不敏感搜索了。现在你已经知道如何处理命令行参数或环境变量了!

-

一些程序允许对相同配置同时使用参数_和_环境变量。在这种情况下,程序来决定参数和环境变量的优先级。作为一个留给你的测试,尝试同时通过一个命令行参数来控制大小写不敏感搜索,并在程序遇到矛盾值时决定其优先级。

+

好极了,我们也得到了包含"To"的行!现在greprs程序可以通过环境变量控制进行大小写不敏感搜索了。现在你知道了如何管理由命令行参数或环境变量设置的选项了!

+

一些程序允许对相同配置同时使用参数环境变量。在这种情况下,程序来决定参数和环境变量的优先级。作为一个留给你的测试,尝试同时通过一个命令行参数来控制大小写不敏感搜索,并在程序遇到矛盾值时决定其优先级。

std::env模块还包含了更多处理环境变量的实用功能;请查看官方文档来了解其可用的功能。

输出到stderr而不是stdout

ch12-06-writing-to-stderr-instead-of-stdout.md
-commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

+commit d09cfb51a239c0ebfc056a64df48fe5f1f96b207

-

目前为止,我们将所有的输出都println!到了终端。这是可以的,不过大部分终端都提供了两种输出:“标准输出”对应大部分信息,而“标准错误”则用于错误信息。这使得处理类似于“将错误打印到终端而将其他信息输出到文件”的情况变得更容易。

-

可以通过在命令行使用>来将输出重定向到文件中,同时不使用任何参数运行来造成一个错误,就会发现我们的程序只能打印到stdout

+

目前为止,我们将所有的输出都println!到了终端。大部分终端都提供了两种输出:“标准输出”对应大部分信息,而“标准错误”则用于错误信息。这种区别是命令行程序所期望拥有的行为:例如它允许用户选择将程序正常输出定向到一个文件中并仍将错误信息打印到屏幕上。但是println!只能够打印到标准输出,所以我们必需使用其他方法来打印到标准错误。

+

我们可以验证,目前所编写的greprs,所有内容都被打印到了标准输出,包括应该被写入标准错误的错误信息。可以通过故意造成错误来做到这一点,一个发生这种情况的方法是不使用任何参数运行程序。我们准备将标准输出重定向到一个文件中,不过不是标准错误。命令行程序期望以这种方式工作,因为如果输出是错误信息,它应该显示在屏幕上而不是被重定向到文件中。可以看出我们的程序目前并没有满足这个期望,通过使用>并指定一个文件名,output.txt,这是期望将标注输出重定向的文件:

$ cargo run > output.txt
 
-

>语法告诉 shell 将标准输出的内容写入到 output.txt 文件中而不是打印到屏幕上。然而,如果运行命令后打开 output.txt 就会发现错误:

-
Problem parsing arguments: not enough arguments
+
+
+

>语法告诉 shell 将标准输出的内容写入到 output.txt 文件中而不是打印到屏幕上。我们并没有看到期望的错误信息打印到屏幕上,所以这意味着它一定被写入了文件中。让我们看看 output.txt 包含什么:

+
Application error: No search string or filename found
 
-

我们希望这个信息被打印到屏幕上,而只有成功运行产生的输出写入到文件中。让我们如列表 12-17 中所示改变如何打印错误信息的方法:

+ + +

是的,这就是错误信息,这意味着它被打印到了标准输出。这并不是命令行程序所期望拥有的。像这样的错误信息被打印到标准错误,并当以这种方式重定向标注输出时只将运行成功时的数据打印到文件中。让我们像列表 12-23 所示改变错误信息如何被打印的。因为本章早些时候的进行的重构,所有打印错误信息的代码都在一个位置,在main中:

Filename: src/main.rs

extern crate greprs;
 
@@ -7305,8 +7386,8 @@ use std::io::prelude::*;
 use greprs::Config;
 
 fn main() {
-    let mut stderr = std::io::stderr();
     let args: Vec<String> = env::args().collect();
+    let mut stderr = std::io::stderr();
 
     let config = Config::new(&args).unwrap_or_else(|err| {
         writeln!(
@@ -7314,12 +7395,10 @@ fn main() {
             "Problem parsing arguments: {}",
             err
         ).expect("Could not write to stderr");
-
         process::exit(1);
     });
 
     if let Err(e) = greprs::run(config) {
-
         writeln!(
             &mut stderr,
             "Application error: {}",
@@ -7330,24 +7409,26 @@ fn main() {
     }
 }
 
-

Listing 12-17: Writing error messages to stderr instead -of stdout

+

Listing 12-23: Writing error messages to stderr instead +of stdout using writeln!

Rust 并没有类似println!这样的方便写入标准错误的函数。相反,我们使用writeln!宏,它有点像println!,不过它获取一个额外的参数。第一个参数是希望写入内容的位置。可以通过std::io::stderr函数获取一个标准错误的句柄。我们将一个stderr的可变引用传递给writeln!;它需要是可变的因为这样才能写入信息!第二个和第三个参数就像println!的第一个和第二参数:一个格式化字符串和任何需要插入的变量。

-

让我们再次用相同方式运行程序,不带任何参数并用 >重定向stdout

+

再次用相同方式运行程序,不带任何参数并用>重定向stdout

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

终端将没有输出,不过 output.txt 将会包含其结果:

+

我们并不会在终端看到任何输出,同时output.txt将会包含其结果:

Filename: output.txt

Are you nobody, too?
 How dreary to be somebody!
 
+

这一部分展示了现在我们使用的成功时产生的标准输出和错误时产生的标准错误是恰当的。

总结

-

在这一章,我们涉及了如果在 Rust 中进行常规的 I/O 操作。通过使用命令行参数、文件、环境变量和写入stderr的功能。现在你已经准备好编写命令行程序了。结合前几章的知识,你的代码将会是组织良好的,并能有效的将数据存储到合适的数据结构中、更好的处理错误,并且还是经过良好测试的。我们也接触了一个真实情况下需要生命周期注解来保证引用一直有效的场景。

+

在这一章中,我们回顾了目前为止的一些主要章节并涉及了如何在 Rust 中进行常规的 I/O 操作。通过使用命令行参数、文件、环境变量和writeln!宏与writeln!,现在你已经准备好编写命令行程序了。结合前几章的知识,你的代码将会是组织良好的,并能有效的将数据存储到合适的数据结构中、更好的处理错误,并且还是经过良好测试的。

接下来,让我们探索如何利用一些 Rust 中受函数式编程语言影响的功能:闭包和迭代器。

Rust 中的函数式语言功能 —— 迭代器和闭包

@@ -9660,6 +9741,294 @@ impl AveragedCollection {

为了支持这种样式,Rust有trait对象,这样我们可以指定给任何类型的值,只要值实现了一种特定的trait。

继承最近在很多编程语言的设计方案中失宠了。使用继承类实现代码重用需要共享比你需要共享的代码。子类不应该经常共享它们的父类的所有特色,但是继承意味着子类得到了它的父类的数据和行为。这使得一个程序的设计不灵活,创建了无意义的子类的方法被调用的可能性或者由于方法不适用于子类但是必须从父类继承,从而触发错误。另外,很多语言只允许从一个类继承,更加限制了程序设计的灵活性。

因为这些原因,Rust选择了一个另外的途径,使用trait替代继承。让我们看一下在Rust中trait对象是如何实现多态的。

+

为使用不同类型的值而设计的Trait对象

+
+

ch17-02-trait-objects.md +
+commit 872dc793f7017f815fb1e5389200fd208e12792d

+
+

在第8章,我们谈到了vector的局限是vectors只能存储同种类型的元素。我们在Listing 8-1有一个例子,其中定义了一个SpreadsheetCell 枚举类型,可以存储整形、浮点型和text,这样我们就可以在每个cell存储不同的数据类型了,同时还有一个代表一行cell的vector。当我们的代码编译的时候,如果交换地处理的各种东西是固定的类型是已知的,那么这是可行的。

+
<!-- The code example I want to reference did not have a listing number; it's
+the one with SpreadsheetCell. I will go back and add Listing 8-1 next time I
+get Chapter 8 for editing. /Carol -->
+
+

有时,我们想我们使用的类型集合是可扩展的,可以被使用我们的库的程序员扩展。比如很多图形化接口工具有一个条目列表,从这个列表迭代和调用draw方法在每个条目上。我们将要创建一个库crate,包含称为rust_gui的CUI库的结构体。我们的GUI库可以包含一些给开发者使用的类型,比如Button或者TextField。使用rust_gui的程序员会创建更多可以在屏幕绘图的类型:一个程序员可能会增加Image,另外一个可能会增加SelectBox。我们不会在本章节实现一个完善的GUI库,但是我们会展示如何把各部分组合在一起。

+

当要写一个rust_gui库时,我们不知道其他程序员要创建什么类型,所以我们无法定义一个enum来包含所有的类型。我们知道的是rust_gui需要有能力跟踪所有这些不同类型的大量的值,需要有能力在每个值上调用draw方法。我们的GUI库不需要确切地知道当调用draw方法时会发生什么,只要值有可用的方法供我们调用就可以。

+

在有继承的语言里,我们可能会定义一个名为Component的类,该类上有一个draw方法。其他的类比如ButtonImageSelectBox会从Component继承并继承draw方法。它们会各自覆写draw方法来自定义行为,但是框架会把所有的类型当作是Component的实例,并在它们上调用draw

+

定义一个带有自定义行为的Trait

+

不过,在Rust语言中,我们可以定义一个名为Draw的trait,其上有一个名为draw的方法。我们定义一个带有trait对象的vector,绑定了一种指针的trait,比如&引用或者一个Box<T>智能指针。

+

我们提到,我们不会调用结构体和枚举的对象,从而区分于其他语言的对象。在结构体的数据或者枚举的字段和impl块中的行为是分开的,而其他语言则是数据和行为被组合到一个概念里。Trait对象更像其他语言的对象,在这种场景下,他们组合了由指针组成的数据到实体对象,该对象带有在trait中定义的方法行为。但是,trait对象是和其他语言是不同的,因为我们不能向一个trait对象增加数据。trait对象不像其他语言那样有用:它们的目的是允许从公有的行为上抽象。

+

trait定义了在给定场景下我们所需要的行为。在我们会使用一个实体类型或者一个通用类型的地方,我们可以把trait当作trait对象使用。Rust的类型系统会保证我们为trait对象带入的任何值会实现trait的方法。我们不需要在编译阶段知道所有可能的类型,我们可以把所有的实例统一对待。Listing 17-03展示了如何定义一个名为Draw的带有draw方法的trait。

+

Filename: src/lib.rs

+
pub trait Draw {
+    fn draw(&self);
+}
+
+

Listing 17-3:Draw trait的定义

+ +

因为我们已经在第10章讨论过如何定义trait,你可能比较熟悉。下面是新的定义:Listing 17-4有一个名为Screen的结构体,里面有一个名为components的vector,components的类型是BoxBox<Draw>是一个trait对象:它是一个任何Box内部的实现了Drawtrait的类型的替身。

+

Filename: src/lib.rs

+
# pub trait Draw {
+#     fn draw(&self);
+# }
+#
+pub struct Screen {
+    pub components: Vec<Box<Draw>>,
+}
+
+

Listing 17-4: 定义一个Screen结构体,带有一个含有实现了Drawtrait的components vector成员

+

+

Screen结构体上,我们将要定义一个run方法,该方法会在它的components上调用draw方法,如Listing 17-5所示:

+

Filename: src/lib.rs

+
# pub trait Draw {
+#     fn draw(&self);
+# }
+#
+# pub struct Screen {
+#     pub components: Vec<Box<Draw>>,
+# }
+#
+impl Screen {
+    pub fn run(&self) {
+        for component in self.components.iter() {
+            component.draw();
+        }
+    }
+}
+
+

Listing 17-5:在Screen上实现一个run方法,该方法在每个组件上调用draw方法 +

+

这是区别于定义一个使用带有trait绑定的通用类型参数的结构体。通用类型参数一次只能被一个实体类型替代,而trait对象可以在运行时允许多种实体类型填充trait对象。比如,我们已经定义了Screen结构体使用通用类型和一个trait绑定,如Listing 17-6所示:

+

Filename: src/lib.rs

+
# pub trait Draw {
+#     fn draw(&self);
+# }
+#
+pub struct Screen<T: Draw> {
+    pub components: Vec<T>,
+}
+
+impl<T> Screen<T>
+    where T: Draw {
+    pub fn run(&self) {
+        for component in self.components.iter() {
+            component.draw();
+        }
+    }
+}
+
+

Listing 17-6: 一种Screen结构体的替代实现,它的run方法使用通用类型和trait绑定 +

+

这个例子只能使我们有一个Screen实例,这个实例有一个组件列表,所有的组件类型是Button或者TextField。如果你有同种的集合,那么可以优先使用通用和trait绑定,这是因为为了使用具体的类型,定义是在编译阶段是单一的。

+

而如果使用内部有Vec<Box<Draw>> trait对象的列表的Screen结构体,Screen实例可以同时包含Box<Button>Box<TextField>Vec。我们看它是怎么工作的,然后讨论运行时性能的实现。

+

来自我们或者库使用者的实现

+

现在,我们增加一些实现了Drawtrait的类型。我们会再次提供Button,实际上实现一个GUI库超出了本书的范围,所以draw方法的内部不会有任何有用的实现。为了想象一下实现可能的样子,Button结构体可能有 widthheightlabel`字段,如Listing 17-7所示:

+

Filename: src/lib.rs

+
# pub trait Draw {
+#     fn draw(&self);
+# }
+#
+pub struct Button {
+    pub width: u32,
+    pub height: u32,
+    pub label: String,
+}
+
+impl Draw for Button {
+    fn draw(&self) {
+        // Code to actually draw a button
+    }
+}
+
+

Listing 17-7: 实现了Draw trait的Button 结构体

+

Button上的 widthheightlabel会和其他组件不同,比如TextField可能有widthheight, +labelplaceholder字段。每个我们可以在屏幕上绘制的类型会实现Drawtrait,在draw方法中使用不同的代码,定义了如何绘制Button(GUI代码的具体实现超出了本章节的范围)。除了Draw trait,Button可能也有另一个impl块,包含了当按钮被点击的时候的响应方法。这类方法不适用于TextField这样的类型。

+

有时,使用我们的库决定了实现一个包含widthheightoptions``SelectBox结构体。它们在SelectBox类型上实现了Drawtrait,如 Listing 17-8所示:

+

Filename: src/main.rs

+
extern crate rust_gui;
+use rust_gui::Draw;
+
+struct SelectBox {
+    width: u32,
+    height: u32,
+    options: Vec<String>,
+}
+
+impl Draw for SelectBox {
+    fn draw(&self) {
+        // Code to actually draw a select box
+    }
+}
+
+

Listing 17-8: 另外一个crate中,在SelectBox结构体上使用rust_gui和实现了Draw trait +

+

我们的库的使用者现在可以写他们的main函数来创建一个Screen实例,然后通过把自身放入Box<T>变成trait对象,向screen增加SelectBoxButton。它们可以在每个Screen实例上调用run方法,这会调用每个组件的draw方法。 Listing 17-9展示了实现:

+

Filename: src/main.rs

+
use rust_gui::{Screen, Button};
+
+fn main() {
+    let screen = Screen {
+        components: vec![
+            Box::new(SelectBox {
+                width: 75,
+                height: 10,
+                options: vec![
+                    String::from("Yes"),
+                    String::from("Maybe"),
+                    String::from("No")
+                ],
+            }),
+            Box::new(Button {
+                width: 50,
+                height: 10,
+                label: String::from("OK"),
+            }),
+        ],
+    };
+
+    screen.run();
+}
+
+

Listing 17-9: 使用trait对象来存储实现了相同trait的不同类型 +

+

虽然我们不知道有些人可能有一天会增加SelectBox类型,但是我们的Screen 有能力操作SelectBox和绘制,因为SelectBox实现了Draw类型,这意味着它实现了draw方法。

+

只关心值响应的消息,而不关心值的具体类型,这类似于动态类型语言中的duck typing:如果它像鸭子一样走路,像鸭子一样叫,那么它肯定是只鸭子!在Listing 17-5的Screenrun方法的实现中,run不需要知道每个组件的具体类型。它也不检查是否一个组件是Button或者SelectBox的实例,只是调用组件的draw方法即可。通过指定Box<Draw>作为componentsvector中的值类型,我们定义了:Screen需要可以被调用其draw方法的值。

+

使用trait对象和支持duck typing的Rust类型系统的好处是,我们永远不需要在运行时检查一个值是否实现了一个特殊方法,或者担心因为调用了一个值没有实现方法而遇到错误。如果值没有实现trait对象需要的trait,Rust不会编译我们的代码。

+

比如,Listing 17-10展示了当我们创建一个把String当做其成员的Screen时发生的情况:

+

Filename: src/main.rs

+
extern crate rust_gui;
+use rust_gui::Draw;
+
+fn main() {
+    let screen = Screen {
+        components: vec![
+            Box::new(String::from("Hi")),
+        ],
+    };
+
+    screen.run();
+}
+
+

Listing 17-10: 尝试使用一种没有实现trait对象的trait的类型

+

+

我们会遇到这个错误,因为String没有实现 Drawtrait:

+
error[E0277]: the trait bound `std::string::String: Draw` is not satisfied
+  -->
+   |
+ 4 |             Box::new(String::from("Hi")),
+   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Draw` is not
+   implemented for `std::string::String`
+   |
+   = note: required for the cast to the object type `Draw`
+
+

这个报错让我们知道,或者我们传入了本来不想传给Screen的东西,我们应该传入一个不同的类型,或者是我们应该在String上实现Draw,这样,Screen才能调用它的draw方法。

+

Trait对象执行动态分发

+

回忆一下第10章,我们讨论过当我们使用通用类型的trait绑定时,编译器执行单类型的处理过程:在我们需要使用通用类型参数的地方,编译器为每个实体类型产生了非通用的函数实现和方法。由于非单类型而产生的代码是 static dispatch:当方法被调用,代码会执行在编译阶段就决定的方法,这样寻找那段代码是非常快速的。

+

当我们使用trait对象,编译器不能执行单类型的,因为我们不知道可能被代码调用的类型。而,当方法被调用的时候,Rust跟踪可能被使用的代码,然后在运行时找出为了方法被调用时该使用哪些代码。这也是我们熟知的dynamic dispatch,当运行时的查找发生时是比较耗费资源的。动态分发也防止编译器选择内联函数的代码,这样防止了一些优化。虽然我们写代码时得到了额外的代码灵活性,不过,这是一个权衡考虑。

+

Trait 对象需要对象安全

+ +

Not all traits can be made into trait objects; only object safe traits can. A +trait is object safe as long as both of the following are true:

+
    +
  • The trait does not require Self to be Sized
  • +
  • All of the trait's methods are object safe.
  • +
+

Self is a keyword that is an alias for the type that we're implementing +traits or methods on. Sized is a marker trait like the Send and Sync +traits that we talked about in Chapter 16. Sized is automatically implemented +on types that have a known size at compile time, such as i32 and references. +Types that do not have a known size include slices ([T]) and trait objects.

+

Sized is an implicit trait bound on all generic type parameters by default. +Most useful operations in Rust require a type to be Sized, so making Sized +a default requirement on trait bounds means we don't have to write T: Sized +with most every use of generics. If we want to be able to use a trait on +slices, however, we need to opt out of the Sized trait bound, and we can do +that by specifying T: ?Sized as a trait bound.

+

Traits have a default bound of Self: ?Sized, which means that they can be +implemented on types that may or may not be Sized. If we create a trait Foo +that opts out of the Self: ?Sized bound, that would look like the following:

+
trait Foo: Sized {
+    fn some_method(&self);
+}
+
+

The trait Sized is now a super trait of trait Foo, which means trait +Foo requires types that implement Foo (that is, Self) to be Sized. +We're going to talk about super traits in more detail in Chapter 19.

+

The reason a trait like Foo that requires Self to be Sized is not allowed +to be a trait object is that it would be impossible to implement the trait +Foo for the trait object Foo: trait objects aren't sized, but Foo +requires Self to be Sized. A type can't be both sized and unsized at the +same time!

+

For the second object safety requirement that says all of a trait's methods +must be object safe, a method is object safe if either:

+
    +
  • It requires Self to be Sized or
  • +
  • It meets all three of the following: +
      +
    • It must not have any generic type parameters
    • +
    • Its first argument must be of type Self or a type that dereferences to +the Self type (that is, it must be a method rather than an associated +function and have self, &self, or &mut self as the first argument)
    • +
    • It must not use Self anywhere else in the signature except for the +first argument
    • +
    +
  • +
+

Those rules are a bit formal, but think of it this way: if your method requires +the concrete Self type somewhere in its signature, but an object forgets the +exact type that it is, there's no way that the method can use the original +concrete type that it's forgotten. Same with generic type parameters that are +filled in with concrete type parameters when the trait is used: the concrete +types become part of the type that implements the trait. When the type is +erased by the use of a trait object, there's no way to know what types to fill +in the generic type parameters with.

+

An example of a trait whose methods are not object safe is the standard +library's Clone trait. The signature for the clone method in the Clone +trait looks like this:

+
pub trait Clone {
+    fn clone(&self) -> Self;
+}
+
+

String implements the Clone trait, and when we call the clone method on +an instance of String we get back an instance of String. Similarly, if we +call clone on an instance of Vec, we get back an instance of Vec. The +signature of clone needs to know what type will stand in for Self, since +that's the return type.

+

If we try to implement Clone on a trait like the Draw trait from Listing +17-3, we wouldn't know whether Self would end up being a Button, a +SelectBox, or some other type that will implement the Draw trait in the +future.

+

The compiler will tell you if you're trying to do something that violates the +rules of object safety in regards to trait objects. For example, if we had +tried to implement the Screen struct in Listing 17-4 to hold types that +implement the Clone trait instead of the Draw trait, like this:

+
pub struct Screen {
+    pub components: Vec<Box<Clone>>,
+}
+
+

We'll get this error:

+
error[E0038]: the trait `std::clone::Clone` cannot be made into an object
+ -->
+  |
+2 |     pub components: Vec<Box<Clone>>,
+  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::clone::Clone` cannot be
+  made into an object
+  |
+  = note: the trait cannot require that `Self : Sized`
+
+
diff --git a/src/ch12-04-testing-the-librarys-functionality.md b/src/ch12-04-testing-the-librarys-functionality.md index 0132c74..018d75d 100644 --- a/src/ch12-04-testing-the-librarys-functionality.md +++ b/src/ch12-04-testing-the-librarys-functionality.md @@ -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) >
-> 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 展示了这个测试: 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, +# fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { +# vec![] # } # -// ...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; + use super::*; #[test] fn one_result() { - let search = "duct"; + let query = "duct"; let contents = "\ Rust: safe, fast, productive. @@ -51,36 +42,54 @@ Pick three."; assert_eq!( vec!["safe, fast, productive."], - grep(search, contents) + search(query, contents) ); } } ``` -Listing 12-14: Creating a function where our logic will -go and a failing test for that function +Listing 12-15: Creating a failing test for the `search` +function we wish we had + +这里选择使用 "duct" 作为这个测试中需要搜索的字符串。用来搜索的文本有三行,其中只有一行包含 "duct"。我们断言`search`函数的返回值只包含期望的那一行。 + +我们还不能运行这个测试并看到它失败,因为它甚至都还不能编译!我们将增加足够的代码来使其能够编译:一个总是会返回空 vector 的`search`函数定义,如列表 12-16 所示。一旦有了它,这个测试应该能够编译并因为空 vector 并不匹配一个包含一行`"safe, fast, productive."`的 vector 而失败。 + +Filename: src/lib.rs + +```rust +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 -注意需要在`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 - --> 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` + --> src/lib.rs:5:47 + | +5 | fn search(query: &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 `query` or `contents` ``` Rust 不可能知道我们需要的是哪一个参数,所以需要告诉它。因为参数`contents`包含了所有的文本而且我们希望返回匹配的那部分文本,而我们知道`contents`是应该要使用生命周期语法来与返回值相关联的参数。 -在函数签名中将参数与返回值相关联是其他语言不会让你做的工作,所以不用担心这感觉很奇怪!掌握如何指定生命周期会随着时间的推移越来越容易,熟能生巧。你可能想要重新阅读上一部分或返回与第十章中生命周期语法部分的例子做对比。 +其他语言中并不需要你在函数签名中将参数与返回值相关联,所以这么做可能仍然感觉有些陌生,随着时间的推移会越来越容易。你可能想要将这个例子与第十章中生命周期语法部分做对比。 -现在试试运行测试: +现在试尝试运行测试: ``` $ cargo test @@ -107,54 +116,81 @@ test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured error: test failed ``` -好的,测试失败了,这正是我们所期望的。修改代码来让测试通过吧!之所以会失败是因为我们总是返回一个空的 vector。如下是如何实现`grep`的步骤: +好的,测试失败了,这正是我们所期望的。修改代码来让测试通过吧! + +### 编写使测试通过的代码 + +目前测试之所以会失败是因为我们总是返回一个空的 vector。为了修复并实现`search`,我们的程序需要遵循如下步骤: 1. 遍历每一行文本。 2. 查看这一行是否包含要搜索的字符串。 - * 如果有,将这一行加入返回列表中 - * 如果没有,什么也不做 + * 如果有,将这一行加入返回列表中。 + * 如果没有,什么也不做。 3. 返回匹配到的列表 -让我们一步一步的来,从遍历每行开始。字符串类型有一个有用的方法来处理这种情况,它刚好叫做`lines`: +让我们一步一步的来,从遍历每行开始。 + +#### 使用`lines`方法遍历每一行 + +Rust 有一个有助于一行一行遍历字符串的方法,出于方便它被成为`lines`,它如列表 12-17 这样工作: Filename: src/lib.rs ```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() { // do something with line } } ``` +Listing 12-17: Iterating through each line in +`contents` + -我们使用了一个`for`循环和`lines`方法来依次获得每一行。接下来,让我们看看这些行是否包含要搜索的字符串。幸运的是,字符串类型为此也有一个有用的方法`contains`!`contains`的用法看起来像这样: +`lines`方法返回一个迭代器。第十三张会深入了解迭代器,不过我们已经在列表 3-6 中见过使用迭代器的方法,在那里使用了一个`for`循环和迭代器在一个集合的每一项上运行一些代码。 + + + + +#### 用查询字符串搜索每一行 + +接下来将会增加检查当前行是否包含查询字符串的功能。幸运的是,字符串类型为此也有一个有用的方法叫做`contains`!如列表 12-18 所示在`search`函数中加入`contains`方法: Filename: src/lib.rs ```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() { - if line.contains(search) { + if line.contains(query) { // do something with line } } } ``` +Listing 12-18: Adding functionality to see if the line +contains the string in `query` + -最终,我们需要一个方法来存储包含要搜索字符串的行。为此可以在`for`循环之前创建一个可变的 vector 并调用`push`方法来存放一个`line`。在`for`循环之后,返回这个 vector。列表 12-15 中为完整的实现: +#### 存储匹配的行 + +最后我们需要一个方法来存储包含查询字符串的行。为此可以在`for`循环之前创建一个可变的 vector 并调用`push`方法在 vector 中存放一个`line`。在`for`循环之后,返回这个 vector,如列表 12-19 所示: Filename: src/lib.rs -```rust -fn grep<'a>(search: &str, contents: &'a str) -> Vec<&'a str> { +```rust,ignore +fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { let mut results = Vec::new(); for line in contents.lines() { - if line.contains(search) { + if line.contains(query) { results.push(line); } } @@ -163,12 +199,12 @@ fn grep<'a>(search: &str, contents: &'a str) -> Vec<&'a str> { } ``` -Listing 12-15: Fully functioning implementation of the -`grep` function +Listing 12-19: Storing the lines that match so that we +can return them -尝试运行一下: +现在`search`函数应该返回只包含`query`的那些行,而测试应该会通过。让我们运行测试: ``` $ cargo test @@ -190,9 +226,24 @@ running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured ``` -非常好!它可以工作了。现在测试通过了,我们可以考虑一下重构`grep`的实现并时刻保持其功能不变。这些代码并不坏,不过并没有利用迭代器的一些实用功能。第十三章将回到这个例子并探索迭代器和如何改进代码。 +测试通过了,很好,它可以工作了! -现在`grep`函数是可以工作的,我们还需在在`run`函数中做最后一件事:还没有打印出结果呢!增加一个`for`循环来打印出`grep`函数返回的每一行: +现在测试通过了,我们可以考虑一下重构`search`的实现并时刻保持测试通过来保持其功能不变的机会了。这些代码并不坏,不过并没有利用迭代器的一些实用功能。第十三章将回到这个例子并深入探索迭代器并看看如何改进代码。 + + + + +#### 在`run`函数中使用`search`函数 + +现在`search`函数是可以工作并测试通过了的,我们需要实际在`run`函数中调用`search`。需要将`config.query`值和`run`从文件中读取的`contents`传递给`search`函数。接着`run`会打印出`search`返回的每一行: Filename: src/lib.rs @@ -203,7 +254,7 @@ pub fn run(config: Config) -> Result<(), Box> { let mut contents = String::new(); f.read_to_string(&mut contents)?; - for line in grep(&config.search, &contents) { + for line in search(&config.query, &contents) { println!("{}", line); } @@ -213,26 +264,36 @@ pub fn run(config: Config) -> Result<(), Box> { -现在程序应该能正常运行了!试试吧: +这里再一次使用了`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 - 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 + Running `target/debug/greprs the poem.txt` +Then there's a pair of us — don't tell! To tell your name the livelong day -To an admiring bog! ``` -好极了!我们创建了一个属于自己的经典工具,并学习了很多如何组织程序的知识。我们还学习了一些文件输入输出、生命周期、测试和命令行解析的内容。 \ No newline at end of file +最后,让我们确保搜索一个在诗中哪里都没有的单词时不会得到任何行,比如 "monomorphization": + +``` +$ cargo run monomorphization poem.txt + Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs + Running `target/debug/greprs monomorphization poem.txt` +``` + +非常好!我们创建了一个属于自己的经典工具,并学习了很多如何组织程序的知识。我们还学习了一些文件输入输出、生命周期、测试和命令行解析的内容。 + +现在如果你希望的话请随意移动到第十三章。为了使这个项目章节更丰满,我们将简要的展示如何处理环境变量和打印到标准错误,这两者在编写命令行程序时都很有用。 \ 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 5e3a84d..de13051 100644 --- a/src/ch12-05-working-with-environment-variables.md +++ b/src/ch12-05-working-with-environment-variables.md @@ -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) >
-> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56 +> commit 0db6a0a34886bf02feabcab8b430b5d332a8bdf5 -让我们再增加一个功能:大小写不敏感搜索。另外,这个设定将不是一个命令行参数:相反它将是一个环境变量。当然可以选择创建一个大小写不敏感的命令行参数,不过用户要求提供一个环境变量这样设置一次之后在整个终端会话中所有的搜索都将是大小写不敏感的了。 +我们将用一个额外的功能来改进我们的工具:一个通过环境变量启用的大小写不敏感搜索的选项。我们将其设计为一个命令行参数并要求用户每次需要时都加上它,不过相反我们将使用环境变量。这允许用户设置环境变脸一次之后在整个终端会话中所有的搜索都将是大小写不敏感的了。 -### 实现并测试一个大小写不敏感`grep`函数 +### 编写一个大小写不敏感`search`函数的失败测试 -首先,让我们增加一个新函数,当设置了环境变量时会调用它。增加一个新测试并重命名已经存在的那个: +首先,增加一个新函数,当设置了环境变量时会调用它。 -```rust,ignore + + + +这里将继续遵循上一部分开始使用的 TDD 过程,其第一步是再次编写一个失败测试。我们将为新的大小写不敏感搜索函数新增一个测试函数,并将老的测试函数从`one_result`改名为`case_sensitive`来更清除的表明这两个测试的区别,如列表 12-20 所示: + +Filename: src/lib.rs + +```rust #[cfg(test)] mod test { - use {grep, grep_case_insensitive}; + use super::*; #[test] fn case_sensitive() { - let search = "duct"; + let query = "duct"; let contents = "\ Rust: safe, fast, productive. @@ -26,13 +38,13 @@ Duct tape."; assert_eq!( vec!["safe, fast, productive."], - grep(search, contents) + search(query, contents) ); } #[test] fn case_insensitive() { - let search = "rust"; + let query = "rUsT"; let contents = "\ Rust: safe, fast, productive. @@ -41,7 +53,7 @@ Trust me."; assert_eq!( vec!["Rust:", "Trust me."], - grep_case_insensitive(search, contents) + search_case_insensitive(query, contents) ); } } @@ -49,17 +61,26 @@ Trust me."; -我们将定义一个叫做`grep_case_insensitive`的新函数。它的实现与`grep`函数大体上相似,不过列表 12-16 展示了一些小的区别: +Listing 12-20: Adding a new failing test for the case +insensitive function we're about to add + +注意我们也改变了老测试中`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`都变为小写,这样不管输入参数是大写还是小写,在检查该行是否包含查询字符串时都会是小写。 Filename: src/lib.rs ```rust -fn grep_case_insensitive<'a>(search: &str, contents: &'a str) -> Vec<&'a str> { - let search = search.to_lowercase(); +fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { + let query = query.to_lowercase(); let mut results = Vec::new(); for line in contents.lines() { - if line.to_lowercase().contains(&search) { + if line.to_lowercase().contains(&query) { results.push(line); } } @@ -68,19 +89,32 @@ fn grep_case_insensitive<'a>(search: &str, contents: &'a str) -> Vec<&'a str> { } ``` -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 +Listing 12-21: Defining the `search_case_insensitive` +function to lowercase both the query and the line before comparing them -首先,将`search`字符串转换为小写,并存放于一个同名的覆盖变量中。注意现在`search`是一个`String`而不是字符串 slice,所以在将`search`传递给`contains`时需要加上 &,因为`contains`获取一个字符串 slice。 + + -接着在检查每个`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 - Running target\debug\deps\greprs-e58e9b12d35dc861.exe + Running target/debug/deps/greprs-e58e9b12d35dc861 running 2 tests 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 - Running target\debug\greprs-8a7faa2662b5030a.exe + Running target/debug/greprs-8a7faa2662b5030a running 0 tests @@ -101,13 +135,13 @@ running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured ``` -好的!现在,我们必须真正的使用新的`grep_case_insensitive`函数。首先,在`Config`结构体中增加一个配置项: +好的!现在,让我们在`run`函数中调用真正的新`search_case_insensitive`函数。首先,我们将在`Config`结构体中增加一个配置项来切换大小写敏感和大小写不敏感搜索。 Filename: src/lib.rs ```rust pub struct Config { - pub search: String, + pub query: String, pub filename: String, pub case_sensitive: bool, } @@ -115,11 +149,29 @@ pub struct Config { -接着在`run`函数中检查这个选项,并根据`case_sensitive`函数的值来决定调用哪个函数: +这里增加了`case_sensitive`字符来存放一个布尔值。接着我们需要`run`函数检查`case_sensitive`字段的值并使用它来决定是否调用`search`函数或`search_case_insensitive`函数,如列表 12-22所示: Filename: src/lib.rs -```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>{ let mut f = File::open(config.filename)?; @@ -127,9 +179,9 @@ pub fn run(config: Config) -> Result<(), Box>{ f.read_to_string(&mut contents)?; let results = if config.case_sensitive { - grep(&config.search, &contents) + search(&config.query, &contents) } else { - grep_case_insensitive(&config.search, &contents) + search_case_insensitive(&config.query, &contents) }; for line in results { @@ -140,48 +192,38 @@ 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` + -最后需要真正的检查环境变量。为了将标准库中的`env`模块引入作用域,在 *src/lib.rs* 开头增加一个`use`行: +最后需要实际检查环境变量。处理环境变量的函数位于标准库的`env`模块中,所以我们需要在 *src/lib.rs* 的开头增加一个`use std::env;`行将这个模块引入作用域中。接着在`Config::new`中使用`env`模块的`var`方法检查一个叫做`CASE_INSENSITIVE`的环境变量,如列表 12-23 所示: Filename: src/lib.rs ```rust use std::env; -``` - -并接着在`Config::new`中使用`env`模块的`vars`方法: - -Filename: src/lib.rs - -```rust -# use std::env; -# # struct Config { -# search: String, +# query: String, # filename: String, # case_sensitive: bool, # } -# + +// ...snip... + impl Config { pub fn new(args: &[String]) -> Result { if args.len() < 3 { return Err("not enough arguments"); } - let search = args[1].clone(); + let query = args[1].clone(); let filename = args[2].clone(); - let mut case_sensitive = true; - - for (name, _) in env::vars() { - if name == "CASE_INSENSITIVE" { - case_sensitive = false; - } - } + let case_sensitive = env::var("CASE_INSENSITIVE").is_err(); Ok(Config { - search: search, + query: query, filename: filename, case_sensitive: case_sensitive, }) @@ -189,32 +231,37 @@ impl Config { } ``` +Listing 12-23: Checking for an environment variable named +`CASE_INSENSITIVE` + -这里我们调用了`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 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? How dreary to be somebody! ``` +看起来程序仍然能够工作!现在将`CASE_INSENSITIVE`设置为 1 并仍使用相同的查询"to",这回应该得到包含可能有大写字母的"to"的行: + ``` $ CASE_INSENSITIVE=1 cargo run to poem.txt 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? How dreary to be somebody! To tell your name the livelong day To an admiring bog! ``` -好极了!`greprs`现在可以通过环境变量的控制来进行大小写不敏感搜索了。现在你已经知道如何处理命令行参数或环境变量了! +好极了,我们也得到了包含"To"的行!现在`greprs`程序可以通过环境变量控制进行大小写不敏感搜索了。现在你知道了如何管理由命令行参数或环境变量设置的选项了! -一些程序允许对相同配置同时使用参数_和_环境变量。在这种情况下,程序来决定参数和环境变量的优先级。作为一个留给你的测试,尝试同时通过一个命令行参数来控制大小写不敏感搜索,并在程序遇到矛盾值时决定其优先级。 +一些程序允许对相同配置同时使用参数**和**环境变量。在这种情况下,程序来决定参数和环境变量的优先级。作为一个留给你的测试,尝试同时通过一个命令行参数来控制大小写不敏感搜索,并在程序遇到矛盾值时决定其优先级。 `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 c8651cb..c7e98f8 100644 --- a/src/ch12-06-writing-to-stderr-instead-of-stdout.md +++ b/src/ch12-06-writing-to-stderr-instead-of-stdout.md @@ -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) >
-> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56 +> commit d09cfb51a239c0ebfc056a64df48fe5f1f96b207 -目前为止,我们将所有的输出都`println!`到了终端。这是可以的,不过大部分终端都提供了两种输出:“标准输出”对应大部分信息,而“标准错误”则用于错误信息。这使得处理类似于“将错误打印到终端而将其他信息输出到文件”的情况变得更容易。 +目前为止,我们将所有的输出都`println!`到了终端。大部分终端都提供了两种输出:“标准输出”对应大部分信息,而“标准错误”则用于错误信息。这种区别是命令行程序所期望拥有的行为:例如它允许用户选择将程序正常输出定向到一个文件中并仍将错误信息打印到屏幕上。但是`println!`只能够打印到标准输出,所以我们必需使用其他方法来打印到标准错误。 -可以通过在命令行使用`>`来将输出重定向到文件中,同时不使用任何参数运行来造成一个错误,就会发现我们的程序只能打印到`stdout`: +我们可以验证,目前所编写的`greprs`,所有内容都被打印到了标准输出,包括应该被写入标准错误的错误信息。可以通过故意造成错误来做到这一点,一个发生这种情况的方法是不使用任何参数运行程序。我们准备将标准输出重定向到一个文件中,不过不是标准错误。命令行程序期望以这种方式工作,因为如果输出是错误信息,它应该显示在屏幕上而不是被重定向到文件中。可以看出我们的程序目前并没有满足这个期望,通过使用`>`并指定一个文件名,*output.txt*,这是期望将标注输出重定向的文件: ``` $ cargo run > output.txt ``` -`>`语法告诉 shell 将标准输出的内容写入到 *output.txt* 文件中而不是打印到屏幕上。然而,如果运行命令后打开 *output.txt* 就会发现错误: + + + +`>`语法告诉 shell 将标准输出的内容写入到 *output.txt* 文件中而不是打印到屏幕上。我们并没有看到期望的错误信息打印到屏幕上,所以这意味着它一定被写入了文件中。让我们看看 *output.txt* 包含什么: ``` -Problem parsing arguments: not enough arguments +Application error: No search string or filename found ``` -我们希望这个信息被打印到屏幕上,而只有成功运行产生的输出写入到文件中。让我们如列表 12-17 中所示改变如何打印错误信息的方法: + + + +是的,这就是错误信息,这意味着它被打印到了标准输出。这并不是命令行程序所期望拥有的。像这样的错误信息被打印到标准错误,并当以这种方式重定向标注输出时只将运行成功时的数据打印到文件中。让我们像列表 12-23 所示改变错误信息如何被打印的。因为本章早些时候的进行的重构,所有打印错误信息的代码都在一个位置,在`main`中: Filename: src/main.rs @@ -32,8 +48,8 @@ use std::io::prelude::*; use greprs::Config; fn main() { - let mut stderr = std::io::stderr(); let args: Vec = env::args().collect(); + let mut stderr = std::io::stderr(); let config = Config::new(&args).unwrap_or_else(|err| { writeln!( @@ -41,12 +57,10 @@ fn main() { "Problem parsing arguments: {}", err ).expect("Could not write to stderr"); - process::exit(1); }); if let Err(e) = greprs::run(config) { - writeln!( &mut stderr, "Application error: {}", @@ -58,27 +72,29 @@ fn main() { } ``` -Listing 12-17: Writing error messages to `stderr` instead -of `stdout` +Listing 12-23: Writing error messages to `stderr` instead +of `stdout` using `writeln!` Rust 并没有类似`println!`这样的方便写入标准错误的函数。相反,我们使用`writeln!`宏,它有点像`println!`,不过它获取一个额外的参数。第一个参数是希望写入内容的位置。可以通过`std::io::stderr`函数获取一个标准错误的句柄。我们将一个`stderr`的可变引用传递给`writeln!`;它需要是可变的因为这样才能写入信息!第二个和第三个参数就像`println!`的第一个和第二参数:一个格式化字符串和任何需要插入的变量。 -让我们再次用相同方式运行程序,不带任何参数并用 `>`重定向`stdout`: +再次用相同方式运行程序,不带任何参数并用`>`重定向`stdout`: ``` $ 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 ``` -终端将没有输出,不过 `output.txt` 将会包含其结果: +我们并不会在终端看到任何输出,同时`output.txt`将会包含其结果: Filename: output.txt @@ -87,8 +103,10 @@ Are you nobody, too? How dreary to be somebody! ``` +这一部分展示了现在我们使用的成功时产生的标准输出和错误时产生的标准错误是恰当的。 + ## 总结 -在这一章,我们涉及了如果在 Rust 中进行常规的 I/O 操作。通过使用命令行参数、文件、环境变量和写入`stderr`的功能。现在你已经准备好编写命令行程序了。结合前几章的知识,你的代码将会是组织良好的,并能有效的将数据存储到合适的数据结构中、更好的处理错误,并且还是经过良好测试的。我们也接触了一个真实情况下需要生命周期注解来保证引用一直有效的场景。 +在这一章中,我们回顾了目前为止的一些主要章节并涉及了如何在 Rust 中进行常规的 I/O 操作。通过使用命令行参数、文件、环境变量和`writeln!`宏与`writeln!`,现在你已经准备好编写命令行程序了。结合前几章的知识,你的代码将会是组织良好的,并能有效的将数据存储到合适的数据结构中、更好的处理错误,并且还是经过良好测试的。 接下来,让我们探索如何利用一些 Rust 中受函数式编程语言影响的功能:闭包和迭代器。 \ No newline at end of file