diff --git a/README.md b/README.md index ba6dcdd..c8965ee 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,6 @@ 还在施工中:目前翻译到第十六章 -目前正在解决代码排版问题:已检查到第十一章第一部分 \ No newline at end of file +目前官方进度:[第十六章](https://github.com/rust-lang/book/projects/1)(17~20 章还在编写当中) + +GitBook 代码排版已大体解决,已不影响阅读 \ No newline at end of file diff --git a/docs/ch11-02-running-tests.html b/docs/ch11-02-running-tests.html index 7ec57f9..acfd72c 100644 --- a/docs/ch11-02-running-tests.html +++ b/docs/ch11-02-running-tests.html @@ -69,134 +69,161 @@

运行测试

-

ch11-02-running-tests.md +

ch11-02-running-tests.md
-commit cf52d81371e24e14ce31a5582bfcb8c5b80d26cc

+commit 55b294f20fc846a13a9be623bf322d8b364cee77

-

类似于cargo run会编译代码并运行生成的二进制文件,cargo test在测试模式下编译代码并运行生成的测试二进制文件。cargo test生成的二进制文件默认会并行的运行所有测试并在测试过程中捕获生成的输出,这样就更容易阅读测试结果的输出。

-

可以通过指定命令行选项来改变这些运行测试的默认行为。这些选项的一部分可以传递给cargo test,而另一些则需要传递给生成的测试二进制文件。分隔这些参数的方法是--cargo test之后列出了传递给cargo test的参数,接着是分隔符--,之后是传递给测试二进制文件的参数。

-

并行运行测试

-

测试使用线程来并行运行。为此,编写测试时需要注意测试之间不要相互依赖或者存在任何共享状态。共享状态也可能包含在运行环境中,比如当前工作目录或者环境变量。

-

如果你不希望它这样运行,或者想要更加精确的控制使用线程的数量,可以传递--test-threads参数和线程的数量给测试二进制文件。将线程数设置为 1 意味着没有任何并行操作:

+

就像cargo run会编译代码并运行生成的二进制文件,cargo test在测试模式下编译代码并运行生成的测试二进制文件。这里有一些选项可以用来改变cargo test的默认行为。例如,cargo test生成的二进制文件的默认行为是并行的运行所有测试,并捕获测试运行过程中产生的输出避免他们被显示出来使得阅读测试结果相关的内容变得更容易。可以指定命令行参数来改变这些默认行为。

+

这些选项的一部分可以传递给cargo test,而另一些则需要传递给生成的测试二进制文件。为了分隔两种类型的参数,首先列出传递给cargo test的参数,接着是分隔符--,再之后是传递给测试二进制文件的参数。运行cargo test --help会告诉你cargo test的相关参数,而运行cargo test -- --help则会告诉你位于分隔符--之后的相关参数。

+

并行或连续的运行测试

+ + +

当运行多个测试时,他们默认使用线程来并行的运行。这意味着测试会更快的运行完毕,所以可以更快的得到代码能否工作的反馈。因为测试是在同时运行的,你应该小心测试不能相互依赖或任何共享状态,包括类似于当前工作目录或者环境变量这样的共享环境。

+

例如,每一个测试都运行一些代码在硬盘上创建一个test-output.txt文件并写入一些数据。接着每一个测试都读取文件中的数据并断言这个文件包含特定的值,而这个值在每个测试中都是不同的。因为所有测试都是同时运行的,一个测试可能会在另一个测试读写文件过程中覆盖了文件。那么第二个测试就会失败,并不是因为代码不正确,而是因为测试并行运行时相互干涉。一个解决方案是使每一个测试读写不同的文件;另一个是一次运行一个测试。

+

如果你不希望测试并行运行,或者想要更加精确的控制使用线程的数量,可以传递--test-threads参数和希望使用线程的数量给测试二进制文件。例如:

$ cargo test -- --test-threads=1
 
-

捕获测试输出

-

Rust 的测试库默认捕获并丢弃标准输出和标准错误中的输出,除非测试失败了。例如,如果在测试中调用了println!而测试通过了,你将不会在终端看到println!的输出。这个行为可以通过向测试二进制文件传递--nocapture参数来禁用:

+

这里将测试线程设置为 1,告诉程序不要使用任何并行机制。这也会比并行运行花费更多时间,不过测试就不会在存在共享状态时潜在的相互干涉了。

+

显示测试输出

+

如果测试通过了,Rust 的测试库默认会捕获打印到标准输出的任何内容。例如,如果在测试中调用println!而测试通过了,我们将不会在终端看到println!的输出:只会看到说明测试通过的行。如果测试失败了,就会看到任何标准输出和其他错误信息。

+

例如,列表 11-20 有一个无意义的函数它打印出其参数的值并接着返回 10。接着还有一个会通过的测试和一个会失败的测试:

+

Filename: src/lib.rs

+
fn prints_and_returns_10(a: i32) -> i32 {
+    println!("I got the value {}", a);
+    10
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn this_test_will_pass() {
+        let value = prints_and_returns_10(4);
+        assert_eq!(10, value);
+    }
+
+    #[test]
+    fn this_test_will_fail() {
+        let value = prints_and_returns_10(8);
+        assert_eq!(5, value);
+    }
+}
+
+

Listing 11-10: Tests for a function that calls println! +

+

运行cargo test将会看到这些测试的输出:

+
running 2 tests
+test tests::this_test_will_pass ... ok
+test tests::this_test_will_fail ... FAILED
+
+failures:
+
+---- tests::this_test_will_fail stdout ----
+    I got the value 8
+thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left ==
+right)` (left: `5`, right: `10`)', src/lib.rs:19
+note: Run with `RUST_BACKTRACE=1` for a backtrace.
+
+failures:
+    tests::this_test_will_fail
+
+test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
+
+

注意输出中哪里也不会出现I got the value 4,这是当测试通过时打印的内容。这些输出被捕获。失败测试的输出,I got the value 8,则出现在输出的测试总结部分,它也显示了测试失败的原因。

+

如果你希望也能看到通过的测试中打印的值,捕获输出的行为可以通过--nocapture参数来禁用:

$ cargo test -- --nocapture
 
+

使用--nocapture参数再次运行列表 11-10 中的测试会显示:

+
running 2 tests
+I got the value 4
+I got the value 8
+test tests::this_test_will_pass ... ok
+thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left ==
+right)` (left: `5`, right: `10`)', src/lib.rs:19
+note: Run with `RUST_BACKTRACE=1` for a backtrace.
+test tests::this_test_will_fail ... FAILED
+
+failures:
+
+failures:
+    tests::this_test_will_fail
+
+test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
+
+

注意测试的输出和测试结果的输出是相互交叉的;这是由于上一部分讲到的测试是并行运行的。尝试一同使用--test-threads=1--nocapture功能来看看输出是什么样子!

通过名称来运行测试的子集

-

有时运行整个测试集会耗费很多时间。如果你负责特定位置的代码,你可能会希望只与这些代码相关的测试。cargo test有一个参数允许你通过指定名称来运行特定的测试。

-

列表 11-3 中创建了三个如下名称的测试:

-
-Filename: src/lib.rs -
#[test]
-fn add_two_and_two() {
-    assert_eq!(4, 2 + 2);
+

有时运行整个测试集会耗费很多时间。如果你负责特定位置的代码,你可能会希望只与这些代码相关的测试。可以向cargo test传递希望运行的测试的(部分)名称作为参数来选择运行哪些测试。

+

为了展示如何运行测试的子集,列表 11-11 使用add_two函数创建了三个测试来供我们选择运行哪一个:

+

Filename: src/lib.rs

+
pub fn add_two(a: i32) -> i32 {
+    a + 2
 }
 
-#[test]
-fn add_three_and_two() {
-    assert_eq!(5, 3 + 2);
-}
+#[cfg(test)]
+mod tests {
+    use super::*;
 
-#[test]
-fn one_hundred() {
-    assert_eq!(102, 100 + 2);
-}
-
-
-

Listing 11-3: Three tests with a variety of names

-
-
-

使用不同的参数会运行不同的测试子集。没有参数的话,如你所见会运行所有的测试:

-
$ cargo test
-    Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
-     Running target/debug/deps/adder-abcabcabc
-
-running 3 tests
-test add_three_and_two ... ok
-test one_hundred ... ok
-test add_two_and_two ... ok
-
-test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured
-
-

可以传递任意测试的名称来只运行那个测试:

-
$ cargo test one_hundred
-    Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
-     Running target/debug/deps/adder-abcabcabc
-
-running 1 test
-test one_hundred ... ok
-
-test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
-
-

也可以传递名称的一部分,cargo test会运行所有匹配的测试:

-
$ cargo test add
-    Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
-     Running target/debug/deps/adder-abcabcabc
-
-running 2 tests
-test add_three_and_two ... ok
-test add_two_and_two ... ok
-
-test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured
-
-

模块名也作为测试名的一部分,所以类似的模块名也可以用来指定测试特定模块。例如,如果将我们的代码组织成一个叫adding的模块和一个叫subtracting的模块并分别带有测试,如列表 11-4 所示:

-
-Filename: src/lib.rs -
mod adding {
     #[test]
     fn add_two_and_two() {
-        assert_eq!(4, 2 + 2);
+        assert_eq!(4, add_two(2));
     }
 
     #[test]
     fn add_three_and_two() {
-        assert_eq!(5, 3 + 2);
+        assert_eq!(5, add_two(3));
     }
 
     #[test]
     fn one_hundred() {
-        assert_eq!(102, 100 + 2);
-    }
-}
-
-mod subtracting {
-    #[test]
-    fn subtract_three_and_two() {
-        assert_eq!(1, 3 - 2);
+        assert_eq!(102, add_two(100));
     }
 }
 
-
-

Listing 11-4: Tests in two modules named adding and subtracting

-
-
-

执行cargo test会运行所有的测试,而模块名会出现在输出的测试名中:

-
$ cargo test
-    Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
-     Running target/debug/deps/adder-abcabcabc
-
-running 4 tests
-test adding::add_two_and_two ... ok
-test adding::add_three_and_two ... ok
-test subtracting::subtract_three_and_two ... ok
-test adding::one_hundred ... ok
-
-

运行cargo test adding将只会运行对应模块的测试而不会运行任何 subtracting 模块中的测试:

-
$ cargo test adding
-    Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
-     Running target/debug/deps/adder-abcabcabc
-
-running 3 tests
-test adding::add_three_and_two ... ok
-test adding::one_hundred ... ok
-test adding::add_two_and_two ... ok
+

Listing 11-11: Three tests with a variety of names

+

如果没有传递任何参数就运行测试,如你所见,所有测试都会并行运行:

+
running 3 tests
+test tests::add_two_and_two ... ok
+test tests::add_three_and_two ... ok
+test tests::one_hundred ... ok
 
 test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured
 
+

运行单个测试

+

可以向cargo test传递任意测试的名称来只运行这个测试:

+
$ cargo test one_hundred
+    Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
+     Running target/debug/deps/adder-06a75b4a1f2515e9
+
+running 1 test
+test tests::one_hundred ... ok
+
+test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
+
+

不能像这样指定多个测试名称,只有传递给cargo test的第一个值才会被使用。

+

过滤运行多个测试

+

然而,可以指定测试的部分名称,这样任何名称匹配这个值的测试会被运行。例如,因为头两个测试的名称包含add,可以通过cargo test add来运行这两个测试:

+
$ cargo test add
+    Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
+     Running target/debug/deps/adder-06a75b4a1f2515e9
+
+running 2 tests
+test tests::add_two_and_two ... ok
+test tests::add_three_and_two ... ok
+
+test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured
+
+

这运行了所有名字中带有add的测试。同时注意测试所在的模块作为测试名称的一部分,所以可以通过模块名来过滤运行一个模块中的所有测试。

+ +

除非指定否则忽略某些测试

-

有时一些特定的测试执行起来是非常耗费时间的,所以对于大多数cargo test命令,我们希望能排除它。无需为cargo test创建一个用来在运行所有测试时排除特定测试的参数并每次都要记得使用它,我们可以对这些测试使用ignore属性:

+

有时一些特定的测试执行起来是非常耗费时间的,所以在运行大多数cargo test的时候希望能排除他们。与其通过参数列举出所有希望运行的测试,也可以使用ignore属性来标记耗时的测试来排除他们:

Filename: src/lib.rs

#[test]
 fn it_works() {
@@ -209,11 +236,11 @@ fn expensive_test() {
     // code that takes an hour to run
 }
 
-

现在运行测试,将会发现it_works运行了,而expensive_test没有:

+

我们对想要排除的测试的#[test]之后增加了#[ignore]行。现在如果运行测试,就会发现it_works运行了,而expensive_test没有运行:

$ cargo test
    Compiling adder v0.1.0 (file:///projects/adder)
     Finished debug [unoptimized + debuginfo] target(s) in 0.24 secs
-     Running target/debug/deps/adder-abcabcabc
+     Running target/debug/deps/adder-ce99bcc2479f4607
 
 running 2 tests
 test expensive_test ... ignored
@@ -227,17 +254,26 @@ running 0 tests
 
 test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
 
-

我们可以通过cargo test -- --ignored来明确请求只运行那些耗时的测试:

-
$ cargo test -- --ignored
+

expensive_test被列为ignored,如果只希望运行被忽略的测试,可以使用cargo test -- --ignored来请求运行他们:

+ + + + +
$ cargo test -- --ignored
     Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
-     Running target/debug/deps/adder-abcabcabc
+     Running target/debug/deps/adder-ce99bcc2479f4607
 
 running 1 test
 test expensive_test ... ok
 
 test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
 
-

通过这种方式,大部分时间运行cargo test将是快速的。当需要检查ignored测试的结果而且你也有时间等待这个结果的话,可以选择执行cargo test -- --ignored

+

通过控制运行哪些测试,可以确保运行cargo test的结果是快速的。当某个时刻需要检查ignored测试的结果而且你也有时间等待这个结果的话,可以选择执行cargo test -- --ignored

diff --git a/docs/ch11-03-test-organization.html b/docs/ch11-03-test-organization.html index 572be63..e8d2ddc 100644 --- a/docs/ch11-03-test-organization.html +++ b/docs/ch11-03-test-organization.html @@ -69,16 +69,17 @@

测试的组织结构

-

ch11-03-test-organization.md +

ch11-03-test-organization.md
-commit cf52d81371e24e14ce31a5582bfcb8c5b80d26cc

+commit 55b294f20fc846a13a9be623bf322d8b364cee77

-

正如之前提到的,测试是一个很广泛的学科,而且不同的人有时也采用不同的技术和组织。Rust 社区倾向于根据测试的两个主要分类来考虑问题:单元测试unit tests)与集成测试unit tests)。单元测试倾向于更小而更专注,在隔离的环境中一次测试一个模块。他们也可以测试私有接口。集成测试对于你的库来说则完全是外部的。他们与其他用户使用相同的方式使用你得代码,他们只针对共有接口而且每个测试会测试多个模块。这两类测试对于从独立和整体的角度保证你的库符合期望是非常重要的。

+

正如之前提到的,测试是一个很广泛的学科,而且不同的开发者也采用不同的技术和组织。Rust 社区倾向于根据测试的两个主要分类来考虑问题:单元测试unit tests)与集成测试unit tests)。单元测试倾向于更小而更专注,在隔离的环境中一次测试一个模块。他们也可以测试私有接口。集成测试对于你的库来说则完全是外部的。他们与其他用户使用相同的方式使用你的代码,他们只针对共有接口而且每个测试都会测试多个模块。

+

这两类测试对于从独立和整体的角度保证你的库符合期望是非常重要的。

单元测试

-

单元测试的目的是在隔离与其他部分的环境中测试每一个单元的代码,以便于快速而准确的定位何处的代码是否符合预期。单元测试位于 src 目录中,与他们要测试的代码存在于相同的文件中。他们被分离进每个文件中他们自有的tests模块中。

+

单元测试的目的是在与其他部分隔离的环境中测试每一个单元的代码,以便于快速而准确的定位何处的代码是否符合预期。单元测试位于 src 目录中,与他们要测试的代码存在于相同的文件中。传统做法是在每个文件中创建包含测试函数的tests模块,并使用cfg(test)标注模块。

测试模块和cfg(test)

-

通过将测试放进他们自己的模块并对该模块使用cfg注解,我们可以告诉 Rust 只在执行cargo test时才编译和运行测试代码。这在当我们只希望用cargo build编译库代码时可以节省编译时间,并减少编译产物的大小因为并没有包含测试。

-

还记得上一部分新建的adder项目吗?Cargo 为我们生成了如下代码:

+

测试模块的#[cfg(test)]注解告诉 Rust 只在执行cargo test时才编译和运行测试代码,而在运行cargo build时不这么做。这在只希望构建库的时候可以节省编译时间,并能节省编译产物的空间因为他们并没有包含测试。我们将会看到因为集成测试位于另一个文件夹,他们并不需要#[cfg(test)]注解。但是因为单元测试位于与源码相同的文件中,所以使用#[cfg(test)]来指定他们不应该被包含进编译产物中。

+

还记得本章第一部分新建的adder项目吗?Cargo 为我们生成了如下代码:

Filename: src/lib.rs

#[cfg(test)]
 mod tests {
@@ -87,48 +88,10 @@ mod tests {
     }
 }
 
-

我们忽略了模块相关的信息以便更关注模块中测试代码的机制,不过现在让我们看看测试周围的代码。

-

首先,这里有一个属性cfgcfg属性让我们声明一些内容只在给定特定的配置configuration)时才被包含进来。Rust 提供了test配置用来编译和运行测试。通过这个属性,Cargo 只会在尝试运行测试时才编译测试代码。

-

接下来,tests包含了所有测试函数,而我们的代码则位于tests模块之外。tests模块的名称是一个惯例,除此之外这是一个遵守第七章讲到的常见可见性规则的普通模块。因为这是一个内部模块,我们需要将要测试的代码引入作用域。这对于一个大的模块来说是很烦人的,所以这里经常使用全局导入。

-

从本章到现在,我们一直在为adder项目编写并没有实际调用任何代码的测试。现在让我们做一些改变!在 src/lib.rs 中,放入add_two函数和带有一个检验代码的测试的tests模块,如列表 11-5 所示:

-
-Filename: src/lib.rs -
pub fn add_two(a: i32) -> i32 {
-    a + 2
-}
-
-#[cfg(test)]
-mod tests {
-    use add_two;
-
-    #[test]
-    fn it_works() {
-        assert_eq!(4, add_two(2));
-    }
-}
-
-
-

Listing 11-5: Testing the function add_two in a child tests module

-
-
-

注意除了测试函数之外,我们还在tests模块中添加了use add_two;。这将我们想要测试的代码引入到了内部的tests模块的作用域中,正如任何内部模块需要做的那样。如果现在使用cargo test运行测试,它会通过:

-
running 1 test
-test tests::it_works ... ok
-
-test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
-
-

如果我们忘记将add_two函数引入作用域,将会得到一个 unresolved name 错误,因为tests模块并不知道任何关于add_two函数的信息:

-
error[E0425]: unresolved name `add_two`
- --> src/lib.rs:9:23
-  |
-9 |         assert_eq!(4, add_two(2));
-  |                       ^^^^^^^ unresolved name
-
-

如果这个模块包含很多希望测试的代码,在测试中列出每一个use语句将是很烦人的。相反在测试子模块中使用use super::*;来一次将所有内容导入作用域中是很常见的。

+

这里自动生成了测试模块。cfg属性代表 configuration ,它告诉 Rust 其之后的项只被包含进特定配置中。在这个例子中,配置是test,Rust 所提供的用于编译和运行测试的配置。通过使用这个属性,Cargo 只会在我们主动使用cargo test运行测试时才编译测试代码。除了标注为#[test]的函数之外,这还包括测试模块中可能存在的帮助函数。

测试私有函数

-

测试社区中一直存在关于是否应该对私有函数进行单元测试的论战。不过无论你坚持哪种测试意识形态,Rust 确实允许你测试私有函数,由于私有性规则。考虑列表 11-6 中带有私有函数internal_adder的代码:

-
-Filename: src/lib.rs +

测试社区中一直存在关于是否应该对私有函数进行单元测试的论战,而其他语言中难以甚至不可能测试私有函数。不过无论你坚持哪种测试意识形态,Rust 的私有性规则确实允许你测试私有函数,由于私有性规则。考虑列表 11-12 中带有私有函数internal_adder的代码:

+

Filename: src/lib.rs

pub fn add_two(a: i32) -> i32 {
     internal_adder(a, 2)
 }
@@ -139,7 +102,7 @@ fn internal_adder(a: i32, b: i32) -> i32 {
 
 #[cfg(test)]
 mod tests {
-    use internal_adder;
+    use super::*;
 
     #[test]
     fn internal() {
@@ -147,18 +110,21 @@ mod tests {
     }
 }
 
-
-

Listing 11-6: Testing a private function

-
-
-

因为测试也不过是 Rust 代码而tests也只是另一个模块,我们完全可以在一个测试中导入并调用internal_adder。如果你并不认为私有函数应该被测试,Rust 也不会强迫你这么做。

+

Listing 11-12: Testing a private function

+ + +

注意internal_adder函数并没有标记为pub,不过因为测试也不过是 Rust 代码而tests也仅仅是另一个模块,我们完全可以在测试中导入和调用internal_adder。如果你并不认为私有函数应该被测试,Rust 也不会强迫你这么做。

集成测试

-

在 Rust 中,集成测试对于需要测试的库来说是完全独立。他们同其他代码一样使用库文件。他们的目的是测试库的个个部分结合起来能否正常工作。每个能正确运行的代码单元集成在一起也可能会出现问题,所以集成测试的覆盖率也是很重要的。

+

在 Rust 中,集成测试对于需要测试的库来说是完全独立。他们同其他代码一样使用库文件,这意味着他们只能调用作为库公有 API 的一部分的函数。他们的目的是测试库的多个部分能否一起正常工作。每个能单独正确运行的代码单元集成在一起也可能会出现问题,所以集成测试的覆盖率也是很重要的。为了创建集成测试,首先需要一个 tests 目录。

tests 目录

-

Cargo 支持位于 tests 目录中的集成测试。如果创建它并放入 Rust 源文件,Cargo 会将每一个文件当作单独的 crate 来编译。让我们试一试!

-

首先,在项目根目录创建一个 tests 目录,挨着 src 目录。接着新建一个文件 tests/integration_test.rs,并写入列表 11-7 中的代码:

-
-Filename: tests/integration_test.rs +

为了编写集成测试,需要在项目根目录创建一个 tests 目录,与 src 同级。Cargo 知道如何去寻找这个目录中的集成测试文件。接着可以随意在这个文件夹中创建任意多的测试文件,Cargo 会将每一个文件当作单独的 crate 来编译。

+

让我们试一试吧!保留列表 11-12 中 src/lib.rs 的代码。创建一个 tests 目录,新建一个文件 tests/integration_test.rs,并输入列表 11-13 中的代码。

+

Filename: tests/integration_test.rs

extern crate adder;
 
 #[test]
@@ -166,23 +132,21 @@ fn it_adds_two() {
     assert_eq!(4, adder::add_two(2));
 }
 
-
-

Listing 11-7: An integration test of a function in the adder crate

-
-
-

在开头使用了extern crate adder,单元测试中并不需要它。tests目录中的每一个测试文件都是完全独立的 crate,所以需要在每个文件中导入我们的库。这也就是为何tests是编写集成测试的绝佳场所:他们像任何其他用户那样,需要将库导入 crate 并只能使用公有 API。

-

这个文件中也不需要tests模块。除非运行测试否则整个文件夹都不会被编译,所以无需将任何部分标记为#[cfg(test)]。另外每个测试文件都被隔离进其自己的 crate 中,无需进一步隔离测试代码。

-

让我们运行集成测试,同样使用cargo test来运行:

-
$ cargo test
+

Listing 11-13: An integration test of a function in the +adder crate

+

我们在顶部增加了extern crate adder,这在单元测试中是不需要的。这是因为每一个tests目录中的测试文件都是完全独立的 crate,所以需要在每一个文件中导入库。集成测试就像其他使用者那样通过导入 crate 并只使用公有 API 来使用库文件。

+

并不需要将 tests/integration_test.rs 中的任何代码标注为#[cfg(test)]。Cargo 对tests文件夹特殊处理并只会在运行cargo test时编译这个目录中的文件。现在就试试运行cargo test

+
cargo test
    Compiling adder v0.1.0 (file:///projects/adder)
+    Finished debug [unoptimized + debuginfo] target(s) in 0.31 secs
      Running target/debug/deps/adder-abcabcabc
 
 running 1 test
-test tests::it_works ... ok
+test tests::internal ... ok
 
 test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
 
-     Running target/debug/integration_test-952a27e0126bb565
+     Running target/debug/deps/integration_test-ce99bcc2479f4607
 
 running 1 test
 test it_adds_two ... ok
@@ -195,8 +159,14 @@ running 0 tests
 
 test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
 
-

现在有了三个部分的输出:单元测试、集成测试和文档测试。注意当在任何 src 目录的文件中增加单元测试时,单元测试部分的对应输出也会增加。增加集成测试文件中的测试函数也会对应增加输出。如果在 tests 目录中增加集成测试文件,则会增加更多集成测试部分:一个文件对应一个部分。

-

cargo test指定测试函数名称参数也会匹配集成测试文件中的函数。为了只运行某个特定集成测试文件中的所有测试,可以使用cargo test--test参数:

+ + +

现在有了三个部分的输出:单元测试、集成测试和文档测试。第一部分单元测试与我们之前见过的一样:每一个单元测试一行(列表 11-12 中有一个叫做internal的测试),接着是一个单元测试的总结行。

+

集成测试部分以行Running target/debug/deps/integration-test-ce99bcc2479f4607(输出最后的哈希值可能不同)开头。接着是每一个集成测试中的测试函数一行,以及一个就在Doc-tests adder部分开始之前的集成测试的总结行。

+

注意在任意 src 文件中增加更多单元测试函数会增加更多单元测试部分的测试结果行。在我们创建的集成测试文件中增加更多测试函数会增加更多集成测试部分的行。每一个集成测试文件有其自己的部分,所以如果在 tests 目录中增加更多文件,这里就会有更多集成测试部分。

+

我们仍然可以通过指定测试函数的名称作为cargo test的参数来运行特定集成测试。为了运行某个特定集成测试文件中的所有测试,使用cargo test--test后跟文件的名称:

$ cargo test --test integration_test
     Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
      Running target/debug/integration_test-952a27e0126bb565
@@ -206,13 +176,63 @@ test it_adds_two ... ok
 
 test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
 
+

这些只是 tests 目录中我们指定的文件中的测试。

集成测试中的子模块

-

随着集成测试的增加,你可能希望在 tests 目录增加更多文件,例如根据测试的功能来将测试分组。正如我们之前提到的,这是可以的,Cargo 会将每一个文件当作一个独立的 crate。

-

最终,可能会有一系列在所有集成测试中通用的帮助函数,例如建立通用场景的函数。如果你将这些函数提取到 tests 目录的一个文件中,比如说 tests/common.rs,则这个文件将会像这个目录中的其他包含测试的 Rust 文件一样被编译进一个单独的 crate 中。它也会作为一个独立的部分出现在测试输出中。因为这很可能不是你所希望的,所以建议在子目录中使用 mod.rs 文件,比如 tests/common/mod.rs,来放置帮助函数。tests 的子目录不会被作为单独的 crate 编译或者作为单独的部分出现在测试输出中。

+

随着集成测试的增加,你可能希望在 tests 目录增加更多文件,例如根据测试的功能来将测试分组。正如我们之前提到的,每一个 tests 目录中的文件都被编译为单独的 crate。

+

将每个集成测试文件当作其自己的 crate 来对待有助于创建更类似与终端用户使用 crate 那样的单独的作用域。然而,这意味着考虑到像第七章学习的如何将代码分隔进模块和文件那样,tests 目录中的文件不能像 src 中的文件那样共享相同的行为。

+

对于 tests 目录中文件的不同行为,通常在如果有一系列有助于多个集成测试文件的帮助函数,而你尝试遵循第七章的步骤将他们提取到一个通用的模块中时显得很明显。例如,如果我们创建了 tests/common.rs 并将setup函数放入其中,这里将放入一些希望能够在多个测试文件的多个测试函数中调用的代码:

+

Filename: tests/common.rs

+
pub fn setup() {
+    // setup code specific to your library's tests would go here
+}
+
+

如果再次运行测试,将会在测试结果中看到一个对应 common.rs 文件的新部分,即便这个文件并没有包含任何测试函数,或者没有任何地方调用了setup函数:

+
running 1 test
+test tests::internal ... ok
+
+test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
+
+     Running target/debug/deps/common-b8b07b6f1be2db70
+
+running 0 tests
+
+test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
+
+     Running target/debug/deps/integration_test-d993c68b431d39df
+
+running 1 test
+test it_adds_two ... ok
+
+test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
+
+   Doc-tests adder
+
+running 0 tests
+
+test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
+
+ +

common出现在测试结果中并显示running 0 tests,这不是我们想要的;我们只是希望能够在其他集成测试文件中分享一些代码罢了。

+

为了使common不出现在测试输出中,需要使用第七章学习到的另一个将代码提取到文件的方式:不再创建tests/common.rs,而是创建 tests/common/mod.rs。当将setup代码移动到 tests/common/mod.rs 并去掉 tests/common.rs 文件之后,测试输出中将不会出现这一部分。tests 目录中的子目录不会被作为单独的 crate 编译或作为一部分出现在测试输出中。

+

一旦拥有了 tests/common/mod.rs,就可以将其作为模块来在任何集成测试文件中使用。这里是一个 tests/integration_test.rs 中调用setup函数的it_adds_two测试的例子:

+

Filename: tests/integration_test.rs

+
extern crate adder;
+
+mod common;
+
+#[test]
+fn it_adds_two() {
+    common::setup();
+    assert_eq!(4, adder::add_two(2));
+}
+
+

注意mod common;声明与第七章中的模块声明相同。接着在测试函数中就可以调用common::setup()了。

二进制 crate 的集成测试

-

如果项目是二进制 crate 并且只包含 src/main.rs 而没有 src/lib.rs,这样就不可能在 tests 创建集成测试并使用 extern crate 导入 src/main.rs 中的函数了。这也是 Rust 二进制项目明确采用 src/main.rs 调用 src/lib.rs 中逻辑的结构的原因之一。通过这种结构,集成测试就可以使用extern crate测试库 crate 中的主要功能,而如果这些功能没有问题的话,src/main.rs 中的少量代码也就会正常工作且不需要测试。

+

如果项目是二进制 crate 并且只包含 src/main.rs 而没有 src/lib.rs,这样就不可能在 tests 创建集成测试并使用 extern crate 导入 src/main.rs 中的函数了。只有库 crate 向其他 crate 暴露了可以调用和使用的函数;二进制 crate 只意在单独运行。

+

这也是 Rust 二进制项目明确采用 src/main.rs 调用 src/lib.rs 中逻辑这样的结构的原因之一。通过这种结构,集成测试就可以使用extern crate测试库 crate 中的主要功能,而如果这些重要的功能没有问题的话,src/main.rs 中的少量代码也就会正常工作且不需要测试。

总结

-

Rust 的测试功能提供了一个确保即使改变代码函数也能继续以指定方式运行的途径。单元测试独立的验证库的每一部分并能够测试私有实现细节。集成测试则涉及多个部分结合起来时能否使用,并像其他代码那样测试库的公有 API。Rust 的类型系统和所有权规则可以帮助避免一些 bug,不过测试对于减少代码是否符合期望的逻辑 bug 是很重要的。

+

Rust 的测试功能提供了一个确保即使做出改变函数也能继续以指定方式运行的途径。单元测试独立的验证库的不同部分并能够测试私有实现细节。集成测试则涉及多个部分结合起来工作时的用例,并像其他代码那样测试库的公有 API。即使 Rust 的类型系统和所有权规则可以帮助避免一些 bug,不过测试对于减少代码是否符合期望相关的逻辑 bug 是很重要的。

接下来让我们结合本章所学和其他之前章节的知识,在下一章一起编写一个项目!

diff --git a/docs/ch12-00-an-io-project.html b/docs/ch12-00-an-io-project.html index 890c845..cae0e54 100644 --- a/docs/ch12-00-an-io-project.html +++ b/docs/ch12-00-an-io-project.html @@ -69,9 +69,9 @@

一个 I/O 项目

-

ch12-00-an-io-project.md +

ch12-00-an-io-project.md
-commit efd59dd0fe8e3658563fb5fd289af9d862e07a03

+commit 4f2dc564851dc04b271a2260c834643dfd86c724

之前几个章节我们学习了很多知识。让我们一起运用这些新知识来构建一个项目。在这个过程中,我们还将学习到更多 Rust 标准库的内容。

那么我们应该写点什么呢?这得是一个利用 Rust 优势的项目。Rust 的一个强大的用途是命令行工具:Rust 的运行速度、安全性、“单二进制文件”输出和跨平台支持使得它称为这类工作的绝佳选择。所以我们将创建一个我们自己的经典命令行工具:grepgrep有着极为简单的应用场景,它完成如下工作:

@@ -82,7 +82,7 @@ commit efd59dd0fe8e3658563fb5fd289af9d862e07a03

  • 打印出这些行
  • 另外,我们还将添加一个额外的功能:一个环境变量允许我们大小写不敏感的搜索字符串参数。

    -

    还有另一个很好的理由使用grep作为示例项目:Rust 社区的成员,Andrew Gallant,已经使用 Rust 创建了一个功能非常完整的grep版本。它叫做ripgrep,并且它非常非常快。这样虽然我们的grep将会非常简单,你也会掌握阅读现实生活中项目的基础知识。

    +

    还有另一个很好的理由使用grep作为示例项目:Rust 社区的成员,Andrew Gallant,已经使用 Rust 创建了一个功能非常完整的grep版本。它叫做ripgrep,并且它非常非常快。这样虽然我们的grep将会非常简单,你也会掌握阅读现真实项目的基础知识。

    这个项目将会结合之前所学的一些内容:

    • 代码组织(使用第七章学习的模块)
    • diff --git a/docs/ch12-01-accepting-command-line-arguments.html b/docs/ch12-01-accepting-command-line-arguments.html index 76c02c7..aef7fee 100644 --- a/docs/ch12-01-accepting-command-line-arguments.html +++ b/docs/ch12-01-accepting-command-line-arguments.html @@ -71,7 +71,7 @@

      ch12-01-accepting-command-line-arguments.md
      -commit 4f2dc564851dc04b271a2260c834643dfd86c724

      +commit c49e5ee8859f8eb8f8867cbeafbdf5b802aa5894

      第一个任务是让greprs接受两个命令行参数。crates.io 上有一些现存的库可以帮助我们,不过因为我们正在学习,我们将自己实现一个。

      我们需要调用一个 Rust 标准库提供的函数:std::env::args。这个函数返回一个传递给程序的命令行参数的迭代器iterator)。我们还未讨论到迭代器,第十三章会全面的介绍他们。但是对于我们的目的来说,使用他们并不需要知道多少技术细节。我们只需要明白两点:

      @@ -80,8 +80,7 @@ commit 4f2dc564851dc04b271a2260c834643dfd86c724

    • 在迭代器上调用collect方法可以将其生成的元素转换为一个 vector。
    • 让我们试试列表 12-1 中的代码:

      -
      -Filename: src/main.rs +

      Filename: src/main.rs

      use std::env;
       
       fn main() {
      @@ -89,12 +88,10 @@ fn main() {
           println!("{:?}", args);
       }
       
      -
      -

      Listing 12-1: Collect the command line arguments into a vector and print them out

      -
      -
      +

      Listing 12-1: Collect the command line arguments into a +vector and print them out

      -

      首先使用use语句来将std::env模块引入作用域。当函数嵌套了多于一层模块时,比如说std::env::args,通常使用use将父模块引入作用域,而不是引入其本身。env::args比单独的args要明确一些。当然,如果使用了多余一个std::env中的函数,我们也只需要一个use语句。

      +

      首先使用use语句来将std::env模块引入作用域。当函数嵌套了多于一层模块时,比如说std::env::args,通常使用use将父模块引入作用域,而不是引入其本身。env::args比单独的args要明确一些。当然,如果使用了多于一个std::env中的函数时,我们也只需要一个use语句。

      main函数的第一行,我们调用了env::args,并立即使用collect来创建了一个 vector。这里我们也显式的注明了args的类型:collect可以被用来创建很多类型的集合。Rust 并不能推断出我们需要什么类型,所以类型注解是必须的。在 Rust 中我们很少会需要注明类型,不过collect是就一个通常需要这么做的函数。

      最后,我们使用调试格式:?打印出 vector。让我们尝试不用参数运行代码,接着用两个参数:

      $ cargo run
      @@ -106,9 +103,8 @@ $ cargo run needle haystack
       

      你会注意一个有趣的事情:二进制文件的名字是第一个参数。其原因超出了本章介绍的范围,不过这是我们必须记住的。

      现在我们有了一个访问所有参数的方法,让我们如列表 12-2 中所示将需要的变量存放到变量中:

      -
      -Filename: src/main.rs -
      use std::env;
      +

      Filename: src/main.rs

      +
      use std::env;
       
       fn main() {
           let args: Vec<String> = env::args().collect();
      @@ -120,10 +116,8 @@ fn main() {
           println!("In file {}", filename);
       }
       
      -
      -

      Listing 12-2: Create variables to hold the search argument and filename argument

      -
      -
      +

      Listing 12-2: Create variables to hold the search +argument and filename argument

      记住,程序名称是是第一个参数,所以并不需要args[0]。我们决定从第一个参数将是需要搜索的字符串,所以将第一个参数的引用放入变量search中。第二个参数将是文件名,将其放入变量filename中。再次尝试运行程序:

      $ cargo run test sample.txt
      diff --git a/docs/ch12-02-reading-a-file.html b/docs/ch12-02-reading-a-file.html
      index 04aef1c..2bcb988 100644
      --- a/docs/ch12-02-reading-a-file.html
      +++ b/docs/ch12-02-reading-a-file.html
      @@ -71,7 +71,7 @@
       

      ch12-02-reading-a-file.md
      -commit 4f2dc564851dc04b271a2260c834643dfd86c724

      +commit c49e5ee8859f8eb8f8867cbeafbdf5b802aa5894

      现在有了一些包含我们需要的信息的变量了,让我们试着使用他们。下一步目标是打开需要搜索的文件。为此,我需要一个文件。在项目的根目录创建一个文件poem.txt,并写入一些艾米莉·狄金森(Emily Dickinson)的诗:

      Filename: poem.txt

      @@ -90,9 +90,8 @@ short, but that has multiple lines and some repetition. We could search through code; that gets a bit meta and possibly confusing... Changes to this are most welcome. /Carol -->

      创建完这个文件后,让我们编辑 src/main.rs 并增加如列表 12-3 所示用来打开文件的代码:

      -
      -Filename: src/main.rs -
      use std::env;
      +

      Filename: src/main.rs

      +
      use std::env;
       use std::fs::File;
       use std::io::prelude::*;
       
      @@ -113,10 +112,8 @@ fn main() {
           println!("With text:\n{}", contents);
       }
       
      -
      -

      Listing 12-3: Read the contents of the file specified by the second argument

      -
      -
      +

      Listing 12-3: Read the contents of the file specified by +the second argument

      这里增加了一些新内容。首先,需要更多的use语句来引入标准库中的相关部分:我们需要std::fs::File来处理文件,而std::io::prelude::*则包含许多对于 I/O 包括文件 I/O 有帮助的 trait。类似于 Rust 有一个通用的 prelude 来自动引入特定内容,std::io也有其自己的 prelude 来引入处理 I/O 时需要的内容。不同于默认的 prelude,必须显式use位于std::io中的 prelude。

      main中,我们增加了三点内容:第一,我们获取了文件的句柄并使用File::open函数与第二个参数中指定的文件名来打开这个文件。第二,我们在变量contents中创建了一个空的可变的String,接着对文件句柄调用read_to_string并以contents字符串作为参数,contentsread_to_string将会放置它读取到的数据地方。最后,我们打印出了整个文件的内容,这是一个确认目前为止的程序能够工作的方法。

      diff --git a/docs/ch12-03-improving-error-handling-and-modularity.html b/docs/ch12-03-improving-error-handling-and-modularity.html index dc8ff0a..2e52d2b 100644 --- a/docs/ch12-03-improving-error-handling-and-modularity.html +++ b/docs/ch12-03-improving-error-handling-and-modularity.html @@ -71,13 +71,13 @@

      ch12-03-improving-error-handling-and-modularity.md
      -commit 4f2dc564851dc04b271a2260c834643dfd86c724

      +commit bdab3f38da5b7bf7277bfe21ec59a7a81880e6b4

      为了完善我们程序有四个问题需要修复,而他们都与潜在的错误和程序结构有关。第一个问题是在哪打开文件:我们使用了expect来在打开文件失败时指定一个错误信息,不过这个错误信息只是说“文件不存在”。还有很多打开文件失败的方式,不过我们总是假设是由于缺少文件导致的。例如,文件存在但是没有打开它的权限:这时,我们就打印出了错误不符合事实的错误信息!

      第二,我们不停的使用expect,这就有点类似我们之前在不传递任何命令行参数时索引会panic!时注意到的问题:这虽然时_可以工作_的,不过这有点没有原则性,而且整个程序中都需要他们,将错误处理都置于一处则会显得好很多。

      第三个问题是main函数现在处理两个工作:解析参数,并打开文件。对于一个小的函数来说,这不是什么大问题。然而随着程序中的main函数不断增长,main函数中独立的任务也会越来越多。因为一个函数拥有很多职责,它将难以理解、难以测试并难以在不破坏其他部分的情况下做出修改。

      这也关系到我们的第四个问题:searchfilename是程序中配置性的变量,而像fcontents则用来执行程序逻辑。随着main函数增长,将引入更多的变量到作用域中,而当作用域中有更多的变量,将更难以追踪哪个变量用于什么目的。如果能够将配置型变量组织进一个结构就能使他们的目的更明确了。

      -

      让我们重新组成程序来解决这些问题。

      +

      让我们重新组织程序来解决这些问题。

      二进制项目的关注分离

      这类项目组织上的问题在很多相似类型的项目中很常见,所以 Rust 社区开发出一种关注分离的组织模式。这种模式可以用来组织任何用 Rust 构建的二进制项目,所以可以证明应该更早的开始这项重构,以为我们的项目符合这个模式。这个模式看起来像这样:

        @@ -94,13 +94,8 @@ commit 4f2dc564851dc04b271a2260c834643dfd86c724

      好的!老实说这个模式好像还很复杂。这就是关注分离的所有内容:main.rs 负责实际的程序运行,而 lib.rs 处理所有真正的任务逻辑。让我们将程序重构成这种模式。首先,提取出一个目的只在于解析参数的函数。列表 12-4 中展示了一个新的开始,main函数调用了一个新函数parse_config,它仍然定义于 src/main.rs 中:

      -
      -Filename: src/main.rs -
      # use std::env;
      -# use std::fs::File;
      -# use std::io::prelude::*;
      -#
      -fn main() {
      +

      Filename: src/main.rs

      +
      fn main() {
           let args: Vec<String> = env::args().collect();
       
           let (search, filename) = parse_config(&args);
      @@ -109,13 +104,6 @@ fn main() {
           println!("In file {}", filename);
       
           // ...snip...
      -#
      -#     let mut f = File::open(filename).expect("file not found");
      -#
      -#     let mut contents = String::new();
      -#     f.read_to_string(&mut contents).expect("something went wrong reading the file");
      -#
      -#     println!("With text:\n{}", contents);
       }
       
       fn parse_config(args: &[String]) -> (&str, &str) {
      @@ -125,10 +113,8 @@ fn parse_config(args: &[String]) -> (&str, &str) {
           (search, filename)
       }
       
      -
      -

      Listing 12-4: Extract a parse_config function from main

      -
      -
      +

      Listing 12-4: Extract a parse_config function from +main

      这看起来好像有点复杂,不过我们将一点一点的开展重构。在做出这些改变之后,再次运行程序并验证参数解析是否仍然正常。经常验证你的进展是一个好习惯,这样在遇到问题时就能更好地理解什么修改造成了错误。

      组合配置值

      @@ -137,13 +123,8 @@ fn parse_config(args: &[String]) -> (&str, &str) {

      注意:一些同学将当使用符合类型更为合适的时候使用基本类型当作一种称为基本类型偏执primitive obsession)的反模式。

      让我们引入一个结构体来存放所有的配置。列表 12-5 中展示了新增的Config结构体定义、重构后的parse_configmain函数中的相关更新:

      -
      -Filename: src/main.rs -
      # use std::env;
      -# use std::fs::File;
      -# use std::io::prelude::*;
      -#
      -fn main() {
      +

      Filename: src/main.rs

      +
      fn main() {
           let args: Vec<String> = env::args().collect();
       
           let config = parse_config(&args);
      @@ -154,10 +135,6 @@ fn main() {
           let mut f = File::open(config.filename).expect("file not found");
       
           // ...snip...
      -#     let mut contents = String::new();
      -#     f.read_to_string(&mut contents).expect("something went wrong reading the file");
      -#
      -#    println!("With text:\n{}", contents);
       }
       
       struct Config {
      @@ -175,30 +152,22 @@ fn parse_config(args: &[String]) -> Config {
           }
       }
       
      -
      -

      Listing 12-5: Refactoring parse_config to return an instance of a Config -struct

      -
      -
      +

      Listing 12-5: Refactoring parse_config to return an +instance of a Config struct

      parse_config的签名现在表明它返回一个Config值。在parse_config的函数体中,我们之前返回了argsString值引用的字符串 slice,不过Config定义为拥有两个有所有权的String值。因为parse_config的参数是一个String值的 slice,Config实例不能获取String值的所有权:这违反了 Rust 的借用规则,因为main函数中的args变量拥有这些String值并只允许parse_config函数借用他们。

      还有许多不同的方式可以处理String的数据;现在我们使用简单但低效率的方式,在字符串 slice 上调用clone方法。clone调用会生成一个字符串数据的完整拷贝,而且Config实例可以拥有它,不过这会消耗更多时间和内存来储存拷贝字符串数据的引用,不过拷贝数据让我们使我们的代码显得更加直白。

      使用clone权衡取舍

      -

      由于其运行时消耗,许多 Rustacean 之间有一个趋势是倾向于不使用clone来解决所有权问题。在关于迭代器的第XX章中,我们将会学习如何更有效率的处理这种情况。现在,为了编写我们的程序拷贝一些字符串是没有问题。我们只进行了一次拷贝,而且文件名和要搜索的字符串都比较短。随着你对 Rust 更加熟练,将更轻松的省略这个权衡的步骤,不过现在调用clone是完全可以接受的。

      +

      由于其运行时消耗,许多 Rustacean 之间有一个趋势是倾向于不使用clone来解决所有权问题。在关于迭代器的第十三章中,我们将会学习如何更有效率的处理这种情况。现在,为了编写我们的程序拷贝一些字符串是没有问题。我们只进行了一次拷贝,而且文件名和要搜索的字符串都比较短。随着你对 Rust 更加熟练,将更轻松的省略这个权衡的步骤,不过现在调用clone是完全可以接受的。

      main函数更新为将parse_config返回的Config实例放入变量config中,并将分别使用searchfilename变量的代码更新为使用Config结构体的字段。

      创建一个Config构造函数

      现在让我们考虑一下parse_config的目的:这是一个创建Config示例的函数。我们已经见过了一个创建实例函数的规范:像String::new这样的new函数。列表 12-6 中展示了将parse_config转换为一个Config结构体关联函数new的代码:

      -
      -Filename: src/main.rs -
      # use std::env;
      -# use std::fs::File;
      -# use std::io::prelude::*;
      -#
      -fn main() {
      +

      Filename: src/main.rs

      +
      fn main() {
           let args: Vec<String> = env::args().collect();
       
           let config = Config::new(&args);
      @@ -207,21 +176,8 @@ fn main() {
           println!("In file {}", config.filename);
       
           // ...snip...
      -
      -#     let mut f = File::open(config.filename).expect("file not found");
      -#
      -#     let mut contents = String::new();
      -#     f.read_to_string(&mut contents).expect("something went wrong reading the file");
      -#
      -#    println!("With text:\n{}", contents);
      -
       }
       
      -# struct Config {
      -#     search: String,
      -#     filename: String,
      -# }
      -#
       // ...snip...
       
       impl Config {
      @@ -236,43 +192,14 @@ impl Config {
           }
       }
       
      -
      -

      Listing 12-6: Changing parse_config into Config::new

      -
      -
      +

      Listing 12-6: Changing parse_config into +Config::new

      我们将parse_config的名字改为new并将其移动到impl块中。我们也更新了main中的调用代码。再次尝试编译并确保程序可以运行。

      从构造函数返回Result

      这是我们对这个方法最后的重构:还记得当 vector 含有少于三个项时访问索引 1 和 2 会 panic 并给出一个糟糕的错误信息的代码吗?让我们来修改它!列表 12-7 展示了如何在访问这些位置之前检查 slice 是否足够长,并使用一个更好的 panic 信息:

      -
      -Filename: src/main.rs -
      # use std::env;
      -# use std::fs::File;
      -# use std::io::prelude::*;
      -#
      -# fn main() {
      -#     let args: Vec<String> = env::args().collect();
      -#
      -#     let config = Config::new(&args);
      -#
      -#     println!("Searching for {}", config.search);
      -#     println!("In file {}", config.filename);
      -#
      -#     let mut f = File::open(config.filename).expect("file not found");
      -#
      -#     let mut contents = String::new();
      -#     f.read_to_string(&mut contents).expect("something went wrong reading the file");
      -#
      -#     println!("With text:\n{}", contents);
      -# }
      -#
      -# struct Config {
      -#     search: String,
      -#     filename: String,
      -# }
      -#
      -# impl Config {
      -// ...snip...
      +

      Filename: src/main.rs

      +
      // ...snip...
       fn new(args: &[String]) -> Config {
           if args.len() < 3 {
               panic!("not enough arguments");
      @@ -280,19 +207,10 @@ fn new(args: &[String]) -> Config {
       
           let search = args[1].clone();
           // ...snip...
      -#     let filename = args[2].clone();
      -#
      -#     Config {
      -#         search: search,
      -#         filename: filename,
      -#     }
       }
      -# }
       
      -
      -

      Listing 12-7: Adding a check for the number of arguments

      -
      -
      +

      Listing 12-7: Adding a check for the number of +arguments

      通过在new中添加这额外的几行代码,再次尝试不带参数运行程序:

      $ cargo run
      @@ -302,37 +220,8 @@ thread 'main' panicked at 'not enough arguments', src\main.rs:29
       note: Run with `RUST_BACKTRACE=1` for a backtrace.
       

      这样就好多了!至少有个一个符合常理的错误信息。然而,还有一堆额外的信息我们并不希望提供给用户。可以通过改变new的签名来完善它。现在它只返回了一个Config,所有没有办法表示创建Config失败的情况。相反,可以如列表 12-8 所示返回一个Result

      -
      -Filename: src/main.rs -
      # use std::env;
      -# use std::fs::File;
      -# use std::io::prelude::*;
      -# use std::process;
      -#
      -# fn main() {
      -#     let args: Vec<String> = env::args().collect();
      -#
      -#     let config = Config::new(&args).unwrap_or_else(|err| {
      -#         println!("Problem parsing arguments: {}", err);
      -#         process::exit(1);
      -#     });
      -#
      -#     println!("Searching for {}", config.search);
      -#     println!("In file {}", config.filename);
      -#
      -#     let mut f = File::open(config.filename).expect("file not found");
      -#
      -#     let mut contents = String::new();
      -#     f.read_to_string(&mut contents).expect("something went wrong reading the file");
      -#
      -#     println!("With text:\n{}", contents);
      -# }
      -# struct Config {
      -#     search: String,
      -#     filename: String,
      -# }
      -#
      -impl Config {
      +

      Filename: src/main.rs

      +
      impl Config {
           fn new(args: &[String]) -> Result<Config, &'static str> {
               if args.len() < 3 {
                   return Err("not enough arguments");
      @@ -348,21 +237,14 @@ impl Config {
           }
       }
       
      -
      -

      Listing 12-8: Return a Result from Config::new

      -
      -
      +

      Listing 12-8: Return a Result from Config::new

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

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

      Config::new调用和错误处理

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

      -
      -Filename: src/main.rs -
      # use std::env;
      -# use std::fs::File;
      -# use std::io::prelude::*;
      -// ...snip...
      +

      Filename: src/main.rs

      +
      // ...snip...
       use std::process;
       
       fn main() {
      @@ -377,45 +259,14 @@ fn main() {
           println!("In file {}", config.filename);
       
           // ...snip...
      -#
      -#     let mut f = File::open(config.filename).expect("file not found");
      -#
      -#     let mut contents = String::new();
      -#     f.read_to_string(&mut contents).expect("something went wrong reading the file");
      -#
      -#     println!("With text:\n{}", contents);
      -# }
      -#
      -# struct Config {
      -#     search: String,
      -#     filename: String,
      -# }
      -#
      -# impl Config {
      -#     fn new(args: &[String]) -> Result<Config, &'static str> {
      -#         if args.len() < 3 {
      -#             return Err("not enough arguments");
      -#         }
      -#
      -#         let search = args[1].clone();
      -#         let filename = args[2].clone();
      -#
      -#         Ok(Config {
      -#             search: search,
      -#             filename: filename,
      -#         })
      -#     }
      -# }
       
      -
      -

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

      -
      -
      +

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

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

      -

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

      +

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

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

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

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

      run函数中的错误处理

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

      -
      -Filename: src/main.rs -
      # use std::env;
      -# use std::fs::File;
      -# use std::io::prelude::*;
      -# use std::process;
      -#
      -fn main() {
      -#     let args: Vec<String> = env::args().collect();
      -#
      -#     let config = Config::new(&args).unwrap_or_else(|err| {
      -#         println!("Problem parsing arguments: {}", err);
      -#         process::exit(1);
      -#     });
      +

      Filename: src/main.rs

      +
      fn main() {
           // ...snip...
       
           println!("Searching for {}", config.search);
      @@ -456,57 +295,15 @@ fn run(config: Config) {
       }
       
       // ...snip...
      -#
      -# struct Config {
      -#     search: String,
      -#     filename: String,
      -# }
      -#
      -# impl Config {
      -#     fn new(args: &[String]) -> Result<Config, &'static str> {
      -#         if args.len() < 3 {
      -#             return Err("not enough arguments");
      -#         }
      -#
      -#         let search = args[1].clone();
      -#         let filename = args[2].clone();
      -#
      -#         Ok(Config {
      -#             search: search,
      -#             filename: filename,
      -#         })
      -#     }
      -# }
       
      -
      -

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

      -
      -
      +

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

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

      -
      -Filename: src/main.rs -
      use std::error::Error;
      -# use std::env;
      -# use std::fs::File;
      -# use std::io::prelude::*;
      -# use std::process;
      +

      Filename: src/main.rs

      +
      use std::error::Error;
       
       // ...snip...
      -# fn main() {
      -#     let args: Vec<String> = env::args().collect();
      -#
      -#     let config = Config::new(&args).unwrap_or_else(|err| {
      -#         println!("Problem parsing arguments: {}", err);
      -#         process::exit(1);
      -#     });
      -#
      -#     println!("Searching for {}", config.search);
      -#     println!("In file {}", config.filename);
      -#
      -#     run(config);
      -#
      -# }
       
       fn run(config: Config) -> Result<(), Box<Error>> {
           let mut f = File::open(config.filename)?;
      @@ -518,34 +315,11 @@ fn run(config: Config) -> Result<(), Box<Error>> {
       
           Ok(())
       }
      -#
      -# struct Config {
      -#     search: String,
      -#     filename: String,
      -# }
      -#
      -# impl Config {
      -#     fn new(args: &[String]) -> Result<Config, &'static str> {
      -#         if args.len() < 3 {
      -#             return Err("not enough arguments");
      -#         }
      -#
      -#         let search = args[1].clone();
      -#         let filename = args[2].clone();
      -#
      -#         Ok(Config {
      -#             search: search,
      -#             filename: filename,
      -#         })
      -#     }
      -# }
       
      -
      -

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

      -
      -
      +

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

      -

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

      +

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

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

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

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

      @@ -591,9 +365,8 @@ fn run(config: Config) -> Result<(), Box<Error>> {

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

      将代码拆分到库 crate

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

      -
      -Filename: src/lib.rs -
      use std::error::Error;
      +

      Filename: src/lib.rs

      +
      use std::error::Error;
       use std::fs::File;
       use std::io::prelude::*;
       
      @@ -629,15 +402,12 @@ pub fn run(config: Config) -> Result<(), Box<Error>>{
           Ok(())
       }
       
      -
      -

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

      -
      -
      +

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

      -

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

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

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

      -
      -Filename: src/main.rs +

      Filename: src/main.rs

      extern crate greprs;
       
       use std::env;
      @@ -663,10 +433,9 @@ fn main() {
           }
       }
       
      -
      -

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

      -
      -
      +

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

      +

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

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

      diff --git a/docs/ch12-04-testing-the-librarys-functionality.html b/docs/ch12-04-testing-the-librarys-functionality.html index d9b6c46..cf602d6 100644 --- a/docs/ch12-04-testing-the-librarys-functionality.html +++ b/docs/ch12-04-testing-the-librarys-functionality.html @@ -71,12 +71,11 @@

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

      +commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

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

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

      -
      -Filename: src/lib.rs +

      Filename: src/lib.rs

      # use std::error::Error;
       # use std::fs::File;
       # use std::io::prelude::*;
      @@ -122,11 +121,8 @@ Pick three.";
           }
       }
       
      -
      -

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

      -
      -
      +

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

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

      error[E0106]: missing lifetime specifier
      @@ -197,8 +193,7 @@ error: test failed
       

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

      -
      -Filename: src/lib.rs +

      Filename: src/lib.rs

      fn grep<'a>(search: &str, contents: &'a str) -> Vec<&'a str> {
           let mut results = Vec::new();
       
      @@ -211,10 +206,8 @@ error: test failed
           results
       }
       
      -
      -

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

      -
      -
      +

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

      尝试运行一下:

      $ cargo test
      diff --git a/docs/ch12-05-working-with-environment-variables.html b/docs/ch12-05-working-with-environment-variables.html
      index 26e1d03..98ecf88 100644
      --- a/docs/ch12-05-working-with-environment-variables.html
      +++ b/docs/ch12-05-working-with-environment-variables.html
      @@ -71,7 +71,7 @@
       

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

      +commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

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

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

      @@ -113,8 +113,7 @@ Trust me.";

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

      -
      -Filename: src/lib.rs +

      Filename: src/lib.rs

      fn grep_case_insensitive<'a>(search: &str, contents: &'a str) -> Vec<&'a str> {
           let search = search.to_lowercase();
           let mut results = Vec::new();
      @@ -128,11 +127,9 @@ 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-16: Implementing a grep_case_insensitive +function by changing the search string and the lines of the contents to +lowercase before comparing them

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

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

      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 570fbd6..c3dc1f9 100644 --- a/docs/ch12-06-writing-to-stderr-instead-of-stdout.html +++ b/docs/ch12-06-writing-to-stderr-instead-of-stdout.html @@ -71,7 +71,7 @@

      ch12-06-writing-to-stderr-instead-of-stdout.md
      -commit 4f2dc564851dc04b271a2260c834643dfd86c724

      +commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

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

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

      @@ -81,8 +81,7 @@ commit 4f2dc564851dc04b271a2260c834643dfd86c724

      Problem parsing arguments: not enough arguments
       

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

      -
      -Filename: src/main.rs +

      Filename: src/main.rs

      extern crate greprs;
       
       use std::env;
      @@ -117,10 +116,8 @@ fn main() {
           }
       }
       
      -
      -

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

      -
      -
      +

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

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

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

      @@ -137,7 +134,7 @@ How dreary to be somebody!

      总结

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

      -

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

      +

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

    diff --git a/docs/ch14-02-publishing-to-crates-io.html b/docs/ch14-02-publishing-to-crates-io.html index 9dc5540..b3afaaa 100644 --- a/docs/ch14-02-publishing-to-crates-io.html +++ b/docs/ch14-02-publishing-to-crates-io.html @@ -71,7 +71,7 @@

    ch14-02-publishing-to-crates-io.md
    -commit c49e5ee8859f8eb8f8867cbeafbdf5b802aa5894

    +commit f2eef19b3a39ee68dd363db2fcba173491ba9dc4

    我们曾经在项目中增加 crates.io 上的 crate 作为依赖。也可以选择将代码分享给其他人。Crates.io 用来分发包的源代码,所以它主要用于分发开源代码。

    Rust 和 Cargo 有一些帮助人们找到和使用你发布的包的功能。我们将介绍这些功能,接着讲到如何发布一个包。

    @@ -86,10 +86,7 @@ commit c49e5ee8859f8eb8f8867cbeafbdf5b802aa5894

    /// ``` /// let five = 5; /// -/// assert_eq!(6, add_one(5)); -/// # fn add_one(x: i32) -> i32 { -/// # x + 1 -/// # } +/// assert_eq!(6, add_one(five)); /// ``` pub fn add_one(x: i32) -> i32 { x + 1 @@ -115,7 +112,7 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured

    使用pub use来导出合适的公有 API

    第七章介绍了如何使用mod关键字来将代码组织进模块中,如何使用pub关键字将项变为公有,和如何使用use关键字将项引入作用域。当发布 crate 给并不熟悉其使用的库的实现的人时,就值得花时间考虑 crate 的结构对于开发和对于依赖 crate 的人来说是否同样有用。如果结构对于供其他库使用来说并不方便,也无需重新安排内部组织:可以选择使用pub use来重新导出一个不同的公有结构。

    -

    例如列表 14-2中,我们创建了一个库art,其包含一个kinds模块,模块中包含枚举Color和包含函数mix的模块utils

    +

    例如列表 14-2 中,我们创建了一个库art,其包含一个kinds模块,模块中包含枚举Color和包含函数mix的模块utils

    Filename: src/lib.rs

    //! # Art
     //!
    diff --git a/docs/ch15-02-deref.html b/docs/ch15-02-deref.html
    index a57a909..2291682 100644
    --- a/docs/ch15-02-deref.html
    +++ b/docs/ch15-02-deref.html
    @@ -71,9 +71,9 @@
     

    ch15-02-deref.md
    -commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

    +commit ecc3adfe0cfa0a4a15a178dc002702fd0ea74b3f

    -

    第一个智能指针相关的重要 trait 是Deref,它允许我们重载*,解引用运算符(不同于乘法运算符和全局引用运算符)。重载智能指针的*方便访问其后的数据,在这个部分的稍后介绍解引用强制多态时我们会讨论方便的意义。

    +

    第一个智能指针相关的重要 trait 是Deref,它允许我们重载*,解引用运算符(不同于乘法运算符和全局引用运算符)。重载智能指针的*能使访问其后的数据更为方便,在这个部分的稍后介绍解引用强制多态时我们会讨论方便的意义。

    第八章的哈希 map 的“根据旧值更新一个值”部分简要的提到了解引用运算符。当时有一个可变引用,而我们希望改变这个引用所指向的值。为此,首先我们必须解引用。这是另一个使用i32值引用的例子:

    let mut x = 5;
     {
    @@ -119,14 +119,14 @@ fn main() {
     struct that holds mp3 file data and metadata

    大部分代码看起来都比较熟悉:一个结构体、一个 trait 实现、和一个创建了结构体示例的 main 函数。其中有一部分我们还未全面的讲解:类似于第十三章学习迭代器 trait 时出现的type Itemtype Target = T;语法用于定义关联类型,第十九章会更详细的介绍。不必过分担心例子中的这一部分;它只是一个稍显不同的定义泛型参数的方式。

    assert_eq!中,我们验证vec![1, 2, 3]是否为Mp3实例*my_favorite_song解引用的值,结果正是如此因为我们实现了deref方法来返回音频数据。如果没有为Mp3实现Deref trait,Rust 将不会编译*my_favorite_song:会出现错误说Mp3类型不能被解引用。

    -

    代码能够工作的原因在于调用*my_favorite_song*在背后所做的操作:

    +

    没有Deref trait 的话,编译器只能解引用&引用,而my_favorite_song并不是(它是一个Mp3结构体)。通过Deref trait,编译器知道实现了Deref trait 的类型有一个返回引用的deref方法(在这个例子中,是&self.audio因为列表 15-7 中的deref的定义)。所以为了得到一个*可以解引用的&引用,编译器将*my_favorite_song展开为如下:

    *(my_favorite_song.deref())
     
    -

    这对my_favorite_song调用了deref方法,它借用了my_favorite_song并返回指向my_favorite_song.audio的引用,这正是列表 15-5 中deref所定义的。引用的*被定义为仅仅从引用中返回其数据,所以上面*的展开形式对于外部*来说并不是递归的。最终的数据类型是Vec<u8>,它与列表 15-5 中assert_eq!vec![1, 2, 3]相匹配。

    -

    deref方法的返回值类型仍然是引用和为何必须解引用方法的结果的原因是如果deref方法就返回值,使用*总是会获取其所有权。

    +

    这个就是self.audio中的结果值。deref返回一个引用并接下来必需解引用而不是直接返回值的原因是所有权:如果deref方法直接返回值而不是引用,其值将被移动出self。这里和大部分使用解引用运算符的地方并不想获取my_favorite_song.audio的所有权。

    +

    注意将*替换为deref调用和*调用的过程在每次使用*的时候都会发生一次。*的替换并不会无限递归进行。最终的数据类型是Vec<u8>,它与列表 15-7 中assert_eq!vec![1, 2, 3]相匹配。

    函数和方法的隐式解引用强制多态

    -

    Rust 倾向于偏爱明确而不是隐晦,不过一个情况下这并不成立,就是函数和方法的参数的解引用强制多态deref coercions)。解引用强制多态会自动的将指针或智能指针的引用转换为指针内容的引用。解引用强制多态发生于一个值被传递给函数或方法,并只发生于需要将传递的值类型与签名中参数类型相匹配的时候。解引用强制多态的加入使得 Rust 调用函数或方法时无需很多使用&*的引用和解引用。

    -

    使用列表 15-5 中的Mp3结构体,如下是一个获取u8 slice 并压缩 mp3 音频数据的函数签名:

    +

    Rust 倾向于偏爱明确而不是隐晦,不过一个情况下这并不成立,就是函数和方法的参数的解引用强制多态deref coercions)。解引用强制多态会自动的将指针或智能指针的引用转换为指针内容的引用。解引用强制多态发生于当传递给函数的参数类型不同于函数签名中定义参数类型的时候。解引用强制多态的加入使得 Rust 调用函数或方法时无需很多使用&*的引用和解引用。

    +

    使用列表 15-7 中的Mp3结构体,如下是一个获取u8 slice 并压缩 mp3 音频数据的函数签名:

    fn compress_mp3(audio: &[u8]) -> Vec<u8> {
         // the actual implementation would go here
     }
    @@ -138,8 +138,8 @@ struct that holds mp3 file data and metadata

    然而,因为解引用强制多态和Mp3Deref trait 实现,我们可以使用如下代码使用my_favorite_song中的数据调用这个函数:

    let result = compress_mp3(&my_favorite_song);
     
    -

    只有&和实例,好的!我们可以把智能指针当成普通的引用。也就是说解引用强制多态意味着 Rust 利用了Deref实现的优势:Rust 知道Mp3实现了Deref trait 并从deref方法返回&Vec<u8>。它也知道标准库实现了Vec<T>Deref trait,其deref方法返回&[T](我们也可以通过查阅Vec<T>的 API 文档来发现这一点)。所以,在编译时,Rust 会发现它可以调用两次Deref::deref来将&Mp3变成&Vec<u8>再变成&[T]来满足compress_mp3的签名。这意味着我们可以少写一些代码!Rust 会多次分析Deref::deref的返回值类型直到它满足参数的类型,只要相关类型实现了Deref trait。这些间接转换在编译时进行,所以利用解引用强制多态并没有运行时惩罚。

    -

    这里还有一个重载了&mut T*DerefMut trait,它以与Deref重载&T*相同的方式用于参数中。

    +

    只有&和实例,好的!我们可以把智能指针当成普通的引用。也就是说解引用强制多态意味着 Rust 利用了Deref实现的优势:Rust 知道Mp3实现了Deref trait 并从deref方法返回&Vec<u8>。它也知道标准库实现了Vec<T>Deref trait,其deref方法返回&[T](我们也可以通过查阅Vec<T>的 API 文档来发现这一点)。所以,在编译时,Rust 会发现它可以调用两次Deref::deref来将&Mp3变成&Vec<u8>再变成&[T]来满足compress_mp3的签名。这意味着我们可以少写一些代码!Rust 会多次分析Deref::deref的返回值类型直到它满足参数的类型,只要相关类型实现了Deref trait。这些间接转换在编译时进行,所以利用解引用强制多态并没有运行时惩罚!

    +

    类似于如何使用Deref trait 重载&T*运算符,DerefMut trait用于重载&mut T*运算符。

    Rust 在发现类型和 trait 实现满足三种情况时会进行解引用强制多态:

    • &T&UT: Deref<Target=U>
    • diff --git a/docs/print.html b/docs/print.html index 4e99b8d..51694c1 100644 --- a/docs/print.html +++ b/docs/print.html @@ -5860,134 +5860,161 @@ test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured

      现在我们讲完了编写测试的方法,让我们看看运行测试时会发生什么并讨论可以用于cargo test的不同选项。

      运行测试

      -

      ch11-02-running-tests.md +

      ch11-02-running-tests.md
      -commit cf52d81371e24e14ce31a5582bfcb8c5b80d26cc

      +commit 55b294f20fc846a13a9be623bf322d8b364cee77

      -

      类似于cargo run会编译代码并运行生成的二进制文件,cargo test在测试模式下编译代码并运行生成的测试二进制文件。cargo test生成的二进制文件默认会并行的运行所有测试并在测试过程中捕获生成的输出,这样就更容易阅读测试结果的输出。

      -

      可以通过指定命令行选项来改变这些运行测试的默认行为。这些选项的一部分可以传递给cargo test,而另一些则需要传递给生成的测试二进制文件。分隔这些参数的方法是--cargo test之后列出了传递给cargo test的参数,接着是分隔符--,之后是传递给测试二进制文件的参数。

      -

      并行运行测试

      -

      测试使用线程来并行运行。为此,编写测试时需要注意测试之间不要相互依赖或者存在任何共享状态。共享状态也可能包含在运行环境中,比如当前工作目录或者环境变量。

      -

      如果你不希望它这样运行,或者想要更加精确的控制使用线程的数量,可以传递--test-threads参数和线程的数量给测试二进制文件。将线程数设置为 1 意味着没有任何并行操作:

      +

      就像cargo run会编译代码并运行生成的二进制文件,cargo test在测试模式下编译代码并运行生成的测试二进制文件。这里有一些选项可以用来改变cargo test的默认行为。例如,cargo test生成的二进制文件的默认行为是并行的运行所有测试,并捕获测试运行过程中产生的输出避免他们被显示出来使得阅读测试结果相关的内容变得更容易。可以指定命令行参数来改变这些默认行为。

      +

      这些选项的一部分可以传递给cargo test,而另一些则需要传递给生成的测试二进制文件。为了分隔两种类型的参数,首先列出传递给cargo test的参数,接着是分隔符--,再之后是传递给测试二进制文件的参数。运行cargo test --help会告诉你cargo test的相关参数,而运行cargo test -- --help则会告诉你位于分隔符--之后的相关参数。

      +

      并行或连续的运行测试

      + + +

      当运行多个测试时,他们默认使用线程来并行的运行。这意味着测试会更快的运行完毕,所以可以更快的得到代码能否工作的反馈。因为测试是在同时运行的,你应该小心测试不能相互依赖或任何共享状态,包括类似于当前工作目录或者环境变量这样的共享环境。

      +

      例如,每一个测试都运行一些代码在硬盘上创建一个test-output.txt文件并写入一些数据。接着每一个测试都读取文件中的数据并断言这个文件包含特定的值,而这个值在每个测试中都是不同的。因为所有测试都是同时运行的,一个测试可能会在另一个测试读写文件过程中覆盖了文件。那么第二个测试就会失败,并不是因为代码不正确,而是因为测试并行运行时相互干涉。一个解决方案是使每一个测试读写不同的文件;另一个是一次运行一个测试。

      +

      如果你不希望测试并行运行,或者想要更加精确的控制使用线程的数量,可以传递--test-threads参数和希望使用线程的数量给测试二进制文件。例如:

      $ cargo test -- --test-threads=1
       
      -

      捕获测试输出

      -

      Rust 的测试库默认捕获并丢弃标准输出和标准错误中的输出,除非测试失败了。例如,如果在测试中调用了println!而测试通过了,你将不会在终端看到println!的输出。这个行为可以通过向测试二进制文件传递--nocapture参数来禁用:

      +

      这里将测试线程设置为 1,告诉程序不要使用任何并行机制。这也会比并行运行花费更多时间,不过测试就不会在存在共享状态时潜在的相互干涉了。

      +

      显示测试输出

      +

      如果测试通过了,Rust 的测试库默认会捕获打印到标准输出的任何内容。例如,如果在测试中调用println!而测试通过了,我们将不会在终端看到println!的输出:只会看到说明测试通过的行。如果测试失败了,就会看到任何标准输出和其他错误信息。

      +

      例如,列表 11-20 有一个无意义的函数它打印出其参数的值并接着返回 10。接着还有一个会通过的测试和一个会失败的测试:

      +

      Filename: src/lib.rs

      +
      fn prints_and_returns_10(a: i32) -> i32 {
      +    println!("I got the value {}", a);
      +    10
      +}
      +
      +#[cfg(test)]
      +mod tests {
      +    use super::*;
      +
      +    #[test]
      +    fn this_test_will_pass() {
      +        let value = prints_and_returns_10(4);
      +        assert_eq!(10, value);
      +    }
      +
      +    #[test]
      +    fn this_test_will_fail() {
      +        let value = prints_and_returns_10(8);
      +        assert_eq!(5, value);
      +    }
      +}
      +
      +

      Listing 11-10: Tests for a function that calls println! +

      +

      运行cargo test将会看到这些测试的输出:

      +
      running 2 tests
      +test tests::this_test_will_pass ... ok
      +test tests::this_test_will_fail ... FAILED
      +
      +failures:
      +
      +---- tests::this_test_will_fail stdout ----
      +    I got the value 8
      +thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left ==
      +right)` (left: `5`, right: `10`)', src/lib.rs:19
      +note: Run with `RUST_BACKTRACE=1` for a backtrace.
      +
      +failures:
      +    tests::this_test_will_fail
      +
      +test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
      +
      +

      注意输出中哪里也不会出现I got the value 4,这是当测试通过时打印的内容。这些输出被捕获。失败测试的输出,I got the value 8,则出现在输出的测试总结部分,它也显示了测试失败的原因。

      +

      如果你希望也能看到通过的测试中打印的值,捕获输出的行为可以通过--nocapture参数来禁用:

      $ cargo test -- --nocapture
       
      +

      使用--nocapture参数再次运行列表 11-10 中的测试会显示:

      +
      running 2 tests
      +I got the value 4
      +I got the value 8
      +test tests::this_test_will_pass ... ok
      +thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left ==
      +right)` (left: `5`, right: `10`)', src/lib.rs:19
      +note: Run with `RUST_BACKTRACE=1` for a backtrace.
      +test tests::this_test_will_fail ... FAILED
      +
      +failures:
      +
      +failures:
      +    tests::this_test_will_fail
      +
      +test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
      +
      +

      注意测试的输出和测试结果的输出是相互交叉的;这是由于上一部分讲到的测试是并行运行的。尝试一同使用--test-threads=1--nocapture功能来看看输出是什么样子!

      通过名称来运行测试的子集

      -

      有时运行整个测试集会耗费很多时间。如果你负责特定位置的代码,你可能会希望只与这些代码相关的测试。cargo test有一个参数允许你通过指定名称来运行特定的测试。

      -

      列表 11-3 中创建了三个如下名称的测试:

      -
      -Filename: src/lib.rs -
      #[test]
      -fn add_two_and_two() {
      -    assert_eq!(4, 2 + 2);
      +

      有时运行整个测试集会耗费很多时间。如果你负责特定位置的代码,你可能会希望只与这些代码相关的测试。可以向cargo test传递希望运行的测试的(部分)名称作为参数来选择运行哪些测试。

      +

      为了展示如何运行测试的子集,列表 11-11 使用add_two函数创建了三个测试来供我们选择运行哪一个:

      +

      Filename: src/lib.rs

      +
      pub fn add_two(a: i32) -> i32 {
      +    a + 2
       }
       
      -#[test]
      -fn add_three_and_two() {
      -    assert_eq!(5, 3 + 2);
      -}
      +#[cfg(test)]
      +mod tests {
      +    use super::*;
       
      -#[test]
      -fn one_hundred() {
      -    assert_eq!(102, 100 + 2);
      -}
      -
      -
      -

      Listing 11-3: Three tests with a variety of names

      -
      -
      -

      使用不同的参数会运行不同的测试子集。没有参数的话,如你所见会运行所有的测试:

      -
      $ cargo test
      -    Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
      -     Running target/debug/deps/adder-abcabcabc
      -
      -running 3 tests
      -test add_three_and_two ... ok
      -test one_hundred ... ok
      -test add_two_and_two ... ok
      -
      -test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured
      -
      -

      可以传递任意测试的名称来只运行那个测试:

      -
      $ cargo test one_hundred
      -    Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
      -     Running target/debug/deps/adder-abcabcabc
      -
      -running 1 test
      -test one_hundred ... ok
      -
      -test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
      -
      -

      也可以传递名称的一部分,cargo test会运行所有匹配的测试:

      -
      $ cargo test add
      -    Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
      -     Running target/debug/deps/adder-abcabcabc
      -
      -running 2 tests
      -test add_three_and_two ... ok
      -test add_two_and_two ... ok
      -
      -test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured
      -
      -

      模块名也作为测试名的一部分,所以类似的模块名也可以用来指定测试特定模块。例如,如果将我们的代码组织成一个叫adding的模块和一个叫subtracting的模块并分别带有测试,如列表 11-4 所示:

      -
      -Filename: src/lib.rs -
      mod adding {
           #[test]
           fn add_two_and_two() {
      -        assert_eq!(4, 2 + 2);
      +        assert_eq!(4, add_two(2));
           }
       
           #[test]
           fn add_three_and_two() {
      -        assert_eq!(5, 3 + 2);
      +        assert_eq!(5, add_two(3));
           }
       
           #[test]
           fn one_hundred() {
      -        assert_eq!(102, 100 + 2);
      -    }
      -}
      -
      -mod subtracting {
      -    #[test]
      -    fn subtract_three_and_two() {
      -        assert_eq!(1, 3 - 2);
      +        assert_eq!(102, add_two(100));
           }
       }
       
      -
      -

      Listing 11-4: Tests in two modules named adding and subtracting

      -
      -
      -

      执行cargo test会运行所有的测试,而模块名会出现在输出的测试名中:

      -
      $ cargo test
      -    Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
      -     Running target/debug/deps/adder-abcabcabc
      -
      -running 4 tests
      -test adding::add_two_and_two ... ok
      -test adding::add_three_and_two ... ok
      -test subtracting::subtract_three_and_two ... ok
      -test adding::one_hundred ... ok
      -
      -

      运行cargo test adding将只会运行对应模块的测试而不会运行任何 subtracting 模块中的测试:

      -
      $ cargo test adding
      -    Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
      -     Running target/debug/deps/adder-abcabcabc
      -
      -running 3 tests
      -test adding::add_three_and_two ... ok
      -test adding::one_hundred ... ok
      -test adding::add_two_and_two ... ok
      +

      Listing 11-11: Three tests with a variety of names

      +

      如果没有传递任何参数就运行测试,如你所见,所有测试都会并行运行:

      +
      running 3 tests
      +test tests::add_two_and_two ... ok
      +test tests::add_three_and_two ... ok
      +test tests::one_hundred ... ok
       
       test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured
       
      +

      运行单个测试

      +

      可以向cargo test传递任意测试的名称来只运行这个测试:

      +
      $ cargo test one_hundred
      +    Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
      +     Running target/debug/deps/adder-06a75b4a1f2515e9
      +
      +running 1 test
      +test tests::one_hundred ... ok
      +
      +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
      +
      +

      不能像这样指定多个测试名称,只有传递给cargo test的第一个值才会被使用。

      +

      过滤运行多个测试

      +

      然而,可以指定测试的部分名称,这样任何名称匹配这个值的测试会被运行。例如,因为头两个测试的名称包含add,可以通过cargo test add来运行这两个测试:

      +
      $ cargo test add
      +    Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
      +     Running target/debug/deps/adder-06a75b4a1f2515e9
      +
      +running 2 tests
      +test tests::add_two_and_two ... ok
      +test tests::add_three_and_two ... ok
      +
      +test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured
      +
      +

      这运行了所有名字中带有add的测试。同时注意测试所在的模块作为测试名称的一部分,所以可以通过模块名来过滤运行一个模块中的所有测试。

      + +

      除非指定否则忽略某些测试

      -

      有时一些特定的测试执行起来是非常耗费时间的,所以对于大多数cargo test命令,我们希望能排除它。无需为cargo test创建一个用来在运行所有测试时排除特定测试的参数并每次都要记得使用它,我们可以对这些测试使用ignore属性:

      +

      有时一些特定的测试执行起来是非常耗费时间的,所以在运行大多数cargo test的时候希望能排除他们。与其通过参数列举出所有希望运行的测试,也可以使用ignore属性来标记耗时的测试来排除他们:

      Filename: src/lib.rs

      #[test]
       fn it_works() {
      @@ -6000,11 +6027,11 @@ fn expensive_test() {
           // code that takes an hour to run
       }
       
      -

      现在运行测试,将会发现it_works运行了,而expensive_test没有:

      +

      我们对想要排除的测试的#[test]之后增加了#[ignore]行。现在如果运行测试,就会发现it_works运行了,而expensive_test没有运行:

      $ cargo test
          Compiling adder v0.1.0 (file:///projects/adder)
           Finished debug [unoptimized + debuginfo] target(s) in 0.24 secs
      -     Running target/debug/deps/adder-abcabcabc
      +     Running target/debug/deps/adder-ce99bcc2479f4607
       
       running 2 tests
       test expensive_test ... ignored
      @@ -6018,29 +6045,39 @@ running 0 tests
       
       test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
       
      -

      我们可以通过cargo test -- --ignored来明确请求只运行那些耗时的测试:

      -
      $ cargo test -- --ignored
      +

      expensive_test被列为ignored,如果只希望运行被忽略的测试,可以使用cargo test -- --ignored来请求运行他们:

      + + + + +
      $ cargo test -- --ignored
           Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
      -     Running target/debug/deps/adder-abcabcabc
      +     Running target/debug/deps/adder-ce99bcc2479f4607
       
       running 1 test
       test expensive_test ... ok
       
       test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
       
      -

      通过这种方式,大部分时间运行cargo test将是快速的。当需要检查ignored测试的结果而且你也有时间等待这个结果的话,可以选择执行cargo test -- --ignored

      +

      通过控制运行哪些测试,可以确保运行cargo test的结果是快速的。当某个时刻需要检查ignored测试的结果而且你也有时间等待这个结果的话,可以选择执行cargo test -- --ignored

      测试的组织结构

      -

      ch11-03-test-organization.md +

      ch11-03-test-organization.md
      -commit cf52d81371e24e14ce31a5582bfcb8c5b80d26cc

      +commit 55b294f20fc846a13a9be623bf322d8b364cee77

      -

      正如之前提到的,测试是一个很广泛的学科,而且不同的人有时也采用不同的技术和组织。Rust 社区倾向于根据测试的两个主要分类来考虑问题:单元测试unit tests)与集成测试unit tests)。单元测试倾向于更小而更专注,在隔离的环境中一次测试一个模块。他们也可以测试私有接口。集成测试对于你的库来说则完全是外部的。他们与其他用户使用相同的方式使用你得代码,他们只针对共有接口而且每个测试会测试多个模块。这两类测试对于从独立和整体的角度保证你的库符合期望是非常重要的。

      +

      正如之前提到的,测试是一个很广泛的学科,而且不同的开发者也采用不同的技术和组织。Rust 社区倾向于根据测试的两个主要分类来考虑问题:单元测试unit tests)与集成测试unit tests)。单元测试倾向于更小而更专注,在隔离的环境中一次测试一个模块。他们也可以测试私有接口。集成测试对于你的库来说则完全是外部的。他们与其他用户使用相同的方式使用你的代码,他们只针对共有接口而且每个测试都会测试多个模块。

      +

      这两类测试对于从独立和整体的角度保证你的库符合期望是非常重要的。

      单元测试

      -

      单元测试的目的是在隔离与其他部分的环境中测试每一个单元的代码,以便于快速而准确的定位何处的代码是否符合预期。单元测试位于 src 目录中,与他们要测试的代码存在于相同的文件中。他们被分离进每个文件中他们自有的tests模块中。

      +

      单元测试的目的是在与其他部分隔离的环境中测试每一个单元的代码,以便于快速而准确的定位何处的代码是否符合预期。单元测试位于 src 目录中,与他们要测试的代码存在于相同的文件中。传统做法是在每个文件中创建包含测试函数的tests模块,并使用cfg(test)标注模块。

      测试模块和cfg(test)

      -

      通过将测试放进他们自己的模块并对该模块使用cfg注解,我们可以告诉 Rust 只在执行cargo test时才编译和运行测试代码。这在当我们只希望用cargo build编译库代码时可以节省编译时间,并减少编译产物的大小因为并没有包含测试。

      -

      还记得上一部分新建的adder项目吗?Cargo 为我们生成了如下代码:

      +

      测试模块的#[cfg(test)]注解告诉 Rust 只在执行cargo test时才编译和运行测试代码,而在运行cargo build时不这么做。这在只希望构建库的时候可以节省编译时间,并能节省编译产物的空间因为他们并没有包含测试。我们将会看到因为集成测试位于另一个文件夹,他们并不需要#[cfg(test)]注解。但是因为单元测试位于与源码相同的文件中,所以使用#[cfg(test)]来指定他们不应该被包含进编译产物中。

      +

      还记得本章第一部分新建的adder项目吗?Cargo 为我们生成了如下代码:

      Filename: src/lib.rs

      #[cfg(test)]
       mod tests {
      @@ -6049,48 +6086,10 @@ mod tests {
           }
       }
       
      -

      我们忽略了模块相关的信息以便更关注模块中测试代码的机制,不过现在让我们看看测试周围的代码。

      -

      首先,这里有一个属性cfgcfg属性让我们声明一些内容只在给定特定的配置configuration)时才被包含进来。Rust 提供了test配置用来编译和运行测试。通过这个属性,Cargo 只会在尝试运行测试时才编译测试代码。

      -

      接下来,tests包含了所有测试函数,而我们的代码则位于tests模块之外。tests模块的名称是一个惯例,除此之外这是一个遵守第七章讲到的常见可见性规则的普通模块。因为这是一个内部模块,我们需要将要测试的代码引入作用域。这对于一个大的模块来说是很烦人的,所以这里经常使用全局导入。

      -

      从本章到现在,我们一直在为adder项目编写并没有实际调用任何代码的测试。现在让我们做一些改变!在 src/lib.rs 中,放入add_two函数和带有一个检验代码的测试的tests模块,如列表 11-5 所示:

      -
      -Filename: src/lib.rs -
      pub fn add_two(a: i32) -> i32 {
      -    a + 2
      -}
      -
      -#[cfg(test)]
      -mod tests {
      -    use add_two;
      -
      -    #[test]
      -    fn it_works() {
      -        assert_eq!(4, add_two(2));
      -    }
      -}
      -
      -
      -

      Listing 11-5: Testing the function add_two in a child tests module

      -
      -
      -

      注意除了测试函数之外,我们还在tests模块中添加了use add_two;。这将我们想要测试的代码引入到了内部的tests模块的作用域中,正如任何内部模块需要做的那样。如果现在使用cargo test运行测试,它会通过:

      -
      running 1 test
      -test tests::it_works ... ok
      -
      -test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
      -
      -

      如果我们忘记将add_two函数引入作用域,将会得到一个 unresolved name 错误,因为tests模块并不知道任何关于add_two函数的信息:

      -
      error[E0425]: unresolved name `add_two`
      - --> src/lib.rs:9:23
      -  |
      -9 |         assert_eq!(4, add_two(2));
      -  |                       ^^^^^^^ unresolved name
      -
      -

      如果这个模块包含很多希望测试的代码,在测试中列出每一个use语句将是很烦人的。相反在测试子模块中使用use super::*;来一次将所有内容导入作用域中是很常见的。

      +

      这里自动生成了测试模块。cfg属性代表 configuration ,它告诉 Rust 其之后的项只被包含进特定配置中。在这个例子中,配置是test,Rust 所提供的用于编译和运行测试的配置。通过使用这个属性,Cargo 只会在我们主动使用cargo test运行测试时才编译测试代码。除了标注为#[test]的函数之外,这还包括测试模块中可能存在的帮助函数。

      测试私有函数

      -

      测试社区中一直存在关于是否应该对私有函数进行单元测试的论战。不过无论你坚持哪种测试意识形态,Rust 确实允许你测试私有函数,由于私有性规则。考虑列表 11-6 中带有私有函数internal_adder的代码:

      -
      -Filename: src/lib.rs +

      测试社区中一直存在关于是否应该对私有函数进行单元测试的论战,而其他语言中难以甚至不可能测试私有函数。不过无论你坚持哪种测试意识形态,Rust 的私有性规则确实允许你测试私有函数,由于私有性规则。考虑列表 11-12 中带有私有函数internal_adder的代码:

      +

      Filename: src/lib.rs

      pub fn add_two(a: i32) -> i32 {
           internal_adder(a, 2)
       }
      @@ -6101,7 +6100,7 @@ fn internal_adder(a: i32, b: i32) -> i32 {
       
       #[cfg(test)]
       mod tests {
      -    use internal_adder;
      +    use super::*;
       
           #[test]
           fn internal() {
      @@ -6109,18 +6108,21 @@ mod tests {
           }
       }
       
      -
      -

      Listing 11-6: Testing a private function

      -
      -
      -

      因为测试也不过是 Rust 代码而tests也只是另一个模块,我们完全可以在一个测试中导入并调用internal_adder。如果你并不认为私有函数应该被测试,Rust 也不会强迫你这么做。

      +

      Listing 11-12: Testing a private function

      + + +

      注意internal_adder函数并没有标记为pub,不过因为测试也不过是 Rust 代码而tests也仅仅是另一个模块,我们完全可以在测试中导入和调用internal_adder。如果你并不认为私有函数应该被测试,Rust 也不会强迫你这么做。

      集成测试

      -

      在 Rust 中,集成测试对于需要测试的库来说是完全独立。他们同其他代码一样使用库文件。他们的目的是测试库的个个部分结合起来能否正常工作。每个能正确运行的代码单元集成在一起也可能会出现问题,所以集成测试的覆盖率也是很重要的。

      +

      在 Rust 中,集成测试对于需要测试的库来说是完全独立。他们同其他代码一样使用库文件,这意味着他们只能调用作为库公有 API 的一部分的函数。他们的目的是测试库的多个部分能否一起正常工作。每个能单独正确运行的代码单元集成在一起也可能会出现问题,所以集成测试的覆盖率也是很重要的。为了创建集成测试,首先需要一个 tests 目录。

      tests 目录

      -

      Cargo 支持位于 tests 目录中的集成测试。如果创建它并放入 Rust 源文件,Cargo 会将每一个文件当作单独的 crate 来编译。让我们试一试!

      -

      首先,在项目根目录创建一个 tests 目录,挨着 src 目录。接着新建一个文件 tests/integration_test.rs,并写入列表 11-7 中的代码:

      -
      -Filename: tests/integration_test.rs +

      为了编写集成测试,需要在项目根目录创建一个 tests 目录,与 src 同级。Cargo 知道如何去寻找这个目录中的集成测试文件。接着可以随意在这个文件夹中创建任意多的测试文件,Cargo 会将每一个文件当作单独的 crate 来编译。

      +

      让我们试一试吧!保留列表 11-12 中 src/lib.rs 的代码。创建一个 tests 目录,新建一个文件 tests/integration_test.rs,并输入列表 11-13 中的代码。

      +

      Filename: tests/integration_test.rs

      extern crate adder;
       
       #[test]
      @@ -6128,23 +6130,21 @@ fn it_adds_two() {
           assert_eq!(4, adder::add_two(2));
       }
       
      -
      -

      Listing 11-7: An integration test of a function in the adder crate

      -
      -
      -

      在开头使用了extern crate adder,单元测试中并不需要它。tests目录中的每一个测试文件都是完全独立的 crate,所以需要在每个文件中导入我们的库。这也就是为何tests是编写集成测试的绝佳场所:他们像任何其他用户那样,需要将库导入 crate 并只能使用公有 API。

      -

      这个文件中也不需要tests模块。除非运行测试否则整个文件夹都不会被编译,所以无需将任何部分标记为#[cfg(test)]。另外每个测试文件都被隔离进其自己的 crate 中,无需进一步隔离测试代码。

      -

      让我们运行集成测试,同样使用cargo test来运行:

      -
      $ cargo test
      +

      Listing 11-13: An integration test of a function in the +adder crate

      +

      我们在顶部增加了extern crate adder,这在单元测试中是不需要的。这是因为每一个tests目录中的测试文件都是完全独立的 crate,所以需要在每一个文件中导入库。集成测试就像其他使用者那样通过导入 crate 并只使用公有 API 来使用库文件。

      +

      并不需要将 tests/integration_test.rs 中的任何代码标注为#[cfg(test)]。Cargo 对tests文件夹特殊处理并只会在运行cargo test时编译这个目录中的文件。现在就试试运行cargo test

      +
      cargo test
          Compiling adder v0.1.0 (file:///projects/adder)
      +    Finished debug [unoptimized + debuginfo] target(s) in 0.31 secs
            Running target/debug/deps/adder-abcabcabc
       
       running 1 test
      -test tests::it_works ... ok
      +test tests::internal ... ok
       
       test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
       
      -     Running target/debug/integration_test-952a27e0126bb565
      +     Running target/debug/deps/integration_test-ce99bcc2479f4607
       
       running 1 test
       test it_adds_two ... ok
      @@ -6157,8 +6157,14 @@ running 0 tests
       
       test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
       
      -

      现在有了三个部分的输出:单元测试、集成测试和文档测试。注意当在任何 src 目录的文件中增加单元测试时,单元测试部分的对应输出也会增加。增加集成测试文件中的测试函数也会对应增加输出。如果在 tests 目录中增加集成测试文件,则会增加更多集成测试部分:一个文件对应一个部分。

      -

      cargo test指定测试函数名称参数也会匹配集成测试文件中的函数。为了只运行某个特定集成测试文件中的所有测试,可以使用cargo test--test参数:

      + + +

      现在有了三个部分的输出:单元测试、集成测试和文档测试。第一部分单元测试与我们之前见过的一样:每一个单元测试一行(列表 11-12 中有一个叫做internal的测试),接着是一个单元测试的总结行。

      +

      集成测试部分以行Running target/debug/deps/integration-test-ce99bcc2479f4607(输出最后的哈希值可能不同)开头。接着是每一个集成测试中的测试函数一行,以及一个就在Doc-tests adder部分开始之前的集成测试的总结行。

      +

      注意在任意 src 文件中增加更多单元测试函数会增加更多单元测试部分的测试结果行。在我们创建的集成测试文件中增加更多测试函数会增加更多集成测试部分的行。每一个集成测试文件有其自己的部分,所以如果在 tests 目录中增加更多文件,这里就会有更多集成测试部分。

      +

      我们仍然可以通过指定测试函数的名称作为cargo test的参数来运行特定集成测试。为了运行某个特定集成测试文件中的所有测试,使用cargo test--test后跟文件的名称:

      $ cargo test --test integration_test
           Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
            Running target/debug/integration_test-952a27e0126bb565
      @@ -6168,19 +6174,69 @@ test it_adds_two ... ok
       
       test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
       
      +

      这些只是 tests 目录中我们指定的文件中的测试。

      集成测试中的子模块

      -

      随着集成测试的增加,你可能希望在 tests 目录增加更多文件,例如根据测试的功能来将测试分组。正如我们之前提到的,这是可以的,Cargo 会将每一个文件当作一个独立的 crate。

      -

      最终,可能会有一系列在所有集成测试中通用的帮助函数,例如建立通用场景的函数。如果你将这些函数提取到 tests 目录的一个文件中,比如说 tests/common.rs,则这个文件将会像这个目录中的其他包含测试的 Rust 文件一样被编译进一个单独的 crate 中。它也会作为一个独立的部分出现在测试输出中。因为这很可能不是你所希望的,所以建议在子目录中使用 mod.rs 文件,比如 tests/common/mod.rs,来放置帮助函数。tests 的子目录不会被作为单独的 crate 编译或者作为单独的部分出现在测试输出中。

      +

      随着集成测试的增加,你可能希望在 tests 目录增加更多文件,例如根据测试的功能来将测试分组。正如我们之前提到的,每一个 tests 目录中的文件都被编译为单独的 crate。

      +

      将每个集成测试文件当作其自己的 crate 来对待有助于创建更类似与终端用户使用 crate 那样的单独的作用域。然而,这意味着考虑到像第七章学习的如何将代码分隔进模块和文件那样,tests 目录中的文件不能像 src 中的文件那样共享相同的行为。

      +

      对于 tests 目录中文件的不同行为,通常在如果有一系列有助于多个集成测试文件的帮助函数,而你尝试遵循第七章的步骤将他们提取到一个通用的模块中时显得很明显。例如,如果我们创建了 tests/common.rs 并将setup函数放入其中,这里将放入一些希望能够在多个测试文件的多个测试函数中调用的代码:

      +

      Filename: tests/common.rs

      +
      pub fn setup() {
      +    // setup code specific to your library's tests would go here
      +}
      +
      +

      如果再次运行测试,将会在测试结果中看到一个对应 common.rs 文件的新部分,即便这个文件并没有包含任何测试函数,或者没有任何地方调用了setup函数:

      +
      running 1 test
      +test tests::internal ... ok
      +
      +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
      +
      +     Running target/debug/deps/common-b8b07b6f1be2db70
      +
      +running 0 tests
      +
      +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
      +
      +     Running target/debug/deps/integration_test-d993c68b431d39df
      +
      +running 1 test
      +test it_adds_two ... ok
      +
      +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
      +
      +   Doc-tests adder
      +
      +running 0 tests
      +
      +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
      +
      + +

      common出现在测试结果中并显示running 0 tests,这不是我们想要的;我们只是希望能够在其他集成测试文件中分享一些代码罢了。

      +

      为了使common不出现在测试输出中,需要使用第七章学习到的另一个将代码提取到文件的方式:不再创建tests/common.rs,而是创建 tests/common/mod.rs。当将setup代码移动到 tests/common/mod.rs 并去掉 tests/common.rs 文件之后,测试输出中将不会出现这一部分。tests 目录中的子目录不会被作为单独的 crate 编译或作为一部分出现在测试输出中。

      +

      一旦拥有了 tests/common/mod.rs,就可以将其作为模块来在任何集成测试文件中使用。这里是一个 tests/integration_test.rs 中调用setup函数的it_adds_two测试的例子:

      +

      Filename: tests/integration_test.rs

      +
      extern crate adder;
      +
      +mod common;
      +
      +#[test]
      +fn it_adds_two() {
      +    common::setup();
      +    assert_eq!(4, adder::add_two(2));
      +}
      +
      +

      注意mod common;声明与第七章中的模块声明相同。接着在测试函数中就可以调用common::setup()了。

      二进制 crate 的集成测试

      -

      如果项目是二进制 crate 并且只包含 src/main.rs 而没有 src/lib.rs,这样就不可能在 tests 创建集成测试并使用 extern crate 导入 src/main.rs 中的函数了。这也是 Rust 二进制项目明确采用 src/main.rs 调用 src/lib.rs 中逻辑的结构的原因之一。通过这种结构,集成测试就可以使用extern crate测试库 crate 中的主要功能,而如果这些功能没有问题的话,src/main.rs 中的少量代码也就会正常工作且不需要测试。

      +

      如果项目是二进制 crate 并且只包含 src/main.rs 而没有 src/lib.rs,这样就不可能在 tests 创建集成测试并使用 extern crate 导入 src/main.rs 中的函数了。只有库 crate 向其他 crate 暴露了可以调用和使用的函数;二进制 crate 只意在单独运行。

      +

      这也是 Rust 二进制项目明确采用 src/main.rs 调用 src/lib.rs 中逻辑这样的结构的原因之一。通过这种结构,集成测试就可以使用extern crate测试库 crate 中的主要功能,而如果这些重要的功能没有问题的话,src/main.rs 中的少量代码也就会正常工作且不需要测试。

      总结

      -

      Rust 的测试功能提供了一个确保即使改变代码函数也能继续以指定方式运行的途径。单元测试独立的验证库的每一部分并能够测试私有实现细节。集成测试则涉及多个部分结合起来时能否使用,并像其他代码那样测试库的公有 API。Rust 的类型系统和所有权规则可以帮助避免一些 bug,不过测试对于减少代码是否符合期望的逻辑 bug 是很重要的。

      +

      Rust 的测试功能提供了一个确保即使做出改变函数也能继续以指定方式运行的途径。单元测试独立的验证库的不同部分并能够测试私有实现细节。集成测试则涉及多个部分结合起来工作时的用例,并像其他代码那样测试库的公有 API。即使 Rust 的类型系统和所有权规则可以帮助避免一些 bug,不过测试对于减少代码是否符合期望相关的逻辑 bug 是很重要的。

      接下来让我们结合本章所学和其他之前章节的知识,在下一章一起编写一个项目!

      一个 I/O 项目

      -

      ch12-00-an-io-project.md +

      ch12-00-an-io-project.md
      -commit efd59dd0fe8e3658563fb5fd289af9d862e07a03

      +commit 4f2dc564851dc04b271a2260c834643dfd86c724

      之前几个章节我们学习了很多知识。让我们一起运用这些新知识来构建一个项目。在这个过程中,我们还将学习到更多 Rust 标准库的内容。

      那么我们应该写点什么呢?这得是一个利用 Rust 优势的项目。Rust 的一个强大的用途是命令行工具:Rust 的运行速度、安全性、“单二进制文件”输出和跨平台支持使得它称为这类工作的绝佳选择。所以我们将创建一个我们自己的经典命令行工具:grepgrep有着极为简单的应用场景,它完成如下工作:

      @@ -6191,7 +6247,7 @@ commit efd59dd0fe8e3658563fb5fd289af9d862e07a03

    • 打印出这些行
    • 另外,我们还将添加一个额外的功能:一个环境变量允许我们大小写不敏感的搜索字符串参数。

      -

      还有另一个很好的理由使用grep作为示例项目:Rust 社区的成员,Andrew Gallant,已经使用 Rust 创建了一个功能非常完整的grep版本。它叫做ripgrep,并且它非常非常快。这样虽然我们的grep将会非常简单,你也会掌握阅读现实生活中项目的基础知识。

      +

      还有另一个很好的理由使用grep作为示例项目:Rust 社区的成员,Andrew Gallant,已经使用 Rust 创建了一个功能非常完整的grep版本。它叫做ripgrep,并且它非常非常快。这样虽然我们的grep将会非常简单,你也会掌握阅读现真实项目的基础知识。

      这个项目将会结合之前所学的一些内容:

      • 代码组织(使用第七章学习的模块)
      • @@ -6211,7 +6267,7 @@ $ cd greprs

        ch12-01-accepting-command-line-arguments.md
        -commit 4f2dc564851dc04b271a2260c834643dfd86c724

        +commit c49e5ee8859f8eb8f8867cbeafbdf5b802aa5894

        第一个任务是让greprs接受两个命令行参数。crates.io 上有一些现存的库可以帮助我们,不过因为我们正在学习,我们将自己实现一个。

        我们需要调用一个 Rust 标准库提供的函数:std::env::args。这个函数返回一个传递给程序的命令行参数的迭代器iterator)。我们还未讨论到迭代器,第十三章会全面的介绍他们。但是对于我们的目的来说,使用他们并不需要知道多少技术细节。我们只需要明白两点:

        @@ -6220,8 +6276,7 @@ commit 4f2dc564851dc04b271a2260c834643dfd86c724

      • 在迭代器上调用collect方法可以将其生成的元素转换为一个 vector。
      • 让我们试试列表 12-1 中的代码:

        -
        -Filename: src/main.rs +

        Filename: src/main.rs

        use std::env;
         
         fn main() {
        @@ -6229,12 +6284,10 @@ fn main() {
             println!("{:?}", args);
         }
         
        -
        -

        Listing 12-1: Collect the command line arguments into a vector and print them out

        -
        -
        +

        Listing 12-1: Collect the command line arguments into a +vector and print them out

        -

        首先使用use语句来将std::env模块引入作用域。当函数嵌套了多于一层模块时,比如说std::env::args,通常使用use将父模块引入作用域,而不是引入其本身。env::args比单独的args要明确一些。当然,如果使用了多余一个std::env中的函数,我们也只需要一个use语句。

        +

        首先使用use语句来将std::env模块引入作用域。当函数嵌套了多于一层模块时,比如说std::env::args,通常使用use将父模块引入作用域,而不是引入其本身。env::args比单独的args要明确一些。当然,如果使用了多于一个std::env中的函数时,我们也只需要一个use语句。

        main函数的第一行,我们调用了env::args,并立即使用collect来创建了一个 vector。这里我们也显式的注明了args的类型:collect可以被用来创建很多类型的集合。Rust 并不能推断出我们需要什么类型,所以类型注解是必须的。在 Rust 中我们很少会需要注明类型,不过collect是就一个通常需要这么做的函数。

        最后,我们使用调试格式:?打印出 vector。让我们尝试不用参数运行代码,接着用两个参数:

        $ cargo run
        @@ -6246,9 +6299,8 @@ $ cargo run needle haystack
         

        你会注意一个有趣的事情:二进制文件的名字是第一个参数。其原因超出了本章介绍的范围,不过这是我们必须记住的。

        现在我们有了一个访问所有参数的方法,让我们如列表 12-2 中所示将需要的变量存放到变量中:

        -
        -Filename: src/main.rs -
        use std::env;
        +

        Filename: src/main.rs

        +
        use std::env;
         
         fn main() {
             let args: Vec<String> = env::args().collect();
        @@ -6260,10 +6312,8 @@ fn main() {
             println!("In file {}", filename);
         }
         
        -
        -

        Listing 12-2: Create variables to hold the search argument and filename argument

        -
        -
        +

        Listing 12-2: Create variables to hold the search +argument and filename argument

        记住,程序名称是是第一个参数,所以并不需要args[0]。我们决定从第一个参数将是需要搜索的字符串,所以将第一个参数的引用放入变量search中。第二个参数将是文件名,将其放入变量filename中。再次尝试运行程序:

        $ cargo run test sample.txt
        @@ -6285,7 +6335,7 @@ note: Run with `RUST_BACKTRACE=1` for a backtrace.
         

        ch12-02-reading-a-file.md
        -commit 4f2dc564851dc04b271a2260c834643dfd86c724

        +commit c49e5ee8859f8eb8f8867cbeafbdf5b802aa5894

        现在有了一些包含我们需要的信息的变量了,让我们试着使用他们。下一步目标是打开需要搜索的文件。为此,我需要一个文件。在项目的根目录创建一个文件poem.txt,并写入一些艾米莉·狄金森(Emily Dickinson)的诗:

        Filename: poem.txt

        @@ -6304,9 +6354,8 @@ short, but that has multiple lines and some repetition. We could search through code; that gets a bit meta and possibly confusing... Changes to this are most welcome. /Carol -->

        创建完这个文件后,让我们编辑 src/main.rs 并增加如列表 12-3 所示用来打开文件的代码:

        -
        -Filename: src/main.rs -
        use std::env;
        +

        Filename: src/main.rs

        +
        use std::env;
         use std::fs::File;
         use std::io::prelude::*;
         
        @@ -6327,10 +6376,8 @@ fn main() {
             println!("With text:\n{}", contents);
         }
         
        -
        -

        Listing 12-3: Read the contents of the file specified by the second argument

        -
        -
        +

        Listing 12-3: Read the contents of the file specified by +the second argument

        这里增加了一些新内容。首先,需要更多的use语句来引入标准库中的相关部分:我们需要std::fs::File来处理文件,而std::io::prelude::*则包含许多对于 I/O 包括文件 I/O 有帮助的 trait。类似于 Rust 有一个通用的 prelude 来自动引入特定内容,std::io也有其自己的 prelude 来引入处理 I/O 时需要的内容。不同于默认的 prelude,必须显式use位于std::io中的 prelude。

        main中,我们增加了三点内容:第一,我们获取了文件的句柄并使用File::open函数与第二个参数中指定的文件名来打开这个文件。第二,我们在变量contents中创建了一个空的可变的String,接着对文件句柄调用read_to_string并以contents字符串作为参数,contentsread_to_string将会放置它读取到的数据地方。最后,我们打印出了整个文件的内容,这是一个确认目前为止的程序能够工作的方法。

        @@ -6356,13 +6403,13 @@ To an admiring bog!

        ch12-03-improving-error-handling-and-modularity.md
        -commit 4f2dc564851dc04b271a2260c834643dfd86c724

        +commit bdab3f38da5b7bf7277bfe21ec59a7a81880e6b4

        为了完善我们程序有四个问题需要修复,而他们都与潜在的错误和程序结构有关。第一个问题是在哪打开文件:我们使用了expect来在打开文件失败时指定一个错误信息,不过这个错误信息只是说“文件不存在”。还有很多打开文件失败的方式,不过我们总是假设是由于缺少文件导致的。例如,文件存在但是没有打开它的权限:这时,我们就打印出了错误不符合事实的错误信息!

        第二,我们不停的使用expect,这就有点类似我们之前在不传递任何命令行参数时索引会panic!时注意到的问题:这虽然时_可以工作_的,不过这有点没有原则性,而且整个程序中都需要他们,将错误处理都置于一处则会显得好很多。

        第三个问题是main函数现在处理两个工作:解析参数,并打开文件。对于一个小的函数来说,这不是什么大问题。然而随着程序中的main函数不断增长,main函数中独立的任务也会越来越多。因为一个函数拥有很多职责,它将难以理解、难以测试并难以在不破坏其他部分的情况下做出修改。

        这也关系到我们的第四个问题:searchfilename是程序中配置性的变量,而像fcontents则用来执行程序逻辑。随着main函数增长,将引入更多的变量到作用域中,而当作用域中有更多的变量,将更难以追踪哪个变量用于什么目的。如果能够将配置型变量组织进一个结构就能使他们的目的更明确了。

        -

        让我们重新组成程序来解决这些问题。

        +

        让我们重新组织程序来解决这些问题。

        二进制项目的关注分离

        这类项目组织上的问题在很多相似类型的项目中很常见,所以 Rust 社区开发出一种关注分离的组织模式。这种模式可以用来组织任何用 Rust 构建的二进制项目,所以可以证明应该更早的开始这项重构,以为我们的项目符合这个模式。这个模式看起来像这样:

          @@ -6379,13 +6426,8 @@ commit 4f2dc564851dc04b271a2260c834643dfd86c724

        好的!老实说这个模式好像还很复杂。这就是关注分离的所有内容:main.rs 负责实际的程序运行,而 lib.rs 处理所有真正的任务逻辑。让我们将程序重构成这种模式。首先,提取出一个目的只在于解析参数的函数。列表 12-4 中展示了一个新的开始,main函数调用了一个新函数parse_config,它仍然定义于 src/main.rs 中:

        -
        -Filename: src/main.rs -
        # use std::env;
        -# use std::fs::File;
        -# use std::io::prelude::*;
        -#
        -fn main() {
        +

        Filename: src/main.rs

        +
        fn main() {
             let args: Vec<String> = env::args().collect();
         
             let (search, filename) = parse_config(&args);
        @@ -6394,13 +6436,6 @@ fn main() {
             println!("In file {}", filename);
         
             // ...snip...
        -#
        -#     let mut f = File::open(filename).expect("file not found");
        -#
        -#     let mut contents = String::new();
        -#     f.read_to_string(&mut contents).expect("something went wrong reading the file");
        -#
        -#     println!("With text:\n{}", contents);
         }
         
         fn parse_config(args: &[String]) -> (&str, &str) {
        @@ -6410,10 +6445,8 @@ fn parse_config(args: &[String]) -> (&str, &str) {
             (search, filename)
         }
         
        -
        -

        Listing 12-4: Extract a parse_config function from main

        -
        -
        +

        Listing 12-4: Extract a parse_config function from +main

        这看起来好像有点复杂,不过我们将一点一点的开展重构。在做出这些改变之后,再次运行程序并验证参数解析是否仍然正常。经常验证你的进展是一个好习惯,这样在遇到问题时就能更好地理解什么修改造成了错误。

        组合配置值

        @@ -6422,13 +6455,8 @@ fn parse_config(args: &[String]) -> (&str, &str) {

        注意:一些同学将当使用符合类型更为合适的时候使用基本类型当作一种称为基本类型偏执primitive obsession)的反模式。

        让我们引入一个结构体来存放所有的配置。列表 12-5 中展示了新增的Config结构体定义、重构后的parse_configmain函数中的相关更新:

        -
        -Filename: src/main.rs -
        # use std::env;
        -# use std::fs::File;
        -# use std::io::prelude::*;
        -#
        -fn main() {
        +

        Filename: src/main.rs

        +
        fn main() {
             let args: Vec<String> = env::args().collect();
         
             let config = parse_config(&args);
        @@ -6439,10 +6467,6 @@ fn main() {
             let mut f = File::open(config.filename).expect("file not found");
         
             // ...snip...
        -#     let mut contents = String::new();
        -#     f.read_to_string(&mut contents).expect("something went wrong reading the file");
        -#
        -#    println!("With text:\n{}", contents);
         }
         
         struct Config {
        @@ -6460,30 +6484,22 @@ fn parse_config(args: &[String]) -> Config {
             }
         }
         
        -
        -

        Listing 12-5: Refactoring parse_config to return an instance of a Config -struct

        -
        -
        +

        Listing 12-5: Refactoring parse_config to return an +instance of a Config struct

        parse_config的签名现在表明它返回一个Config值。在parse_config的函数体中,我们之前返回了argsString值引用的字符串 slice,不过Config定义为拥有两个有所有权的String值。因为parse_config的参数是一个String值的 slice,Config实例不能获取String值的所有权:这违反了 Rust 的借用规则,因为main函数中的args变量拥有这些String值并只允许parse_config函数借用他们。

        还有许多不同的方式可以处理String的数据;现在我们使用简单但低效率的方式,在字符串 slice 上调用clone方法。clone调用会生成一个字符串数据的完整拷贝,而且Config实例可以拥有它,不过这会消耗更多时间和内存来储存拷贝字符串数据的引用,不过拷贝数据让我们使我们的代码显得更加直白。

        使用clone权衡取舍

        -

        由于其运行时消耗,许多 Rustacean 之间有一个趋势是倾向于不使用clone来解决所有权问题。在关于迭代器的第XX章中,我们将会学习如何更有效率的处理这种情况。现在,为了编写我们的程序拷贝一些字符串是没有问题。我们只进行了一次拷贝,而且文件名和要搜索的字符串都比较短。随着你对 Rust 更加熟练,将更轻松的省略这个权衡的步骤,不过现在调用clone是完全可以接受的。

        +

        由于其运行时消耗,许多 Rustacean 之间有一个趋势是倾向于不使用clone来解决所有权问题。在关于迭代器的第十三章中,我们将会学习如何更有效率的处理这种情况。现在,为了编写我们的程序拷贝一些字符串是没有问题。我们只进行了一次拷贝,而且文件名和要搜索的字符串都比较短。随着你对 Rust 更加熟练,将更轻松的省略这个权衡的步骤,不过现在调用clone是完全可以接受的。

        main函数更新为将parse_config返回的Config实例放入变量config中,并将分别使用searchfilename变量的代码更新为使用Config结构体的字段。

        创建一个Config构造函数

        现在让我们考虑一下parse_config的目的:这是一个创建Config示例的函数。我们已经见过了一个创建实例函数的规范:像String::new这样的new函数。列表 12-6 中展示了将parse_config转换为一个Config结构体关联函数new的代码:

        -
        -Filename: src/main.rs -
        # use std::env;
        -# use std::fs::File;
        -# use std::io::prelude::*;
        -#
        -fn main() {
        +

        Filename: src/main.rs

        +
        fn main() {
             let args: Vec<String> = env::args().collect();
         
             let config = Config::new(&args);
        @@ -6492,21 +6508,8 @@ fn main() {
             println!("In file {}", config.filename);
         
             // ...snip...
        -
        -#     let mut f = File::open(config.filename).expect("file not found");
        -#
        -#     let mut contents = String::new();
        -#     f.read_to_string(&mut contents).expect("something went wrong reading the file");
        -#
        -#    println!("With text:\n{}", contents);
        -
         }
         
        -# struct Config {
        -#     search: String,
        -#     filename: String,
        -# }
        -#
         // ...snip...
         
         impl Config {
        @@ -6521,43 +6524,14 @@ impl Config {
             }
         }
         
        -
        -

        Listing 12-6: Changing parse_config into Config::new

        -
        -
        +

        Listing 12-6: Changing parse_config into +Config::new

        我们将parse_config的名字改为new并将其移动到impl块中。我们也更新了main中的调用代码。再次尝试编译并确保程序可以运行。

        从构造函数返回Result

        这是我们对这个方法最后的重构:还记得当 vector 含有少于三个项时访问索引 1 和 2 会 panic 并给出一个糟糕的错误信息的代码吗?让我们来修改它!列表 12-7 展示了如何在访问这些位置之前检查 slice 是否足够长,并使用一个更好的 panic 信息:

        -
        -Filename: src/main.rs -
        # use std::env;
        -# use std::fs::File;
        -# use std::io::prelude::*;
        -#
        -# fn main() {
        -#     let args: Vec<String> = env::args().collect();
        -#
        -#     let config = Config::new(&args);
        -#
        -#     println!("Searching for {}", config.search);
        -#     println!("In file {}", config.filename);
        -#
        -#     let mut f = File::open(config.filename).expect("file not found");
        -#
        -#     let mut contents = String::new();
        -#     f.read_to_string(&mut contents).expect("something went wrong reading the file");
        -#
        -#     println!("With text:\n{}", contents);
        -# }
        -#
        -# struct Config {
        -#     search: String,
        -#     filename: String,
        -# }
        -#
        -# impl Config {
        -// ...snip...
        +

        Filename: src/main.rs

        +
        // ...snip...
         fn new(args: &[String]) -> Config {
             if args.len() < 3 {
                 panic!("not enough arguments");
        @@ -6565,19 +6539,10 @@ fn new(args: &[String]) -> Config {
         
             let search = args[1].clone();
             // ...snip...
        -#     let filename = args[2].clone();
        -#
        -#     Config {
        -#         search: search,
        -#         filename: filename,
        -#     }
         }
        -# }
         
        -
        -

        Listing 12-7: Adding a check for the number of arguments

        -
        -
        +

        Listing 12-7: Adding a check for the number of +arguments

        通过在new中添加这额外的几行代码,再次尝试不带参数运行程序:

        $ cargo run
        @@ -6587,37 +6552,8 @@ thread 'main' panicked at 'not enough arguments', src\main.rs:29
         note: Run with `RUST_BACKTRACE=1` for a backtrace.
         

        这样就好多了!至少有个一个符合常理的错误信息。然而,还有一堆额外的信息我们并不希望提供给用户。可以通过改变new的签名来完善它。现在它只返回了一个Config,所有没有办法表示创建Config失败的情况。相反,可以如列表 12-8 所示返回一个Result

        -
        -Filename: src/main.rs -
        # use std::env;
        -# use std::fs::File;
        -# use std::io::prelude::*;
        -# use std::process;
        -#
        -# fn main() {
        -#     let args: Vec<String> = env::args().collect();
        -#
        -#     let config = Config::new(&args).unwrap_or_else(|err| {
        -#         println!("Problem parsing arguments: {}", err);
        -#         process::exit(1);
        -#     });
        -#
        -#     println!("Searching for {}", config.search);
        -#     println!("In file {}", config.filename);
        -#
        -#     let mut f = File::open(config.filename).expect("file not found");
        -#
        -#     let mut contents = String::new();
        -#     f.read_to_string(&mut contents).expect("something went wrong reading the file");
        -#
        -#     println!("With text:\n{}", contents);
        -# }
        -# struct Config {
        -#     search: String,
        -#     filename: String,
        -# }
        -#
        -impl Config {
        +

        Filename: src/main.rs

        +
        impl Config {
             fn new(args: &[String]) -> Result<Config, &'static str> {
                 if args.len() < 3 {
                     return Err("not enough arguments");
        @@ -6633,21 +6569,14 @@ impl Config {
             }
         }
         
        -
        -

        Listing 12-8: Return a Result from Config::new

        -
        -
        +

        Listing 12-8: Return a Result from Config::new

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

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

        Config::new调用和错误处理

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

        -
        -Filename: src/main.rs -
        # use std::env;
        -# use std::fs::File;
        -# use std::io::prelude::*;
        -// ...snip...
        +

        Filename: src/main.rs

        +
        // ...snip...
         use std::process;
         
         fn main() {
        @@ -6662,45 +6591,14 @@ fn main() {
             println!("In file {}", config.filename);
         
             // ...snip...
        -#
        -#     let mut f = File::open(config.filename).expect("file not found");
        -#
        -#     let mut contents = String::new();
        -#     f.read_to_string(&mut contents).expect("something went wrong reading the file");
        -#
        -#     println!("With text:\n{}", contents);
        -# }
        -#
        -# struct Config {
        -#     search: String,
        -#     filename: String,
        -# }
        -#
        -# impl Config {
        -#     fn new(args: &[String]) -> Result<Config, &'static str> {
        -#         if args.len() < 3 {
        -#             return Err("not enough arguments");
        -#         }
        -#
        -#         let search = args[1].clone();
        -#         let filename = args[2].clone();
        -#
        -#         Ok(Config {
        -#             search: search,
        -#             filename: filename,
        -#         })
        -#     }
        -# }
         
        -
        -

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

        -
        -
        +

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

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

        -

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

        +

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

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

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

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

        run函数中的错误处理

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

        -
        -Filename: src/main.rs -
        # use std::env;
        -# use std::fs::File;
        -# use std::io::prelude::*;
        -# use std::process;
        -#
        -fn main() {
        -#     let args: Vec<String> = env::args().collect();
        -#
        -#     let config = Config::new(&args).unwrap_or_else(|err| {
        -#         println!("Problem parsing arguments: {}", err);
        -#         process::exit(1);
        -#     });
        +

        Filename: src/main.rs

        +
        fn main() {
             // ...snip...
         
             println!("Searching for {}", config.search);
        @@ -6741,57 +6627,15 @@ fn run(config: Config) {
         }
         
         // ...snip...
        -#
        -# struct Config {
        -#     search: String,
        -#     filename: String,
        -# }
        -#
        -# impl Config {
        -#     fn new(args: &[String]) -> Result<Config, &'static str> {
        -#         if args.len() < 3 {
        -#             return Err("not enough arguments");
        -#         }
        -#
        -#         let search = args[1].clone();
        -#         let filename = args[2].clone();
        -#
        -#         Ok(Config {
        -#             search: search,
        -#             filename: filename,
        -#         })
        -#     }
        -# }
         
        -
        -

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

        -
        -
        +

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

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

        -
        -Filename: src/main.rs -
        use std::error::Error;
        -# use std::env;
        -# use std::fs::File;
        -# use std::io::prelude::*;
        -# use std::process;
        +

        Filename: src/main.rs

        +
        use std::error::Error;
         
         // ...snip...
        -# fn main() {
        -#     let args: Vec<String> = env::args().collect();
        -#
        -#     let config = Config::new(&args).unwrap_or_else(|err| {
        -#         println!("Problem parsing arguments: {}", err);
        -#         process::exit(1);
        -#     });
        -#
        -#     println!("Searching for {}", config.search);
        -#     println!("In file {}", config.filename);
        -#
        -#     run(config);
        -#
        -# }
         
         fn run(config: Config) -> Result<(), Box<Error>> {
             let mut f = File::open(config.filename)?;
        @@ -6803,34 +6647,11 @@ fn run(config: Config) -> Result<(), Box<Error>> {
         
             Ok(())
         }
        -#
        -# struct Config {
        -#     search: String,
        -#     filename: String,
        -# }
        -#
        -# impl Config {
        -#     fn new(args: &[String]) -> Result<Config, &'static str> {
        -#         if args.len() < 3 {
        -#             return Err("not enough arguments");
        -#         }
        -#
        -#         let search = args[1].clone();
        -#         let filename = args[2].clone();
        -#
        -#         Ok(Config {
        -#             search: search,
        -#             filename: filename,
        -#         })
        -#     }
        -# }
         
        -
        -

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

        -
        -
        +

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

        -

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

        +

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

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

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

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

        @@ -6876,9 +6697,8 @@ fn run(config: Config) -> Result<(), Box<Error>> {

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

        将代码拆分到库 crate

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

        -
        -Filename: src/lib.rs -
        use std::error::Error;
        +

        Filename: src/lib.rs

        +
        use std::error::Error;
         use std::fs::File;
         use std::io::prelude::*;
         
        @@ -6914,15 +6734,12 @@ pub fn run(config: Config) -> Result<(), Box<Error>>{
             Ok(())
         }
         
        -
        -

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

        -
        -
        +

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

        -

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

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

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

        -
        -Filename: src/main.rs +

        Filename: src/main.rs

        extern crate greprs;
         
         use std::env;
        @@ -6948,10 +6765,9 @@ fn main() {
             }
         }
         
        -
        -

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

        -
        -
        +

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

        +

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

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

        @@ -6959,12 +6775,11 @@ fn main() {

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

        +commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

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

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

        -
        -Filename: src/lib.rs +

        Filename: src/lib.rs

        # use std::error::Error;
         # use std::fs::File;
         # use std::io::prelude::*;
        @@ -7010,11 +6825,8 @@ Pick three.";
             }
         }
         
        -
        -

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

        -
        -
        +

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

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

        error[E0106]: missing lifetime specifier
        @@ -7085,8 +6897,7 @@ error: test failed
         

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

        -
        -Filename: src/lib.rs +

        Filename: src/lib.rs

        fn grep<'a>(search: &str, contents: &'a str) -> Vec<&'a str> {
             let mut results = Vec::new();
         
        @@ -7099,10 +6910,8 @@ error: test failed
             results
         }
         
        -
        -

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

        -
        -
        +

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

        尝试运行一下:

        $ cargo test
        @@ -7164,7 +6973,7 @@ To an admiring bog!
         

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

        +commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

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

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

        @@ -7206,8 +7015,7 @@ Trust me.";

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

        -
        -Filename: src/lib.rs +

        Filename: src/lib.rs

        fn grep_case_insensitive<'a>(search: &str, contents: &'a str) -> Vec<&'a str> {
             let search = search.to_lowercase();
             let mut results = Vec::new();
        @@ -7221,11 +7029,9 @@ 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-16: Implementing a grep_case_insensitive +function by changing the search string and the lines of the contents to +lowercase before comparing them

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

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

        @@ -7344,7 +7150,7 @@ To an admiring bog!

        ch12-06-writing-to-stderr-instead-of-stdout.md
        -commit 4f2dc564851dc04b271a2260c834643dfd86c724

        +commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

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

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

        @@ -7354,8 +7160,7 @@ commit 4f2dc564851dc04b271a2260c834643dfd86c724

        Problem parsing arguments: not enough arguments
         

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

        -
        -Filename: src/main.rs +

        Filename: src/main.rs

        extern crate greprs;
         
         use std::env;
        @@ -7390,10 +7195,8 @@ fn main() {
             }
         }
         
        -
        -

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

        -
        -
        +

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

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

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

        @@ -7410,7 +7213,7 @@ How dreary to be somebody!

        总结

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

        -

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

        +

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

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

        ch13-00-functional-features.md @@ -7901,7 +7704,7 @@ opt-level = 1

        ch14-02-publishing-to-crates-io.md
        -commit c49e5ee8859f8eb8f8867cbeafbdf5b802aa5894

        +commit f2eef19b3a39ee68dd363db2fcba173491ba9dc4

        我们曾经在项目中增加 crates.io 上的 crate 作为依赖。也可以选择将代码分享给其他人。Crates.io 用来分发包的源代码,所以它主要用于分发开源代码。

        Rust 和 Cargo 有一些帮助人们找到和使用你发布的包的功能。我们将介绍这些功能,接着讲到如何发布一个包。

        @@ -7916,10 +7719,7 @@ commit c49e5ee8859f8eb8f8867cbeafbdf5b802aa5894

        /// ``` /// let five = 5; /// -/// assert_eq!(6, add_one(5)); -/// # fn add_one(x: i32) -> i32 { -/// # x + 1 -/// # } +/// assert_eq!(6, add_one(five)); /// ``` pub fn add_one(x: i32) -> i32 { x + 1 @@ -7945,7 +7745,7 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured

        使用pub use来导出合适的公有 API

        第七章介绍了如何使用mod关键字来将代码组织进模块中,如何使用pub关键字将项变为公有,和如何使用use关键字将项引入作用域。当发布 crate 给并不熟悉其使用的库的实现的人时,就值得花时间考虑 crate 的结构对于开发和对于依赖 crate 的人来说是否同样有用。如果结构对于供其他库使用来说并不方便,也无需重新安排内部组织:可以选择使用pub use来重新导出一个不同的公有结构。

        -

        例如列表 14-2中,我们创建了一个库art,其包含一个kinds模块,模块中包含枚举Color和包含函数mix的模块utils

        +

        例如列表 14-2 中,我们创建了一个库art,其包含一个kinds模块,模块中包含枚举Color和包含函数mix的模块utils

        Filename: src/lib.rs

        //! # Art
         //!
        @@ -8355,9 +8155,9 @@ order to have a known size

        ch15-02-deref.md
        -commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

        +commit ecc3adfe0cfa0a4a15a178dc002702fd0ea74b3f

        -

        第一个智能指针相关的重要 trait 是Deref,它允许我们重载*,解引用运算符(不同于乘法运算符和全局引用运算符)。重载智能指针的*方便访问其后的数据,在这个部分的稍后介绍解引用强制多态时我们会讨论方便的意义。

        +

        第一个智能指针相关的重要 trait 是Deref,它允许我们重载*,解引用运算符(不同于乘法运算符和全局引用运算符)。重载智能指针的*能使访问其后的数据更为方便,在这个部分的稍后介绍解引用强制多态时我们会讨论方便的意义。

        第八章的哈希 map 的“根据旧值更新一个值”部分简要的提到了解引用运算符。当时有一个可变引用,而我们希望改变这个引用所指向的值。为此,首先我们必须解引用。这是另一个使用i32值引用的例子:

        let mut x = 5;
         {
        @@ -8403,14 +8203,14 @@ fn main() {
         struct that holds mp3 file data and metadata

        大部分代码看起来都比较熟悉:一个结构体、一个 trait 实现、和一个创建了结构体示例的 main 函数。其中有一部分我们还未全面的讲解:类似于第十三章学习迭代器 trait 时出现的type Itemtype Target = T;语法用于定义关联类型,第十九章会更详细的介绍。不必过分担心例子中的这一部分;它只是一个稍显不同的定义泛型参数的方式。

        assert_eq!中,我们验证vec![1, 2, 3]是否为Mp3实例*my_favorite_song解引用的值,结果正是如此因为我们实现了deref方法来返回音频数据。如果没有为Mp3实现Deref trait,Rust 将不会编译*my_favorite_song:会出现错误说Mp3类型不能被解引用。

        -

        代码能够工作的原因在于调用*my_favorite_song*在背后所做的操作:

        +

        没有Deref trait 的话,编译器只能解引用&引用,而my_favorite_song并不是(它是一个Mp3结构体)。通过Deref trait,编译器知道实现了Deref trait 的类型有一个返回引用的deref方法(在这个例子中,是&self.audio因为列表 15-7 中的deref的定义)。所以为了得到一个*可以解引用的&引用,编译器将*my_favorite_song展开为如下:

        *(my_favorite_song.deref())
         
        -

        这对my_favorite_song调用了deref方法,它借用了my_favorite_song并返回指向my_favorite_song.audio的引用,这正是列表 15-5 中deref所定义的。引用的*被定义为仅仅从引用中返回其数据,所以上面*的展开形式对于外部*来说并不是递归的。最终的数据类型是Vec<u8>,它与列表 15-5 中assert_eq!vec![1, 2, 3]相匹配。

        -

        deref方法的返回值类型仍然是引用和为何必须解引用方法的结果的原因是如果deref方法就返回值,使用*总是会获取其所有权。

        +

        这个就是self.audio中的结果值。deref返回一个引用并接下来必需解引用而不是直接返回值的原因是所有权:如果deref方法直接返回值而不是引用,其值将被移动出self。这里和大部分使用解引用运算符的地方并不想获取my_favorite_song.audio的所有权。

        +

        注意将*替换为deref调用和*调用的过程在每次使用*的时候都会发生一次。*的替换并不会无限递归进行。最终的数据类型是Vec<u8>,它与列表 15-7 中assert_eq!vec![1, 2, 3]相匹配。

        函数和方法的隐式解引用强制多态

        -

        Rust 倾向于偏爱明确而不是隐晦,不过一个情况下这并不成立,就是函数和方法的参数的解引用强制多态deref coercions)。解引用强制多态会自动的将指针或智能指针的引用转换为指针内容的引用。解引用强制多态发生于一个值被传递给函数或方法,并只发生于需要将传递的值类型与签名中参数类型相匹配的时候。解引用强制多态的加入使得 Rust 调用函数或方法时无需很多使用&*的引用和解引用。

        -

        使用列表 15-5 中的Mp3结构体,如下是一个获取u8 slice 并压缩 mp3 音频数据的函数签名:

        +

        Rust 倾向于偏爱明确而不是隐晦,不过一个情况下这并不成立,就是函数和方法的参数的解引用强制多态deref coercions)。解引用强制多态会自动的将指针或智能指针的引用转换为指针内容的引用。解引用强制多态发生于当传递给函数的参数类型不同于函数签名中定义参数类型的时候。解引用强制多态的加入使得 Rust 调用函数或方法时无需很多使用&*的引用和解引用。

        +

        使用列表 15-7 中的Mp3结构体,如下是一个获取u8 slice 并压缩 mp3 音频数据的函数签名:

        fn compress_mp3(audio: &[u8]) -> Vec<u8> {
             // the actual implementation would go here
         }
        @@ -8422,8 +8222,8 @@ struct that holds mp3 file data and metadata

        然而,因为解引用强制多态和Mp3Deref trait 实现,我们可以使用如下代码使用my_favorite_song中的数据调用这个函数:

        let result = compress_mp3(&my_favorite_song);
         
        -

        只有&和实例,好的!我们可以把智能指针当成普通的引用。也就是说解引用强制多态意味着 Rust 利用了Deref实现的优势:Rust 知道Mp3实现了Deref trait 并从deref方法返回&Vec<u8>。它也知道标准库实现了Vec<T>Deref trait,其deref方法返回&[T](我们也可以通过查阅Vec<T>的 API 文档来发现这一点)。所以,在编译时,Rust 会发现它可以调用两次Deref::deref来将&Mp3变成&Vec<u8>再变成&[T]来满足compress_mp3的签名。这意味着我们可以少写一些代码!Rust 会多次分析Deref::deref的返回值类型直到它满足参数的类型,只要相关类型实现了Deref trait。这些间接转换在编译时进行,所以利用解引用强制多态并没有运行时惩罚。

        -

        这里还有一个重载了&mut T*DerefMut trait,它以与Deref重载&T*相同的方式用于参数中。

        +

        只有&和实例,好的!我们可以把智能指针当成普通的引用。也就是说解引用强制多态意味着 Rust 利用了Deref实现的优势:Rust 知道Mp3实现了Deref trait 并从deref方法返回&Vec<u8>。它也知道标准库实现了Vec<T>Deref trait,其deref方法返回&[T](我们也可以通过查阅Vec<T>的 API 文档来发现这一点)。所以,在编译时,Rust 会发现它可以调用两次Deref::deref来将&Mp3变成&Vec<u8>再变成&[T]来满足compress_mp3的签名。这意味着我们可以少写一些代码!Rust 会多次分析Deref::deref的返回值类型直到它满足参数的类型,只要相关类型实现了Deref trait。这些间接转换在编译时进行,所以利用解引用强制多态并没有运行时惩罚!

        +

        类似于如何使用Deref trait 重载&T*运算符,DerefMut trait用于重载&mut T*运算符。

        Rust 在发现类型和 trait 实现满足三种情况时会进行解引用强制多态:

        • &T&UT: Deref<Target=U>
        • diff --git a/src/PREFACE.md b/src/PREFACE.md index ba6dcdd..c8965ee 100644 --- a/src/PREFACE.md +++ b/src/PREFACE.md @@ -2,4 +2,6 @@ 还在施工中:目前翻译到第十六章 -目前正在解决代码排版问题:已检查到第十一章第一部分 \ No newline at end of file +目前官方进度:[第十六章](https://github.com/rust-lang/book/projects/1)(17~20 章还在编写当中) + +GitBook 代码排版已大体解决,已不影响阅读 \ No newline at end of file diff --git a/src/ch11-02-running-tests.md b/src/ch11-02-running-tests.md index 085647a..229bde1 100644 --- a/src/ch11-02-running-tests.md +++ b/src/ch11-02-running-tests.md @@ -1,176 +1,210 @@ ## 运行测试 -> [ch11-02-running-tests.md](https://github.com/rust-lang/book/blob/master/src/ch11-02-running-tests.md) +> [ch11-02-running-tests.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch11-02-running-tests.md) >
          -> commit cf52d81371e24e14ce31a5582bfcb8c5b80d26cc +> commit 55b294f20fc846a13a9be623bf322d8b364cee77 -类似于`cargo run`会编译代码并运行生成的二进制文件,`cargo test`在测试模式下编译代码并运行生成的测试二进制文件。`cargo test`生成的二进制文件默认会并行的运行所有测试并在测试过程中捕获生成的输出,这样就更容易阅读测试结果的输出。 +就像`cargo run`会编译代码并运行生成的二进制文件,`cargo test`在测试模式下编译代码并运行生成的测试二进制文件。这里有一些选项可以用来改变`cargo test`的默认行为。例如,`cargo test`生成的二进制文件的默认行为是并行的运行所有测试,并捕获测试运行过程中产生的输出避免他们被显示出来使得阅读测试结果相关的内容变得更容易。可以指定命令行参数来改变这些默认行为。 -可以通过指定命令行选项来改变这些运行测试的默认行为。这些选项的一部分可以传递给`cargo test`,而另一些则需要传递给生成的测试二进制文件。分隔这些参数的方法是`--`:`cargo test`之后列出了传递给`cargo test`的参数,接着是分隔符`--`,之后是传递给测试二进制文件的参数。 +这些选项的一部分可以传递给`cargo test`,而另一些则需要传递给生成的测试二进制文件。为了分隔两种类型的参数,首先列出传递给`cargo test`的参数,接着是分隔符`--`,再之后是传递给测试二进制文件的参数。运行`cargo test --help`会告诉你`cargo test`的相关参数,而运行`cargo test -- --help`则会告诉你位于分隔符`--`之后的相关参数。 -### 并行运行测试 +### 并行或连续的运行测试 -测试使用线程来并行运行。为此,编写测试时需要注意测试之间不要相互依赖或者存在任何共享状态。共享状态也可能包含在运行环境中,比如当前工作目录或者环境变量。 + + -如果你不希望它这样运行,或者想要更加精确的控制使用线程的数量,可以传递`--test-threads`参数和线程的数量给测试二进制文件。将线程数设置为 1 意味着没有任何并行操作: +当运行多个测试时,他们默认使用线程来并行的运行。这意味着测试会更快的运行完毕,所以可以更快的得到代码能否工作的反馈。因为测试是在同时运行的,你应该小心测试不能相互依赖或任何共享状态,包括类似于当前工作目录或者环境变量这样的共享环境。 + +例如,每一个测试都运行一些代码在硬盘上创建一个`test-output.txt`文件并写入一些数据。接着每一个测试都读取文件中的数据并断言这个文件包含特定的值,而这个值在每个测试中都是不同的。因为所有测试都是同时运行的,一个测试可能会在另一个测试读写文件过程中覆盖了文件。那么第二个测试就会失败,并不是因为代码不正确,而是因为测试并行运行时相互干涉。一个解决方案是使每一个测试读写不同的文件;另一个是一次运行一个测试。 + +如果你不希望测试并行运行,或者想要更加精确的控制使用线程的数量,可以传递`--test-threads`参数和希望使用线程的数量给测试二进制文件。例如: ``` $ cargo test -- --test-threads=1 ``` -### 捕获测试输出 +这里将测试线程设置为 1,告诉程序不要使用任何并行机制。这也会比并行运行花费更多时间,不过测试就不会在存在共享状态时潜在的相互干涉了。 -Rust 的测试库默认捕获并丢弃标准输出和标准错误中的输出,除非测试失败了。例如,如果在测试中调用了`println!`而测试通过了,你将不会在终端看到`println!`的输出。这个行为可以通过向测试二进制文件传递`--nocapture`参数来禁用: +### 显示测试输出 + +如果测试通过了,Rust 的测试库默认会捕获打印到标准输出的任何内容。例如,如果在测试中调用`println!`而测试通过了,我们将不会在终端看到`println!`的输出:只会看到说明测试通过的行。如果测试失败了,就会看到任何标准输出和其他错误信息。 + +例如,列表 11-20 有一个无意义的函数它打印出其参数的值并接着返回 10。接着还有一个会通过的测试和一个会失败的测试: + +Filename: src/lib.rs + +```rust +fn prints_and_returns_10(a: i32) -> i32 { + println!("I got the value {}", a); + 10 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn this_test_will_pass() { + let value = prints_and_returns_10(4); + assert_eq!(10, value); + } + + #[test] + fn this_test_will_fail() { + let value = prints_and_returns_10(8); + assert_eq!(5, value); + } +} +``` + +Listing 11-10: Tests for a function that calls `println!` + + +运行`cargo test`将会看到这些测试的输出: + +``` +running 2 tests +test tests::this_test_will_pass ... ok +test tests::this_test_will_fail ... FAILED + +failures: + +---- tests::this_test_will_fail stdout ---- + I got the value 8 +thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left == +right)` (left: `5`, right: `10`)', src/lib.rs:19 +note: Run with `RUST_BACKTRACE=1` for a backtrace. + +failures: + tests::this_test_will_fail + +test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured +``` + +注意输出中哪里也不会出现`I got the value 4`,这是当测试通过时打印的内容。这些输出被捕获。失败测试的输出,`I got the value 8`,则出现在输出的测试总结部分,它也显示了测试失败的原因。 + +如果你希望也能看到通过的测试中打印的值,捕获输出的行为可以通过`--nocapture`参数来禁用: ``` $ cargo test -- --nocapture ``` +使用`--nocapture`参数再次运行列表 11-10 中的测试会显示: + +``` +running 2 tests +I got the value 4 +I got the value 8 +test tests::this_test_will_pass ... ok +thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left == +right)` (left: `5`, right: `10`)', src/lib.rs:19 +note: Run with `RUST_BACKTRACE=1` for a backtrace. +test tests::this_test_will_fail ... FAILED + +failures: + +failures: + tests::this_test_will_fail + +test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured +``` + +注意测试的输出和测试结果的输出是相互交叉的;这是由于上一部分讲到的测试是并行运行的。尝试一同使用`--test-threads=1`和`--nocapture`功能来看看输出是什么样子! + ### 通过名称来运行测试的子集 -有时运行整个测试集会耗费很多时间。如果你负责特定位置的代码,你可能会希望只与这些代码相关的测试。`cargo test`有一个参数允许你通过指定名称来运行特定的测试。 +有时运行整个测试集会耗费很多时间。如果你负责特定位置的代码,你可能会希望只与这些代码相关的测试。可以向`cargo test`传递希望运行的测试的(部分)名称作为参数来选择运行哪些测试。 -列表 11-3 中创建了三个如下名称的测试: +为了展示如何运行测试的子集,列表 11-11 使用`add_two`函数创建了三个测试来供我们选择运行哪一个: -
          Filename: src/lib.rs ```rust -#[test] -fn add_two_and_two() { - assert_eq!(4, 2 + 2); +pub fn add_two(a: i32) -> i32 { + a + 2 } -#[test] -fn add_three_and_two() { - assert_eq!(5, 3 + 2); -} +#[cfg(test)] +mod tests { + use super::*; -#[test] -fn one_hundred() { - assert_eq!(102, 100 + 2); -} -``` - -
          - -Listing 11-3: Three tests with a variety of names - -
          -
          - -使用不同的参数会运行不同的测试子集。没有参数的话,如你所见会运行所有的测试: - -``` -$ cargo test - Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs - Running target/debug/deps/adder-abcabcabc - -running 3 tests -test add_three_and_two ... ok -test one_hundred ... ok -test add_two_and_two ... ok - -test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured -``` - -可以传递任意测试的名称来只运行那个测试: - -``` -$ cargo test one_hundred - Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs - Running target/debug/deps/adder-abcabcabc - -running 1 test -test one_hundred ... ok - -test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured -``` - -也可以传递名称的一部分,`cargo test`会运行所有匹配的测试: - -``` -$ cargo test add - Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs - Running target/debug/deps/adder-abcabcabc - -running 2 tests -test add_three_and_two ... ok -test add_two_and_two ... ok - -test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured -``` - -模块名也作为测试名的一部分,所以类似的模块名也可以用来指定测试特定模块。例如,如果将我们的代码组织成一个叫`adding`的模块和一个叫`subtracting`的模块并分别带有测试,如列表 11-4 所示: - -
          -Filename: src/lib.rs - -```rust -mod adding { #[test] fn add_two_and_two() { - assert_eq!(4, 2 + 2); + assert_eq!(4, add_two(2)); } #[test] fn add_three_and_two() { - assert_eq!(5, 3 + 2); + assert_eq!(5, add_two(3)); } #[test] fn one_hundred() { - assert_eq!(102, 100 + 2); - } -} - -mod subtracting { - #[test] - fn subtract_three_and_two() { - assert_eq!(1, 3 - 2); + assert_eq!(102, add_two(100)); } } ``` -
          +Listing 11-11: Three tests with a variety of names -Listing 11-4: Tests in two modules named `adding` and `subtracting` - -
          -
          - -执行`cargo test`会运行所有的测试,而模块名会出现在输出的测试名中: +如果没有传递任何参数就运行测试,如你所见,所有测试都会并行运行: ``` -$ cargo test - Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs - Running target/debug/deps/adder-abcabcabc - -running 4 tests -test adding::add_two_and_two ... ok -test adding::add_three_and_two ... ok -test subtracting::subtract_three_and_two ... ok -test adding::one_hundred ... ok -``` - -运行`cargo test adding`将只会运行对应模块的测试而不会运行任何 subtracting 模块中的测试: - -``` -$ cargo test adding - Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs - Running target/debug/deps/adder-abcabcabc - running 3 tests -test adding::add_three_and_two ... ok -test adding::one_hundred ... ok -test adding::add_two_and_two ... ok +test tests::add_two_and_two ... ok +test tests::add_three_and_two ... ok +test tests::one_hundred ... ok test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured ``` +#### 运行单个测试 + +可以向`cargo test`传递任意测试的名称来只运行这个测试: + +``` +$ cargo test one_hundred + Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs + Running target/debug/deps/adder-06a75b4a1f2515e9 + +running 1 test +test tests::one_hundred ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured +``` + +不能像这样指定多个测试名称,只有传递给`cargo test`的第一个值才会被使用。 + +#### 过滤运行多个测试 + +然而,可以指定测试的部分名称,这样任何名称匹配这个值的测试会被运行。例如,因为头两个测试的名称包含`add`,可以通过`cargo test add`来运行这两个测试: + +``` +$ cargo test add + Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs + Running target/debug/deps/adder-06a75b4a1f2515e9 + +running 2 tests +test tests::add_two_and_two ... ok +test tests::add_three_and_two ... ok + +test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured +``` + +这运行了所有名字中带有`add`的测试。同时注意测试所在的模块作为测试名称的一部分,所以可以通过模块名来过滤运行一个模块中的所有测试。 + + + + ### 除非指定否则忽略某些测试 -有时一些特定的测试执行起来是非常耗费时间的,所以对于大多数`cargo test`命令,我们希望能排除它。无需为`cargo test`创建一个用来在运行所有测试时排除特定测试的参数并每次都要记得使用它,我们可以对这些测试使用`ignore`属性: +有时一些特定的测试执行起来是非常耗费时间的,所以在运行大多数`cargo test`的时候希望能排除他们。与其通过参数列举出所有希望运行的测试,也可以使用`ignore`属性来标记耗时的测试来排除他们: Filename: src/lib.rs @@ -187,13 +221,13 @@ fn expensive_test() { } ``` -现在运行测试,将会发现`it_works`运行了,而`expensive_test`没有: +我们对想要排除的测试的`#[test]`之后增加了`#[ignore]`行。现在如果运行测试,就会发现`it_works`运行了,而`expensive_test`没有运行: ``` $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished debug [unoptimized + debuginfo] target(s) in 0.24 secs - Running target/debug/deps/adder-abcabcabc + Running target/debug/deps/adder-ce99bcc2479f4607 running 2 tests test expensive_test ... ignored @@ -208,12 +242,23 @@ running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured ``` -我们可以通过`cargo test -- --ignored`来明确请求只运行那些耗时的测试: +`expensive_test`被列为`ignored`,如果只希望运行被忽略的测试,可以使用`cargo test -- --ignored`来请求运行他们: -``` + + + + + + +```text $ cargo test -- --ignored Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs - Running target/debug/deps/adder-abcabcabc + Running target/debug/deps/adder-ce99bcc2479f4607 running 1 test test expensive_test ... ok @@ -221,4 +266,5 @@ test expensive_test ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured ``` -通过这种方式,大部分时间运行`cargo test`将是快速的。当需要检查`ignored`测试的结果而且你也有时间等待这个结果的话,可以选择执行`cargo test -- --ignored`。 \ No newline at end of file + +通过控制运行哪些测试,可以确保运行`cargo test`的结果是快速的。当某个时刻需要检查`ignored`测试的结果而且你也有时间等待这个结果的话,可以选择执行`cargo test -- --ignored`。 \ No newline at end of file diff --git a/src/ch11-03-test-organization.md b/src/ch11-03-test-organization.md index 75736aa..81ff91c 100644 --- a/src/ch11-03-test-organization.md +++ b/src/ch11-03-test-organization.md @@ -1,20 +1,22 @@ ## 测试的组织结构 -> [ch11-03-test-organization.md](https://github.com/rust-lang/book/blob/master/src/ch11-03-test-organization.md) +> [ch11-03-test-organization.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch11-03-test-organization.md) >
          -> commit cf52d81371e24e14ce31a5582bfcb8c5b80d26cc +> commit 55b294f20fc846a13a9be623bf322d8b364cee77 -正如之前提到的,测试是一个很广泛的学科,而且不同的人有时也采用不同的技术和组织。Rust 社区倾向于根据测试的两个主要分类来考虑问题:**单元测试**(*unit tests*)与**集成测试**(*unit tests*)。单元测试倾向于更小而更专注,在隔离的环境中一次测试一个模块。他们也可以测试私有接口。集成测试对于你的库来说则完全是外部的。他们与其他用户使用相同的方式使用你得代码,他们只针对共有接口而且每个测试会测试多个模块。这两类测试对于从独立和整体的角度保证你的库符合期望是非常重要的。 +正如之前提到的,测试是一个很广泛的学科,而且不同的开发者也采用不同的技术和组织。Rust 社区倾向于根据测试的两个主要分类来考虑问题:**单元测试**(*unit tests*)与**集成测试**(*unit tests*)。单元测试倾向于更小而更专注,在隔离的环境中一次测试一个模块。他们也可以测试私有接口。集成测试对于你的库来说则完全是外部的。他们与其他用户使用相同的方式使用你的代码,他们只针对共有接口而且每个测试都会测试多个模块。 + +这两类测试对于从独立和整体的角度保证你的库符合期望是非常重要的。 ### 单元测试 -单元测试的目的是在隔离与其他部分的环境中测试每一个单元的代码,以便于快速而准确的定位何处的代码是否符合预期。单元测试位于 *src* 目录中,与他们要测试的代码存在于相同的文件中。他们被分离进每个文件中他们自有的`tests`模块中。 +单元测试的目的是在与其他部分隔离的环境中测试每一个单元的代码,以便于快速而准确的定位何处的代码是否符合预期。单元测试位于 *src* 目录中,与他们要测试的代码存在于相同的文件中。传统做法是在每个文件中创建包含测试函数的`tests`模块,并使用`cfg(test)`标注模块。 #### 测试模块和`cfg(test)` -通过将测试放进他们自己的模块并对该模块使用`cfg`注解,我们可以告诉 Rust 只在执行`cargo test`时才编译和运行测试代码。这在当我们只希望用`cargo build`编译库代码时可以节省编译时间,并减少编译产物的大小因为并没有包含测试。 +测试模块的`#[cfg(test)]`注解告诉 Rust 只在执行`cargo test`时才编译和运行测试代码,而在运行`cargo build`时不这么做。这在只希望构建库的时候可以节省编译时间,并能节省编译产物的空间因为他们并没有包含测试。我们将会看到因为集成测试位于另一个文件夹,他们并不需要`#[cfg(test)]`注解。但是因为单元测试位于与源码相同的文件中,所以使用`#[cfg(test)]`来指定他们不应该被包含进编译产物中。 -还记得上一部分新建的`adder`项目吗?Cargo 为我们生成了如下代码: +还记得本章第一部分新建的`adder`项目吗?Cargo 为我们生成了如下代码: Filename: src/lib.rs @@ -27,66 +29,12 @@ mod tests { } ``` -我们忽略了模块相关的信息以便更关注模块中测试代码的机制,不过现在让我们看看测试周围的代码。 - -首先,这里有一个属性`cfg`。`cfg`属性让我们声明一些内容只在给定特定的**配置**(*configuration*)时才被包含进来。Rust 提供了`test`配置用来编译和运行测试。通过这个属性,Cargo 只会在尝试运行测试时才编译测试代码。 - -接下来,`tests`包含了所有测试函数,而我们的代码则位于`tests`模块之外。`tests`模块的名称是一个惯例,除此之外这是一个遵守第七章讲到的常见可见性规则的普通模块。因为这是一个内部模块,我们需要将要测试的代码引入作用域。这对于一个大的模块来说是很烦人的,所以这里经常使用全局导入。 - -从本章到现在,我们一直在为`adder`项目编写并没有实际调用任何代码的测试。现在让我们做一些改变!在 *src/lib.rs* 中,放入`add_two`函数和带有一个检验代码的测试的`tests`模块,如列表 11-5 所示: - -
          -Filename: src/lib.rs - -```rust -pub fn add_two(a: i32) -> i32 { - a + 2 -} - -#[cfg(test)] -mod tests { - use add_two; - - #[test] - fn it_works() { - assert_eq!(4, add_two(2)); - } -} -``` - -
          - -Listing 11-5: Testing the function `add_two` in a child `tests` module - -
          -
          - -注意除了测试函数之外,我们还在`tests`模块中添加了`use add_two;`。这将我们想要测试的代码引入到了内部的`tests`模块的作用域中,正如任何内部模块需要做的那样。如果现在使用`cargo test`运行测试,它会通过: - -``` -running 1 test -test tests::it_works ... ok - -test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured -``` - -如果我们忘记将`add_two`函数引入作用域,将会得到一个 unresolved name 错误,因为`tests`模块并不知道任何关于`add_two`函数的信息: - -``` -error[E0425]: unresolved name `add_two` - --> src/lib.rs:9:23 - | -9 | assert_eq!(4, add_two(2)); - | ^^^^^^^ unresolved name -``` - -如果这个模块包含很多希望测试的代码,在测试中列出每一个`use`语句将是很烦人的。相反在测试子模块中使用`use super::*;`来一次将所有内容导入作用域中是很常见的。 +这里自动生成了测试模块。`cfg`属性代表 *configuration* ,它告诉 Rust 其之后的项只被包含进特定配置中。在这个例子中,配置是`test`,Rust 所提供的用于编译和运行测试的配置。通过使用这个属性,Cargo 只会在我们主动使用`cargo test`运行测试时才编译测试代码。除了标注为`#[test]`的函数之外,这还包括测试模块中可能存在的帮助函数。 #### 测试私有函数 -测试社区中一直存在关于是否应该对私有函数进行单元测试的论战。不过无论你坚持哪种测试意识形态,Rust 确实允许你测试私有函数,由于私有性规则。考虑列表 11-6 中带有私有函数`internal_adder`的代码: +测试社区中一直存在关于是否应该对私有函数进行单元测试的论战,而其他语言中难以甚至不可能测试私有函数。不过无论你坚持哪种测试意识形态,Rust 的私有性规则确实允许你测试私有函数,由于私有性规则。考虑列表 11-12 中带有私有函数`internal_adder`的代码: -
          Filename: src/lib.rs ```rust @@ -100,7 +48,7 @@ fn internal_adder(a: i32, b: i32) -> i32 { #[cfg(test)] mod tests { - use internal_adder; + use super::*; #[test] fn internal() { @@ -109,27 +57,28 @@ mod tests { } ``` -
          +Listing 11-12: Testing a private function -Listing 11-6: Testing a private function + + -
          -
          - -因为测试也不过是 Rust 代码而`tests`也只是另一个模块,我们完全可以在一个测试中导入并调用`internal_adder`。如果你并不认为私有函数应该被测试,Rust 也不会强迫你这么做。 +注意`internal_adder`函数并没有标记为`pub`,不过因为测试也不过是 Rust 代码而`tests`也仅仅是另一个模块,我们完全可以在测试中导入和调用`internal_adder`。如果你并不认为私有函数应该被测试,Rust 也不会强迫你这么做。 ### 集成测试 -在 Rust 中,集成测试对于需要测试的库来说是完全独立。他们同其他代码一样使用库文件。他们的目的是测试库的个个部分结合起来能否正常工作。每个能正确运行的代码单元集成在一起也可能会出现问题,所以集成测试的覆盖率也是很重要的。 +在 Rust 中,集成测试对于需要测试的库来说是完全独立。他们同其他代码一样使用库文件,这意味着他们只能调用作为库公有 API 的一部分的函数。他们的目的是测试库的多个部分能否一起正常工作。每个能单独正确运行的代码单元集成在一起也可能会出现问题,所以集成测试的覆盖率也是很重要的。为了创建集成测试,首先需要一个 *tests* 目录。 #### *tests* 目录 -Cargo 支持位于 *tests* 目录中的集成测试。如果创建它并放入 Rust 源文件,Cargo 会将每一个文件当作单独的 crate 来编译。让我们试一试! +为了编写集成测试,需要在项目根目录创建一个 *tests* 目录,与 *src* 同级。Cargo 知道如何去寻找这个目录中的集成测试文件。接着可以随意在这个文件夹中创建任意多的测试文件,Cargo 会将每一个文件当作单独的 crate 来编译。 -首先,在项目根目录创建一个 *tests* 目录,挨着 *src* 目录。接着新建一个文件 *tests/integration_test.rs*,并写入列表 11-7 中的代码: +让我们试一试吧!保留列表 11-12 中 *src/lib.rs* 的代码。创建一个 *tests* 目录,新建一个文件 *tests/integration_test.rs*,并输入列表 11-13 中的代码。 - -
          Filename: tests/integration_test.rs ```rust,ignore @@ -141,30 +90,25 @@ fn it_adds_two() { } ``` -
          +Listing 11-13: An integration test of a function in the +`adder` crate -Listing 11-7: An integration test of a function in the `adder` crate +我们在顶部增加了`extern crate adder`,这在单元测试中是不需要的。这是因为每一个`tests`目录中的测试文件都是完全独立的 crate,所以需要在每一个文件中导入库。集成测试就像其他使用者那样通过导入 crate 并只使用公有 API 来使用库文件。 -
          -
          - -在开头使用了`extern crate adder`,单元测试中并不需要它。`tests`目录中的每一个测试文件都是完全独立的 crate,所以需要在每个文件中导入我们的库。这也就是为何`tests`是编写集成测试的绝佳场所:他们像任何其他用户那样,需要将库导入 crate 并只能使用公有 API。 - -这个文件中也不需要`tests`模块。除非运行测试否则整个文件夹都不会被编译,所以无需将任何部分标记为`#[cfg(test)]`。另外每个测试文件都被隔离进其自己的 crate 中,无需进一步隔离测试代码。 - -让我们运行集成测试,同样使用`cargo test`来运行: +并不需要将 *tests/integration_test.rs* 中的任何代码标注为`#[cfg(test)]`。Cargo 对`tests`文件夹特殊处理并只会在运行`cargo test`时编译这个目录中的文件。现在就试试运行`cargo test`: ``` -$ cargo test +cargo test Compiling adder v0.1.0 (file:///projects/adder) + Finished debug [unoptimized + debuginfo] target(s) in 0.31 secs Running target/debug/deps/adder-abcabcabc running 1 test -test tests::it_works ... ok +test tests::internal ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured - Running target/debug/integration_test-952a27e0126bb565 + Running target/debug/deps/integration_test-ce99bcc2479f4607 running 1 test test it_adds_two ... ok @@ -178,9 +122,18 @@ running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured ``` -现在有了三个部分的输出:单元测试、集成测试和文档测试。注意当在任何 *src* 目录的文件中增加单元测试时,单元测试部分的对应输出也会增加。增加集成测试文件中的测试函数也会对应增加输出。如果在 *tests* 目录中增加集成测试**文件**,则会增加更多集成测试部分:一个文件对应一个部分。 + + -为`cargo test`指定测试函数名称参数也会匹配集成测试文件中的函数。为了只运行某个特定集成测试文件中的所有测试,可以使用`cargo test`的`--test`参数: +现在有了三个部分的输出:单元测试、集成测试和文档测试。第一部分单元测试与我们之前见过的一样:每一个单元测试一行(列表 11-12 中有一个叫做`internal`的测试),接着是一个单元测试的总结行。 + +集成测试部分以行`Running target/debug/deps/integration-test-ce99bcc2479f4607`(输出最后的哈希值可能不同)开头。接着是每一个集成测试中的测试函数一行,以及一个就在`Doc-tests adder`部分开始之前的集成测试的总结行。 + +注意在任意 *src* 文件中增加更多单元测试函数会增加更多单元测试部分的测试结果行。在我们创建的集成测试文件中增加更多测试函数会增加更多集成测试部分的行。每一个集成测试文件有其自己的部分,所以如果在 *tests* 目录中增加更多文件,这里就会有更多集成测试部分。 + +我们仍然可以通过指定测试函数的名称作为`cargo test`的参数来运行特定集成测试。为了运行某个特定集成测试文件中的所有测试,使用`cargo test`的`--test`后跟文件的名称: ``` $ cargo test --test integration_test @@ -193,18 +146,86 @@ test it_adds_two ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured ``` +这些只是 *tests* 目录中我们指定的文件中的测试。 + #### 集成测试中的子模块 -随着集成测试的增加,你可能希望在 `tests` 目录增加更多文件,例如根据测试的功能来将测试分组。正如我们之前提到的,这是可以的,Cargo 会将每一个文件当作一个独立的 crate。 +随着集成测试的增加,你可能希望在 `tests` 目录增加更多文件,例如根据测试的功能来将测试分组。正如我们之前提到的,每一个 *tests* 目录中的文件都被编译为单独的 crate。 -最终,可能会有一系列在所有集成测试中通用的帮助函数,例如建立通用场景的函数。如果你将这些函数提取到 *tests* 目录的一个文件中,比如说 *tests/common.rs*,则这个文件将会像这个目录中的其他包含测试的 Rust 文件一样被编译进一个单独的 crate 中。它也会作为一个独立的部分出现在测试输出中。因为这很可能不是你所希望的,所以建议在子目录中使用 *mod.rs* 文件,比如 *tests/common/mod.rs*,来放置帮助函数。*tests* 的子目录不会被作为单独的 crate 编译或者作为单独的部分出现在测试输出中。 +将每个集成测试文件当作其自己的 crate 来对待有助于创建更类似与终端用户使用 crate 那样的单独的作用域。然而,这意味着考虑到像第七章学习的如何将代码分隔进模块和文件那样,*tests* 目录中的文件不能像 *src* 中的文件那样共享相同的行为。 + +对于 *tests* 目录中文件的不同行为,通常在如果有一系列有助于多个集成测试文件的帮助函数,而你尝试遵循第七章的步骤将他们提取到一个通用的模块中时显得很明显。例如,如果我们创建了 *tests/common.rs* 并将`setup`函数放入其中,这里将放入一些希望能够在多个测试文件的多个测试函数中调用的代码: + +Filename: tests/common.rs + +```rust +pub fn setup() { + // setup code specific to your library's tests would go here +} +``` + +如果再次运行测试,将会在测试结果中看到一个对应 *common.rs* 文件的新部分,即便这个文件并没有包含任何测试函数,或者没有任何地方调用了`setup`函数: + +``` +running 1 test +test tests::internal ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured + + Running target/debug/deps/common-b8b07b6f1be2db70 + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured + + Running target/debug/deps/integration_test-d993c68b431d39df + +running 1 test +test it_adds_two ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured + + Doc-tests adder + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured +``` + + + + +`common`出现在测试结果中并显示`running 0 tests`,这不是我们想要的;我们只是希望能够在其他集成测试文件中分享一些代码罢了。 + +为了使`common`不出现在测试输出中,需要使用第七章学习到的另一个将代码提取到文件的方式:不再创建*tests/common.rs*,而是创建 *tests/common/mod.rs*。当将`setup`代码移动到 *tests/common/mod.rs* 并去掉 *tests/common.rs* 文件之后,测试输出中将不会出现这一部分。*tests* 目录中的子目录不会被作为单独的 crate 编译或作为一部分出现在测试输出中。 + +一旦拥有了 *tests/common/mod.rs*,就可以将其作为模块来在任何集成测试文件中使用。这里是一个 *tests/integration_test.rs* 中调用`setup`函数的`it_adds_two`测试的例子: + +Filename: tests/integration_test.rs + +```rust,ignore +extern crate adder; + +mod common; + +#[test] +fn it_adds_two() { + common::setup(); + assert_eq!(4, adder::add_two(2)); +} +``` + +注意`mod common;`声明与第七章中的模块声明相同。接着在测试函数中就可以调用`common::setup()`了。 #### 二进制 crate 的集成测试 -如果项目是二进制 crate 并且只包含 *src/main.rs* 而没有 *src/lib.rs*,这样就不可能在 *tests* 创建集成测试并使用 `extern crate` 导入 *src/main.rs* 中的函数了。这也是 Rust 二进制项目明确采用 *src/main.rs* 调用 *src/lib.rs* 中逻辑的结构的原因之一。通过这种结构,集成测试**就可以**使用`extern crate`测试库 crate 中的主要功能,而如果这些功能没有问题的话,*src/main.rs* 中的少量代码也就会正常工作且不需要测试。 +如果项目是二进制 crate 并且只包含 *src/main.rs* 而没有 *src/lib.rs*,这样就不可能在 *tests* 创建集成测试并使用 `extern crate` 导入 *src/main.rs* 中的函数了。只有库 crate 向其他 crate 暴露了可以调用和使用的函数;二进制 crate 只意在单独运行。 + +这也是 Rust 二进制项目明确采用 *src/main.rs* 调用 *src/lib.rs* 中逻辑这样的结构的原因之一。通过这种结构,集成测试**就可以**使用`extern crate`测试库 crate 中的主要功能,而如果这些重要的功能没有问题的话,*src/main.rs* 中的少量代码也就会正常工作且不需要测试。 ## 总结 -Rust 的测试功能提供了一个确保即使改变代码函数也能继续以指定方式运行的途径。单元测试独立的验证库的每一部分并能够测试私有实现细节。集成测试则涉及多个部分结合起来时能否使用,并像其他代码那样测试库的公有 API。Rust 的类型系统和所有权规则可以帮助避免一些 bug,不过测试对于减少代码是否符合期望的逻辑 bug 是很重要的。 +Rust 的测试功能提供了一个确保即使做出改变函数也能继续以指定方式运行的途径。单元测试独立的验证库的不同部分并能够测试私有实现细节。集成测试则涉及多个部分结合起来工作时的用例,并像其他代码那样测试库的公有 API。即使 Rust 的类型系统和所有权规则可以帮助避免一些 bug,不过测试对于减少代码是否符合期望相关的逻辑 bug 是很重要的。 接下来让我们结合本章所学和其他之前章节的知识,在下一章一起编写一个项目! \ No newline at end of file diff --git a/src/ch12-00-an-io-project.md b/src/ch12-00-an-io-project.md index 54a22aa..26be4d8 100644 --- a/src/ch12-00-an-io-project.md +++ b/src/ch12-00-an-io-project.md @@ -1,8 +1,8 @@ # 一个 I/O 项目 -> [ch12-00-an-io-project.md](https://github.com/rust-lang/book/blob/master/src/ch12-00-an-io-project.md) +> [ch12-00-an-io-project.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-00-an-io-project.md) >
          -> commit efd59dd0fe8e3658563fb5fd289af9d862e07a03 +> commit 4f2dc564851dc04b271a2260c834643dfd86c724 之前几个章节我们学习了很多知识。让我们一起运用这些新知识来构建一个项目。在这个过程中,我们还将学习到更多 Rust 标准库的内容。 @@ -15,7 +15,7 @@ 另外,我们还将添加一个额外的功能:一个环境变量允许我们大小写不敏感的搜索字符串参数。 -还有另一个很好的理由使用`grep`作为示例项目:Rust 社区的成员,Andrew Gallant,已经使用 Rust 创建了一个功能非常完整的`grep`版本。它叫做`ripgrep`,并且它非常非常快。这样虽然我们的`grep`将会非常简单,你也会掌握阅读现实生活中项目的基础知识。 +还有另一个很好的理由使用`grep`作为示例项目:Rust 社区的成员,Andrew Gallant,已经使用 Rust 创建了一个功能非常完整的`grep`版本。它叫做`ripgrep`,并且它非常非常快。这样虽然我们的`grep`将会非常简单,你也会掌握阅读现真实项目的基础知识。 这个项目将会结合之前所学的一些内容: diff --git a/src/ch12-01-accepting-command-line-arguments.md b/src/ch12-01-accepting-command-line-arguments.md index 0b8717c..006dd3a 100644 --- a/src/ch12-01-accepting-command-line-arguments.md +++ b/src/ch12-01-accepting-command-line-arguments.md @@ -2,7 +2,7 @@ > [ch12-01-accepting-command-line-arguments.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-01-accepting-command-line-arguments.md) >
          -> commit 4f2dc564851dc04b271a2260c834643dfd86c724 +> commit c49e5ee8859f8eb8f8867cbeafbdf5b802aa5894 第一个任务是让`greprs`接受两个命令行参数。crates.io 上有一些现存的库可以帮助我们,不过因为我们正在学习,我们将自己实现一个。 @@ -13,7 +13,6 @@ 让我们试试列表 12-1 中的代码: -
          Filename: src/main.rs ```rust @@ -25,16 +24,12 @@ fn main() { } ``` -
          - -Listing 12-1: Collect the command line arguments into a vector and print them out - -
          -
          +Listing 12-1: Collect the command line arguments into a +vector and print them out -首先使用`use`语句来将`std::env`模块引入作用域。当函数嵌套了多于一层模块时,比如说`std::env::args`,通常使用`use`将父模块引入作用域,而不是引入其本身。`env::args`比单独的`args`要明确一些。当然,如果使用了多余一个`std::env`中的函数,我们也只需要一个`use`语句。 +首先使用`use`语句来将`std::env`模块引入作用域。当函数嵌套了多于一层模块时,比如说`std::env::args`,通常使用`use`将父模块引入作用域,而不是引入其本身。`env::args`比单独的`args`要明确一些。当然,如果使用了多于一个`std::env`中的函数时,我们也只需要一个`use`语句。 在`main`函数的第一行,我们调用了`env::args`,并立即使用`collect`来创建了一个 vector。这里我们也显式的注明了`args`的类型:`collect`可以被用来创建很多类型的集合。Rust 并不能推断出我们需要什么类型,所以类型注解是必须的。在 Rust 中我们很少会需要注明类型,不过`collect`是就一个通常需要这么做的函数。 @@ -53,10 +48,9 @@ $ cargo run needle haystack 现在我们有了一个访问所有参数的方法,让我们如列表 12-2 中所示将需要的变量存放到变量中: -
          Filename: src/main.rs -```rust +```rust,ignore use std::env; fn main() { @@ -70,12 +64,8 @@ fn main() { } ``` -
          - -Listing 12-2: Create variables to hold the search argument and filename argument - -
          -
          +Listing 12-2: Create variables to hold the search +argument and filename argument diff --git a/src/ch12-02-reading-a-file.md b/src/ch12-02-reading-a-file.md index 860cb7b..a0e0933 100644 --- a/src/ch12-02-reading-a-file.md +++ b/src/ch12-02-reading-a-file.md @@ -2,7 +2,7 @@ > [ch12-02-reading-a-file.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-02-reading-a-file.md) >
          -> commit 4f2dc564851dc04b271a2260c834643dfd86c724 +> commit c49e5ee8859f8eb8f8867cbeafbdf5b802aa5894 现在有了一些包含我们需要的信息的变量了,让我们试着使用他们。下一步目标是打开需要搜索的文件。为此,我需要一个文件。在项目的根目录创建一个文件`poem.txt`,并写入一些艾米莉·狄金森(Emily Dickinson)的诗: @@ -27,10 +27,9 @@ welcome. /Carol --> 创建完这个文件后,让我们编辑 *src/main.rs* 并增加如列表 12-3 所示用来打开文件的代码: -
          Filename: src/main.rs -```rust +```rust,ignore use std::env; use std::fs::File; use std::io::prelude::*; @@ -53,12 +52,8 @@ fn main() { } ``` -
          - -Listing 12-3: Read the contents of the file specified by the second argument - -
          -
          +Listing 12-3: Read the contents of the file specified by +the second argument diff --git a/src/ch12-03-improving-error-handling-and-modularity.md b/src/ch12-03-improving-error-handling-and-modularity.md index 2d65ec4..93a011e 100644 --- a/src/ch12-03-improving-error-handling-and-modularity.md +++ b/src/ch12-03-improving-error-handling-and-modularity.md @@ -2,7 +2,7 @@ > [ch12-03-improving-error-handling-and-modularity.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-03-improving-error-handling-and-modularity.md) >
          -> commit 4f2dc564851dc04b271a2260c834643dfd86c724 +> commit bdab3f38da5b7bf7277bfe21ec59a7a81880e6b4 为了完善我们程序有四个问题需要修复,而他们都与潜在的错误和程序结构有关。第一个问题是在哪打开文件:我们使用了`expect`来在打开文件失败时指定一个错误信息,不过这个错误信息只是说“文件不存在”。还有很多打开文件失败的方式,不过我们总是假设是由于缺少文件导致的。例如,文件存在但是没有打开它的权限:这时,我们就打印出了错误不符合事实的错误信息! @@ -12,7 +12,7 @@ 这也关系到我们的第四个问题:`search`和`filename`是程序中配置性的变量,而像`f`和`contents`则用来执行程序逻辑。随着`main`函数增长,将引入更多的变量到作用域中,而当作用域中有更多的变量,将更难以追踪哪个变量用于什么目的。如果能够将配置型变量组织进一个结构就能使他们的目的更明确了。 -让我们重新组成程序来解决这些问题。 +让我们重新组织程序来解决这些问题。 ### 二进制项目的关注分离 @@ -29,14 +29,9 @@ 好的!老实说这个模式好像还很复杂。这就是关注分离的所有内容:*main.rs* 负责实际的程序运行,而 *lib.rs* 处理所有真正的任务逻辑。让我们将程序重构成这种模式。首先,提取出一个目的只在于解析参数的函数。列表 12-4 中展示了一个新的开始,`main`函数调用了一个新函数`parse_config`,它仍然定义于 *src/main.rs* 中: -
          Filename: src/main.rs -```rust -# use std::env; -# use std::fs::File; -# use std::io::prelude::*; -# +```rust,ignore fn main() { let args: Vec = env::args().collect(); @@ -46,13 +41,6 @@ fn main() { println!("In file {}", filename); // ...snip... -# -# let mut f = File::open(filename).expect("file not found"); -# -# let mut contents = String::new(); -# f.read_to_string(&mut contents).expect("something went wrong reading the file"); -# -# println!("With text:\n{}", contents); } fn parse_config(args: &[String]) -> (&str, &str) { @@ -63,12 +51,8 @@ fn parse_config(args: &[String]) -> (&str, &str) { } ``` -
          - -Listing 12-4: Extract a `parse_config` function from `main` - -
          -
          +Listing 12-4: Extract a `parse_config` function from +`main` @@ -82,14 +66,9 @@ Listing 12-4: Extract a `parse_config` function from `main` 让我们引入一个结构体来存放所有的配置。列表 12-5 中展示了新增的`Config`结构体定义、重构后的`parse_config`和`main`函数中的相关更新: -
          Filename: src/main.rs -```rust -# use std::env; -# use std::fs::File; -# use std::io::prelude::*; -# +```rust,ignore fn main() { let args: Vec = env::args().collect(); @@ -101,10 +80,6 @@ fn main() { let mut f = File::open(config.filename).expect("file not found"); // ...snip... -# let mut contents = String::new(); -# f.read_to_string(&mut contents).expect("something went wrong reading the file"); -# -# println!("With text:\n{}", contents); } struct Config { @@ -123,13 +98,8 @@ fn parse_config(args: &[String]) -> Config { } ``` -
          - -Listing 12-5: Refactoring `parse_config` to return an instance of a `Config` -struct - -
          -
          +Listing 12-5: Refactoring `parse_config` to return an +instance of a `Config` struct @@ -141,7 +111,7 @@ struct > #### 使用`clone`权衡取舍 > -> 由于其运行时消耗,许多 Rustacean 之间有一个趋势是倾向于不使用`clone`来解决所有权问题。在关于迭代器的第XX章中,我们将会学习如何更有效率的处理这种情况。现在,为了编写我们的程序拷贝一些字符串是没有问题。我们只进行了一次拷贝,而且文件名和要搜索的字符串都比较短。随着你对 Rust 更加熟练,将更轻松的省略这个权衡的步骤,不过现在调用`clone`是完全可以接受的。 +> 由于其运行时消耗,许多 Rustacean 之间有一个趋势是倾向于不使用`clone`来解决所有权问题。在关于迭代器的第十三章中,我们将会学习如何更有效率的处理这种情况。现在,为了编写我们的程序拷贝一些字符串是没有问题。我们只进行了一次拷贝,而且文件名和要搜索的字符串都比较短。随着你对 Rust 更加熟练,将更轻松的省略这个权衡的步骤,不过现在调用`clone`是完全可以接受的。 @@ -151,14 +121,9 @@ struct 现在让我们考虑一下`parse_config`的目的:这是一个创建`Config`示例的函数。我们已经见过了一个创建实例函数的规范:像`String::new`这样的`new`函数。列表 12-6 中展示了将`parse_config`转换为一个`Config`结构体关联函数`new`的代码: -
          Filename: src/main.rs -```rust -# use std::env; -# use std::fs::File; -# use std::io::prelude::*; -# +```rust,ignore fn main() { let args: Vec = env::args().collect(); @@ -168,21 +133,8 @@ fn main() { println!("In file {}", config.filename); // ...snip... - -# let mut f = File::open(config.filename).expect("file not found"); -# -# let mut contents = String::new(); -# f.read_to_string(&mut contents).expect("something went wrong reading the file"); -# -# println!("With text:\n{}", contents); - } -# struct Config { -# search: String, -# filename: String, -# } -# // ...snip... impl Config { @@ -198,12 +150,8 @@ impl Config { } ``` -
          - -Listing 12-6: Changing `parse_config` into `Config::new` - -
          -
          +Listing 12-6: Changing `parse_config` into +`Config::new` @@ -213,36 +161,9 @@ Listing 12-6: Changing `parse_config` into `Config::new` 这是我们对这个方法最后的重构:还记得当 vector 含有少于三个项时访问索引 1 和 2 会 panic 并给出一个糟糕的错误信息的代码吗?让我们来修改它!列表 12-7 展示了如何在访问这些位置之前检查 slice 是否足够长,并使用一个更好的 panic 信息: -
          Filename: src/main.rs -```rust -# use std::env; -# use std::fs::File; -# use std::io::prelude::*; -# -# fn main() { -# let args: Vec = env::args().collect(); -# -# let config = Config::new(&args); -# -# println!("Searching for {}", config.search); -# println!("In file {}", config.filename); -# -# let mut f = File::open(config.filename).expect("file not found"); -# -# let mut contents = String::new(); -# f.read_to_string(&mut contents).expect("something went wrong reading the file"); -# -# println!("With text:\n{}", contents); -# } -# -# struct Config { -# search: String, -# filename: String, -# } -# -# impl Config { +```rust,ignore // ...snip... fn new(args: &[String]) -> Config { if args.len() < 3 { @@ -251,22 +172,11 @@ fn new(args: &[String]) -> Config { let search = args[1].clone(); // ...snip... -# let filename = args[2].clone(); -# -# Config { -# search: search, -# filename: filename, -# } } -# } ``` -
          - -Listing 12-7: Adding a check for the number of arguments - -
          -
          +Listing 12-7: Adding a check for the number of +arguments @@ -282,38 +192,9 @@ note: Run with `RUST_BACKTRACE=1` for a backtrace. 这样就好多了!至少有个一个符合常理的错误信息。然而,还有一堆额外的信息我们并不希望提供给用户。可以通过改变`new`的签名来完善它。现在它只返回了一个`Config`,所有没有办法表示创建`Config`失败的情况。相反,可以如列表 12-8 所示返回一个`Result`: -
          Filename: src/main.rs -```rust -# use std::env; -# use std::fs::File; -# use std::io::prelude::*; -# use std::process; -# -# fn main() { -# let args: Vec = env::args().collect(); -# -# let config = Config::new(&args).unwrap_or_else(|err| { -# println!("Problem parsing arguments: {}", err); -# process::exit(1); -# }); -# -# println!("Searching for {}", config.search); -# println!("In file {}", config.filename); -# -# let mut f = File::open(config.filename).expect("file not found"); -# -# let mut contents = String::new(); -# f.read_to_string(&mut contents).expect("something went wrong reading the file"); -# -# println!("With text:\n{}", contents); -# } -# struct Config { -# search: String, -# filename: String, -# } -# +```rust,ignore impl Config { fn new(args: &[String]) -> Result { if args.len() < 3 { @@ -331,12 +212,7 @@ impl Config { } ``` -
          - -Listing 12-8: Return a `Result` from `Config::new` - -
          -
          +Listing 12-8: Return a `Result` from `Config::new` @@ -348,13 +224,9 @@ Listing 12-8: Return a `Result` from `Config::new` 现在我们需要对`main`做一些修改,如列表 12-9 所示: -
          Filename: src/main.rs -```rust -# use std::env; -# use std::fs::File; -# use std::io::prelude::*; +```rust,ignore // ...snip... use std::process; @@ -370,53 +242,20 @@ fn main() { println!("In file {}", config.filename); // ...snip... -# -# let mut f = File::open(config.filename).expect("file not found"); -# -# let mut contents = String::new(); -# f.read_to_string(&mut contents).expect("something went wrong reading the file"); -# -# println!("With text:\n{}", contents); -# } -# -# struct Config { -# search: String, -# filename: String, -# } -# -# impl Config { -# fn new(args: &[String]) -> Result { -# if args.len() < 3 { -# return Err("not enough arguments"); -# } -# -# let search = args[1].clone(); -# let filename = args[2].clone(); -# -# Ok(Config { -# search: search, -# filename: filename, -# }) -# } -# } ``` -
          - -Listing 12-9: Exiting with an error code if creating a new `Config` fails - -
          -
          +Listing 12-9: Exiting with an error code if creating a +new `Config` fails 新增了一个`use`行来从标准库中导入`process`。在`main`函数中我们将处理`new`函数返回的`Result`值,并在其返回`Config::new`时以一种更加清楚的方式结束进程。 -这里使用了一个之前没有讲到的标准库中定义的`Result`的方法:`unwrap_or_else`。当`Result`是`Ok`时其行为类似于`unwrap`:它返回`Ok`内部封装的值。与`unwrap`不同的是,当`Result`是`Err`时,它调用一个**闭包**(*closure*),也就是一个我们定义的作为参数传递给`unwrap_or_else`的匿名函数。第XX章会更详细的介绍闭包;这里需要理解的重要部分是`unwrap_or_else`会将`Err`的内部值传递给闭包中位于两道竖线间的参数`err`。使用`unwrap_or_else`允许我们进行一些自定义的非`panic!`的错误处理。 +这里使用了一个之前没有讲到的标准库中定义的`Result`的方法:`unwrap_or_else`。当`Result`是`Ok`时其行为类似于`unwrap`:它返回`Ok`内部封装的值。与`unwrap`不同的是,当`Result`是`Err`时,它调用一个**闭包**(*closure*),也就是一个我们定义的作为参数传递给`unwrap_or_else`的匿名函数。第十三章会更详细的介绍闭包;这里需要理解的重要部分是`unwrap_or_else`会将`Err`的内部值传递给闭包中位于两道竖线间的参数`err`。使用`unwrap_or_else`允许我们进行一些自定义的非`panic!`的错误处理。 上述的错误处理其实只有两行:我们打印出了错误,接着调用了`std::process::exit`。这个函数立刻停止程序的执行并将传递给它的数组作为返回码。依照惯例,零代表成功而任何其他数字表示失败。就结果来说这依然类似于列表 12-7 中的基于`panic!`的错误处理,但是不再会有额外的输出了,让我们试一试: -```text +``` $ cargo run Compiling greprs v0.1.0 (file:///projects/greprs) Finished debug [unoptimized + debuginfo] target(s) in 0.48 secs @@ -430,22 +269,10 @@ Problem parsing arguments: not enough arguments 现在重构完了参数解析部分,让我们再改进一下程序的逻辑。列表 12-10 中展示了在`main`函数中调用提取出函数`run`之后的代码。`run`函数包含之前位于`main`中的部分代码: -
          Filename: src/main.rs -```rust -# use std::env; -# use std::fs::File; -# use std::io::prelude::*; -# use std::process; -# +```rust,ignore fn main() { -# let args: Vec = env::args().collect(); -# -# let config = Config::new(&args).unwrap_or_else(|err| { -# println!("Problem parsing arguments: {}", err); -# process::exit(1); -# }); // ...snip... println!("Searching for {}", config.search); @@ -464,65 +291,21 @@ fn run(config: Config) { } // ...snip... -# -# struct Config { -# search: String, -# filename: String, -# } -# -# impl Config { -# fn new(args: &[String]) -> Result { -# if args.len() < 3 { -# return Err("not enough arguments"); -# } -# -# let search = args[1].clone(); -# let filename = args[2].clone(); -# -# Ok(Config { -# search: search, -# filename: filename, -# }) -# } -# } ``` -
          - -Listing 12-10: Extracting a `run` functionality for the rest of the program logic - -
          -
          +Listing 12-10: Extracting a `run` functionality for the +rest of the program logic `run`函数的内容是之前位于`main`中的几行,而且`run`函数获取一个`Config`作为参数。现在有了一个单独的函数了,我们就可以像列表 12-8 中的`Config::new`那样进行类似的改进了。列表 12-11 展示了另一个`use`语句将`std::error::Error`结构引入了作用域,还有使`run`函数返回`Result`的修改: -
          Filename: src/main.rs -```rust +```rust,ignore use std::error::Error; -# use std::env; -# use std::fs::File; -# use std::io::prelude::*; -# use std::process; // ...snip... -# fn main() { -# let args: Vec = env::args().collect(); -# -# let config = Config::new(&args).unwrap_or_else(|err| { -# println!("Problem parsing arguments: {}", err); -# process::exit(1); -# }); -# -# println!("Searching for {}", config.search); -# println!("In file {}", config.filename); -# -# run(config); -# -# } fn run(config: Config) -> Result<(), Box> { let mut f = File::open(config.filename)?; @@ -534,39 +317,14 @@ fn run(config: Config) -> Result<(), Box> { Ok(()) } -# -# struct Config { -# search: String, -# filename: String, -# } -# -# impl Config { -# fn new(args: &[String]) -> Result { -# if args.len() < 3 { -# return Err("not enough arguments"); -# } -# -# let search = args[1].clone(); -# let filename = args[2].clone(); -# -# Ok(Config { -# search: search, -# filename: filename, -# }) -# } -# } ``` -
          - -Listing 12-11: Changing the `run` function to return `Result` - -
          -
          +Listing 12-11: Changing the `run` function to return +`Result` -这里有三个大的修改。第一个是现在`run`函数的返回值是`Result<(), Box>`类型的。之前,函数返回 unit 类型`()`,现在它仍然是`Ok`时的返回值。对于错误类型,我们将使用`Box`。这是一个**trait 对象**(*trait object*),第XX章会讲到。现在可以这样理解它:`Box`意味着函数返回了某个实现了`Error` trait 的类型,不过并没有指定具体的返回值类型。这样就比较灵活,因为在不同的错误场景可能有不同类型的错误返回值。`Box`是一个堆数据的智能指针,第YY章将会详细介绍`Box`。 +这里有三个大的修改。第一个是现在`run`函数的返回值是`Result<(), Box>`类型的。之前,函数返回 unit 类型`()`,现在它仍然是`Ok`时的返回值。对于错误类型,我们将使用`Box`。这是一个**trait 对象**(*trait object*),第XX章会讲到。现在可以这样理解它:`Box`意味着函数返回了某个实现了`Error` trait 的类型,不过并没有指定具体的返回值类型。这样就比较灵活,因为在不同的错误场景可能有不同类型的错误返回值。`Box`是一个堆数据的智能指针,第十五章将会详细介绍`Box`。 第二个改变是我们去掉了`expect`调用并替换为第9章讲到的`?`。不同于遇到错误就`panic!`,这会从函数中返回错误值并让调用者来处理它。 @@ -627,10 +385,9 @@ fn run(config: Config) -> Result<(), Box> { 现在项目看起来好多了!还有一件我们尚未开始的工作:拆分 *src/main.rs* 并将一些代码放入 *src/lib.rs* 中。让我们现在就开始吧:将 *src/main.rs* 中的`run`函数移动到新建的 *src/lib.rs* 中。还需要移动相关的`use`语句和`Config`的定义,以及其`new`方法。现在 *src/lib.rs* 应该如列表 12-12 所示: -
          Filename: src/lib.rs -```rust +```rust,ignore use std::error::Error; use std::fs::File; use std::io::prelude::*; @@ -668,20 +425,14 @@ pub fn run(config: Config) -> Result<(), Box>{ } ``` -
          - -Listing 12-12: Moving `Config` and `run` into *src/lib.rs* - -
          -
          +Listing 12-12: Moving `Config` and `run` into +*src/lib.rs* - 注意我们还需要使用公有的`pub`:在`Config`和其字段、它的`new`方法和`run`函数上。 现在在 *src/main.rs* 中,我们需要通过`extern crate greprs`来引入现在位于 *src/lib.rs* 的代码。接着需要增加一行`use greprs::Config`来引入`Config`到作用域,并对`run`函数加上 crate 名称前缀,如列表 12-13 所示: -
          Filename: src/main.rs ```rust,ignore @@ -711,12 +462,10 @@ fn main() { } ``` -
          +Listing 12-13: Bringing the `greprs` crate into the scope +of *src/main.rs* -Listing 12-13: Bringing the `greprs` crate into the scope of *src/main.rs* - -
          -
          + diff --git a/src/ch12-04-testing-the-librarys-functionality.md b/src/ch12-04-testing-the-librarys-functionality.md index 03d9f58..0132c74 100644 --- a/src/ch12-04-testing-the-librarys-functionality.md +++ b/src/ch12-04-testing-the-librarys-functionality.md @@ -2,13 +2,12 @@ > [ch12-04-testing-the-librarys-functionality.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-04-testing-the-librarys-functionality.md) >
          -> commit 4f2dc564851dc04b271a2260c834643dfd86c724 +> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56 现在为项目的核心功能编写测试将更加容易,因为我们将逻辑提取到了 *src/lib.rs* 中并将参数解析和错误处理都留在了 *src/main.rs* 里。现在我们可以直接使用多种参数调用代码并检查返回值而不用从命令行运行二进制文件了。 我们将要编写的是一个叫做`grep`的函数,它获取要搜索的项以及文本并产生一个搜索结果列表。让我们从`run`中去掉那行`println!`(也去掉 *src/main.rs* 中的,因为再也不需要他们了),并使用之前收集的选项来调用新的`grep`函数。眼下我们只增加一个空的实现,和指定`grep`期望行为的测试。当然,这个测试对于空的实现来说是会失败的,不过可以确保代码是可以编译的并得到期望的错误信息。列表 12-14 展示了这些修改: -
          Filename: src/lib.rs ```rust @@ -58,13 +57,8 @@ Pick three."; } ``` -
          - -Listing 12-14: Creating a function where our logic will go and a failing test -for that function - -
          -
          +Listing 12-14: Creating a function where our logic will +go and a failing test for that function @@ -153,7 +147,6 @@ fn grep<'a>(search: &str, contents: &'a str) -> Vec<&'a str> { 最终,我们需要一个方法来存储包含要搜索字符串的行。为此可以在`for`循环之前创建一个可变的 vector 并调用`push`方法来存放一个`line`。在`for`循环之后,返回这个 vector。列表 12-15 中为完整的实现: -
          Filename: src/lib.rs ```rust @@ -170,18 +163,13 @@ fn grep<'a>(search: &str, contents: &'a str) -> Vec<&'a str> { } ``` -
          - -Listing 12-15: Fully functioning implementation of the `grep` function - -
          -
          +Listing 12-15: Fully functioning implementation of the +`grep` function 尝试运行一下: - ``` $ cargo test running 1 test diff --git a/src/ch12-05-working-with-environment-variables.md b/src/ch12-05-working-with-environment-variables.md index 4969197..5e3a84d 100644 --- a/src/ch12-05-working-with-environment-variables.md +++ b/src/ch12-05-working-with-environment-variables.md @@ -2,7 +2,7 @@ > [ch12-05-working-with-environment-variables.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-05-working-with-environment-variables.md) >
          -> commit 4f2dc564851dc04b271a2260c834643dfd86c724 +> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56 让我们再增加一个功能:大小写不敏感搜索。另外,这个设定将不是一个命令行参数:相反它将是一个环境变量。当然可以选择创建一个大小写不敏感的命令行参数,不过用户要求提供一个环境变量这样设置一次之后在整个终端会话中所有的搜索都将是大小写不敏感的了。 @@ -51,7 +51,6 @@ Trust me."; 我们将定义一个叫做`grep_case_insensitive`的新函数。它的实现与`grep`函数大体上相似,不过列表 12-16 展示了一些小的区别: -
          Filename: src/lib.rs ```rust @@ -69,13 +68,9 @@ 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-16: Implementing a `grep_case_insensitive` +function by changing the search string and the lines of the contents to +lowercase before comparing them 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 b61709d..c8651cb 100644 --- a/src/ch12-06-writing-to-stderr-instead-of-stdout.md +++ b/src/ch12-06-writing-to-stderr-instead-of-stdout.md @@ -2,7 +2,7 @@ > [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 4f2dc564851dc04b271a2260c834643dfd86c724 +> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56 目前为止,我们将所有的输出都`println!`到了终端。这是可以的,不过大部分终端都提供了两种输出:“标准输出”对应大部分信息,而“标准错误”则用于错误信息。这使得处理类似于“将错误打印到终端而将其他信息输出到文件”的情况变得更容易。 @@ -20,7 +20,6 @@ Problem parsing arguments: not enough arguments 我们希望这个信息被打印到屏幕上,而只有成功运行产生的输出写入到文件中。让我们如列表 12-17 中所示改变如何打印错误信息的方法: -
          Filename: src/main.rs ```rust,ignore @@ -59,12 +58,8 @@ fn main() { } ``` -
          - -Listing 12-17: Writing error messages to `stderr` instead of `stdout` - -
          -
          +Listing 12-17: Writing error messages to `stderr` instead +of `stdout` @@ -96,4 +91,4 @@ How dreary to be somebody! 在这一章,我们涉及了如果在 Rust 中进行常规的 I/O 操作。通过使用命令行参数、文件、环境变量和写入`stderr`的功能。现在你已经准备好编写命令行程序了。结合前几章的知识,你的代码将会是组织良好的,并能有效的将数据存储到合适的数据结构中、更好的处理错误,并且还是经过良好测试的。我们也接触了一个真实情况下需要生命周期注解来保证引用一直有效的场景。 -接下来,让我们探索如何利用一些 Rust 中受函数式编程语言影响的功能”闭包和迭代器。 \ No newline at end of file +接下来,让我们探索如何利用一些 Rust 中受函数式编程语言影响的功能:闭包和迭代器。 \ No newline at end of file diff --git a/src/ch14-02-publishing-to-crates-io.md b/src/ch14-02-publishing-to-crates-io.md index f1e4b3e..17584cc 100644 --- a/src/ch14-02-publishing-to-crates-io.md +++ b/src/ch14-02-publishing-to-crates-io.md @@ -2,7 +2,7 @@ > [ch14-02-publishing-to-crates-io.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch14-02-publishing-to-crates-io.md) >
          -> commit c49e5ee8859f8eb8f8867cbeafbdf5b802aa5894 +> commit f2eef19b3a39ee68dd363db2fcba173491ba9dc4 我们曾经在项目中增加 crates.io 上的 crate 作为依赖。也可以选择将代码分享给其他人。Crates.io 用来分发包的源代码,所以它主要用于分发开源代码。 @@ -24,10 +24,7 @@ Rust 和 Cargo 有一些帮助人们找到和使用你发布的包的功能。 /// ``` /// let five = 5; /// -/// assert_eq!(6, add_one(5)); -/// # fn add_one(x: i32) -> i32 { -/// # x + 1 -/// # } +/// assert_eq!(6, add_one(five)); /// ``` pub fn add_one(x: i32) -> i32 { x + 1 @@ -65,7 +62,7 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured 第七章介绍了如何使用`mod`关键字来将代码组织进模块中,如何使用`pub`关键字将项变为公有,和如何使用`use`关键字将项引入作用域。当发布 crate 给并不熟悉其使用的库的实现的人时,就值得花时间考虑 crate 的结构对于开发和对于依赖 crate 的人来说是否同样有用。如果结构对于供其他库使用来说并不方便,也无需重新安排内部组织:可以选择使用`pub use`来重新导出一个不同的公有结构。 -例如列表 14-2中,我们创建了一个库`art`,其包含一个`kinds`模块,模块中包含枚举`Color`和包含函数`mix`的模块`utils`: +例如列表 14-2 中,我们创建了一个库`art`,其包含一个`kinds`模块,模块中包含枚举`Color`和包含函数`mix`的模块`utils`: Filename: src/lib.rs diff --git a/src/ch15-02-deref.md b/src/ch15-02-deref.md index 09d7fc4..f1fdd90 100644 --- a/src/ch15-02-deref.md +++ b/src/ch15-02-deref.md @@ -2,9 +2,9 @@ > [ch15-02-deref.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch15-02-deref.md) >
          -> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56 +> commit ecc3adfe0cfa0a4a15a178dc002702fd0ea74b3f -第一个智能指针相关的重要 trait 是`Deref`,它允许我们重载`*`,解引用运算符(不同于乘法运算符和全局引用运算符)。重载智能指针的`*`方便访问其后的数据,在这个部分的稍后介绍解引用强制多态时我们会讨论方便的意义。 +第一个智能指针相关的重要 trait 是`Deref`,它允许我们重载`*`,解引用运算符(不同于乘法运算符和全局引用运算符)。重载智能指针的`*`能使访问其后的数据更为方便,在这个部分的稍后介绍解引用强制多态时我们会讨论方便的意义。 第八章的哈希 map 的“根据旧值更新一个值”部分简要的提到了解引用运算符。当时有一个可变引用,而我们希望改变这个引用所指向的值。为此,首先我们必须解引用。这是另一个使用`i32`值引用的例子: @@ -63,21 +63,21 @@ struct that holds mp3 file data and metadata 在`assert_eq!`中,我们验证`vec![1, 2, 3]`是否为`Mp3`实例`*my_favorite_song`解引用的值,结果正是如此因为我们实现了`deref`方法来返回音频数据。如果没有为`Mp3`实现`Deref` trait,Rust 将不会编译`*my_favorite_song`:会出现错误说`Mp3`类型不能被解引用。 -代码能够工作的原因在于调用`*my_favorite_song`时`*`在背后所做的操作: +没有`Deref` trait 的话,编译器只能解引用`&`引用,而`my_favorite_song`并不是(它是一个`Mp3`结构体)。通过`Deref` trait,编译器知道实现了`Deref` trait 的类型有一个返回引用的`deref`方法(在这个例子中,是`&self.audio`因为列表 15-7 中的`deref`的定义)。所以为了得到一个`*`可以解引用的`&`引用,编译器将`*my_favorite_song`展开为如下: ```rust,ignore *(my_favorite_song.deref()) ``` -这对`my_favorite_song`调用了`deref`方法,它借用了`my_favorite_song`并返回指向`my_favorite_song.audio`的引用,这正是列表 15-5 中`deref`所定义的。引用的`*`被定义为仅仅从引用中返回其数据,所以上面`*`的展开形式对于外部`*`来说并不是递归的。最终的数据类型是`Vec`,它与列表 15-5 中`assert_eq!`的`vec![1, 2, 3]`相匹配。 +这个就是`self.audio`中的结果值。`deref`返回一个引用并接下来必需解引用而不是直接返回值的原因是所有权:如果`deref`方法直接返回值而不是引用,其值将被移动出`self`。这里和大部分使用解引用运算符的地方并不想获取`my_favorite_song.audio`的所有权。 -`deref`方法的返回值类型仍然是引用和为何必须解引用方法的结果的原因是如果`deref`方法就返回值,使用`*`总是会获取其所有权。 +注意将`*`替换为`deref`调用和`*`调用的过程在每次使用`*`的时候都会发生一次。`*`的替换并不会无限递归进行。最终的数据类型是`Vec`,它与列表 15-7 中`assert_eq!`的`vec![1, 2, 3]`相匹配。 ### 函数和方法的隐式解引用强制多态 -Rust 倾向于偏爱明确而不是隐晦,不过一个情况下这并不成立,就是函数和方法的参数的**解引用强制多态**(*deref coercions*)。解引用强制多态会自动的将指针或智能指针的引用转换为指针内容的引用。解引用强制多态发生于一个值被传递给函数或方法,并只发生于需要将传递的值类型与签名中参数类型相匹配的时候。解引用强制多态的加入使得 Rust 调用函数或方法时无需很多使用`&`和`*`的引用和解引用。 +Rust 倾向于偏爱明确而不是隐晦,不过一个情况下这并不成立,就是函数和方法的参数的**解引用强制多态**(*deref coercions*)。解引用强制多态会自动的将指针或智能指针的引用转换为指针内容的引用。解引用强制多态发生于当传递给函数的参数类型不同于函数签名中定义参数类型的时候。解引用强制多态的加入使得 Rust 调用函数或方法时无需很多使用`&`和`*`的引用和解引用。 -使用列表 15-5 中的`Mp3`结构体,如下是一个获取`u8` slice 并压缩 mp3 音频数据的函数签名: +使用列表 15-7 中的`Mp3`结构体,如下是一个获取`u8` slice 并压缩 mp3 音频数据的函数签名: ```rust,ignore fn compress_mp3(audio: &[u8]) -> Vec { @@ -99,9 +99,9 @@ compress_mp3(my_favorite_song.audio.as_slice()) let result = compress_mp3(&my_favorite_song); ``` -只有`&`和实例,好的!我们可以把智能指针当成普通的引用。也就是说解引用强制多态意味着 Rust 利用了`Deref`实现的优势:Rust 知道`Mp3`实现了`Deref` trait 并从`deref`方法返回`&Vec`。它也知道标准库实现了`Vec`的`Deref` trait,其`deref`方法返回`&[T]`(我们也可以通过查阅`Vec`的 API 文档来发现这一点)。所以,在编译时,Rust 会发现它可以调用两次`Deref::deref`来将`&Mp3`变成`&Vec`再变成`&[T]`来满足`compress_mp3`的签名。这意味着我们可以少写一些代码!Rust 会多次分析`Deref::deref`的返回值类型直到它满足参数的类型,只要相关类型实现了`Deref` trait。这些间接转换在编译时进行,所以利用解引用强制多态并没有运行时惩罚。 +只有`&`和实例,好的!我们可以把智能指针当成普通的引用。也就是说解引用强制多态意味着 Rust 利用了`Deref`实现的优势:Rust 知道`Mp3`实现了`Deref` trait 并从`deref`方法返回`&Vec`。它也知道标准库实现了`Vec`的`Deref` trait,其`deref`方法返回`&[T]`(我们也可以通过查阅`Vec`的 API 文档来发现这一点)。所以,在编译时,Rust 会发现它可以调用两次`Deref::deref`来将`&Mp3`变成`&Vec`再变成`&[T]`来满足`compress_mp3`的签名。这意味着我们可以少写一些代码!Rust 会多次分析`Deref::deref`的返回值类型直到它满足参数的类型,只要相关类型实现了`Deref` trait。这些间接转换在编译时进行,所以利用解引用强制多态并没有运行时惩罚! -这里还有一个重载了`&mut T`的`*`的`DerefMut` trait,它以与`Deref`重载`&T`的`*`相同的方式用于参数中。 +类似于如何使用`Deref` trait 重载`&T`的`*`运算符,`DerefMut` trait用于重载`&mut T`的`*`运算符。 Rust 在发现类型和 trait 实现满足三种情况时会进行解引用强制多态: