From 41c62e1b94abb5fa2ceea8484cb4ad9bdaa2a28b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BA=84=E7=A7=8B=E5=BD=AC?= Date: Sat, 27 Oct 2018 23:58:41 +0800 Subject: [PATCH 01/49] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=B8=80=E4=B8=AA?= =?UTF-8?q?=E5=8F=A5=E5=AD=90=E7=9A=84=E7=BF=BB=E8=AF=91=EF=BC=8C=E5=8F=8A?= =?UTF-8?q?=E9=94=99=E5=88=AB=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原本的翻译读起来感觉不通顺 --- src/ch06-02-match.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ch06-02-match.md b/src/ch06-02-match.md index a950e5c..47671ea 100644 --- a/src/ch06-02-match.md +++ b/src/ch06-02-match.md @@ -120,7 +120,7 @@ fn value_in_cents(coin: Coin) -> u32 { ### 匹配 `Option` -在之前的部分在使用 `Option` 时我们想要从 `Some` 中取出其内部的 `T` 值;也可以像处理 `Coin` 枚举那样使用 `match` 处理 `Option`!与其直接比较硬币,我们将比较 `Option` 的成员,不过 `match` 表达式的工作方式保持不变。 +我们在之前的部分中使用 `Option` 时,是为了从 `Some` 中取出其内部的 `T` 值;我们还可以像处理 `Coin` 枚举那样使用 `match` 处理 `Option`!与其直接比较硬币,我们将比较 `Option` 的成员,不过 `match` 表达式的工作方式保持不变。 比如我们想要编写一个函数,它获取一个 `Option` 并且如果其中有一个值,将其加一。如果其中没有值,函数应该返回 `None` 值并不尝试执行任何操作。 @@ -143,7 +143,7 @@ let none = plus_one(None); #### 匹配 `Some(T)` -让我们更仔细的检查 `plus_one` 的第一行操作。当调用 `plus_one(five)` 时,`plus_one` 函数体中的 `x` 将会是值 `Some(5)`。接着将其与每个分支比较。 +让我们更仔细地检查 `plus_one` 的第一行操作。当调用 `plus_one(five)` 时,`plus_one` 函数体中的 `x` 将会是值 `Some(5)`。接着将其与每个分支比较。 ```rust,ignore None => None, @@ -191,7 +191,7 @@ error[E0004]: non-exhaustive patterns: `None` not covered | ^ pattern `None` not covered ``` -Rust 知道我们没有覆盖所有可能的情况甚至知道那些模式被忘记了!Rust 中的匹配是 **穷尽的**(*exhaustive*):必须穷举到最后的可能性来使代码有效。特别的在这个 `Option` 的例子中,Rust 防止我们忘记明确的处理 `None` 的情况,这使我们免于假设拥有一个实际上为空的值,这造成了之前提到过的价值亿万的错误。 +Rust 知道我们没有覆盖所有可能的情况甚至知道哪些模式被忘记了!Rust 中的匹配是 **穷尽的**(*exhaustive*):必须穷举到最后的可能性来使代码有效。特别的在这个 `Option` 的例子中,Rust 防止我们忘记明确的处理 `None` 的情况,这使我们免于假设拥有一个实际上为空的值,这造成了之前提到过的价值亿万的错误。 ### `_` 通配符 From da4eecf8eea991535b9112e476b64d6b63f74a50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BA=84=E7=A7=8B=E5=BD=AC?= Date: Sun, 28 Oct 2018 17:52:31 +0800 Subject: [PATCH 02/49] Update ch07-02-controlling-visibility-with-pub.md --- src/ch07-02-controlling-visibility-with-pub.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ch07-02-controlling-visibility-with-pub.md b/src/ch07-02-controlling-visibility-with-pub.md index 19b8d39..af0f343 100644 --- a/src/ch07-02-controlling-visibility-with-pub.md +++ b/src/ch07-02-controlling-visibility-with-pub.md @@ -87,7 +87,7 @@ error[E0603]: function `connect` is private | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ``` -非常好!另一个不同的错误!好的,不同的错误信息也是值得庆祝的(可能是程序员被黑的最惨的一次)。新错误表明“函数 `connect` 是私有的”,那么让我们修改 *src/client.rs* 将 `client::connect` 也设为公有: +非常好!另一个不同的错误!好的,不同的错误信息也是值得庆祝的。新错误表明“函数 `connect` 是私有的”,那么让我们修改 *src/client.rs* 将 `client::connect` 也设为公有: 文件名: src/client.rs @@ -118,7 +118,7 @@ warning: function is never used: `connect` 编译通过了,关于 `client::connect` 未被使用的警告消失了! -未被使用的代码并不总是意味着它们需要被设为公有的:如果你 **不** 希望这些函数成为公有 API 的一部分,未被使用的代码警告可能是在提醒你这些代码不再需要并可以安全的删除它们。这也可能是警告你出 bug 了,如果你刚刚不小心删除了库中所有这个函数的调用。 +未被使用的代码并不总是意味着它们需要被设为公有的:如果你 **不** 希望这些函数成为公有 API 的一部分,未被使用的代码警告可能是在提醒你这些代码不再需要并可以安全地删除它们。这也可能是警告你出 bug 了,如果你刚刚不小心删除了库中所有这个函数的调用。 当然我们的情况是,**确实** 希望另外两个函数也作为 crate 公有 API 的一部分,所以让我们也将其标记为 `pub` 并去掉剩余的警告。修改 *src/network/mod.rs* 为: @@ -151,7 +151,7 @@ warning: function is never used: `connect` | |_^ ``` -虽然将 `network::connect` 设为 `pub` 了我们仍然得到了一个未被使用函数的警告。这是因为模块中的函数是公有的,不过函数所在的 `network` 模块却不是公有的。这回我们是自内向外修改库文件的,而 `client::connect` 的时候是自外向内修改的。我们需要修改 *src/lib.rs* 让 `network` 也是公有的,如下: +虽然将 `network::connect` 设为 `pub` 了,我们仍然得到了一个未被使用函数的警告。这是因为模块中的函数是公有的,不过函数所在的 `network` 模块却不是公有的。这回我们是自内向外修改库文件的,而 `client::connect` 的时候是自外向内修改的。我们需要修改 *src/lib.rs* 让 `network` 也是公有的,如下: 文件名: src/lib.rs @@ -178,7 +178,7 @@ warning: function is never used: `connect` ### 私有性规则 -总的来说,有如下项的可见性规则: +总的来说,有如下可见性规则: 1. 如果一个项是公有的,它能被任何父模块访问 2. 如果一个项是私有的,它能被其直接父模块及其任何子模块访问 From 62904131d97b1400a7ec35dea3dc9598c45d4e1f Mon Sep 17 00:00:00 2001 From: Gary Wang Date: Sun, 4 Nov 2018 17:47:34 +0800 Subject: [PATCH 03/49] update ch19-05-advanced-functions-and-closures.md --- ...ch19-05-advanced-functions-and-closures.md | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/ch19-05-advanced-functions-and-closures.md b/src/ch19-05-advanced-functions-and-closures.md index 5748383..74c4fdc 100644 --- a/src/ch19-05-advanced-functions-and-closures.md +++ b/src/ch19-05-advanced-functions-and-closures.md @@ -2,18 +2,13 @@ > [ch19-05-advanced-functions-and-closures.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch19-05-advanced-functions-and-closures.md) >
-> commit 9d5b9a573daf5fa0c98b3a3005badcea4a0a5211 +> commit 509cb42ece610bdac8eaad26d57fb604dc078623 -最后让我们讨论一些有关函数和闭包的高级功能:函数指针、发散函数和返回值闭包。 +最后我们将探索一些有关函数和闭包的高级功能:函数指针以及返回值闭包。 ### 函数指针 - - - -我们讨论过了如何向函数传递闭包;也可以向函数传递常规函数!这在我们希望传递已经定义的函数而不是重新定义闭包作为参数是很有用。通过函数指针允许我们使用函数作为另一个函数的参数。函数的类型是 `fn`,使用小写的 “f” 以便不与 `Fn` 闭包 trait 向混淆。`fn` 被称为**函数指针**(*function pointer*)。指定参数为函数指针的语法类似于闭包,如示例 19-34 所示: +我们讨论过了如何向函数传递闭包;也可以向函数传递常规函数!这在我们希望传递已经定义的函数而不是重新定义闭包作为参数是很有用。通过函数指针允许我们使用函数作为另一个函数的参数。函数的类型是 `fn` (使用小写的 “f” )以免与 `Fn` 闭包 trait 相混淆。`fn` 被称为**函数指针**(*function pointer*)。指定参数为函数指针的语法类似于闭包,如示例 19-34 所示: 文件名: src/main.rs @@ -43,7 +38,7 @@ fn main() { 一个只期望接受 `fn` 而不接受闭包的情况的例子是与不存在闭包的外部代码交互时:C 语言的函数可以接受函数作为参数,但没有闭包。 -作为一个既可以使用内联定义的闭包又可以使用命名函数的例子,让我们看看一个 `map` 的应用。使用 `map` 函数将一个数字 vector 转换为一个字符串 vector,就可以使用闭包: +作为一个既可以使用内联定义的闭包又可以使用命名函数的例子,让我们看看一个 `map` 的应用。使用 `map` 函数将一个数字 vector 转换为一个字符串 vector,就可以使用闭包,比如这样: ```rust let list_of_numbers = vec![1, 2, 3]; @@ -53,7 +48,7 @@ let list_of_strings: Vec = list_of_numbers .collect(); ``` -或者可以将函数作为 `map` 的参数来代替闭包: +或者可以将函数作为 `map` 的参数来代替闭包,像是这样: ```rust let list_of_numbers = vec![1, 2, 3]; @@ -65,11 +60,11 @@ let list_of_strings: Vec = list_of_numbers 注意这里必须使用 “高级 trait” 部分讲到的完全限定语法,因为存在多个叫做 `to_string` 的函数;这里使用了定义于 `ToString` trait 的 `to_string` 函数,标准库为所有实现了 `Display` 的类型实现了这个 trait。 -一些人倾向于函数风格,一些人喜欢闭包。他们最终都会产生同样的代码,所以请使用对你来说更明白的吧。 +一些人倾向于函数风格,一些人喜欢闭包。这两种形式最终都会产生同样的代码,所以请使用对你来说更明白的形式吧。 ### 返回闭包 -闭包表现为 trait,这意味着不能直接返回闭包。对于大部分需要返回 trait 的情况,可以使用是实现了期望返回的 trait 的具体类型替代函数的返回值。但是这不能用于闭包,因为他们没有一个可返回的具体类型;例如不允许使用函数指针 `fn` 作为返回值类型。 +闭包表现为 trait,这意味着不能直接返回闭包。对于大部分需要返回 trait 的情况,可以使用实现了期望返回的 trait 的具体类型来替代函数的返回值。但是这不能用于闭包,因为他们没有一个可返回的具体类型;例如不允许使用函数指针 `fn` 作为返回值类型。 这段代码尝试直接返回闭包,它并不能编译: @@ -95,7 +90,7 @@ std::marker::Sized` is not satisfied = note: the return type of a function must have a statically known size ``` -错误有一次指向了 `Sized` trait!Rust 并不知道需要多少空间来储存闭包。不过我们在上一部分见过这种情况的解决办法:可以使用 trait 对象: +错误又一次指向了 `Sized` trait!Rust 并不知道需要多少空间来储存闭包。不过我们在上一部分见过这种情况的解决办法:可以使用 trait 对象: ```rust fn returns_closure() -> Box i32> { @@ -103,10 +98,10 @@ fn returns_closure() -> Box i32> { } ``` -这段代码正好可以编译。关于 trait 对象的更多内容,请回顾第十七章的 “trait 对象” 部分。 +这段代码正好可以编译。关于 trait 对象的更多内容,请回顾第十七章的 “为使用不同类型的值而设计的 trait 对象” 部分。 ## 总结 好的!现在我们学习了 Rust 并不常用但在特定情况下你可能用得着的功能。我们介绍了很多复杂的主题,这样若你在错误信息提示或阅读他人代码时遇到他们,至少可以说之前已经见过这些概念和语法了。你可以使用本章作为一个解决方案的参考。 -现在,让我们再开始一个项目,将本书所学的所有内容付与实践! \ No newline at end of file +接下来,我们将再开始一个项目,将本书所学的所有内容付与实践! \ No newline at end of file From 4c17810909bca60bd1002043a36a4e44a80f0740 Mon Sep 17 00:00:00 2001 From: KaiserY Date: Wed, 7 Nov 2018 10:24:53 +0800 Subject: [PATCH 04/49] Update ch12-05-working-with-environment-variables.md --- src/ch12-05-working-with-environment-variables.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ch12-05-working-with-environment-variables.md b/src/ch12-05-working-with-environment-variables.md index a61ef43..26283e9 100644 --- a/src/ch12-05-working-with-environment-variables.md +++ b/src/ch12-05-working-with-environment-variables.md @@ -215,7 +215,7 @@ To an admiring bog! 如果你使用 PowerShell,则需要用两句命令而不是一句来设置环境变量并运行程序: ```text -$ $env.CASE_INSENSITIVE=1 +$ $env:CASE_INSENSITIVE=1 $ cargo run to poem.txt ``` @@ -223,4 +223,4 @@ $ cargo run to poem.txt 一些程序允许对相同配置同时使用参数 **和** 环境变量。在这种情况下,程序来决定参数和环境变量的优先级。作为一个留给你的测试,尝试通过一个命令行参数或一个环境变量来控制大小写不敏感搜索。并在运行程序时遇到矛盾值时决定命令行参数和环境变量的优先级。 -`std::env` 模块还包含了更多处理环境变量的实用功能;请查看官方文档来了解其可用的功能。 \ No newline at end of file +`std::env` 模块还包含了更多处理环境变量的实用功能;请查看官方文档来了解其可用的功能。 From c6c63aefe9000550abb582fd4106304dfedfa913 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=92=E8=97=A4=E6=9C=A8=E5=AD=90?= <36725224+senseiod@users.noreply.github.com> Date: Tue, 20 Nov 2018 09:38:14 +0800 Subject: [PATCH 05/49] Update ch07-02-controlling-visibility-with-pub.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将“错误“改为”警告“ --- src/ch07-02-controlling-visibility-with-pub.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ch07-02-controlling-visibility-with-pub.md b/src/ch07-02-controlling-visibility-with-pub.md index af0f343..c5f7a0d 100644 --- a/src/ch07-02-controlling-visibility-with-pub.md +++ b/src/ch07-02-controlling-visibility-with-pub.md @@ -31,7 +31,7 @@ warning: function is never used: `connect` | |_^ ``` -那么为什么会出现这些错误信息呢?毕竟我们构建的是一个库,它的函数的目的是被 **用户** 使用,而不一定要被项目自身使用,所以不应该担心这些 `connect` 函数是未使用的。创建它们的意义就在于被另一个项目而不是被我们自己使用。 +那么为什么会出现这些警告信息呢?毕竟我们构建的是一个库,它的函数的目的是被 **用户** 使用,而不一定要被项目自身使用,所以不应该担心这些 `connect` 函数是未使用的。创建它们的意义就在于被另一个项目而不是被我们自己使用。 为了理解为什么这个程序出现了这些警告,尝试在另一个项目中使用这个 `connect` 库,从外部调用它们。为此,通过创建一个包含这些代码的 *src/main.rs* 文件,在与库 crate 相同的目录创建一个二进制 crate: From 2dc79727dd9713199fb14c182b30a74232d0ce1c Mon Sep 17 00:00:00 2001 From: KaiserY Date: Fri, 23 Nov 2018 22:49:35 +0800 Subject: [PATCH 06/49] Update ch19-01-unsafe-rust.md --- src/ch19-01-unsafe-rust.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ch19-01-unsafe-rust.md b/src/ch19-01-unsafe-rust.md index da2d6d2..b4f61f8 100644 --- a/src/ch19-01-unsafe-rust.md +++ b/src/ch19-01-unsafe-rust.md @@ -238,9 +238,7 @@ let slice = unsafe { -示例 19-8 展示了如何集成 C 标准库中的 `abs` 函数。`extern` 块中声明的函数在 Rust 代码中总是不安全的,因为其他语言不会强制执行 Rust 的规则且 Rust 无法检查它们,所以确保其安全是程序员的责任: - -有时, 你的Rust代码需要与其它语言交互. Rust有一个`extern`关键字可以实现这个功能, 这有助于创建并使用*外部功能接口(Foreign Function Interface)* (FFI). 例19-8演示了如何与定义在一个非Rust语言编写的外部库中的`some_function`进行交互. 在Rust中调用`extern`声明的代码块永远都是不安全的: +示例 19-8 展示了如何集成 C 标准库中的 `abs` 函数。`extern` 块中声明的函数在 Rust 代码中总是不安全的。因为其他语言不会强制执行 Rust 的规则且 Rust 无法检查它们,所以确保其安全是程序员的责任: 文件名: src/main.rs From 4e394fe608ddd6ee7ccf2a4a0a33cd916e7fdfab Mon Sep 17 00:00:00 2001 From: CAi <8044101+AlfnXd@users.noreply.github.com> Date: Sun, 25 Nov 2018 23:05:05 +0800 Subject: [PATCH 07/49] Update ch15-02-deref.md --- src/ch15-02-deref.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ch15-02-deref.md b/src/ch15-02-deref.md index 286ff09..27aecf0 100644 --- a/src/ch15-02-deref.md +++ b/src/ch15-02-deref.md @@ -79,7 +79,7 @@ not satisfied ### 像引用一样使用 `Box` -可以重写示例 15-8 中的代码来使用 `Box` 而不是引用,同时借引用运算符也一样能工作,如示例 15-9 所示: +可以重写示例 15-8 中的代码,使用 `Box` 来代替引用,解引用运算符也一样能工作,如示例 15-9 所示: 文件名: src/main.rs @@ -177,7 +177,7 @@ this is what you have to specify in order to implement it. /Carol --> `deref` 方法体中写入了 `&self.0`,这样 `deref` 返回了我希望通过 `*` 运算符访问的值的引用。示例 15-11 中的 `main` 函数中对 `MyBox` 值的 `*` 调用现在可以编译并能通过断言了! -没有 `Deref` trait 的话,编译器只能解引用 `&` 引用。`Deref` trait 的 `deref` 方法为编译器提供了获取任何实现了 `Deref` 的类型值的能力,为了获取其知道如何解引用的 `&` 引用编译器可以调用 `deref` 方法。 +没有 `Deref` trait 的话,编译器可以解引用的只有 `&` 引用类型;有了 `Deref` trait 之后,对任何实现 `Deref` trait 的类型,编译器都能(通过解引用的形式)从其获取一个值。只要调用这个类型的 `deref` 方法,编译器就可以得到一个 `&` 引用,再对 `&` 引用进行解引用对它来说就是熟悉的操作了。 当我们在示例 15-11 中输入 `*y` 时,Rust 事实上在底层运行了如下代码: @@ -189,11 +189,11 @@ this is what you have to specify in order to implement it. /Carol --> up front? --> -Rust 将 `*` 运算符替换为 `deref` 方法调用和一个普通解引用,如此我们便无需担心是否需要调用 `deref` 方法。Rust 的这个功能让我们可以编写同时处理常规引用或实现了 `Deref` 的类型的代码。 +Rust 将 `*` 运算符替换为先调用 `deref` 方法再进行直接引用的操作,如此我们便不用担心是不是还需要手动调用 `deref` 方法了。Rust 的这个特性可以让我们写出行为一致的代码,无论是面对的是常规引用还是实现了 `Deref` 的类型。 `deref` 方法返回值的引用,以及 `*(y.deref())` 括号外边的普通解引用仍为必须的原因在于所有权。如果 `deref` 方法直接返回值而不是值的引用,其值(的所有权)将被移出 `self`。在这里以及大部分使用解引用运算符的情况下我们并不希望获取 `MyBox` 内部值的所有权。 -注意将 `*` 替换为 `deref` 调用和 `*` 调用的过程在每次使用 `*` 的时候都会发生一次。`*` 的替换并不会无限递归进行。最终的数据类型是 `i32`,它与示例 15-11 中 `assert_eq!` 的 `5` 相匹配。 +注意,每次当我们在代码中使用 `*` 时, `*` 运算符都被替换成了先调用 `deref` 方法再接着使用 `*` 解引用的操作,且只会发生一次,不会对 `*` 操作符无限递归替换,解引用出上面 `i32` 类型的值就停止了,这个值与示例 15-11 中 `assert_eq!` 的 `5` 相匹配。 ### 函数和方法的隐式解引用强制多态 @@ -204,7 +204,7 @@ not, can you change this to an active tone? --> -**解引用强制多态**(*deref coercions*)是 Rust 出于方便的考虑作用于函数或方法的参数的。其将实现了 `Deref` 的类型的引用转换为 `Deref` 所能够将原始类型转换的类型的引用。解引用强制多态发生于当作为参数传递给函数或方法的特定类型的引用不同于函数或方法签名中定义参数类型的时候,这时会有一系列的 `deref` 方法调用会将提供的类型转换为参数所需的类型。 +**解引用强制多态**(*deref coercions*)是 Rust 表现在函数或方法传参上的一种便利。其将实现了 `Deref` 的类型的引用转换为原始类型通过 `Deref` 所能够转换的类型的引用。当这种特定类型的引用作为实参传递给和形参类型不同的函数或方法时,解引用强制多态将自动发生。这时会有一系列的 `deref` 方法被调用,把我们提供的类型转换成了参数所需的类型。 解引用强制多态的加入使得 Rust 程序员编写函数和方法调用时无需增加过多显式使用 `&` 和 `*` 的引用和解引用。这个功能也使得我们可以编写更多同时作用于引用或智能指针的代码。 From dcf04d8ffa181da532274eded15e87f7c422489b Mon Sep 17 00:00:00 2001 From: KaiserY Date: Wed, 28 Nov 2018 22:47:37 +0800 Subject: [PATCH 08/49] check to ch01-02 --- src/ch00-00-introduction.md | 53 +++-- src/ch01-00-getting-started.md | 6 +- src/ch01-01-installation.md | 18 +- src/ch01-02-hello-world.md | 29 ++- src/img/ferris/does_not_compile.svg | 72 ++++++ src/img/ferris/not_desired_behavior.svg | 75 ++++++ src/img/ferris/panics.svg | 70 ++++++ src/img/ferris/unsafe.svg | 291 ++++++++++++++++++++++++ src/img/trpl04-01.svg | 101 ++++---- src/img/trpl04-02.svg | 145 ++++++------ src/img/trpl04-03.svg | 196 ++++++++-------- src/img/trpl04-04.svg | 147 ++++++------ src/img/trpl04-05.svg | 129 ++++++----- src/img/trpl04-06.svg | 185 +++++++-------- src/img/trpl15-01.svg | 61 ++--- src/img/trpl15-02.svg | 27 +-- src/img/trpl15-03.svg | 121 +++++----- 17 files changed, 1149 insertions(+), 577 deletions(-) create mode 100644 src/img/ferris/does_not_compile.svg create mode 100644 src/img/ferris/not_desired_behavior.svg create mode 100644 src/img/ferris/panics.svg create mode 100644 src/img/ferris/unsafe.svg diff --git a/src/ch00-00-introduction.md b/src/ch00-00-introduction.md index 8fba530..e23ba86 100644 --- a/src/ch00-00-introduction.md +++ b/src/ch00-00-introduction.md @@ -1,8 +1,8 @@ # 介绍 -> [ch00-00-introduction.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch00-00-introduction.md) +> [ch00-00-introduction.md](https://github.com/rust-lang/book/blob/master/src/ch00-00-introduction.md) >
-> commit 7480e811ab5ad8d53a5b854d9b0c7a5a4f58499f +> commit 0aa307c7d79d2cbf83cdf5d47780b2904e9cb03f > 注意:本书的版本与出版的 [The Rust Programming Language][nsprust] > 和电子版的 [No Starch Press][nsp] 一致 @@ -10,9 +10,7 @@ [nsprust]: https://nostarch.com/rust [nsp]: https://nostarch.com/ -欢迎阅读 “Rust 程序设计语言”,一本介绍 Rust 的书。 - -Rust 程序设计语言能帮你编写出更快、更可靠的软件。在其他编程语言设计中,高层工程学和底层控制往往不能兼得;Rust 则试图挑战该现象。通过权衡强大的技术能力与优秀的开发体验,Rust 允许你控制底层细节(比如内存使用),并免受过去做此类控制所经历的烦恼。 +欢迎阅读 “Rust 程序设计语言”,一本介绍 Rust 的书。Rust 程序设计语言能帮助你编写更快、更可靠的软件。在编程语言设计中,高层工程学和底层控制往往不能兼得;Rust 则试图挑战这一矛盾。通过权衡强大的技术能力与优秀的开发体验,Rust 允许你控制底层细节(比如内存使用),并免受以往进行此类控制所经受的所有烦恼。 ## 谁会使用 Rust @@ -20,11 +18,11 @@ Rust 因多种原因适用于很多开发者。让我们讨论几个最重要的 ### 开发者团队 -Rust 被证明是可用于大型的、拥有不同层次系统编程知识的开发者团队间协作的高效工具。底层代码中容易出现大量隐晦的 bug,在其他编程语言中,只能通过大量的测试和经验丰富的开发者细心的代码评审来捕获。在 Rust 中,编译器充当了守门员的角色,它拒绝编译存在隐晦 bug 的代码,包括并发 bug。通过与编译器合作,团队将更多的时间聚焦在程序逻辑上,而不是追踪 bug。 +Rust 被证明是可用于大型的、拥有不同层次系统编程知识的开发者团队间协作的高效工具。底层代码中容易出现种种隐晦的 bug,在其他编程语言中,只能通过大量的测试和经验丰富的开发者细心的代码评审来捕获它们。在 Rust 中,编译器充当了守门员的角色,它拒绝编译存在这些难以捕获的 bug 的代码,这其中包括并发 bug。通过与编译器合作,团队将更多的时间聚焦在程序逻辑上,而不是追踪 bug。 -Rust 也将当前的开发工具带到了系统编程世界: +Rust 也为系统编程世界带来了现代化的开发工具: -* Cargo,内置的依赖管理器和构建工具,它能轻松增加、编译和管理依赖,并在 Rust 生态系统中保持一致。 +* Cargo,内置的依赖管理器和构建工具,它能轻松增加、编译和管理依赖,并使其在 Rust 生态系统中保持一致。 * Rustfmt 确保开发者遵循一致的代码风格。 * Rust Language Server 为集成开发环境(IDE)提供了强大的代码补全和内联错误信息功能。 @@ -32,7 +30,7 @@ Rust 也将当前的开发工具带到了系统编程世界: ### 学生 -Rust 适用于学生或任何对操作系统概念感兴趣的人。通过 Rust,很多人已经了解操作系统开发这样的主题。社区非常欢迎并乐于解答学生们的问题。通过类似于本书这样的努力,Rust 团队希望更多人了解操作系统的概念,特别是编程新手。 +Rust 适用于学生和任何对学习操作系统概念感兴趣的人。通过 Rust,很多人已经了解了像操作系统开发这样的主题。社区非常欢迎并乐于解答学生们的问题。通过类似于本书这样的努力,Rust 团队希望更多人了解操作系统的概念,特别是编程新手。 ### 公司 @@ -44,47 +42,58 @@ Rust 适用于希望构建 Rust 编程语言、社区、开发工具和库的开 ### 重视速度和稳定性的开发者 -Rust 适用于追求编程语言的速度与稳定性的开发者。所谓速度,我们指你用 Rust 开发出的程序运行速度,以及 Rust 提供的程序开发速度。Rust 的编译器检查确保了增加功能和重构代码时的稳定性。这与缺少这些检查的语言形成鲜明对比,开发者通常害怕修改那些脆弱的遗留代码。通过力求零开销抽象(zero-cost abstractions),高层级的特性被编译为底层代码,与手写的一样快,Rust 致力于使安全的代码也同样快速。 +Rust 适用于追求编程语言的速度与稳定性的开发者。所谓速度,是指你用 Rust 开发出的程序运行速度,以及 Rust 提供的程序开发速度。Rust 的编译器检查确保了增加功能和重构代码时的稳定性。这与缺少这些检查的语言形成鲜明对比,开发者通常害怕修改那些脆弱的遗留代码。通过力求零开销抽象(zero-cost abstractions),高层级的特性被编译为与手写一样快的底层代码,Rust 致力于使安全的代码也同样快速。 -Rust 语言希望能支持更多用户,这里提及的只是最大的利益相关者。总的来讲,Rust 最重要的目标是消除数十年来程序员不得不做的权衡:安全 **与** 生产力、速度 **与** 工程学。请尝试 Rust,看看它的选择是否适合你。 +Rust 语言也希望能支持很多其他用户,这里提及的只是最大的利益相关者。总的来讲,Rust 最重要的目标是消除数十年来程序员不得不做的权衡:安全 **与** 生产力、速度 **与** 工程学。请尝试 Rust,看看这个选择是否适合你。 -## 谁会阅读本书 +## 本书是写给谁的 本书假设你已经使用其他编程语言编写过代码,但并不假设你使用的是何种语言。我们尝试使这些材料能广泛的适用于来自很多不同编程背景的开发者。我们不会花费很多时间讨论编程 **是** 什么或者如何理解它。如果编程对于你来说是完全陌生的,你最好先阅读专门介绍编程的书籍。 ## 如何阅读本书 -总体来说,本书假设你会从头到尾顺序阅读。稍后的章节建立在之前章节概念的基础上,同时之前的章节可能不会深入讨论一个主题的细节;通常稍后的章节会重新讨论这些主题。 +总体来说,本书假设你会从头到尾顺序阅读。稍后的章节建立在之前章节概念的基础上,同时之前的章节可能不会深入讨论某个主题的细节;通常稍后的章节会重新讨论这些主题。 你会在本书中发现两类章节:概念章节和项目章节。在概念章节中,我们学习 Rust 的某个方面。在项目章节中,我们应用目前所学的知识一同构建小的程序。第二、十二和二十章是项目章节;其余都是概念章节。 -第一章介绍如何安装 Rust,如何编写 Hello world 程序,以及如何使用 Rust 的包管理器和构建工具 Cargo。第二章是 Rust 语言的实战介绍。我们会介绍一些高层级的概念,在稍后章节会详细介绍。如果你希望立刻就动手实践一下,第二章正好适合你。开始阅读时,你甚至可能希望略过第三章,它介绍了 Rust 中类似其他编程语言中的功能,并直接阅读第四章学习 Rust 的所有权系统。然而,如果你是特别重视细节的学习者,并倾向于在继续之前学习每一个细节,你可能希望略过第二章并直接阅读第三章,并在想要构建项目来实践这些细节时再回来阅读第二章。 +第一章介绍如何安装 Rust,如何编写 Hello, world! 程序,以及如何使用 Rust 的包管理器和构建工具 Cargo。第二章是 Rust 语言的实战介绍。我们会站在较高的层次介绍一些的概念,在稍后的章节种会做详细介绍。如果你希望立刻就动手实践一下,第二章正好适合你。开始阅读时,你甚至可能希望略过第三章,它介绍了 Rust 中类似其他编程语言中的功能,并直接阅读第四章学习 Rust 的所有权系统。然而,如果你是特别重视细节的学习者,并倾向于在继续之前学习每一个细节,你可能希望略过第二章并直接阅读第三章,并在想要构建项目来实践这些细节时再回来阅读第二章。 第五章讨论结构体和方法,第六章介绍枚举、`match` 表达式和 `if let` 控制流结构。在 Rust 中,你将使用结构体和枚举创建自定义类型。 -第七章,你会学习 Rust 的模块系统和私有性规则来组织代码和公有应用程序设计接口(Application Programming Interface, API)。第八章讨论了一些标准库提供的通用集合数据结构,比如 vector、字符串和哈希 map。第九章探索了 Rust 的错误处理哲学和技术。 +第七章,你会学习 Rust 的模块系统和私有性规则来组织代码和公有应用程序接口(Application Programming Interface, API)。第八章讨论了一些标准库提供的通用集合数据结构,比如 vector、字符串和哈希 map。第九章探索了 Rust 的错误处理哲学和技术。 -第十章深入介绍泛型、trait 和生命周期,他们提供了定义出适用于多种类型的代码的能力。第十一章介绍测试,即使 Rust 有安全保证,也需要测试确保程序逻辑正确。第十二章,我们构建了属于自己的在文件中搜索文本的命令行工具 `grep` 的子集功能实现。为此会利用之前章节讨论的很多概念。 +第十章深入介绍泛型、trait 和生命周期,他们提供了定义出适用于多种类型的代码的能力。第十一章全部关于测试,即使 Rust 有安全保证,也需要测试确保程序逻辑正确。第十二章,我们构建了属于自己的在文件中搜索文本的命令行工具 `grep` 的子集功能实现。为此会利用之前章节讨论的很多概念。 第十三章探索了闭包和迭代器:Rust 中来自函数式编程语言的功能。第十四章会更深层次的理解 Cargo 并讨论向他人分享库的最佳实践。第十五章讨论标准库提供的智能指针以及启用这些功能的 trait。 第十六章会学习不同的并发编程模型,并讨论 Rust 如何助你无畏的编写多线程程序。第十七章着眼于比较 Rust 风格与你可能熟悉的面向对象编程原则。 -第十八章是一个模式与模式匹配的参考章节,他们是在整个 Rust 程序中表达意图的强大方式。第十九章是一个高级主题大杂烩,包括 unsafe Rust 和更多关于生命周期、 trait、类型、函数和闭包的内容。 +第十八章是一个模式与模式匹配的参考章节,他们是在整个 Rust 程序中表达意图的强大方式。第十九章是一个高级主题大杂烩,包括 unsafe Rust、宏和更多关于生命周期、 trait、类型、函数和闭包的内容。 + +第二十章将会完成一个项目,我们会实现一个底层的、多线程的 web server! + +最后是一些附录,包含了一些关于语言的参考风格格式的实用信息。附录 A 介绍了 Rust 的关键字。附录 B 介绍 Rust 的运算符和符号。附录 C 介绍标准库提供的派生 trait。附录 D 涉及了一些有用的开发工具,附录 E 介绍了 Rust 的不同版本。 + +怎样阅读本书都不会有任何问题:如果你希望略过一些内容,请继续!如果你发现疑惑可能会再跳回之前的章节。请随意阅读。 -第二十章会完成一个项目,实现了一个底层的、多线程的 web server! + -最后是一些附录,包含了一些关于语言的参考风格格式的实用信息。附录 A 介绍了 Rust 的关键字。附录 B 介绍 Rust 的运算符和符号。附录 C 介绍标准库提供的派生 trait。附录 D 介绍宏。 +学习 Rust 的过程中一个重要的部分是学习如何阅读编译器提供的错误信息:它们会指导你编写出能工作的代码。为此,我们会提供很多不能编译的示例代码,以及各个情况下编译器会展示的错误信息。请注意如果随便输入并运行随机的示例代码,它们可能无法编译!请确保阅读任何你尝试运行的示例周围的内容,检视他们是否有意写错。Ferris 也会帮助你区别那些有意无法工作的代码: -怎样阅读本书都不会有任何问题:如果你希望略过一些内容,请继续!如果你发现疑惑可能会再跳回之前的章节。无论怎样都是可以的。 +| Ferris | 意义 | +|------------------------------------------------------------------------|--------------------------------------------------| +| | 这些代码不能编译! | +| | 这些代码会 panic! | +| | 这些代码块包含不安全(unsafe)代码 | +| | 这些代码不会产生期望的行为。 | -学习 Rust 的过程中一个重要的部分是学习如何阅读编译器提供的错误信息:它们会指导你编写出能工作的代码。为此,我们会提供很多不能编译的示例代码,以及各个情况下编译器会展示的错误信息。请注意如果随便输入并运行随机的示例代码,它们可能无法编译!请确保阅读任何你尝试运行的示例周围的内容,检视他们是否有意写错。在大部分情况,我们会指引你将任何不能编译的代码纠正为正确版本。 +在大部分情况,我们会指引你将任何不能编译的代码纠正为正确版本。 ## 源代码 生成本书的源码可以在 [GitHub][book] 上找到。 -[book]: https://github.com/rust-lang/book/tree/master/second-edition/src +[book]: https://github.com/rust-lang/book/tree/master/src > 译者注:本译本的 [GitHub 仓库][trpl-zh-cn],欢迎 Issue 和 PR :) diff --git a/src/ch01-00-getting-started.md b/src/ch01-00-getting-started.md index c898517..aa0c4cb 100644 --- a/src/ch01-00-getting-started.md +++ b/src/ch01-00-getting-started.md @@ -1,10 +1,10 @@ # 入门指南 -> [ch01-00-getting-started.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch01-00-getting-started.md) +> [ch01-00-getting-started.md](https://github.com/rust-lang/book/blob/master/src/ch01-00-getting-started.md) >
-> commit 7480e811ab5ad8d53a5b854d9b0c7a5a4f58499f +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -让我们开始 Rust 之旅!有很多内容要学,但每次旅程总有起点。在本章中,我们会讨论: +让我们开始 Rust 之旅!有很多内容需要学习,但每次旅程总有起点。在本章中,我们会讨论: * 在 Linux、macOS 和 Windows 上安装 Rust * 编写一个打印 `Hello, world!` 的程序 diff --git a/src/ch01-01-installation.md b/src/ch01-01-installation.md index 653045a..e93f773 100644 --- a/src/ch01-01-installation.md +++ b/src/ch01-01-installation.md @@ -1,18 +1,18 @@ ## 安装 -> [ch01-01-installation.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch01-01-installation.md) +> [ch01-01-installation.md](https://github.com/rust-lang/book/blob/master/src/ch01-01-installation.md) >
-> commit 7480e811ab5ad8d53a5b854d9b0c7a5a4f58499f +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 第一步是安装 Rust。我们通过 `rustup` 下载 Rust,这是一个管理 Rust 版本和相关工具的命令行工具。下载时需要联网。 -> 注意:如果出于某些理由你倾向于不使用 `rustup`,请到 [Rust 安装页面](https://www.rust-lang.org/install.html) 查看其它安装选项。 +> 注意:如果你出于某些理由倾向于不使用 `rustup`,请到 [Rust 安装页面](https://www.rust-lang.org/install.html) 查看其它安装选项。 -接下来的步骤会安装最新的稳定版 Rust 编译器。本书所有示例和输出采用稳定版 Rust 1.21.0。Rust 的稳定性确保本书所有示例在最新版本的 Rust 中能够继续编译。不同版本的输出可能略有不同,因为 Rust 经常改进错误信息和警告。也就是说,通过这些步骤安装的最新稳定版 Rust,能正常运行本书中的内容。 +接下来的步骤会安装最新的稳定版 Rust 编译器。Rust 的稳定性确保本书所有示例在最新版本的 Rust 中能够继续编译。不同版本的输出可能略有不同,因为 Rust 经常改进错误信息和警告。也就是说,任何通过这些步骤安装的最新稳定版 Rust,都应该能正常运行本书中的内容。 > ### 命令行标记 > -> 本章和全书中,我们会展示在终端中使用的命令。所有需要输入到终端的行都以 `$` 开头。但无需输入`$`;它代表每行命令的起点。不以 `$` 起始的行通常展示之前命令的输出。另外,PowerShell 专用的示例会采用 `>` 而不是 `$`。 +> 本章和全书中,我们会展示一些在终端中使用的命令。所有需要输入到终端的行都以 `$` 开头。但无需输入`$`;它代表每行命令的起点。不以 `$` 起始的行通常展示之前命令的输出。另外,PowerShell 专用的示例会采用 `>` 而不是 `$`。 ### 在 Linux 或 macOS 上安装 `rustup` @@ -46,12 +46,12 @@ $ export PATH="$HOME/.cargo/bin:$PATH" ### 在 Windows 上安装 `rustup` -在 Windows 上,前往 [https://www.rust-lang.org/install.html][install] 并按照说明安装 Rust。在安装过程的某个步骤,你会收到一个信息说明为什么需要安装 Visual Studio 2013 或之后版本的 C++ build tools。获取这些 build tools 最方便的方法是安装 [Build Tools for Visual Studio 2017][visualstudio]。这个工具在 “Other Tools and Frameworks” 部分。 +在 Windows 上,前往 [https://www.rust-lang.org/install.html][install] 并按照说明安装 Rust。在安装过程的某个步骤,你会收到一个信息说明为什么需要安装 Visual Studio 2013 或更新版本的 C++ build tools。获取这些 build tools 最方便的方法是安装 [Build Tools for Visual Studio 2017][visualstudio]。这个工具在 “Other Tools and Frameworks” 部分。 [install]: https://www.rust-lang.org/install.html [visualstudio]: https://www.visualstudio.com/downloads/ -本书的余下部分,使用能同时运行于 *cmd.exe* 和 PowerShell 的命令。如果存在特定差异,我们会解释使用哪一个。 +本书的余下部分会使用能同时运行于 *cmd.exe* 和 PowerShell 的命令。如果存在特定差异,我们会解释使用哪一个。 ### 更新和卸载 @@ -88,10 +88,10 @@ rustc x.y.z (abcabcabc yyyy-mm-dd) [users]: https://users.rust-lang.org/ [stackoverflow]: http://stackoverflow.com/questions/tagged/rust -恭喜入坑!(此处应该有掌声!) +> 译者:恭喜入坑!(此处应该有掌声!) ### 本地文档 安装程序也自带一份文档的本地拷贝,可以离线阅读。运行 `rustup doc` 在浏览器中查看本地文档。 -任何时候,如果你拿不准标准库中的类型或函数的用途和用法,请查看应用程序接口(application programming interface,API)文档! \ No newline at end of file +任何时候,如果你拿不准标准库中的类型或函数的用途和用法,请查阅应用程序接口(application programming interface,API)文档! \ No newline at end of file diff --git a/src/ch01-02-hello-world.md b/src/ch01-02-hello-world.md index ac166b9..482afe3 100644 --- a/src/ch01-02-hello-world.md +++ b/src/ch01-02-hello-world.md @@ -1,10 +1,10 @@ ## Hello, World! -> [ch01-02-hello-world.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch01-02-hello-world.md) +> [ch01-02-hello-world.md](https://github.com/rust-lang/book/blob/master/src/ch01-02-hello-world.md) >
-> commit 5dfa983aa8fca89f8b70cafe58ab8417491d2018 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -既然安装好了 Rust,我们来编写第一个 Rust 程序。当学习一门新语言的时候,使用该语言在屏幕上打印 `Hello, world!` 是一项传统,这里我们将沿用这个传统! +既然安装好了 Rust,我们来编写第一个 Rust 程序。当学习一门新语言的时候,使用该语言在屏幕上打印 `Hello, world!` 是一项传统,我们将沿用这一传统! > 注意:本书假设你熟悉基本的命令行操作。Rust 对于你的编辑器、工具,以及代码位于何处并没有特定的要求,如果你更倾向于使用集成开发环境(IDE),而不是命令行,请尽管使用你喜欢的 IDE。目前很多 IDE 已经不同程度的支持 Rust;查看 IDE 文档了解更多细节。最近,Rust 团队已经致力于提供强大的 IDE 支持,而且进展飞速! @@ -89,7 +89,7 @@ fn main() { 这几行定义了一个 Rust 函数。`main` 函数是一个特殊的函数:在可执行的 Rust 程序中,它总是最先运行的代码。第一行代码声明了一个叫做 `main` 的函数,它没有参数也没有返回值。如果有参数的话,它们的名称应该出现在小括号中,`()`。 -还须注意,函数体被包裹在花括号中,`{}`。Rust 要求所有函数体都要用花括号包裹起来(译者注:有些语言,当函数体只有一行时可以省略花括号,但在 Rust 中是不行的)。一般来说,将左花括号与函数声明置于同一行并以空格分隔,是良好的代码风格。 +还须注意,函数体被包裹在花括号中,`{}`。Rust 要求所有函数体都要用花括号包裹起来。一般来说,将左花括号与函数声明置于同一行并以空格分隔,是良好的代码风格。 在编写本书的时候,一个叫做 `rustfmt` 的自动格式化工具正在开发中。如果你希望在 Rust 项目中保持一种标准风格,`rustfmt` 会将代码格式化为特定的风格。Rust 团队计划最终将该工具包含在标准 Rust 发行版中,就像 `rustc`。所以根据你阅读本书的时间,它可能已经安装到你的电脑中了!检查在线文档以了解更多细节。 @@ -101,7 +101,7 @@ fn main() { 这行代码完成这个简单程序的所有工作:在屏幕上打印文本。这里有四个重要的细节需要注意。首先 Rust 的缩进风格使用 4 个空格,而不是 1 个制表符(tab)。 -第二,`println!` 调用了一个 Rust 宏(macro)。如果是调用函数,则应输入 `println`(没有`!`)。我们将在附录 D 中详细讨论宏。现在你只需记住,当看到符号 `!` 的时候,就意味着调用的是宏而不是普通函数。 +第二,`println!` 调用了一个 Rust 宏(macro)。如果是调用函数,则应输入 `println`(没有`!`)。我们将在第十九章详细讨论宏。现在你只需记住,当看到符号 `!` 的时候,就意味着调用的是宏而不是普通函数。 第三,`"Hello, world!"` 是一个字符串。我们把这个字符串作为一个参数传递给 `println!`,字符串将被打印到屏幕上。 @@ -119,17 +119,26 @@ $ rustc main.rs 如果你有 C 或 C++ 背景,就会发现这与 `gcc` 和 `clang` 类似。编译成功后,Rust 会输出一个二进制的可执行文件。 -在 Linux、macOS 或 Windows 的 PowerShell 上,在 shell 中输入 `ls` 命令就可看见这个可执行文件,如下: +在 Linux、macOS 或 Windows 的 PowerShell 上,在 shell 中输入 `ls` 命令也可以看见这个可执行文件,如下: ```text -$ ls -main main.rs +> ls + + + Directory: Path:\to\the\project + + +Mode LastWriteTime Length Name +---- ------------- ------ ---- +-a---- 6/1/2018 7:31 AM 137728 main.exe +-a---- 6/1/2018 7:31 AM 1454080 main.pdb +-a---- 6/1/2018 7:31 AM 14 main.rs ``` 在 Windows 的 CMD 上,则输入如下内容: ```cmd -> dir /B %= the /B option says to only show the file names =% +> dir /B %= /B 参数表示只显示文件名 =% main.exe main.pdb main.rs @@ -138,7 +147,7 @@ main.rs 这展示了扩展名为 *.rs* 的源文件、可执行文件(在 Windows 下是 *main.exe*,其它平台是 *main*),以及当使用 CMD 时会有一个包含调试信息、扩展名为 *.pdb* 的文件。从这里开始运行 *main* 或 *main.exe* 文件,如下: ```text -$ ./main # or .\main.exe on Windows +$ ./main # Windows 是 .\main.exe ``` 如果 *main.rs* 是上文所述的 Hello, world! 程序,它将会在终端上打印 `Hello, world!`。 diff --git a/src/img/ferris/does_not_compile.svg b/src/img/ferris/does_not_compile.svg new file mode 100644 index 0000000..5d345f1 --- /dev/null +++ b/src/img/ferris/does_not_compile.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/img/ferris/not_desired_behavior.svg b/src/img/ferris/not_desired_behavior.svg new file mode 100644 index 0000000..47f4024 --- /dev/null +++ b/src/img/ferris/not_desired_behavior.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/img/ferris/panics.svg b/src/img/ferris/panics.svg new file mode 100644 index 0000000..be55f5e --- /dev/null +++ b/src/img/ferris/panics.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/img/ferris/unsafe.svg b/src/img/ferris/unsafe.svg new file mode 100644 index 0000000..d4fdc08 --- /dev/null +++ b/src/img/ferris/unsafe.svg @@ -0,0 +1,291 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/img/trpl04-01.svg b/src/img/trpl04-01.svg index 7f5ee8d..314f53b 100644 --- a/src/img/trpl04-01.svg +++ b/src/img/trpl04-01.svg @@ -1,65 +1,68 @@ - - + %3 - + -table0 - -s1 - -name - -value - -ptr - - -len - -5 - -capacity - -5 + +table0 + +s1 + +name + +value + +ptr + + +len + +5 + +capacity + +5 -table1 - -index - -value - -0 - -h - -1 - -e - -2 - -l - -3 - -l - -4 - -o + +table1 + +index + +value + +0 + +h + +1 + +e + +2 + +l + +3 + +l + +4 + +o -table0:pointer:c->table1:pointee - - + +table0:c->table1:pointee + + diff --git a/src/img/trpl04-02.svg b/src/img/trpl04-02.svg index 7d3a29a..70d490f 100644 --- a/src/img/trpl04-02.svg +++ b/src/img/trpl04-02.svg @@ -1,90 +1,95 @@ - - + %3 - + -table0 - -s1 - -name - -value - -ptr - - -len - -5 - -capacity - -5 + +table0 + +s1 + +name + +value + +ptr + + +len + +5 + +capacity + +5 -table1 - -index - -value - -0 - -h - -1 - -e - -2 - -l - -3 - -l - -4 - -o + +table1 + +index + +value + +0 + +h + +1 + +e + +2 + +l + +3 + +l + +4 + +o -table0:pointer:c->table1:pointee - - + +table0:c->table1:pointee + + -table3 - -s2 - -name - -value - -ptr - - -len - -5 - -capacity - -5 + +table3 + +s2 + +name + +value + +ptr + + +len + +5 + +capacity + +5 -table3:pointer:c->table1:pointee - - + +table3:c->table1:pointee + + diff --git a/src/img/trpl04-03.svg b/src/img/trpl04-03.svg index a606851..7c153e2 100644 --- a/src/img/trpl04-03.svg +++ b/src/img/trpl04-03.svg @@ -1,117 +1,123 @@ - - + %3 - + -table0 - -s2 - -name - -value - -ptr - - -len - -5 - -capacity - -5 + +table0 + +s2 + +name + +value + +ptr + + +len + +5 + +capacity + +5 -table1 - -index - -value - -0 - -h - -1 - -e - -2 - -l - -3 - -l - -4 - -o + +table1 + +index + +value + +0 + +h + +1 + +e + +2 + +l + +3 + +l + +4 + +o -table0:pointer:c->table1:pointee - - + +table0:c->table1:pointee + + -table3 - -s1 - -name - -value - -ptr - - -len - -5 - -capacity - -5 + +table3 + +s1 + +name + +value + +ptr + + +len + +5 + +capacity + +5 -table4 - -index - -value - -0 - -h - -1 - -e - -2 - -l - -3 - -l - -4 - -o + +table4 + +index + +value + +0 + +h + +1 + +e + +2 + +l + +3 + +l + +4 + +o -table3:pointer:c->table4:pointee - - + +table3:c->table4:pointee + + diff --git a/src/img/trpl04-04.svg b/src/img/trpl04-04.svg index 1a17b27..a0513ab 100644 --- a/src/img/trpl04-04.svg +++ b/src/img/trpl04-04.svg @@ -1,91 +1,96 @@ - - + %3 - + -table0 - - -s1 - -name - -value - -ptr - - -len - -5 - -capacity - -5 + +table0 + + +s1 + +name + +value + +ptr + + +len + +5 + +capacity + +5 -table1 - -index - -value - -0 - -h - -1 - -e - -2 - -l - -3 - -l - -4 - -o + +table1 + +index + +value + +0 + +h + +1 + +e + +2 + +l + +3 + +l + +4 + +o -table0:pointer:c->table1:pointee - - + +table0:c->table1:pointee + + -table3 - -s2 - -name - -value - -ptr - - -len - -5 - -capacity - -5 + +table3 + +s2 + +name + +value + +ptr + + +len + +5 + +capacity + +5 -table3:pointer:c->table1:pointee - - + +table3:c->table1:pointee + + diff --git a/src/img/trpl04-05.svg b/src/img/trpl04-05.svg index 33e5b49..b4bf2eb 100644 --- a/src/img/trpl04-05.svg +++ b/src/img/trpl04-05.svg @@ -1,82 +1,87 @@ - - + %3 - + -table0 - -s - -name - -value - -ptr - + +table0 + +s + +name + +value + +ptr + -table1 - -s1 - -name - -value - -ptr - - -len - -5 - -capacity - -5 + +table1 + +s1 + +name + +value + +ptr + + +len + +5 + +capacity + +5 -table0:borrower:c->table1:borrowee - - + +table0:c->table1:borrowee + + -table2 - -index - -value - -0 - -h - -1 - -e - -2 - -l - -3 - -l - -4 - -o + +table2 + +index + +value + +0 + +h + +1 + +e + +2 + +l + +3 + +l + +4 + +o -table1:pointer:c->table2:pointee - - + +table1:c->table2:pointee + + diff --git a/src/img/trpl04-06.svg b/src/img/trpl04-06.svg index 1a87522..e64415f 100644 --- a/src/img/trpl04-06.svg +++ b/src/img/trpl04-06.svg @@ -1,110 +1,115 @@ - - + %3 - + -table0 - -world - -name - -value - -ptr - - -len - -5 + +table0 + +world + +name + +value + +ptr + + +len + +5 -table4 - -index - -value - -0 - -h - -1 - -e - -2 - -l - -3 - -l - -4 - -o - -5 - - - -6 - -w - -7 - -o - -8 - -r - -9 - -l - -10 - -d + +table4 + +index + +value + +0 + +h + +1 + +e + +2 + +l + +3 + +l + +4 + +o + +5 + + + +6 + +w + +7 + +o + +8 + +r + +9 + +l + +10 + +d -table0:pointer2:c->table4:pointee2 - - + +table0:c->table4:pointee2 + + -table3 - -s - -name - -value - -ptr - - -len - -11 - -capacity - -11 + +table3 + +s + +name + +value + +ptr + + +len + +11 + +capacity + +11 -table3:pointer:c->table4:pointee - - + +table3:c->table4:pointee + + diff --git a/src/img/trpl15-01.svg b/src/img/trpl15-01.svg index b873825..bbeef96 100644 --- a/src/img/trpl15-01.svg +++ b/src/img/trpl15-01.svg @@ -1,42 +1,43 @@ - - + %3 - + -table0 - -Cons - -i32 - - -Cons - -i32 - - -Cons - -i32 - - -Cons - -i32 - - -Cons - -i32 - - + +table0 + +Cons + +i32 + + +Cons + +i32 + + +Cons + +i32 + + +Cons + +i32 + + +Cons + +i32 + + diff --git a/src/img/trpl15-02.svg b/src/img/trpl15-02.svg index 5b86985..4454df8 100644 --- a/src/img/trpl15-02.svg +++ b/src/img/trpl15-02.svg @@ -1,25 +1,26 @@ - - + %3 - + -table0 - -Cons - -i32 - - -Box - -usize + +table0 + +Cons + +i32 + + +Box + +usize diff --git a/src/img/trpl15-03.svg b/src/img/trpl15-03.svg index e26024f..dbc3b5c 100644 --- a/src/img/trpl15-03.svg +++ b/src/img/trpl15-03.svg @@ -1,94 +1,109 @@ - - + %3 - + -table4 -b + +table4 +b -table5 - -3 - -   + +table5 + +3 + +   -table4:ptr4:c->table5:pte4 - - + +table4:c->table5:pte4 + + -table1 - -5 - -   + +table1 + +5 + +   -table5:ptr5:c->table1:pte0 - - + +table5:c->table1:pte0 + + -table0 -a + +table0 +a -table0:ptr0:c->table1:pte0 - - + +table0:c->table1:pte0 + + -table2 - -10 - -   + +table2 + +10 + +   -table1:ptr1:c->table2:pte1 - - + +table1:c->table2:pte1 + + -table3 - -Nil + +table3 + +Nil -table2:ptr2:c->table3:pte2 - - + +table2:c->table3:pte2 + + -table6 -c + +table6 +c -table7 - -4 - -   + +table7 + +4 + +   -table6:ptr6:c->table7:pte6 - - + +table6:c->table7:pte6 + + -table7:ptr7:c->table1:pte0 - - + +table7:c->table1:pte0 + + From 0f9f9d55542d7fe9e23b8c5704453a1b90bfa1b9 Mon Sep 17 00:00:00 2001 From: KaiserY Date: Fri, 30 Nov 2018 15:40:10 +0800 Subject: [PATCH 09/49] check to ch03-02 --- src/ch01-03-hello-cargo.md | 32 +++++++++-------- src/ch02-00-guessing-game-tutorial.md | 42 ++++++++++------------ src/ch03-00-common-programming-concepts.md | 38 +++++++++++++++++--- src/ch03-01-variables-and-mutability.md | 16 ++++----- src/ch03-02-data-types.md | 20 +++++++++-- 5 files changed, 94 insertions(+), 54 deletions(-) diff --git a/src/ch01-03-hello-cargo.md b/src/ch01-03-hello-cargo.md index 0acdb0c..7904ebc 100644 --- a/src/ch01-03-hello-cargo.md +++ b/src/ch01-03-hello-cargo.md @@ -1,14 +1,14 @@ ## Hello, Cargo! -> [ch01-03-hello-cargo.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch01-03-hello-cargo.md) +> [ch01-03-hello-cargo.md](https://github.com/rust-lang/book/blob/master/src/ch01-03-hello-cargo.md) >
-> commit 7480e811ab5ad8d53a5b854d9b0c7a5a4f58499f +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f Cargo 是 Rust 的构建系统和包管理器。大多数 Rustacean 们使用 Cargo 来管理他们的 Rust 项目,因为它可以为你处理很多任务,比如构建代码、下载依赖库并编译这些库。(我们把代码所需要的库叫做 **依赖**(*dependencies*))。 -最简单的 Rust 程序,比如我们刚刚编写的,没有任何依赖。所以如果使用 Cargo 来构建 Hello, world! 项目,将只会用到 Cargo 的构建代码那部分功能。随着编写的 Rust 程序更加复杂,你会添加依赖,如果你一开始就使用 Cargo 的话,添加依赖将会变得简单许多。 +最简单的 Rust 程序,比如我们刚刚编写的,没有任何依赖。所以如果使用 Cargo 来构建 Hello, world! 项目,将只会用到 Cargo 的构建代码那部分功能。如果编写更为复杂的 Rust 程序,你会添加依赖,这样如果你一开始就使用 Cargo 的话,添加依赖将会变得简单许多。 -由于绝大多数 Rust 项目使用 Cargo,本书接下来的部分假设你也使用 Cargo。如果使用 “安装” 章节介绍的官方安装包的话,则自带了 Cargo。如果通过其他方式安装的话,可以在终端输入如下命令检查是否安装了 Cargo: +由于绝大多数 Rust 项目使用 Cargo,本书接下来的部分假设你也使用 Cargo。如果使用 “安装” 部分介绍的官方安装包的话,则自带了 Cargo。如果通过其他方式安装的话,可以在终端输入如下命令检查是否安装了 Cargo: ```text $ cargo --version @@ -21,17 +21,17 @@ $ cargo --version 我们使用 Cargo 创建一个新项目,然后看看与上面的 Hello, world! 项目有什么不同。回到 *projects* 目录(或者你存放代码的目录)。接着,可在任何操作系统下运行以下命令: ```text -$ cargo new hello_cargo --bin +$ cargo new hello_cargo $ cd hello_cargo ``` -第一行命令新建了名为 *hello_cargo* 的二进制可执行程序。为 `cargo new` 传入 `--bin` 参数会生成一个可执行程序(通常就叫做 **二进制文件**,*binary*),而不是一个库。项目被命名为 `hello_cargo`,同时 Cargo 在一个同名目录中创建项目文件。 +第一行命令新建了名为 *hello_cargo* 的目录。我们将项目命名为 *hello_cargo*,同时 Cargo 在一个同名目录中创建项目文件。 进入 *hello_cargo* 目录并列出文件。将会看到 Cargo 生成了两个文件和一个目录:一个 *Cargo.toml* 文件,一个 *src* 目录,以及位于 *src* 目录中 *main.rs* 文件。它也在 *hello_cargo* 目录初始化了一个 git 仓库,以及一个 *.gitignore* 文件。 > 注意:Git 是一个常用的版本控制系统(version control system, VCS)。可以通过 `--vcs` 参数使 `cargo new` 切换到其它版本控制系统(VCS),或者不使用 VCS。运行 `cargo new --help` 参看可用的选项。 -请选用文本编辑器打开 *Cargo.toml* 文件。它应该看起来如示例 1-2 所示: +请自行选用文本编辑器打开 *Cargo.toml* 文件。它应该看起来如示例 1-2 所示: 文件名: Cargo.toml @@ -40,6 +40,7 @@ $ cd hello_cargo name = "hello_cargo" version = "0.1.0" authors = ["Your Name "] +edition = "2018" [dependencies] ``` @@ -52,9 +53,9 @@ authors = ["Your Name "] 第一行,`[package]`,是一个片段(section)标题,表明下面的语句用来配置一个包。随着我们在这个文件增加更多的信息,还将增加其他片段(section)。 -接下来的三行设置了 Cargo 编译程序所需的配置:项目的名称、版本和作者。Cargo 从环境中获取你的名字和 email 信息,所以如果这些信息不正确,请修改并保存此文件。 +接下来的四行设置了 Cargo 编译程序所需的配置:项目的名称、版本和作者。Cargo 从环境中获取你的名字和 email 信息,所以如果这些信息不正确,请修改并保存此文件。附录 E 会介绍 `edition` 的值。 -最后一行,`[dependencies]`,是罗列项目依赖的片段。在 Rust 中,代码包被称为 *crates*。这个项目并不需要其他的 crate,不过在第二章的第一个项目会用到依赖,那时会用得上这个片段。 +最后一行,`[dependencies]`,是罗列项目依赖的片段的开始。在 Rust 中,代码包被称为 *crates*。这个项目并不需要其他的 crate,不过在第二章的第一个项目会用到依赖,那时会用得上这个片段。 现在打开 *src/main.rs* 看看: @@ -85,7 +86,7 @@ $ cargo build 这个命令会创建一个可执行文件 *target/debug/hello_cargo* (在 Windows 上是 *target\debug\hello_cargo.exe*),而不是放在目前目录下。可以通过这个命令运行可执行文件: ```text -$ ./target/debug/hello_cargo # or .\target\debug\hello_cargo.exe on Windows +$ ./target/debug/hello_cargo # 或者在 Windows 下为 .\target\debug\hello_cargo.exe Hello, world! ``` @@ -109,15 +110,16 @@ $ cargo run Running `target/debug/hello_cargo` Hello, world! ``` + Cargo 还提供了一个叫 `cargo check` 的命令。该命令快速检查代码确保其可以编译,但并不产生可执行文件: ```text $ cargo check - Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo) + Checking hello_cargo v0.1.0 (file:///projects/hello_cargo) Finished dev [unoptimized + debuginfo] target(s) in 0.32 secs ``` -为什么你会不需要可执行文件呢?通常 `cargo check` 要比 `cargo build` 快得多,因为它省略了生成可执行文件的步骤。如果编写代码时持续的进行检查,`cargo check` 会加速开发!为此很多 Rustaceans 编写代码时定期运行 `cargo check` 确保它们可以编译。当准备好使用可执行文件时才运行 `cargo build`。 +为什么你会不需要可执行文件呢?通常 `cargo check` 要比 `cargo build` 快得多,因为它省略了生成可执行文件的步骤。如果你在编写代码时持续的进行检查,`cargo check` 会加速开发!为此很多 Rustaceans 编写代码时定期运行 `cargo check` 确保它们可以编译。当准备好使用可执行文件时才运行 `cargo build`。 我们回顾下已学习的 Cargo 内容: @@ -125,7 +127,7 @@ $ cargo check * 可以使用 `cargo run` 一步构建并运行项目。 * 有别于将构建结果放在与源码相同的目录,Cargo 会将其放到 *target/debug* 目录。 -使用 Cargo 的一个额外的优点是,不管你使用什么操作系统,其命令都是一样的。所以从此以后本书将不再为 Linux 和 macOS 以及 Windows 提供相应的命令。 +使用 Cargo 的一个额外的优点是,不管你使用什么操作系统,其命令都是一样的。所以从现在开始本书将不再为 Linux 和 macOS 以及 Windows 提供相应的命令。 ### 发布(release)构建 @@ -149,7 +151,7 @@ $ cargo build ## 总结 -你已经踏上了 Rust 之旅!在本章中,你学习了如何: +你已经准备好开启 Rust 之旅了!在本章中,你学习了如何: * 使用 `rustup` 安装最新稳定版的 Rust * 更新到新版的 Rust @@ -157,4 +159,4 @@ $ cargo build * 直接通过 `rustc` 编写并运行 Hello, world! 程序 * 使用 Cargo 创建并运行新项目 -是时候通过构建更真实的程序来熟悉读写 Rust 代码了。所以在下一章,我们会构建一个猜猜看游戏程序。如果你更愿意从学习 Rust 常用的编程概念开始,请阅读第三章,接着再回到第二章。 +是时候通过构建更实质性的程序来熟悉读写 Rust 代码了。所以在第二章我们会构建一个猜猜看游戏程序。如果你更愿意从学习 Rust 常用的编程概念开始,请阅读第三章,接着再回到第二章。 diff --git a/src/ch02-00-guessing-game-tutorial.md b/src/ch02-00-guessing-game-tutorial.md index 61d0a03..7c43602 100644 --- a/src/ch02-00-guessing-game-tutorial.md +++ b/src/ch02-00-guessing-game-tutorial.md @@ -1,8 +1,8 @@ # 编写 猜猜看 游戏 -> [ch02-00-guessing-game-tutorial.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch02-00-guessing-game-tutorial.md) +> [ch02-00-guessing-game-tutorial.md](https://github.com/rust-lang/book/blob/master/src/ch02-00-guessing-game-tutorial.md) >
-> commit 7480e811ab5ad8d53a5b854d9b0c7a5a4f58499f +> commit a86c1d315789b3ca13b20d50ad5005c62bdd9e37 让我们一起动手完成一个项目,来快速上手 Rust!本章将介绍 Rust 中一些常用概念,并通过真实的程序来展示如何运用它们。你将会学到 `let`、`match`、方法、关联函数、外部 crate 等知识!后续章节会深入探讨这些概念的细节。在这一章,我们将做基础练习。 @@ -13,11 +13,11 @@ 要创建一个新项目,进入第一章中创建的 *projects* 目录,使用 Cargo 新建一个项目,如下: ```text -$ cargo new guessing_game --bin +$ cargo new guessing_game $ cd guessing_game ``` -第一个命令,`cargo new`,它获取项目的名称(`guessing_game`)作为第一个参数。`--bin` 参数告诉 Cargo 创建一个二进制项目,与第一章类似。第二个命令进入到新创建的项目目录。 +第一个命令,`cargo new`,它获取项目的名称(`guessing_game`)作为第一个参数。第二个命令进入到新创建的项目目录。 看看生成的 *Cargo.toml* 文件: @@ -119,7 +119,7 @@ println!("Please input your guess."); let mut guess = String::new(); ``` -现在程序开始变得有意思了!这一小行代码发生了很多事。注意这是一个 `let` 语句,用来创建 **变量**。这里是另外一个例子: +现在程序开始变得有意思了!这一小行代码发生了很多事。注意这是一个 `let` 语句,用来创建 **变量**(*variable*)。这里是另外一个例子: ```rust,ignore let foo = bar; @@ -128,11 +128,11 @@ let foo = bar; 这行代码新建了一个叫做 `foo` 的变量并把它绑定到值 `bar` 上。在 Rust 中,变量默认是不可变的。我们将会在第三章的 “变量与可变性” 部分详细讨论这个概念。下面的例子展示了如何在变量名前使用 `mut` 来使一个变量可变: ```rust,ignore -let foo = 5; // immutable -let mut bar = 5; // mutable +let foo = 5; // 不可变 +let mut bar = 5; // 可变 ``` -> 注意:`//` 语法开始一个注释,持续到行尾。Rust 忽略注释中的所有内容,将在第三章中详细介绍注释。 +> 注意:`//` 语法开始一个注释,持续到行尾。Rust 忽略注释中的所有内容,第三章将会详细介绍注释。 让我们回到猜猜看程序中。现在我们知道了 `let mut guess` 会引入一个叫做 `guess` 的可变变量。等号(`=`)的右边是 `guess` 所绑定的值,它是 `String::new` 的结果,这个函数会返回一个 `String` 的新实例。[`String`][string] 是一个标准库提供的字符串类型,它是 UTF-8 编码的可增长文本块。 @@ -214,7 +214,7 @@ Rust 警告我们没有使用 `read_line` 的返回值 `Result`,说明有一 ### 使用 `println!` 占位符打印值 -除了位于结尾的大括号,目前为止就只有一行代码值得讨论一下了,就是这一行: +除了位于结尾的大括号,目前为止就只有这一行代码值得讨论一下了,就是这一行: ```rust,ignore println!("You guessed: {}", guess); @@ -295,9 +295,9 @@ $ cargo build 在更新完 registry 后,Cargo 检查 `[dependencies]` 片段并下载缺失的 crate 。本例中,虽然只声明了 `rand` 一个依赖,然而 Cargo 还是额外获取了 `libc` 的拷贝,因为 `rand` 依赖 `libc` 来正常工作。下载完成后,Rust 编译依赖,然后使用这些依赖编译项目。 -如果不做任何修改,立刻再次运行 `cargo build`,则不会看到任何除了 `Finished` 完成提示之外的输出。Cargo 知道它已经下载并编译了依赖,同时 *Cargo.toml* 文件也没有变动。Cargo 还知道代码也没有任何修改,所以它不会重新编译代码。因为无事可做,它简单的退出了。 +如果不做任何修改,立刻再次运行 `cargo build`,则不会看到任何除了 `Finished` 行之外的输出。Cargo 知道它已经下载并编译了依赖,同时 *Cargo.toml* 文件也没有变动。Cargo 还知道代码也没有任何修改,所以它不会重新编译代码。因为无事可做,它简单的退出了。 -如果打开 *src/main.rs* 文件,做一些无关紧要的修改,保存并再次构建,只会出现两行输出: +如果打开 *src/main.rs* 文件,做一些无关紧要的修改,保存并再次构建,则会出现两行输出: ```text $ cargo build @@ -311,7 +311,7 @@ $ cargo build Cargo 有一个机制来确保任何人在任何时候重新构建代码,都会产生相同的结果:Cargo 只会使用你指定的依赖版本,除非你又手动指定了别的。例如,如果下周 `rand` crate 的 `0.3.15` 版本出来了,它修复了一个重要的 bug,同时也含有一个会破坏代码运行的缺陷,这时会发生什么呢? -这个问题的答案是 *Cargo.lock* 文件。它在第一次运行 `cargo build` 时创建,并放在 *guessing_game* 目录。当第一次构建项目时,Cargo 计算出所有符合要求的依赖版本并写入 *Cargo.lock* 文件。当将来构建项目时,Cargo 会发现 *Cargo.lock* 已存在并使用其中指定的版本,而不是再次计算所有的版本。这使得你拥有了一个自动化的可重现的构建。换句话说,项目会持续使用 `0.3.14` 直到你显式升级,感谢 *Cargo.lock* 文件。 +这个问题的答案是 *Cargo.lock* 文件。它在第一次运行 `cargo build` 时创建,并放在 *guessing_game* 目录。当第一次构建项目时,Cargo 计算出所有符合要求的依赖版本并写入 *Cargo.lock* 文件。当将来构建项目时,Cargo 会发现 *Cargo.lock* 已存在并使用其中指定的版本,而不是再次计算所有的版本。这使得你拥有了一个自动化的可重现的构建。换句话说,项目会持续使用 `0.3.14` 直到你显式升级,多亏有了 *Cargo.lock* 文件。 #### 更新 crate 到一个新版本 @@ -337,20 +337,18 @@ rand = "0.4.0" 下一次运行 `cargo build` 时,Cargo 会从 registry 更新可用的 crate,并根据你指定的新版本重新计算。 -第十四章会讲到 [Cargo][doccargo] 及其[生态系统][doccratesio]的更多内容,不过目前你只需要了解这么多。通过 Cargo 复用库文件非常容易,因此 Rustacean 能够编写出由很多包组装而成的更轻巧的项目。 +第十四章会讲到 [Cargo][doccargo] 及其[生态系统][doccratesio] 的更多内容,不过目前你只需要了解这么多。通过 Cargo 复用库文件非常容易,因此 Rustacean 能够编写出由很多包组装而成的更轻巧的项目。 [doccargo]: http://doc.crates.io [doccratesio]: http://doc.crates.io/crates-io.html ### 生成一个随机数 -你已经把 `rand` crate 添加到 *Cargo.toml* 了,让我们开始 **使用** `rand` 吧。下一步是更新 *src/main.rs*,如示例 2-3 所示: +你已经把 `rand` crate 添加到 *Cargo.toml* 了,让我们开始 **使用** `rand` 吧。下一步是更新 *src/main.rs*,如示例 2-3 所示。 文件名: src/main.rs ```rust,ignore -extern crate rand; - use std::io; use rand::Rng; @@ -378,11 +376,11 @@ fn main() { 接下来增加了另一行 `use`:`use rand::Rng`。`Rng` 是一个 trait,它定义了随机数生成器应实现的方法,想使用这些方法的话,此 trait 必须在作用域中。第十章会详细介绍 trait。 -另外,中间还新增加了两行。`rand::thread_rng` 函数提供实际使用的随机数生成器:它位于当前执行线程本地,并从操作系统获取 seed。接下来,调用随机数生成器的 `gen_range` 方法。这个方法由刚才引入到作用域的 `Rng` trait 定义。`gen_range` 方法获取两个数字作为参数,并生成一个范围在两者之间的随机数。它包含下限但不包含上限,所以需要指定 `1` 和 `101` 来请求一个 1 和 100 之间的数。 +另外,中间还新增加了两行。`rand::thread_rng` 函数提供实际使用的随机数生成器:它位于当前执行线程的本地环境中,并从操作系统获取 seed。接下来,调用随机数生成器的 `gen_range` 方法。这个方法由刚才引入到作用域的 `Rng` trait 定义。`gen_range` 方法获取两个数字作为参数,并生成一个范围在两者之间的随机数。它包含下限但不包含上限,所以需要指定 `1` 和 `101` 来请求一个 1 和 100 之间的数。 > 注意:你不可能凭空就知道应该 use 哪个 trait 以及该从 crate 中调用哪个方法。crate 的使用说明位于其文档中。Cargo 有一个很棒的功能是:运行 `cargo doc --open` 命令来构建所有本地依赖提供的文档,并在浏览器中打开。例如,假设你对 `rand` crate 中的其他功能感兴趣,你可以运行 `cargo doc --open` 并点击左侧导航栏中的 `rand`。 -新增加的第二行代码打印出了秘密数字。这在开发程序时很有用,因为可以测试它,不过在最终版本中会删掉它。游戏一开始就打印出结果就没什么可玩的了! +新增加的第二行代码打印出了秘密数字。这在开发程序时很有用,因为可以测试它,不过在最终版本中会删掉它。如果游戏一开始就打印出结果就没什么可玩的了! 尝试运行程序几次: @@ -500,13 +498,13 @@ let guess: u32 = guess.trim().parse() 这里创建了一个叫做 `guess` 的变量。不过等等,不是已经有了一个叫做 `guess` 的变量了吗?确实如此,不过 Rust 允许用一个新值来 **隐藏** (*shadow*) `guess` 之前的值。这个功能常用在需要转换值类型之类的场景。它允许我们复用 `guess` 变量的名字,而不是被迫创建两个不同变量,诸如 `guess_str` 和 `guess` 之类。(第三章会介绍 shadowing 的更多细节。) -我们将 `guess` 绑定到 `guess.trim().parse()` 表达式上。表达式中的 `guess` 是包含输入的原始 `String` 类型。`String` 实例的 `trim` 方法会去除字符串开头和结尾的空白字符。`u32` 只能由数字字符转换,不过用户必须输入 return 键才能让 `read_line` 返回,然而用户按下 return 键时,会在字符串中增加一个换行(newline)符。例如,用户输入 5 并按下 return,`guess` 看起来像这样:`5\n`。`\n` 代表 “换行”,回车键。`trim` 方法消除 `\n`,只留下`5`。 +我们将 `guess` 绑定到 `guess.trim().parse()` 表达式上。表达式中的 `guess` 是包含输入的原始 `String` 类型。`String` 实例的 `trim` 方法会去除字符串开头和结尾的空白字符。`u32` 只能由数字字符转换,不过用户必须输入 enter 键才能让 `read_line` 返回,然而用户按下 enter 键时,会在字符串中增加一个换行(newline)符。例如,用户输入 5 并按下 enter,`guess` 看起来像这样:`5\n`。`\n` 代表 “换行”,回车键。`trim` 方法消除 `\n`,只留下 `5`。 [字符串的 `parse` 方法][parse] 将字符串解析成数字。因为这个方法可以解析多种数字类型,因此需要告诉 Rust 具体的数字类型,这里通过 `let guess: u32` 指定。`guess` 后面的冒号(`:`)告诉 Rust 我们指定了变量的类型。Rust 有一些内建的数字类型;`u32` 是一个无符号的 32 位整型。对于不大的正整数来说,它是不错的类型,第三章还会讲到其他数字类型。另外,程序中的 `u32` 注解以及与 `secret_number` 的比较,意味着 Rust 会推断出 `secret_number` 也是 `u32` 类型。现在可以使用相同类型比较两个值了! [parse]: https://doc.rust-lang.org/std/primitive.str.html#method.parse -`parse` 调用很容易产生错误。例如,字符串中包含 `A👍%`,就无法将其转换为一个数字。因此,`parse` 方法返回一个 `Result` 类型。像之前 “使用 `Result` 类型来处理潜在的错误” 讨论的 `read_line` 方法那样,再次按部就班的用 `expect` 方法处理即可。如果 `parse` 不能从字符串生成一个数字,返回一个 `Result::Err` 时,`expect` 会使游戏崩溃并打印附带的信息。如果 `parse` 成功地将字符串转换为一个数字,它会返回 `Result::Ok`,然后 `expect` 会返回 `Ok` 中的数字。 +`parse` 调用很容易产生错误。例如,字符串中包含 `A👍%`,就无法将其转换为一个数字。因此,`parse` 方法返回一个 `Result` 类型。像之前 “使用 `Result` 类型来处理潜在的错误” 讨论的 `read_line` 方法那样,再次按部就班的用 `expect` 方法处理即可。如果 `parse` 不能从字符串生成一个数字,返回一个 `Result` 的 `Err` 成员时,`expect` 会使游戏崩溃并打印附带的信息。如果 `parse` 成功地将字符串转换为一个数字,它会返回 `Result` 的 `Ok` 成员,然后 `expect` 会返回 `Ok` 值中的数字。 现在让我们运行程序! @@ -552,7 +550,7 @@ Too big! } ``` -如上所示,我们将提示用户猜测之后的所有内容放入了循环。确保 loop 循环中的代码多缩进四个空格,再次运行程序。注意这里有一个新问题,因为程序忠实地执行了我们的要求:永远地请求另一个猜测,用户好像没法退出啊! +如上所示,我们将提示用户猜测之后的所有内容放入了循环。确保 loop 循环中的代码多缩进四个空格,再次运行程序。注意这里有一个新问题,因为程序忠实地执行了我们的要求:永远地请求另一个猜测,用户好像无法退出啊! 用户总能使用 ctrl-c 终止程序。不过还有另一个方法跳出无限循环,就是 “比较猜测与秘密数字” 部分提到的 `parse`:如果用户输入的答案不是一个数字,程序会崩溃。用户可以利用这一点来退出,如下所示: @@ -666,8 +664,6 @@ You win! 文件名: src/main.rs ```rust,ignore -extern crate rand; - use std::io; use std::cmp::Ordering; use rand::Rng; diff --git a/src/ch03-00-common-programming-concepts.md b/src/ch03-00-common-programming-concepts.md index 0c5ea69..fa9f844 100644 --- a/src/ch03-00-common-programming-concepts.md +++ b/src/ch03-00-common-programming-concepts.md @@ -1,13 +1,41 @@ # 通用编程概念 -> [ch03-00-common-programming-concepts.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch03-00-common-programming-concepts.md) +> [ch03-00-common-programming-concepts.md](https://github.com/rust-lang/book/blob/master/src/ch03-00-common-programming-concepts.md) >
-> commit b64de01431cdf1020ad3358d2f83e46af68a39ed +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 本章介绍一些几乎所有编程语言都有的概念,以及它们在 Rust 中是如何工作的。很多编程语言的核心概念都是共通的,本章中展示的概念都不是 Rust 所特有的,不过我们会在 Rust 上下文中讨论它们,并解释使用这些概念的惯例。 具体来说,我们将会学习变量、基本类型、函数、注释和控制流。每一个 Rust 程序中都会用到这些基础知识,提早学习这些概念会让你在起步时就打下坚实的基础。 -> ### 关键字 -> -> Rust 语言有一组保留的 **关键字**(*keywords*),就像大部分语言一样,它们只能由语言本身使用。记住,你不能使用这些关键字作为变量或函数的名称。大部分关键字有特殊的意义,你将在你的 Rust 程序中使用它们完成各种任务;一些关键字目前没有相应的功能,是为将来可能添加的功能保留的。可以在附录 A 中找到关键字的列表。 +## 关键字 + +Rust 语言有一组保留的 **关键字**(*keywords*),就像大部分语言一样,它们只能由语言本身使用。记住,你不能使用这些关键字作为变量或函数的名称。大部分关键字有特殊的意义,你将在 Rust 程序中使用它们完成各种任务;一些关键字目前没有相应的功能,是为将来可能添加的功能保留的。可以在附录 A 中找到关键字的列表。 + +## 标识符 + +这里我们将对本书中的一些概念做一些解释:变量、函数、结构体等等。所有这些都需要名称。Rust 中的名称被称为 “标识符”(“identifier”),它们可以是任意非空的 ASCII 字符串,不过有如下限制: + +要么是: + +* 第一个字符是字母。 +* 其它字符是字母数字或者 _。 + +或者是: + +* 第一个字符是 _。 +* 标识符需多于一个字符。单独的 _ 不是标识符。 +* 其它字符是字母数字或者 _。 + +### 原始标识符 + +有时出于某种原因你可能需要将关键字作为名称。比如你需要调用 C 语言库中名为 *match* 的函数,在 C 语言中 *match* 不是关键字。为此你可以使用 “原始标识符”(“raw identifier”)。原始标识符以 `r#` 开头: + +```rust,ignore +let r#fn = "this variable is named 'fn' even though that's a keyword"; + +// 调用名为 'match' 的函数 +r#match(); +``` + +你无需经常用到原始标识符,但是当你 **真正** 需要它们时可以这么做。 \ No newline at end of file diff --git a/src/ch03-01-variables-and-mutability.md b/src/ch03-01-variables-and-mutability.md index 68af919..f5ec328 100644 --- a/src/ch03-01-variables-and-mutability.md +++ b/src/ch03-01-variables-and-mutability.md @@ -1,18 +1,18 @@ ## 变量和可变性 -> [ch03-01-variables-and-mutability.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch03-01-variables-and-mutability.md) +> [ch03-01-variables-and-mutability.md](https://github.com/rust-lang/book/blob/master/src/ch03-01-variables-and-mutability.md) >
-> commit f949ff883628db8ed2f2f5f19e146ebf19ed6a6f +> commit a86c1d315789b3ca13b20d50ad5005c62bdd9e37 -第二章中提到过,变量默认是不可改变的(immutable)。这是推动你以充分利用 Rust 提供的安全性和简单并发性编写代码的众多方式之一。不过,你仍然可以使用可变变量。让我们探讨一下 Rust 拥抱不可变性的原因及方法,以及何时你不想使用不可变性。 +第二章中提到过,变量默认是不可改变的(immutable)。这是推动你以充分利用 Rust 提供的安全性和简单并发性来编写代码的众多方式之一。不过,你仍然可以使用可变变量。让我们探讨一下 Rust 拥抱不可变性的原因及方法,以及何时你不想使用不可变性。 -当变量不可变时,一旦值被绑定一个名称上,你就不能改变这个值。为了对此进行说明,使用 `cargo new --bin variables` 命令在 *projects* 目录生成一个叫做 *variables* 的新项目。 +当变量不可变时,一旦值被绑定一个名称上,你就不能改变这个值。为了对此进行说明,使用 `cargo new variables` 命令在 *projects* 目录生成一个叫做 *variables* 的新项目。 接着,在新建的 *variables* 目录,打开 *src/main.rs* 并将代码替换为如下代码,这些代码还不能编译: 文件名: src/main.rs -```rust,ignore +```rust,ignore,does_not_compile fn main() { let x = 5; println!("The value of x is: {}", x); @@ -40,7 +40,7 @@ error[E0384]: cannot assign twice to immutable variable `x` 在尝试改变预设为不可变的值时,产生编译时错误是很重要的,因为这种情况可能导致 bug。如果一部分代码假设一个值永远也不会改变,而另一部分代码改变了这个值,第一部分代码就有可能以不可预料的方式运行。不得不承认这种 bug 的起因难以跟踪,尤其是第二部分代码只是 **有时** 会改变值。 -Rust 编译器保证,如果声明一个值不会变,它就真的不会变。这意味着当阅读和编写代码时,不需要追踪一个值如何以及哪里可能会被改变,从而使得代码易于推导。 +Rust 编译器保证,如果声明一个值不会变,它就真的不会变。这意味着当阅读和编写代码时,不需要追踪一个值如何和在哪可能会被改变,从而使得代码易于推导。 不过可变性也是非常有用的。变量只是默认不可变;正如在第二章所做的那样,你可以在变量名之前加 `mut` 来使其可变。除了允许改变值之外,`mut` 向读者表明了其他代码将会改变这个变量值的意图。 @@ -84,7 +84,7 @@ The value of x is: 6 最后一个区别是,常量只能被设置为常量表达式,而不能是函数调用的结果,或任何其他只能在运行时计算出的值。 -这是一个声明常量的例子,它的名称是 `MAX_POINTS`,值是 100,000。(Rust 常量的命名规范是使用下划线分隔的大写字母): +这是一个声明常量的例子,它的名称是 `MAX_POINTS`,值是 100,000。(Rust 常量的命名规范是使用下划线分隔的大写字母单词,并且可以在数字字面值中插入下划线来提升可读性): ```rust const MAX_POINTS: u32 = 100_000; @@ -133,7 +133,7 @@ let spaces = spaces.len(); 这里允许第一个 `spaces` 变量是字符串类型,而第二个 `spaces` 变量,它是一个恰巧与第一个变量同名的崭新变量,是数字类型。隐藏使我们不必使用不同的名字,如 `spaces_str` 和 `spaces_num`;相反,我们可以复用 `spaces` 这个更简单的名字。然而,如果尝试使用 `mut`,将会得到一个编译时错误,如下所示: -```rust,ignore +```rust,ignore,does_not_compile let mut spaces = " "; spaces = spaces.len(); ``` diff --git a/src/ch03-02-data-types.md b/src/ch03-02-data-types.md index 7c6900a..e69bfd4 100644 --- a/src/ch03-02-data-types.md +++ b/src/ch03-02-data-types.md @@ -1,10 +1,10 @@ ## 数据类型 -> [ch03-02-data-types.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch03-02-data-types.md) +> [ch03-02-data-types.md](https://github.com/rust-lang/book/blob/master/src/ch03-02-data-types.md) >
-> commit f949ff883628db8ed2f2f5f19e146ebf19ed6a6f +> commit a86c1d315789b3ca13b20d50ad5005c62bdd9e37 -在 Rust 中,每一个值都属于某一个 **数据类型**(*data type*),这告诉 Rust 它被指定为何种数据,以便明确数据处理方式。我们将看到两类数据类型:标量(scalar)和复合(compound)。 +在 Rust 中,每一个值都属于某一个 **数据类型**(*data type*),这告诉 Rust 它被指定为何种数据,以便明确数据处理方式。我们将看到两类数据类型子集:标量(scalar)和复合(compound)。 记住,Rust 是 **静态类型**(*statically typed*)语言,也就是说在编译时就必须知道所有变量的类型。根据值及其使用方式,编译器通常可以推断出我们想要用的类型。当多种类型均有可能时,比如第二章的 “比较猜测的数字和秘密数字” 使用 `parse` 将 `String` 转换为数字时,必须增加类型注解,像这样: @@ -65,6 +65,12 @@ error[E0282]: type annotations needed 那么该使用哪种类型的数字呢?如果拿不定主意,Rust 的默认类型通常就很好,数字类型默认是 `i32`:它通常是最快的,甚至在 64 位系统上也是。`isize` 或 `usize` 主要作为某些集合的索引。 +##### 整型溢出 + +比方说有一个 `u8` ,它可以存放从零到 `255` 的值。那么当你将其修改为 `256` 时会发生什么呢?这被称为 “整型溢出”(“integer overflow” ),关于这一行为 Rust 有一些有趣的规则。当在 debug 模式编译时,Rust 检查这类问题并使程序 *panic*,这个术语被 Rust 用来表明程序因错误而退出。第九章会详细介绍 panic。 + +在 release 构建中,Rust 不检测溢出,相反会进行一种被称为 “two’s complement wrapping” 的操作。简而言之,`256` 变成 `0`,`257` 变成 `1`,依此类推。依赖溢出被认为是一种错误,即便可能出现这种行为。如果你确实需要这种行为,标准库中有一个类型显式提供此功能,`Wrapping`。 + #### 浮点型 Rust 也有两个原生的 **浮点数**(*floating-point numbers*)类型,它们是带小数点的数字。Rust 的浮点数类型是 `f32` 和 `f64`,分别占 32 位和 64 位。默认类型是 `f64`,因为在现代 CPU 中,它与 `f32` 速度几乎一样,不过精度更高。 @@ -217,6 +223,14 @@ let months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; ``` +数组的类型比较有趣;它看起来像 `[type; number]`。例如: + +```rust +let a: [i32; 5] = [1, 2, 3, 4, 5]; +``` + +首先是方括号;这看起来像创建数组的语法。其中有两部分由分号分割的信息。第一部分是数组中每个元素的类型。因为所有元素都是相同类型的,所以只需列出一次。分号之后,是一个表示数组长度的数字。因为数组是固定长度的,该数字也一直保持不变,即便数组的元素被修改,它也不会增长火缩小。 + ##### 访问数组元素 数组是一整块分配在栈上的内存。可以使用索引来访问数组的元素,像这样: From f8fe51658aff648b05fe606292e6ae4d5375e53e Mon Sep 17 00:00:00 2001 From: KaiserY Date: Sat, 1 Dec 2018 21:13:43 +0800 Subject: [PATCH 10/49] check to ch04-00 --- src/ch03-03-how-functions-work.md | 10 ++++----- src/ch03-04-comments.md | 4 ++-- src/ch03-05-control-flow.md | 30 +++++++++++++++++++++----- src/ch04-00-understanding-ownership.md | 4 ++-- 4 files changed, 34 insertions(+), 14 deletions(-) diff --git a/src/ch03-03-how-functions-work.md b/src/ch03-03-how-functions-work.md index 6035378..987e945 100644 --- a/src/ch03-03-how-functions-work.md +++ b/src/ch03-03-how-functions-work.md @@ -1,8 +1,8 @@ ## 函数 -> [ch03-03-how-functions-work.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch03-03-how-functions-work.md) +> [ch03-03-how-functions-work.md](https://github.com/rust-lang/book/blob/master/src/ch03-03-how-functions-work.md) >
-> commit f949ff883628db8ed2f2f5f19e146ebf19ed6a6f +> commit a86c1d315789b3ca13b20d50ad5005c62bdd9e37 函数遍布于 Rust 代码中。你已经见过语言中最重要的函数之一:`main` 函数,它是很多程序的入口点。你也见过 `fn` 关键字,它用来声明新函数。 @@ -41,7 +41,7 @@ Another function. ### 函数参数 -函数也可以被定义为拥有 **参数**(*parameters*),参数是特殊变量,是函数签名的一部分。当函数拥有参数(形参)时,可以为这些参数提供具体的值(实参)。技术上讲,这些具体值被称为参数(*arguments*),但是在闲谈时,人们倾向于互换着使用 *parameter* 和 *argument* 来表示函数定义中的变量或调用函数时传入的具体值。 +函数也可以被定义为拥有 **参数**(*parameters*),参数是特殊变量,是函数签名的一部分。当函数拥有参数(形参)时,可以为这些参数提供具体的值(实参)。技术上讲,这些具体值被称为参数(*arguments*),但是在日常交流中,人们倾向于不区分使用 *parameter* 和 *argument* 来表示函数定义中的变量或调用函数时传入的具体值。 下面被重写的 `another_function` 版本展示了 Rust 中参数是什么样的: @@ -125,7 +125,7 @@ fn main() { 文件名: src/main.rs -```rust,ignore +```rust,ignore,does_not_compile fn main() { let x = (let y = 6); } @@ -231,7 +231,7 @@ fn plus_one(x: i32) -> i32 { 文件名: src/main.rs -```rust,ignore +```rust,ignore,does_not_compile fn main() { let x = plus_one(5); diff --git a/src/ch03-04-comments.md b/src/ch03-04-comments.md index 2aecde8..84bcd55 100644 --- a/src/ch03-04-comments.md +++ b/src/ch03-04-comments.md @@ -1,8 +1,8 @@ ## 注释 -> [ch03-04-comments.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch03-04-comments.md) +> [ch03-04-comments.md](https://github.com/rust-lang/book/blob/master/src/ch03-04-comments.md) >
-> commit f949ff883628db8ed2f2f5f19e146ebf19ed6a6f +> commit a86c1d315789b3ca13b20d50ad5005c62bdd9e37 所有程序员都力求使其代码易于理解,不过有时还需要提供额外的解释。在这种情况下,程序员在源码中留下记录,或者 **注释**(*comments*),编译器会忽略它们,不过阅读代码的人可能觉得有用。 diff --git a/src/ch03-05-control-flow.md b/src/ch03-05-control-flow.md index c0295d7..2494982 100644 --- a/src/ch03-05-control-flow.md +++ b/src/ch03-05-control-flow.md @@ -1,8 +1,8 @@ ## 控制流 -> [ch03-05-control-flow.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch03-05-control-flow.md) +> [ch03-05-control-flow.md](https://github.com/rust-lang/book/blob/master/src/ch03-05-control-flow.md) >
-> commit f949ff883628db8ed2f2f5f19e146ebf19ed6a6f +> commit a86c1d315789b3ca13b20d50ad5005c62bdd9e37 根据条件是否为真来决定是否执行某些代码,以及根据条件是否为真来重复运行一段代码是大部分编程语言的基本组成部分。Rust 代码中最常见的用来控制执行流的结构是 `if` 表达式和循环。 @@ -62,7 +62,7 @@ condition was false 文件名: src/main.rs -```rust,ignore +```rust,ignore,does_not_compile fn main() { let number = 3; @@ -172,7 +172,7 @@ The value of number is: 5 文件名: src/main.rs -```rust,ignore +```rust,ignore,does_not_compile fn main() { let condition = true; @@ -246,6 +246,26 @@ again! 幸运的是,Rust 提供了另一种更可靠的退出循环的方式。可以使用 `break` 关键字来告诉程序何时停止循环。回忆一下在第二章猜猜看游戏的 “猜测正确后退出” 部分使用过它来在用户猜对数字赢得游戏后退出程序。 +#### 从循环返回 + +`loop` 的一个用例是重试可能会失败的操作,比如检查线程是否完成了任务。然而你可能会需要将操作的结果传递给其它的代码。如果将返回值加入你用来停止循环的 `break` 表达式,它会被停止的循环返回: + +```rust +fn main() { + let mut counter = 0; + + let result = loop { + counter += 1; + + if counter == 10 { + break counter * 2; + } + }; + + assert_eq!(result, 20); +} +``` + #### `while` 条件循环 在程序中计算循环的条件也很常见。当条件为真,执行循环。当条件不再为真,调用 `break` 停止循环。这个循环类型可以通过组合 `loop`、`if`、`else` 和 `break` 来实现;如果你喜欢的话,现在就可以在程序中试试。 @@ -270,7 +290,7 @@ fn main() { 示例 3-3: 当条件为真时,使用 `while` 循环运行代码 -这种结构消除了使用 `loop`、`if`、`else` 和 `break` 时必须有的很多嵌套,代码更加清晰。当条件为真就执行,否则退出循环。 +这种结构消除了很多使用 `loop`、`if`、`else` 和 `break` 时所必须的嵌套,这样更加清晰。当条件为真就执行,否则退出循环。 #### 使用 `for` 遍历集合 diff --git a/src/ch04-00-understanding-ownership.md b/src/ch04-00-understanding-ownership.md index d0045c9..327c43c 100644 --- a/src/ch04-00-understanding-ownership.md +++ b/src/ch04-00-understanding-ownership.md @@ -1,7 +1,7 @@ # 认识所有权 -> [ch04-00-understanding-ownership.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch04-00-understanding-ownership.md) +> [ch04-00-understanding-ownership.md](https://github.com/rust-lang/book/blob/master/src/ch04-00-understanding-ownership.md) >
-> commit aff4f619c4d6dc138b57b74c3a898ba9bce06649 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 所有权(系统)是 Rust 最独特的功能,其令 Rust 无需垃圾回收(garbage collector)即可保障内存安全。因此,理解 Rust 中所有权如何工作是十分重要的。本章,我们将讲到所有权以及相关功能:借用、slice 以及 Rust 如何在内存中布局数据。 \ No newline at end of file From 8fbfa72b0b11d46dd2dc5e526936c172c974be2a Mon Sep 17 00:00:00 2001 From: Zhao Sizhe Date: Sun, 2 Dec 2018 10:10:25 +0800 Subject: [PATCH 11/49] fix typo --- src/ch03-02-data-types.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ch03-02-data-types.md b/src/ch03-02-data-types.md index e69bfd4..4c7fdc3 100644 --- a/src/ch03-02-data-types.md +++ b/src/ch03-02-data-types.md @@ -229,7 +229,7 @@ let months = ["January", "February", "March", "April", "May", "June", "July", let a: [i32; 5] = [1, 2, 3, 4, 5]; ``` -首先是方括号;这看起来像创建数组的语法。其中有两部分由分号分割的信息。第一部分是数组中每个元素的类型。因为所有元素都是相同类型的,所以只需列出一次。分号之后,是一个表示数组长度的数字。因为数组是固定长度的,该数字也一直保持不变,即便数组的元素被修改,它也不会增长火缩小。 +首先是方括号;这看起来像创建数组的语法。其中有两部分由分号分割的信息。第一部分是数组中每个元素的类型。因为所有元素都是相同类型的,所以只需列出一次。分号之后,是一个表示数组长度的数字。因为数组是固定长度的,该数字也一直保持不变,即便数组的元素被修改,它也不会增长或缩小。 ##### 访问数组元素 From 257515819d40c94783143105d856b0ac40255675 Mon Sep 17 00:00:00 2001 From: KaiserY Date: Sun, 2 Dec 2018 21:03:07 +0800 Subject: [PATCH 12/49] check to ch05-00 --- src/ch04-01-what-is-ownership.md | 12 ++--- src/ch04-02-references-and-borrowing.md | 63 +++++++++++++------------ src/ch04-03-slices.md | 42 +++++++++++------ src/ch05-00-structs.md | 4 +- 4 files changed, 69 insertions(+), 52 deletions(-) diff --git a/src/ch04-01-what-is-ownership.md b/src/ch04-01-what-is-ownership.md index ddf5a9c..132cccc 100644 --- a/src/ch04-01-what-is-ownership.md +++ b/src/ch04-01-what-is-ownership.md @@ -1,8 +1,8 @@ ## 什么是所有权 -> [ch04-01-what-is-ownership.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch04-01-what-is-ownership.md) +> [ch04-01-what-is-ownership.md](https://github.com/rust-lang/book/blob/master/src/ch04-01-what-is-ownership.md) >
-> commit f949ff883628db8ed2f2f5f19e146ebf19ed6a6f +> commit a86c1d315789b3ca13b20d50ad5005c62bdd9e37 Rust 的核心功能(之一)是 **所有权**(*ownership*)。虽然该功能很容易解释,但它对语言的其他部分有着深刻的影响。 @@ -165,11 +165,11 @@ let s2 = s1; 图 4-3:另一个 `s2 = s1` 时可能的内存表现,如果 Rust 同时也拷贝了堆上的数据的话 -之前,我们提到过当变量离开作用域后,Rust 自动调用 `drop` 函数并清理变量的堆内存。不过图 4-2 展示了两个数据指针指向了同一位置。这就有了一个问题:当 `s2` 和 `s1` 离开作用域,他们都会尝试释放相同的内存。这是一个叫做 **二次释放**(*double free*)的错误,也是之前提到过的内存安全性 bug 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。 +之前我们提到过当变量离开作用域后,Rust 自动调用 `drop` 函数并清理变量的堆内存。不过图 4-2 展示了两个数据指针指向了同一位置。这就有了一个问题:当 `s2` 和 `s1` 离开作用域,他们都会尝试释放相同的内存。这是一个叫做 **二次释放**(*double free*)的错误,也是之前提到过的内存安全性 bug 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。 -为了确保内存安全,这种场景下 Rust 的处理有另一个细节值得注意。与其尝试拷贝被分配的内存,Rust 则认为 `s1` 不再有效,因此 Rust 不需要在 `s1` 离开作用域后清理任何东西。看看在 `s2` 被创建之后尝试使用 `s1` 会发生什么;它不会工作: +为了确保内存安全,这种场景下 Rust 的处理有另一个细节值得注意。与其尝试拷贝被分配的内存,Rust 则认为 `s1` 不再有效,因此 Rust 不需要在 `s1` 离开作用域后清理任何东西。看看在 `s2` 被创建之后尝试使用 `s1` 会发生什么;这段代码不能运行: -```rust,ignore +```rust,ignore,does_not_compile let s1 = String::from("hello"); let s2 = s1; @@ -254,7 +254,7 @@ Rust 有一个叫做 `Copy` trait 的特殊注解,可以用在类似整型这 fn main() { let s = String::from("hello"); // s 进入作用域 - takes_ownership(s); // s 的值移动到函数里... + takes_ownership(s); // s 的值移动到函数里 ... // ... 所以到这里不再有效 let x = 5; // x 进入作用域 diff --git a/src/ch04-02-references-and-borrowing.md b/src/ch04-02-references-and-borrowing.md index 446512b..d0be48c 100644 --- a/src/ch04-02-references-and-borrowing.md +++ b/src/ch04-02-references-and-borrowing.md @@ -1,8 +1,8 @@ ## 引用与借用 -> [ch04-02-references-and-borrowing.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch04-02-references-and-borrowing.md) +> [ch04-02-references-and-borrowing.md](https://github.com/rust-lang/book/blob/master/src/ch04-02-references-and-borrowing.md) >
-> commit f949ff883628db8ed2f2f5f19e146ebf19ed6a6f +> commit a86c1d315789b3ca13b20d50ad5005c62bdd9e37 示例 4-5 中的元组代码有这样一个问题:我们必须将 `String` 返回给调用函数,以便在调用 `calculate_length` 后仍能使用 `String`,因为 `String` 被移动到了 `calculate_length` 内。 @@ -64,7 +64,7 @@ fn calculate_length(s: &String) -> usize { // s 是对 String 的引用 文件名: src/main.rs -```rust,ignore +```rust,ignore,does_not_compile fn main() { let s = String::from("hello"); @@ -114,25 +114,30 @@ fn change(some_string: &mut String) { 不过可变引用有一个很大的限制:在特定作用域中的特定数据有且只有一个可变引用。这些代码会失败: -```rust,ignore +文件名: src/main.rs + +```rust,ignore,does_not_compile let mut s = String::from("hello"); let r1 = &mut s; let r2 = &mut s; + +println!("{}, {}", r1, r2); ``` + 错误如下: ```text error[E0499]: cannot borrow `s` as mutable more than once at a time - --> borrow_twice.rs:5:19 + --> src/main.rs:5:10 | -4 | let r1 = &mut s; - | - first mutable borrow occurs here -5 | let r2 = &mut s; - | ^ second mutable borrow occurs here -6 | } - | - first borrow ends here +4 | let r1 = &mut s; + | ------ first mutable borrow occurs here +5 | let r2 = &mut s; + | ^^^^^^ second mutable borrow occurs here +6 | println!("{}, {}", r1, r2); + | -- borrow later used here ``` 这个限制允许可变性,不过是以一种受限制的方式允许。新 Rustacean 们经常与此作斗争,因为大部分语言中变量任何时候都是可变的。 @@ -160,28 +165,30 @@ let r2 = &mut s; 类似的规则也存在于同时使用可变与不可变引用中。这些代码会导致一个错误: -```rust,ignore +```rust,ignore,does_not_compile let mut s = String::from("hello"); let r1 = &s; // no problem let r2 = &s; // no problem let r3 = &mut s; // BIG PROBLEM + +println!("{}, {}, and {}", r1, r2, r3); ``` 错误如下: ```text -error[E0502]: cannot borrow `s` as mutable because it is also borrowed as -immutable - --> borrow_thrice.rs:6:19 +error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable + --> src/main.rs:6:10 | -4 | let r1 = &s; // no problem - | - immutable borrow occurs here -5 | let r2 = &s; // no problem -6 | let r3 = &mut s; // BIG PROBLEM - | ^ mutable borrow occurs here -7 | } - | - immutable borrow ends here +4 | let r1 = &s; // no problem + | -- immutable borrow occurs here +5 | let r2 = &s; // no problem +6 | let r3 = &mut s; // BIG PROBLEM + | ^^^^^^ mutable borrow occurs here +7 | +8 | println!("{}, {}, and {}", r1, r2, r3); + | -- borrow later used here ``` 哇哦!我们 **也** 不能在拥有不可变引用的同时拥有可变引用。不可变引用的用户可不希望在他们的眼皮底下值就被意外的改变了!然而,多个不可变引用是可以的,因为没有哪个只能读取数据的人有能力影响其他人读取到的数据。 @@ -196,7 +203,7 @@ immutable 文件名: src/main.rs -```rust,ignore +```rust,ignore,does_not_compile fn main() { let reference_to_nothing = dangle(); } @@ -231,15 +238,13 @@ for it to be borrowed from. 让我们仔细看看我们的 `dangle` 代码的每一步到底发生了什么: -Filename: src/main.rs - ```rust,ignore -fn dangle() -> &String { // dangle 返回一个 String 引用 +fn dangle() -> &String { // dangle 返回一个字符串的引用 - let s = String::from("hello"); // s 是新创建的 String + let s = String::from("hello"); // s 是一个新字符串 - &s // 我们返回 String 的引用,s -} // 这里,s 移出了作用域,并被丢弃。它的内存被释放 + &s // 返回字符串 s 的引用 +} // 这里 s 离开作用域并被丢弃。其内存被释放。 // 危险! ``` diff --git a/src/ch04-03-slices.md b/src/ch04-03-slices.md index c4f5a58..ee936f8 100644 --- a/src/ch04-03-slices.md +++ b/src/ch04-03-slices.md @@ -1,8 +1,8 @@ ## Slice 类型 -> [ch04-03-slices.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch04-03-slices.md) +> [ch04-03-slices.md](https://github.com/rust-lang/book/blob/master/src/ch04-03-slices.md) >
-> commit 729f1118332ddcf01138d284eac5db0e85f8c8ed +> commit a86c1d315789b3ca13b20d50ad5005c62bdd9e37 另一个没有所有权的数据类型是 *slice*。slice 允许你引用集合中一段连续的元素序列,而不用引用整个集合。 @@ -115,9 +115,18 @@ let hello = &s[0..5]; let world = &s[6..11]; ``` -这类似于引用整个 `String` 不过带有额外的 `[0..5]` 部分。不是对整个 `String` 的引用,而是对部分 `String` 的引用。`start..end` 语法代表一个以 `start` 开头并一直持续到但不包含 `end` 的 range。 +这类似于引用整个 `String` 不过带有额外的 `[0..5]` 部分。它不是对整个 `String` 的引用,而是对部分 `String` 的引用。`start..end` 语法代表一个以 `start` 开头并一直持续到但不包含 `end` 的 range。如果需要包含 `end`,可以使用 `..=` 而不是 `..`: -使用一个由中括号中的 `[starting_index..ending_index]` 指定的 range 创建一个 slice,其中 `starting_index` 是 slice 的第一个位置,`ending_index` 则是 slice 最后一个位置的后一个值。在其内部,slice 的数据结构存储了 slice 的开始位置和长度,长度对应于 `ending_index` 减去 `starting_index` 的值。所以对于 `let world = &s[6..11];` 的情况,`world` 将是一个包含指向 `s` 第 7 个字节的指针和长度值 5 的 slice。 +```rust +let s = String::from("hello world"); + +let hello = &s[0..=4]; +let world = &s[6..=10]; +``` + +`=` 意味着包含最后的数字,这有助于你记住 `..` 与 `..=` 的区别 + +可以使用一个由中括号中的 `[starting_index..ending_index]` 指定的 range 创建一个 slice,其中 `starting_index` 是 slice 的第一个位置,`ending_index` 则是 slice 最后一个位置的后一个值。在其内部,slice 的数据结构存储了 slice 的开始位置和长度,长度对应于 `ending_index` 减去 `starting_index` 的值。所以对于 `let world = &s[6..11];` 的情况,`world` 将是一个包含指向 `s` 第 7 个字节的指针和长度值 5 的 slice。 图 4-6 展示了一个图例。 @@ -190,13 +199,15 @@ fn second_word(s: &String) -> &str { 文件名: src/main.rs -```rust,ignore +```rust,ignore,does_not_compile fn main() { let mut s = String::from("hello world"); let word = first_word(&s); s.clear(); // error! + + println!("the first word is: {}", word); } ``` @@ -204,15 +215,16 @@ fn main() { ```text error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable - --> src/main.rs:6:5 - | -4 | let word = first_word(&s); - | - immutable borrow occurs here -5 | -6 | s.clear(); // error! - | ^ mutable borrow occurs here -7 | } - | - immutable borrow ends here + --> src/main.rs:10:5 + | +8 | let word = first_word(&s); + | -- immutable borrow occurs here +9 | +10 | s.clear(); // error! + | ^^^^^^^^^ mutable borrow occurs here +11 | +12 | println!("the first word is: {}", word); + | ---- borrow later used here ``` 回忆一下借用规则,当拥有某值的不可变引用时,就不能再获取一个可变引用。因为 `clear` 需要清空 `String`,它尝试获取一个可变引用,它失败了。Rust 不仅使得我们的 API 简单易用,也在编译时就消除了一整类的错误! @@ -245,7 +257,7 @@ fn first_word(s: &str) -> &str { 如果有一个字符串 slice,可以直接传递它。如果有一个 `String`,则可以传递整个 `String` 的 slice。定义一个获取字符串 slice 而不是 `String` 引用的函数使得我们的 API 更加通用并且不会丢失任何功能: -Filename: src/main.rs +文件名: src/main.rs ```rust # fn first_word(s: &str) -> &str { diff --git a/src/ch05-00-structs.md b/src/ch05-00-structs.md index fb9c764..9b07278 100644 --- a/src/ch05-00-structs.md +++ b/src/ch05-00-structs.md @@ -1,7 +1,7 @@ # 使用结构体组织相关联的数据 -> [ch05-00-structs.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch05-00-structs.md) +> [ch05-00-structs.md](https://github.com/rust-lang/book/blob/master/src/ch05-00-structs.md) >
-> commit 5e2368866cb445d912f5d2a2c92f4b42bcb542bb +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f *struct*,或者 *structure*,是一个自定义数据类型,允许你命名和包装多个相关的值,从而形成一个有意义的组合。如果你熟悉一门面向对象语言,*struct* 就像对象中的数据属性。在本章中,我们会对比元组与结构体的异同,演示结构体的用法,并讨论如何在结构体上定义方法和关联函数来指定与结构体数据相关的行为。你在程序中基于结构体和枚举(*enum*)(在第六章介绍)创建新类型,以充分利用 Rust 的编译时类型检查。 From fb1b8919394957b9b3a87a648e95c2a873f520dc Mon Sep 17 00:00:00 2001 From: Azureki <22387028+Azureki@users.noreply.github.com> Date: Mon, 3 Dec 2018 19:11:11 +0800 Subject: [PATCH 13/49] =?UTF-8?q?=E6=B7=BB=E5=8A=A0extern=20crate=20rand;?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ch02-00-guessing-game-tutorial.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ch02-00-guessing-game-tutorial.md b/src/ch02-00-guessing-game-tutorial.md index 7c43602..313feb5 100644 --- a/src/ch02-00-guessing-game-tutorial.md +++ b/src/ch02-00-guessing-game-tutorial.md @@ -349,6 +349,8 @@ rand = "0.4.0" 文件名: src/main.rs ```rust,ignore +extern crate rand; + use std::io; use rand::Rng; From 6f9c0d89a67c533eb5acce5d24d70af0e909f39e Mon Sep 17 00:00:00 2001 From: KaiserY Date: Mon, 3 Dec 2018 21:10:59 +0800 Subject: [PATCH 14/49] update ch02-00 --- src/ch02-00-guessing-game-tutorial.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/ch02-00-guessing-game-tutorial.md b/src/ch02-00-guessing-game-tutorial.md index 313feb5..4ce9676 100644 --- a/src/ch02-00-guessing-game-tutorial.md +++ b/src/ch02-00-guessing-game-tutorial.md @@ -349,8 +349,6 @@ rand = "0.4.0" 文件名: src/main.rs ```rust,ignore -extern crate rand; - use std::io; use rand::Rng; @@ -374,7 +372,7 @@ fn main() { 示例 2-3:添加生成随机数的代码 -首先,这里在顶部增加一行 `extern crate rand;` 通知 Rust 我们要使用外部依赖 `rand`。这也会调用相应的 `use rand`,所以现在可以使用 `rand::` 前缀来调用 `rand` crate 中的任何内容。 +首先,这里新增了一行通知 Rust 我们要使用外部依赖 `rand`。这等同于调用 `use rand`,所以现在可以使用 `rand::` 前缀来调用 `rand` crate 中的任何内容。 接下来增加了另一行 `use`:`use rand::Rng`。`Rng` 是一个 trait,它定义了随机数生成器应实现的方法,想使用这些方法的话,此 trait 必须在作用域中。第十章会详细介绍 trait。 @@ -413,9 +411,7 @@ You guessed: 5 文件名: src/main.rs -```rust,ignore -extern crate rand; - +```rust,ignore,does_not_compile use std::io; use std::cmp::Ordering; use rand::Rng; From cecbf32d79e78d3aed5fe3ff1917739e19e690d1 Mon Sep 17 00:00:00 2001 From: KaiserY Date: Mon, 3 Dec 2018 22:48:24 +0800 Subject: [PATCH 15/49] check to ch06-03 --- src/ch05-01-defining-structs.md | 8 ++++---- src/ch05-02-example-structs.md | 8 ++++---- src/ch05-03-method-syntax.md | 4 ++-- src/ch06-00-enums.md | 4 ++-- src/ch06-01-defining-an-enum.md | 8 ++++---- src/ch06-02-match.md | 18 ++++++++---------- src/ch06-03-if-let.md | 8 ++++---- 7 files changed, 28 insertions(+), 30 deletions(-) diff --git a/src/ch05-01-defining-structs.md b/src/ch05-01-defining-structs.md index 13805b6..56bf7e7 100644 --- a/src/ch05-01-defining-structs.md +++ b/src/ch05-01-defining-structs.md @@ -1,8 +1,8 @@ ## 定义并实例化结构体 -> [ch05-01-defining-structs.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch05-01-defining-structs.md) +> [ch05-01-defining-structs.md](https://github.com/rust-lang/book/blob/master/src/ch05-01-defining-structs.md) >
-> commit c560db1e0145d5a64b9415c9cfe463c7dac31ab8 +> commit a86c1d315789b3ca13b20d50ad5005c62bdd9e37 结构体和我们在第三章讨论过的元组类似。和元组一样,结构体的每一部分可以是不同类型。但不同于元组,结构体需要命名各部分数据以便能清楚的表明其值的意义。由于有了这些名字,结构体比元组更灵活:不需要依赖顺序来指定或访问实例中的值。 @@ -192,7 +192,7 @@ let origin = Point(0, 0, 0); 我们也可以定义一个没有任何字段的结构体!它们被称为 **类单元结构体**(*unit-like structs*)因为它们类似于 `()`,即 unit 类型。类单元结构体常常在你想要在某个类型上实现 trait 但不需要在类型中存储数据的时候发挥作用。我们将在第十章介绍 trait。 -> ## 结构体数据的所有权 +> ### 结构体数据的所有权 > > 在示例 5-1 中的 `User` 结构体的定义中,我们使用了自身拥有所有权的 `String` 类型而不是 `&str` 字符串 slice 类型。这是一个有意而为之的选择,因为我们想要这个结构体拥有它所有的数据,为此只要整个结构体是有效的话其数据也是有效的。 > @@ -200,7 +200,7 @@ let origin = Point(0, 0, 0); > > 文件名: src/main.rs > -> ```rust,ignore +> ```rust,ignore,does_not_compile > struct User { > username: &str, > email: &str, diff --git a/src/ch05-02-example-structs.md b/src/ch05-02-example-structs.md index 956845a..b010581 100644 --- a/src/ch05-02-example-structs.md +++ b/src/ch05-02-example-structs.md @@ -1,8 +1,8 @@ ## 一个使用结构体的示例程序 -> [ch05-02-example-structs.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch05-02-example-structs.md) +> [ch05-02-example-structs.md](https://github.com/rust-lang/book/blob/master/src/ch05-02-example-structs.md) >
-> commit c560db1e0145d5a64b9415c9cfe463c7dac31ab8 +> commit a86c1d315789b3ca13b20d50ad5005c62bdd9e37 为了理解何时会需要使用结构体,让我们编写一个计算长方形面积的程序。我们会从单独的变量开始,接着重构程序直到使用结构体替代他们为止。 @@ -111,7 +111,7 @@ fn area(rectangle: &Rectangle) -> u32 { 文件名: src/main.rs -```rust,ignore +```rust,ignore,does_not_compile struct Rectangle { width: u32, height: u32, @@ -149,7 +149,7 @@ error[E0277]: the trait bound `Rectangle: std::fmt::Display` is not satisfied error[E0277]: the trait bound `Rectangle: std::fmt::Debug` is not satisfied ``` -不过编译器又一次给出了一个有帮助的信息! +不过编译器又一次给出了一个有帮助的信息: ```text `Rectangle` cannot be formatted using `:?`; if it is defined in your diff --git a/src/ch05-03-method-syntax.md b/src/ch05-03-method-syntax.md index 2728c95..12ecdc7 100644 --- a/src/ch05-03-method-syntax.md +++ b/src/ch05-03-method-syntax.md @@ -1,8 +1,8 @@ ## 方法语法 -> [ch05-03-method-syntax.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch05-03-method-syntax.md) +> [ch05-03-method-syntax.md](https://github.com/rust-lang/book/blob/master/src/ch05-03-method-syntax.md) >
-> commit c560db1e0145d5a64b9415c9cfe463c7dac31ab8 +> commit a86c1d315789b3ca13b20d50ad5005c62bdd9e37 **方法** 与函数类似:它们使用 `fn` 关键字和名称声明,可以拥有参数和返回值,同时包含在某处调用该方法时会执行的代码。不过方法与函数是不同的,因为它们在结构体的上下文中被定义(或者是枚举或 trait 对象的上下文,将分别在第六章和第十七章讲解),并且它们第一个参数总是 `self`,它代表调用该方法的结构体实例。 diff --git a/src/ch06-00-enums.md b/src/ch06-00-enums.md index c6fb8c5..98e58f0 100644 --- a/src/ch06-00-enums.md +++ b/src/ch06-00-enums.md @@ -1,8 +1,8 @@ # 枚举和模式匹配 -> [ch06-00-enums.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch06-00-enums.md) +> [ch06-00-enums.md](https://github.com/rust-lang/book/blob/master/src/ch06-00-enums.md) >
-> commit e3be64b3c034de029ae9e4f04e6d2742d799d2b1 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 本章介绍 **枚举**(*enumerations*),也被称作 *enums*。枚举允许你通过列举可能的值来定义一个类型。首先,我们会定义并使用一个枚举来展示它是如何连同数据一起编码信息的。接下来,我们会探索一个特别有用的枚举,叫做 `Option`,它代表一个值要么是某个值要么什么都不是。然后会讲到在 `match` 表达式中用模式匹配,针对不同的枚举值编写相应要执行的代码。最后会介绍 `if let`,另一个简洁方便处理代码中枚举的结构。 diff --git a/src/ch06-01-defining-an-enum.md b/src/ch06-01-defining-an-enum.md index 656aab3..ebb9323 100644 --- a/src/ch06-01-defining-an-enum.md +++ b/src/ch06-01-defining-an-enum.md @@ -1,8 +1,8 @@ # 定义枚举 -> [ch06-01-defining-an-enum.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch06-01-defining-an-enum.md) +> [ch06-01-defining-an-enum.md](https://github.com/rust-lang/book/blob/master/src/ch06-01-defining-an-enum.md) >
-> commit 923201d5117c45bf78ce433422b50e4de9bd9b11 +> commit a86c1d315789b3ca13b20d50ad5005c62bdd9e37 让我们看看一个需要诉诸于代码的场景,来考虑为何此时使用枚举更为合适且实用。假设我们要处理 IP 地址。目前被广泛使用的两个主要 IP 标准:IPv4(version four)和 IPv6(version six)。这是我们的程序可能会遇到的所有可能的 IP 地址类型:所以可以 **枚举** 出所有可能的值,这也正是此枚举名字的由来。 @@ -157,7 +157,7 @@ enum Message { * `Write` 包含单独一个 `String`。 * `ChangeColor` 包含三个 `i32`。 -定义一个如示例 6-2 中所示那样的有关联值的枚举的方式和定义多个不同类型的结构体的方式很相像——除了枚举不使用 `struct` 关键字以及其所有成员都被组合在一起位于 `Message` 类型下。如下这些结构体可以包含与之前枚举成员中相同的数据: +定义一个如示例 6-2 中所示那样的有关联值的枚举的方式和定义多个不同类型的结构体的方式很相像,除了枚举不使用 `struct` 关键字以及其所有成员都被组合在一起位于 `Message` 类型下。如下这些结构体可以包含与之前枚举成员中相同的数据: ```rust struct QuitMessage; // 类单元结构体 @@ -169,7 +169,7 @@ struct WriteMessage(String); // 元组结构体 struct ChangeColorMessage(i32, i32, i32); // 元组结构体 ``` -不过,如果我们使用不同的结构体,由于它们都有不同的类型,我们将不能像使用示例 6-2 中定义的 `Message` 枚举那样,轻易的定义一个能够处理这些不同类型的结构体的函数。因为使用枚举的情况下,“它们”是一个类型。 +不过,如果我们使用不同的结构体,由于它们都有不同的类型,我们将不能像使用示例 6-2 中定义的 `Message` 枚举那样,轻易的定义一个能够处理这些不同类型的结构体的函数,因为枚举是单独一个类型。 结构体和枚举还有另一个相似点:就像可以使用 `impl` 来为结构体定义方法那样,也可以在枚举上定义方法。这是一个定义于我们 `Message` 枚举上的叫做 `call` 的方法: diff --git a/src/ch06-02-match.md b/src/ch06-02-match.md index 47671ea..1c80c06 100644 --- a/src/ch06-02-match.md +++ b/src/ch06-02-match.md @@ -1,8 +1,8 @@ ## `match` 控制流运算符 -> [ch06-02-match.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch06-02-match.md) +> [ch06-02-match.md](https://github.com/rust-lang/book/blob/master/src/ch06-02-match.md) >
-> commit 18fd30d70f4d6ee67e0a808710bf7a3135ef7ed6 +> commit a86c1d315789b3ca13b20d50ad5005c62bdd9e37 Rust 有一个叫做 `match` 的极为强大的控制流运算符,它允许我们将一个值与一系列的模式相比较并根据相匹配的模式执行相应代码。模式可由字面值、变量、通配符和许多其他内容构成;第十八章会涉及到所有不同种类的模式以及它们的作用。`match` 的力量来源于模式的表现力以及编译器检查,它确保了所有可能的情况都得到处理。 @@ -30,11 +30,11 @@ fn value_in_cents(coin: Coin) -> u32 { 示例 6-3:一个枚举和一个以枚举成员作为模式的 `match` 表达式 -拆开 `value_in_cents` 函数中的 `match` 来看。首先,我们列出 `match` 关键字后跟一个表达式,在这个例子中是 `coin` 的值。这看起来非常像 `if` 使用的表达式,不过这里有一个非常大的区别:对于 `if`,表达式必须返回一个布尔值。而这里它可以是任何类型的。例子中的 `coin` 的类型是示例 6-3 中定义的 `Coin` 枚举。 +拆开 `value_in_cents` 函数中的 `match` 来看。首先,我们列出 `match` 关键字后跟一个表达式,在这个例子中是 `coin` 的值。这看起来非常像 `if` 使用的表达式,不过这里有一个非常大的区别:对于 `if`,表达式必须返回一个布尔值,而这里它可以是任何类型的。例子中的 `coin` 的类型是示例 6-3 中定义的 `Coin` 枚举。 接下来是 `match` 的分支。一个分支有两个部分:一个模式和一些代码。第一个分支的模式是值 `Coin::Penny` 而之后的 `=>` 运算符将模式和将要运行的代码分开。这里的代码就仅仅是值 `1`。每一个分支之间使用逗号分隔。 -当 `match` 表达式执行时,它将结果值按顺序与每一个分支的模式相比较,如果模式匹配了这个值,这个模式相关联的代码将被执行。如果模式并不匹配这个值,将继续执行下一个分支,非常类似一个硬币分类器。可以拥有任意多的分支:示例 6-3 中的 `match` 有四个分支。 +当 `match` 表达式执行时,它将结果值按顺序与每一个分支的模式相比较。如果模式匹配了这个值,这个模式相关联的代码将被执行。如果模式并不匹配这个值,将继续执行下一个分支,非常类似一个硬币分类器。可以拥有任意多的分支:示例 6-3 中的 `match` 有四个分支。 每个分支相关联的代码是一个表达式,而表达式的结果值将作为整个 `match` 表达式的返回值。 @@ -68,11 +68,11 @@ fn value_in_cents(coin: Coin) -> u32 { 作为一个例子,让我们修改枚举的一个成员来存放数据。1999 年到 2008 年间,美帝在 25 美分的硬币的一侧为 50 个州的每一个都印刷了不同的设计。其他的硬币都没有这种区分州的设计,所以只有这些 25 美分硬币有特殊的价值。可以将这些信息加入我们的 `enum`,通过改变 `Quarter` 成员来包含一个 `State` 值,示例 6-4 中完成了这些修改: ```rust -#[derive(Debug)] // So we can inspect the state in a minute +#[derive(Debug)] // 这样可以可以立刻看到州的名称 enum UsState { Alabama, Alaska, - // ... etc + // --snip-- } enum Coin { @@ -157,8 +157,6 @@ Some(i) => Some(i + 1), `Some(5)` 与 `Some(i)` 匹配吗?当然匹配!它们是相同的成员。`i` 绑定了 `Some` 中包含的值,所以 `i` 的值是 `5`。接着匹配分支的代码被执行,所以我们将 `i` 的值加一并返回一个含有值 `6` 的新 `Some`。 -#### 匹配 `None` - 接着考虑下示例 6-5 中 `plus_one` 的第二个调用,这里 `x` 是 `None`。我们进入 `match` 并与第一个分支相比较。 ```rust,ignore @@ -171,9 +169,9 @@ None => None, ### 匹配是穷尽的 -`match` 还有另一方面需要讨论。考虑一下 `plus_one` 函数的这个版本: +`match` 还有另一方面需要讨论。考虑一下 `plus_one` 函数的这个版本,它有一个 bug 并不能编译: -```rust,ignore +```rust,ignore,does_not_compile fn plus_one(x: Option) -> Option { match x { Some(i) => Some(i + 1), diff --git a/src/ch06-03-if-let.md b/src/ch06-03-if-let.md index ee60496..a6e75b7 100644 --- a/src/ch06-03-if-let.md +++ b/src/ch06-03-if-let.md @@ -1,10 +1,10 @@ ## `if let` 简单控制流 -> [ch06-03-if-let.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch06-03-if-let.md) +> [ch06-03-if-let.md](https://github.com/rust-lang/book/blob/master/src/ch06-03-if-let.md) >
-> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56 +> commit a86c1d315789b3ca13b20d50ad5005c62bdd9e37 -`if let` 语法让我们以一种不那么冗长的方式结合 `if` 和 `let`,来处理只匹配一个模式的值而忽略其他模式的情况。考虑示例 6-6 中的程序,它匹配一个 `Option` 值并只希望当值为三时执行代码: +`if let` 语法让我们以一种不那么冗长的方式结合 `if` 和 `let`,来处理只匹配一个模式的值而忽略其他模式的情况。考虑示例 6-6 中的程序,它匹配一个 `Option` 值并只希望当值为 3 时执行代码: ```rust let some_u8_value = Some(0u8); @@ -27,7 +27,7 @@ if let Some(3) = some_u8_value { } ``` -`if let` 获取通过 `=` 分隔的一个模式和一个表达式。它的工作方式与 `match` 相同,这里的表达式对应 `match` 而模式则对应第一个分支。 +`if let` 获取通过等号分隔的一个模式和一个表达式。它的工作方式与 `match` 相同,这里的表达式对应 `match` 而模式则对应第一个分支。 使用 `if let` 意味着编写更少代码,更少的缩进和更少的样板代码。然而,这样会失去 `match` 强制要求的穷尽性检查。`match` 和 `if let` 之间的选择依赖特定的环境以及增加简洁度和失去穷尽性检查的权衡取舍。 From 8ebec196243fa38827e46068334a2faace711ab8 Mon Sep 17 00:00:00 2001 From: KaiserY Date: Wed, 5 Dec 2018 00:35:23 +0800 Subject: [PATCH 16/49] update to ch07-02 --- src/SUMMARY.md | 7 +- src/ch07-00-modules.md | 15 - src/ch07-00-packages-crates-and-modules.md.md | 16 + ...es-for-making-libraries-and-executables.md | 31 ++ ...es-and-use-to-control-scope-and-privacy.md | 503 ++++++++++++++++++ 5 files changed, 553 insertions(+), 19 deletions(-) delete mode 100644 src/ch07-00-modules.md create mode 100644 src/ch07-00-packages-crates-and-modules.md.md create mode 100644 src/ch07-01-packages-and-crates-for-making-libraries-and-executables.md create mode 100644 src/ch07-02-modules-and-use-to-control-scope-and-privacy.md diff --git a/src/SUMMARY.md b/src/SUMMARY.md index b217cf3..90bd032 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -36,10 +36,9 @@ ## 基本 Rust 技能 -- [模块](ch07-00-modules.md) - - [`mod` 与文件系统](ch07-01-mod-and-the-filesystem.md) - - [使用 `pub` 控制可见性](ch07-02-controlling-visibility-with-pub.md) - - [在不同的模块中引用命名](ch07-03-importing-names-with-use.md) +- [包、crate 与 模块](ch07-00-packages-crates-and-modules.md) + - [包和 crate 用来创建库和二进制项目](ch07-01-packages-and-crates-for-making-libraries-and-executables.md) + - [模块系统用来控制作用域和私有性](ch07-02-modules-and-use-to-control-scope-and-privacy.md) - [通用集合类型](ch08-00-common-collections.md) - [vector](ch08-01-vectors.md) diff --git a/src/ch07-00-modules.md b/src/ch07-00-modules.md deleted file mode 100644 index 0ac9d9a..0000000 --- a/src/ch07-00-modules.md +++ /dev/null @@ -1,15 +0,0 @@ -# 使用模块组织和复用代码 - -> [ch07-00-modules.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch07-00-modules.md) ->
-> commit 050ff6f34f107b2c8695807fc16aeb827ffe1fa3 - -在你刚开始编写 Rust 程序时,代码可能仅仅位于 `main` 函数中。随着代码量的增长,为了复用和更好地组织代码,最终你会将功能移动到其他函数中。通过将代码拆分成更小的块,每一个块就更易于理解。但是如果你有太多的函数该怎么办呢?Rust 有一个模块系统,可以有组织地复用代码。 - -就跟你将代码行提取到一个函数中一样,也可以将函数(和其他代码,例如结构体和枚举)提取到不同模块中。**模块**(*module*)是一个包含函数或类型定义的命名空间,你可以选择这些定义能(公有)或不能(私有)在其模块外可见。下面是一个模块如何工作的梗概: - -* 使用 `mod` 关键字声明新模块。此模块中的代码要么直接位于声明之后的大括号中,要么位于另一个文件。 -* 函数、类型、常量和模块默认都是私有的。可以使用 `pub` 关键字将其变成公有并在其命名空间之外可见。 -* `use` 关键字将模块或模块中的定义引入到作用域中以便于引用它们。 - -我们会逐一了解这每一部分并学习如何将它们结合在一起。 diff --git a/src/ch07-00-packages-crates-and-modules.md.md b/src/ch07-00-packages-crates-and-modules.md.md new file mode 100644 index 0000000..d2c2dba --- /dev/null +++ b/src/ch07-00-packages-crates-and-modules.md.md @@ -0,0 +1,16 @@ +# 包、crate 与 模块 + +> [ch07-00-packages-crates-and-modules.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch07-00-modules.md) +>
+> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f + +编写程序时一个核心的问题是 **作用域**(*scope*):在代码的某处编译器知道哪些变量名?允许调用哪些函数?这些变量引用的又是什么? + +Rust 有一系列与作用域相关的功能。这有时被称为 “模块系统”(“the module system”),不过又不仅仅是模块: + +* **包**(*Packages*)是 Cargo 的一个功能,它允许你构建、测试核分享 crate。 +* *Crates* 是一个模块的树形结构,它形成了库或二进制项目。 +* **模块**(*Modules*)和 *use* 关键字允许你控制作用域和路径的私有性。 +* **路径**(*path*)是一个命名例如结构体、函数或模块等项的方式 + +本章将会覆盖所有这些概念。很快我们就能像专家一样将命名引入作用域、定义作用域和将命名导出到作用域! diff --git a/src/ch07-01-packages-and-crates-for-making-libraries-and-executables.md b/src/ch07-01-packages-and-crates-for-making-libraries-and-executables.md new file mode 100644 index 0000000..4a82060 --- /dev/null +++ b/src/ch07-01-packages-and-crates-for-making-libraries-and-executables.md @@ -0,0 +1,31 @@ +## 包和 crate 用来创建库和二进制项目 + +> [ch07-01-mod-and-the-filesystem.md](https://github.com/rust-lang/book/blob/master/src/ch07-01-packages-and-crates-for-making-libraries-and-executables.md) +>
+> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f + +让我们聊聊 **模块** 与 *crate*。下面是一个总结: + +* *crate* 是一个二进制或库项目。 +* **crate 根**(*crate root*)是一个用来描述如何构建 crate 的文件。 +* **包** 带有 *Cargo.toml* 文件用以描述如何构建一个或多个 crate。一个包中至多可以有一个库项目。 + +所以当运行 `cargo new` 时是在创建一个包: + +```text +$ cargo new my-project + Created binary (application) `my-project` package +$ ls my-project +Cargo.toml +src +$ ls my-project/src +main.rs +``` + +因为 Cargo 创建了 *Cargo.toml*,这意味着现在我们有了一个包。如果查看 *Cargo.toml* 的内容,会发现并没有提到 *src/main.rs*。然而,Cargo 的约定是如果在代表包的 *Cargo.toml* 的同级目录下包含 *src* 目录且其中包含 *main.rs* 文件的话,Cargo 就知道这个包带有一个与包同名的二进制 crate,且 *src/main.rs* 就是 crate 根。另一个约定如果包目录中包含 *src/lib.rs*,则包带有与其同名的库 crate,且 *src/lib.rs* 是 crate 根。crate 根文件将由 Cargo 传递给 `rustc` 来实际构建库或者二进制项目。 + +一个包可以带有零个或一个库 crate 和任意多个二进制 crate。一个包中必须带有至少一个(库或者二进制)crate。 + +如果包同时包含 *src/main.rs* 和 *src/lib.rs*,那么它带有两个 crate:一个库和一个二进制项目,同名。如果只有其中之一,则包将只有一个库或者二进制 crate。包可以带有多个二进制 crate,需将其文件置于 *src/bin* 目录;每个文件将是一个单独的二进制 crate。 + +接下来让我们讨论模块! \ No newline at end of file diff --git a/src/ch07-02-modules-and-use-to-control-scope-and-privacy.md b/src/ch07-02-modules-and-use-to-control-scope-and-privacy.md new file mode 100644 index 0000000..1136a1e --- /dev/null +++ b/src/ch07-02-modules-and-use-to-control-scope-and-privacy.md @@ -0,0 +1,503 @@ +## 模块系统用来控制作用域和私有性 + +> [ch07-01-mod-and-the-filesystem.md](https://github.com/rust-lang/book/blob/master/src/ch07-02-modules-and-use-to-control-scope-and-privacy.md) +>
+> commit 820ac357f6cf0e866e5a8e7a9c57dd3e17e9f8ca + +Rust 的此部分功能通常被引用为 “模块系统”(“the module system”),不过其包括了一些除模块之外的功能。在本部分我们会讨论: + +* 模块,一个组织代码和控制路径私有性的方式 +* 路径,一个命名项(item)的方式 +* `use` 关键字用来将路径引入作用域 +* `pub` 关键字使项变为公有 +* `as` 关键字用于将项引入作用域时进行重命名 +* 使用外部包 +* 嵌套路径用来消除大量的 `use` 语句 +* 使用 glob 运算符将模块的所有内容引入作用域 +* 如何将不同模块分割到单独的文件中 + +首先讲讲模块。模块允许我们将代码组织起来。示例 7-1 是一个例子,这些代码定义了名为 `sound` 的模块,其包含名为 `guitar` 的函数。 + +文件名: src/main.rs + +```rust +mod sound { + fn guitar() { + // 函数体 + } +} + +fn main() { + +} +``` + +示例 7-1: 包含 `guitar` 函数和 `main` 函数的 `sound` 模块 + +这里定义了两个函数,`guitar` 和 `main`。`guitar` 函数定义于 `mod` 块中。这个块定义了 `sound` 模块。 + +为了将代码组织到模块层次体系中,可以将模块嵌套进其他模块,如示例 7-2 所示: + +文件名: src/main.rs + +```rust +mod sound { + mod instrument { + mod woodwind { + fn clarinet() { + // 函数体 + } + } + } + + mod voice { + + } +} + +fn main() { + +} +``` + +示例 7-2: 模块中的模块 + +在这个例子中,我们像示例 7-1 一样定义了 `sound` 模块。接着在 `sound` 模块中定义了 `instrument` 和 `voice` 模块。`instrument` 模块中定义了另一个模块 `woodwind`,这个模块包含一个函数 `clarinet`。 + +在 “包和 crate 用来创建库和二进制项目” 部分提到 *src/main.rs* 和 *src/lib.rs* 被称为 **crate 根**。他们被称为 crate 根是因为这两个文件在 crate 模块树的根组成了名为 `crate` 模块。所以示例 7-2 中,有如示例 7-3 所示的模块树: + +```text +crate + └── sound + └── instrument + └── woodwind + └── voice +``` + +示例 7-3: 示例 7-2 中代码的模块树 + +这个树展示了模块如何嵌套在其他模块中(比如 `woodwind` 嵌套在 `instrument` 中)以及模块如何作为其他模块的子模块的(`instrument` 和 `voice` 都定义在 `sound` 中)。整个模块树都位于名为 `crate` 这个隐式模块的根下。 + +这个树可能会令你想起电脑上文件系统的目录树;这事一个非常恰当的比喻!就像文件系统的目录,将代码放入任意模块也将创建对应的组织结构体。另一个相似点是为了引用文件系统或模块树中的项,需要使用 **路径**(*path*)。 + +### 路径用来引用模块树中的项 + +如果想要调用函数,需要知道其 **路径**。“路径” 是 “名称”(“name”) 的同义词,不过它用于文件系统语境。另外,函数、结构体和其他项可能会有多个指向相同项的路径,所以 “名称” 这个概念不太准确。 + +**路径** 可以有两种形式: + +* **绝对路径**(*absolute path*)从 crate 根开始,以 crate 名或者字面值 `crate` 开头。 +* **相对路径**(*relative path*)从当前模块开始,以 `self`、`super` 或当前模块的标识符开头。 + +绝对路径和相对路径都后跟一个或多个由双分号(`::`)分割的标识符。 + +如何在示例 7-2 的 `main` 函数中调用 `clarinet` 函数呢?也就是说,`clarinet` 函数的路径是什么呢?在示例 7-4 中稍微简化了代码,移除了一些模块,并展示了两种在 `main` 中调用 `clarinet` 函数的方式。这个例子还不能编译,我们会解释为什么。 + +文件名: src/main.rs + +```rust,ignore,does_not_compile +mod sound { + mod instrument { + fn clarinet() { + // 函数体 + } + } +} + +fn main() { + // 绝对路径 + crate::sound::instrument::clarinet(); + + // Relative path + sound::instrument::clarinet(); +} +``` + +示例 7-4: 在简化的模块树中,分别使用绝对路径和相对路径在 `main` 中调用 `clarinet` 函数 + +第一种从 `main` 函数中调用 `clarinet` 函数的方式使用绝对路径。因为 `clarinet` 与 `main` 定义于同一 crate 中,我们使用 `crate` 关键字来开始绝对路径。接着包含每一个模块直到 `clarinet`。这类似于指定 `/sound/instrument/clarinet` 来运行电脑上这个位置的程序;使用 `crate` 从 crate 根开始就类似于在 shell 中使用 `/` 从文件系统根开始。 + +第二种从 `main` 函数中调用 `clarinet` 函数的方式使用相对路径。该路径以 `sound` 开始,它是定义于与 `main` 函数相同模块树级别的模块。这类似于指定 `sound/instrument/clarinet` 来运行电脑上这个位置的程序;以名称开头意味着路径是相对的。 + +示例 7-4 提到了它并不能编译,让我们尝试编译并看看为什么不行!示例 7-5 展示了错误。 + +```text +$ cargo build + Compiling sampleproject v0.1.0 (file:///projects/sampleproject) +error[E0603]: module `instrument` is private + --> src/main.rs:11:19 + | +11 | crate::sound::instrument::clarinet(); + | ^^^^^^^^^^ + +error[E0603]: module `instrument` is private + --> src/main.rs:14:12 + | +14 | sound::instrument::clarinet(); + | ^^^^^^^^^^ +``` + +示例 7-5: 构建示例 7-4 出现的编译器错误 + +错误信息说 `instrument` 模块是私有的。可以看到 `instrument` 模块和 `clarinet` 函数的路径是正确的,不过 Rust 不让我们使用,因为他们是私有的。现在是学习 `pub` 关键字的时候了! + +### 模块作为私有性的边界 + +之前我们讨论到模块的语法和组织代码的用途。Rust 采用模块还有另一个原因:模块是 Rust 中的 **私有性边界**(*privacy boundary*)。如果你希望函数或结构体是私有的,将其放入模块。私有性规则有如下: + +* 所有项(函数、方法、结构体、枚举、模块和常量)默认是私有的。 +* 可以使用 `pub` 关键字使项变为共有。 +* 不允许使用定义于当前模块的子模块中的私有代码。 +* 允许使用任何定义于父模块或当前模块中的代码。 + +换句话说,对于没有 `pub` 关键字的项,当你从当前模块向 “下” 看时是私有的,不过当你向 “上” 看时是公有的。再一次想象一下文件系统:如果你没有某个目录的权限,则无法从父目录中查看其内容。如果有该目录的权限,则可以查看其中的目录和任何父目录。 + +### 使用 `pub` 关键字使项变为公有 + +示例 7-5 中的错误说 `instrument` 模块使私有的。让我们使用 `pub` 关键字标记 `instrument` 模块使其可以在 `main` 函数中使用。这些改变如示例 7-6 所示,它仍然不能编译,不过会产生一个不同的错误: + +文件名: src/main.rs + +```rust,ignore,does_not_compile +mod sound { + pub mod instrument { + fn clarinet() { + // 函数体 + } + } +} + +fn main() { + // Absolute path + crate::sound::instrument::clarinet(); + + // Relative path + sound::instrument::clarinet(); +} +``` + +示例 7-6: 将 `instrument` 模块声明为 `pub` 以便可以在 `main` 中使用 + +在 `mod instrument` 之前增加 `pub` 关键字使得模块变为公有。通过这个改变如果允许访问 `sound` 的话,我们就可以访问 `instrument`。`instrument` 的内容仍然是私有的;使得模块公有并不使其内容也是公有的。模块上的 `pub` 关键字允许其父模块引用它。 + +不过示例 7-6 中的代码仍然产生错误,如示例 7-7 所示: + +```text +$ cargo build + Compiling sampleproject v0.1.0 (file:///projects/sampleproject) +error[E0603]: function `clarinet` is private + --> src/main.rs:11:31 + | +11 | crate::sound::instrument::clarinet(); + | ^^^^^^^^ + +error[E0603]: function `clarinet` is private + --> src/main.rs:14:24 + | +14 | sound::instrument::clarinet(); + | ^^^^^^^^ +``` + +示例 7-7: 构建示例 7-6 时产生的编译器错误 + +现在的错误表明 `clarinet` 函数是私有的。私有性规则适用于结构体、枚举、函数和方法以及模块。 + +在 `clarinet` 函数前增加 `pub` 关键字使其变为公有,如示例 7-8 所示: + +文件名: src/main.rs + +```rust +mod sound { + pub mod instrument { + pub fn clarinet() { + // 函数体 + } + } +} + +fn main() { + // 绝对路径 + crate::sound::instrument::clarinet(); + + // 相对路径 + sound::instrument::clarinet(); +} +``` + +示例 7-8: 在 `mod +instrument` 和 `fn clarinet` 之前都增加 `pub` 关键字使我们可以在 `main` 中调用此函数 + +现在可以编译了!让我们看看绝对路径和相对路径再次检查为什么增加 `pub` 关键字使得我们可以在 `main` 中调用这些路径。 + +在绝对路径的情况下,我们从 `crate`,也就是 crate 根开始。从这开始有 `sound`,这是一个定义于 crate 根中的模块。`sound` 模块不是公有的,不过因为 `main` 函数与 `sound` 定义于同一模块中,可以从 `main` 中引用 `sound`。接下来是 `instrument`,这个模块标记为 `pub`。我们可以访问 `instrument` 的父模块,所以可以访问 `instrument`。最后,`clarinet` 函数被标记为 `pub` 所以可以访问其父模块,所以这个函数调用是有效的! + +在相对路径的情况下,其逻辑与绝对路径相同,除了第一步。不同于从 crate 根开始,路径从 `sound` 开始。`sound` 模块与 `main` 定义于同一模块,所以从 `main` 所在模块开始定义的路径是有效的。接下来因为 `instrument` 和 `clarinet` 被标记为 `pub`,路径其余的部分也是有效的,因此函数调用也是有效的! + +### 使用 `super` 开始相对路径 + +也可以使用 `super` 开头来构建相对路径。这么做类似于文件系统中以 `..` 开头:该路径从 **父** 模块开始而不是当前模块。这在例如示例 7-9 这样的情况下有用处,在这里 `clarinet` 函数通过指定以 `super` 开头的路径调用 `breathe_in` 函数: + +文件名: src/lib.rs + +```rust +# fn main() {} +# +mod instrument { + fn clarinet() { + super::breathe_in(); + } +} + +fn breathe_in() { + // 函数体 +} +``` + +示例 7-9: 使用以 `super` 开头的路径从父目录开始调用函数 + +`clarinet` 函数位于 `instrument` 模块中,所以可以使用 `super` 进入 `instrument` 的父模块,也就是根 `crate`。从这里可以找到 `breathe_in`。成功! + +你可能想要使用 `super` 开头的相对路而不是以 `crate` 开头的绝对路径的原因是 `super` 可能会使修改有着不同模块层级结构的代码变得更容易,如果定义项和调用项的代码被一同移动的话。例如,如果我们决定将 `instrument` 模块和 `breathe_in` 函数放入 `sound` 模块中,这时我们只需增加 `sound` 模块即可,如示例 7-10 所示。 + +文件名: src/lib.rs + +```rust +mod sound { + mod instrument { + fn clarinet() { + super::breathe_in(); + } + } + + fn breathe_in() { + // Function body code goes here + } +} +``` + +示例 7-10: 增加一个名为 `sound` 的父模块并不影响相对路径 `super::breathe_in` + +示例 7-10 在 `clarinet` 函数中调用 `super::breathe_in` 将如示例 7-9 一样继续有效,无需更新路径。如果在 `clarinet` 函数不使用 `super::breathe_in` 而是使用 `crate::breathe_in` 的话,当增加父模块 `sound` 后,则需要更新 `clarinet` 函数使用 `crate::sound::breathe_in` 路径。使用相对路径可能意味着重新布局模块时需要更少的必要修改。 + +### 对结构体和枚举使用 `pub` + +可以以模块与函数相同的方式来设计公有的结构体和枚举,不过有一些额外的细节。 + +如果在结构体定义中使用 `pub`,可以使结构体公有。然而结构体的字段仍是私有的。可以在每一个字段的基准上选择其是否公有。在示例 7-11 中定义了一个公有结构体 `plant::Vegetable`,其包含公有的 `name` 字段和私有的 `id` 字段。 + +文件名: src/main.rs + +```rust +mod plant { + pub struct Vegetable { + pub name: String, + id: i32, + } + + impl Vegetable { + pub fn new(name: &str) -> Vegetable { + Vegetable { + name: String::from(name), + id: 1, + } + } + } +} + +fn main() { + let mut v = plant::Vegetable::new("squash"); + + v.name = String::from("butternut squash"); + println!("{} are delicious", v.name); + + // 如果将如下行取消注释代码将无法编译: + // println!("The ID is {}", v.id); +} +``` + +示例 7-11: 结构体带有公有和私有的字段 + +因为 `plant::Vegetable` 结构体的 `name` 字段使公有的,在 `main` 中可以使用点号读写 `name` 字段。不允许在 `main` 中使用 `id` 字段因为其使私有的。尝试取消注释的行来打印 `id` 字段的值来看看会出现什么错误!另外注意因为 `plant::Vegetable` 有私有字段,需要提供一个公有的关联函数来构建 `Vegetable` 的实例(这里使用了传统的名称 `new`)。如果 `Vegetable` 没有提供这么一个函数,我们就不能在 `main` 中创建 `Vegetable` 的实例,因为在 `main` 中不允许设置私有字段 `id` 的值。 + +相反,如果有一个公有枚举,其所有成员都是公有。只需在 `enum` 关键词前加上 `pub`,如示例 7-12 所示。 + +文件名: src/main.rs + +```rust +mod menu { + pub enum Appetizer { + Soup, + Salad, + } +} + +fn main() { + let order1 = menu::Appetizer::Soup; + let order2 = menu::Appetizer::Salad; +} +``` + +示例 7-12: 将枚举设计为公有会使其所有成员公有 + +因为 `Appetizer` 枚举是公有的,可以在 `main` 中使用 `Soup` and `Salad` 成员。 + +还有一种使用 `pub` 的场景我们还没有涉及到,而这是我们最后要讲的模块功能:`use` 关键字。我们先单独介绍 `use`,然后展示如何结合使用 `pub` 和 `use`。 + +### 使用 `use` 关键字将名称引入作用域 + +你可能考虑过本章很多的函数调用的路径是冗长和重复的。例如示例 7-8 中,当我们选择 `clarinet` 函数的绝对或相对路径时,每次想要调用 `clarinet` 时都不得不也指定 `sound` 和 `instrument`。幸运的是,有一次性将路径引入作用域然后就像调用本地项那样的方法:使用 `use` 关键字。在示例 7-13 中将 `crate::sound::instrument` 模块引入了 `main` 函数的作用域,以便只需指定 `instrument::clarinet` 来调用 `clarinet` 函数。 + +文件名: src/main.rs + +```rust +mod sound { + pub mod instrument { + pub fn clarinet() { + // 函数体 + } + } +} + +use crate::sound::instrument; + +fn main() { + instrument::clarinet(); + instrument::clarinet(); + instrument::clarinet(); +} +``` + +示例 7-13: 使用 `use` 将模块引入作用域并使用绝对路ing来缩短在模块中调用项所必须的路径 + +当指定 `use` 后以 `self` 开头的相对路径在未来可能不是必须的;这是一个开发者正在尽力消除的语言中的不一致。 + +选择使用 `use` 和指定绝对路径,可以使得当调用项的代码移动到模块树的不同位置,不过定义的代码没有移动的情况的代码更新变得更为轻松,这与示例 7-10 中同时移动的情况相对。例如,如果我们决定采用示例 7-13 的代码,将 `main` 函数的行为提取到函数 `clarinet_trio` 中,并将该函数移动到模块 `performance_group` 中,这时 `use` 所指定的路径无需变化,如示例 7-15 所示。 + +文件名: src/main.rs + +```rust +mod sound { + pub mod instrument { + pub fn clarinet() { + // 函数体 + } + } +} + +mod performance_group { + use crate::sound::instrument; + + pub fn clarinet_trio() { + instrument::clarinet(); + instrument::clarinet(); + instrument::clarinet(); + } +} + +fn main() { + performance_group::clarinet_trio(); +} +``` + +示例 7-15: 移动调用项的代码时绝对路径无需移动 + +相反,如果对示例 7-14 中指定了相对路径的代码做同样的修改,则需要将 `use +self::sound::instrument` 变为 `use super::sound::instrument`。如果你不确定将来模块树会如何变化,那么选择采用相对或绝对路径是否会减少修改可能全靠猜测,不过本书的作者倾向于通过 `crate` 指定绝对路径,因为定义和调用项的代码更有可能相互独立的在模块树中移动,而不是像示例 7-10 那样一同移动。 + +### `use` 函数路径使用习惯 VS 其他项 + +示例 7-13 中,你可能会好奇为什么指定 `use crate::sound::instrument` 接着在 `main` 中调用 `instrument::clarinet`,而不是如示例 7-16 所示的有相同行为的代码: + +文件名: src/main.rs + +```rust +mod sound { + pub mod instrument { + pub fn clarinet() { + // 函数体 + } + } +} + +use crate::sound::instrument::clarinet; + +fn main() { + clarinet(); + clarinet(); + clarinet(); +} +``` + +示例 7-16: 通过 `use` 将 `clarinet` 函数引入作用域,这是不推荐的 + +对于函数来说,通过 `use` 指定函数的父模块接着指定父模块来调用方法被认为是习惯用法。这么做而不是像示例 7-16 那样通过 `use` 指定函数的路径,清楚的表明了函数不是本地定义的,同时仍最小化了指定全路径时的重复。 + +对于结构体、枚举和其它项,通过 `use` 指定项的全路径是习惯用法。例如,示例 7-17 展示了将标准库中 `HashMap` 结构体引入作用域的习惯用法。 + +文件名: src/main.rs + +```rust +use std::collections::HashMap; + +fn main() { + let mut map = HashMap::new(); + map.insert(1, 2); +} +``` + +示例 7-17: 将 `HashMap` 引入作用域的习惯用法 + +相反,示例 7-18 中的代码将 `HashMap` 的父模块引入作用域不被认为是习惯用法。这个习惯并没有很强制的理由;这是慢慢形成的习惯同时人们习惯于这么读写。 + +文件名: src/main.rs + +```rust +use std::collections; + +fn main() { + let mut map = collections::HashMap::new(); + map.insert(1, 2); +} +``` + +示例 7-18: 将 `HashMap` 引入作用域的非习惯方法 + +这个习惯的一个例外是如果 `use` 语句会将两个同名的项引入作用域时,这是不允许的。示例 7-19 展示了如何将两个不同父模块的 `Result` 类型引入作用域并引用它们。 + +文件名: src/lib.rs + +```rust +use std::fmt; +use std::io; + +fn function1() -> fmt::Result { +# Ok(()) +} +fn function2() -> io::Result<()> { +# Ok(()) +} +``` + +示例 7-19: 将两个同名类型引入作用域必须使用父模块 + +因为如果我们指定 `use std::fmt::Result` 和 `use std::io::Result`,则作用域中会有两个 `Result` 类型,Rust 无法知道我们想用哪个 `Result`。尝试这么做并看看编译器错误! + +### 通过 `as` 关键字重命名引入作用域的类型 + +将两个同名类型引入同一作用域这个问题还有另一个解决办法:可以通过在 `use` 后加上 `as` 和一个新名称来为此类型指定一个新的本地名称。示例 7-20 展示了另一个编写示例 7-19 中代码的方法,通过 `as` 重命名了其中一个 `Result` 类型。 + +文件名: src/lib.rs + +```rust +use std::fmt::Result; +use std::io::Result as IoResult; + +fn function1() -> Result { +# Ok(()) +} +fn function2() -> IoResult<()> { +# Ok(()) +} +``` + +示例 7-20: 通过 `as` 关键字重命名引入作用域的类型 \ No newline at end of file From 94e166f41b53ef972d71d5298bd3ce40733bc668 Mon Sep 17 00:00:00 2001 From: Chrislearn Young Date: Wed, 5 Dec 2018 09:26:46 +0800 Subject: [PATCH 17/49] typo --- src/ch07-02-modules-and-use-to-control-scope-and-privacy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ch07-02-modules-and-use-to-control-scope-and-privacy.md b/src/ch07-02-modules-and-use-to-control-scope-and-privacy.md index 1136a1e..a9c38b8 100644 --- a/src/ch07-02-modules-and-use-to-control-scope-and-privacy.md +++ b/src/ch07-02-modules-and-use-to-control-scope-and-privacy.md @@ -78,7 +78,7 @@ crate 这个树展示了模块如何嵌套在其他模块中(比如 `woodwind` 嵌套在 `instrument` 中)以及模块如何作为其他模块的子模块的(`instrument` 和 `voice` 都定义在 `sound` 中)。整个模块树都位于名为 `crate` 这个隐式模块的根下。 -这个树可能会令你想起电脑上文件系统的目录树;这事一个非常恰当的比喻!就像文件系统的目录,将代码放入任意模块也将创建对应的组织结构体。另一个相似点是为了引用文件系统或模块树中的项,需要使用 **路径**(*path*)。 +这个树可能会令你想起电脑上文件系统的目录树;这是一个非常恰当的比喻!就像文件系统的目录,将代码放入任意模块也将创建对应的组织结构体。另一个相似点是为了引用文件系统或模块树中的项,需要使用 **路径**(*path*)。 ### 路径用来引用模块树中的项 From 257b316a9cb1dff5a00a51478f085749cdc76d52 Mon Sep 17 00:00:00 2001 From: KaiserY Date: Thu, 6 Dec 2018 00:02:43 +0800 Subject: [PATCH 18/49] check to ch10-01 --- ...es-and-use-to-control-scope-and-privacy.md | 200 +++++++++++++++++- src/ch08-00-common-collections.md | 6 +- src/ch08-01-vectors.md | 53 ++--- src/ch08-02-strings.md | 38 ++-- src/ch08-03-hash-maps.md | 16 +- src/ch09-00-error-handling.md | 8 +- ...ch09-01-unrecoverable-errors-with-panic.md | 29 ++- src/ch09-02-recoverable-errors-with-result.md | 104 ++++++--- src/ch09-03-to-panic-or-not-to-panic.md | 22 +- src/ch10-00-generics.md | 18 +- src/ch10-01-syntax.md | 76 ++++--- 11 files changed, 401 insertions(+), 169 deletions(-) diff --git a/src/ch07-02-modules-and-use-to-control-scope-and-privacy.md b/src/ch07-02-modules-and-use-to-control-scope-and-privacy.md index 1136a1e..8ea8ec3 100644 --- a/src/ch07-02-modules-and-use-to-control-scope-and-privacy.md +++ b/src/ch07-02-modules-and-use-to-control-scope-and-privacy.md @@ -500,4 +500,202 @@ fn function2() -> IoResult<()> { } ``` -示例 7-20: 通过 `as` 关键字重命名引入作用域的类型 \ No newline at end of file +示例 7-20: 通过 `as` 关键字重命名引入作用域的类型 + +在第二个 `use` 语句中,我们选择 `IoResult` 作为 `std::io::Result` 的新名称,它与从 `std::fmt` 引入作用域的 `Result` 并不冲突。这样做也被认为是惯用的;示例 7-19 还是示例 7-20 全看你的选择。 + +### 通过 `pub use` 重导出名称 + +当使用 `use` 关键字将名称导入作用域时,在新作用域中可用的名称是私有的。如果希望调用你编写的代码的代码能够像你一样在其自己的作用域内引用这些类型,可以结合 `pub` 和 `use`。这个技术被称为 “重导出”(*re-exporting*),因为这样做将项引入作用域并同时使其可供其他代码引入自己的作用域。 + +例如,示例 7-21 展示了示例 7-15 中的代码将 `performance_group` 的 `use` 变为 `pub use` 的版本。 + +文件名: src/main.rs + +```rust +mod sound { + pub mod instrument { + pub fn clarinet() { + // 函数体 + } + } +} + +mod performance_group { + pub use crate::sound::instrument; + + pub fn clarinet_trio() { + instrument::clarinet(); + instrument::clarinet(); + instrument::clarinet(); + } +} + +fn main() { + performance_group::clarinet_trio(); + performance_group::instrument::clarinet(); +} +``` + +示例 7-21: 通过 `pub use` 使名称可引入任何代码的作用域中 + +通过 `pub use`,现在 `main` 函数可以通过新路径 `performance_group::instrument::clarinet` 来调用 `clarinet` 函数。如果没有指定 `pub use`,`clarinet_trio` 函数可以在其作用域中调用 `instrument::clarinet` 但 `main` 则不允许使用这个新路径。 + +### 使用外部包 + +在第二章中我们编写了一个猜猜看游戏。那个项目使用了一个外部包,`rand`,来生成随机数。为了在项目中使用 `rand`,在 *Cargo.toml* 中加入了如下行: + +文件名: Cargo.toml + +```toml +[dependencies] +rand = "0.5.5" +``` + +在 *Cargo.toml* 中加入 `rand` 依赖告诉了 Cargo 要从 *https://crates.io* 下载 `rand` 和其依赖,并使其可在项目代码中使用。 + +接着,为了将 `rand` 定义引入项目包的作用域,加入一行 `use`,它以 `rand` 包名开头并列出了需要引入作用域的项。回忆一下第二章的 “生成一个随机数” 部分,我们曾将 `Rng` trait 引入作用域并调用了 `rand::thread_rng` 函数: + +```rust,ignore +use rand::Rng; + +fn main() { + let secret_number = rand::thread_rng().gen_range(1, 101); +} +``` + +*https://crates.io* 上有很多社区成员发布的包,将其引入你自己的项目涉及到相同的步骤:在 *Cargo.toml* 列出它们并通过 `use` 将其中定义的项引入项目包的作用域中。 + +注意标准库(`std`)对于你的包来说也是外部 crate。因为标准库随 Rust 语言一同分发,无需修改 *Cargo.toml* 来引入 `std`,不过需要通过 `use` 将标准库中定义的项引入项目包的作用域中来引用它们,比如 `HashMap`: + +```rust +use std::collections::HashMap; +``` + +这是一个以标注库 crate 名 `std` 开头的绝对路径。 + +### 嵌套路径来消除大量的 `use` 行 + +当需要引入很多定义于相同包或相同模块的项时,为每一项单独列出一行会占用源码很大的空间。例如猜猜看章节示例 2-4 中有两行 `use` 语句都从 `std` 引入项到作用域: + +文件名: src/main.rs + +```rust +use std::cmp::Ordering; +use std::io; +// ---snip--- +``` + +可以使用嵌套的路径将同样的项在一行中引入而不是两行,这么做需要指定路径的相同部分,接着是两个分号,接着是大括号中的各自不同的路径部分,如示例 7-22 所示。 + +文件名: src/main.rs + +```rust +use std::{cmp::Ordering, io}; +// ---snip--- +``` + +示例 7-22: 指定嵌套的路径在一行中将多个带有相同前缀的项引入作用域 + +在从相同包或模块中引入很多项的程序中,使用嵌套路径显著减少所需的单独 `use` 语句! + +也可以剔除掉完全包含在另一个路径中的路径。例如,示例 7-23 中展示了两个 `use` 语句:一个将 `std::io` 引入作用域,另一个将 `std::io::Write` 引入作用域: + +文件名: src/lib.rs + +```rust +use std::io; +use std::io::Write; +``` + +示例 7-23: 通过两行 `use` 语句引入两个路径,其中一个是另一个的子路径 + +两个路径的相同部分是 `std::io`,这正是第一个路径。为了在一行 `use` 语句中引入这两个路径,可以在嵌套路径中使用 `self`,如示例 7-24 所示。 + +文件名: src/lib.rs + +```rust +use std::io::{self, Write}; +``` + +示例 7-24: 将示例 7-23 中部分重复的路径合并为一个 `use` 语句 + +这将 `std::io` 和 `std::io::Write` 同时引入作用域。 + +### 通过 glob 运算符将所有的公有定义引入作用域 + +如果希望将一个路径下 **所有** 公有项引入作用域,可以指定路径后跟 `*`,glob 运算符: + +```rust +use std::collections::*; +``` + +这个 `use` 语句将 `std::collections` 中定义的所有公有项引入当前作用域。 + +使用 glob 运算符时请多加小心!如此难以推导作用域中有什么名称和它们是在何处定义的。 + +glob 运算符经常用于测试模块 `tests` 中,这时会将所有内容引入作用域;我们将在第十一章 “如何编写测试” 部分讲解。glob 运算符有时也用于 prelude 模式;查看 [标准库中的文档](https://doc.rust-lang.org/std/prelude/index.html#other-preludes) 了解这个模式的更多细节。 + +### 将模块分割进不同文件 + +目前本章所有的例子都在一个文件中定义多个模块。当模块变得更大时,你可能想要将它们的定义移动到一个单独的文件中使代码更容易阅读。 + +例如从示例 7-8 的代码开始,我们可以通过修改 crate 根文件(这里是 *src/main.rs*)将 `sound` 模块移动到其自己的文件 *src/sound.rs* 中,如示例 7-25 所示。 + +文件名: src/main.rs + +```rust,ignore +mod sound; + +fn main() { + // 绝对路径 + crate::sound::instrument::clarinet(); + + // 相对路径 + sound::instrument::clarinet(); +} +``` + +示例 7-25: 声明 `sound` 模块,其内容位于 *src/sound.rs* 文件 + +而 *src/sound.rs* 中会包含 `sound` 模块的内容,如示例 7-26 所示。 + +文件名: src/sound.rs + +```rust +pub mod instrument { + pub fn clarinet() { + // 函数体 + } +} +``` + +示例 7-26: `sound` 模块中的定义位于 *src/sound.rs* 中 + +在 `mod sound` 后使用分号而不是代码块告诉 Rust 在另一个与模块同名的文件中加载模块的内容。 + +继续重构我们例子,将 `instrument` 模块也提取到其自己的文件中,修改 *src/sound.rs* 只包含 `instrument` 模块的声明: + +文件名: src/sound.rs + +```rust +pub mod instrument; +``` + +接着创建 *src/sound* 目录和 *src/sound/instrument.rs* 文件来包含 `instrument` 模块的定义: + +文件名: src/sound/instrument.rs + +```rust +pub fn clarinet() { + // 函数体 +} +``` + +模块树依然保持相同,`main` 中的函数调用也无需修改继续保持有效,即使其定义存在于不同的文件中。这样随着代码增长可以将模块移动到新文件中。 + +## 总结 + +Rust 提供了将包组织进 crate、将 crate 组织进模块和通过指定绝对或相对路径从一个模块引用另一个模块中定义的项的方式。可以通过 `use` 语句将路径引入作用域,这样在多次使用时可以使用更短的路径。模块定义的代码默认时私有的,不过可以选择增加 `pub` 关键字使其定义变为公有。 + +接下来,让我们看看一些标准库提供的集合数据类型,你可以利用它们编写出漂亮整洁的代码。 \ No newline at end of file diff --git a/src/ch08-00-common-collections.md b/src/ch08-00-common-collections.md index 1048c80..c937fb0 100644 --- a/src/ch08-00-common-collections.md +++ b/src/ch08-00-common-collections.md @@ -1,8 +1,8 @@ # 通用集合类型 -> [ch08-00-common-collections.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch08-00-common-collections.md) +> [ch08-00-common-collections.md](https://github.com/rust-lang/book/blob/master/src/ch08-00-common-collections.md) >
-> commit 54e81980185fbb1a4cb5a18dce1dc6deeb66b573 +> commit 820ac357f6cf0e866e5a8e7a9c57dd3e17e9f8ca Rust 标准库中包含一系列被称为 **集合**(*collections*)的非常有用的数据结构。大部分其他数据类型都代表一个特定的值,不过集合可以包含多个值。不同于内建的数组和元组类型,这些集合指向的数据是储存在堆上的,这意味着数据的数量不必在编译时就已知并且可以随着程序的运行增长或缩小。每种集合都有着不同能力和代价,而为所处的场景选择合适的集合则是你将要始终成长的技能。在这一章里,我们将详细的了解三个在 Rust 程序中被广泛使用的集合: @@ -14,4 +14,4 @@ Rust 标准库中包含一系列被称为 **集合**(*collections*)的非常 [collections]: https://doc.rust-lang.org/std/collections -我们将讨论如何创建和更新 vector、字符串和哈希 map,以及它们有什么不同。 +我们将讨论如何创建和更新 vector、字符串和哈希 map,以及它们有什么特别之处。 diff --git a/src/ch08-01-vectors.md b/src/ch08-01-vectors.md index 834d050..5c321a7 100644 --- a/src/ch08-01-vectors.md +++ b/src/ch08-01-vectors.md @@ -1,8 +1,8 @@ ## vector 用来储存一系列的值 -> [ch08-01-vectors.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch08-01-vectors.md) +> [ch08-01-vectors.md](https://github.com/rust-lang/book/blob/master/src/ch08-01-vectors.md) >
-> commit 550c8ea6f74060ff1f7b67e7e1878c4da121682d +> commit a86c1d315789b3ca13b20d50ad5005c62bdd9e37 我们要讲到的第一个类型是 `Vec`,也被称为 *vector*。vector 允许我们在一个单独的数据结构中储存多于一个的值,它在内存中彼此相邻地排列所有的值。vector 只能储存相同类型的值。它们在拥有一系列项的场景下非常实用,例如文件中的文本行或是购物车中商品的价格。 @@ -16,7 +16,7 @@ let v: Vec = Vec::new(); 示例 8-1:新建一个空的 vector 来储存 `i32` 类型的值 -注意这里我们增加了一个类型注解。因为没有向这个 vector 中插入任何值,Rust 并不知道我们想要储存什么类型的元素。这是一个非常重要的点。vector 是用泛型实现的,第十章会涉及到如何对你自己的类型使用它们。现在,所有你需要知道的就是 `Vec` 是一个由标准库提供的类型,它可以存放任何类型,而当 `Vec` 存放某个特定类型时,那个类型位于尖括号中。这里我们告诉 Rust `v` 这个 `Vec` 将存放 `i32` 类型的元素。 +注意这里我们增加了一个类型注解。因为没有向这个 vector 中插入任何值,Rust 并不知道我们想要储存什么类型的元素。这是一个非常重要的点。vector 是用泛型实现的,第十章会涉及到如何对你自己的类型使用它们。现在,所有你需要知道的就是 `Vec` 是一个由标准库提供的类型,它可以存放任何类型,而当 `Vec` 存放某个特定类型时,那个类型位于尖括号中。在示例 8-1 中,我们告诉 Rust `v` 这个 `Vec` 将存放 `i32` 类型的元素。 在更实际的代码中,一旦插入值 Rust 就可以推断出想要存放的类型,所以你很少会需要这些类型注解。更常见的做法是使用初始值来创建一个 `Vec`,而且为了方便 Rust 提供了 `vec!` 宏。这个宏会根据我们提供的值来创建一个新的 `Vec`。示例 8-2 新建一个拥有值 `1`、`2` 和 `3` 的 `Vec`: @@ -53,9 +53,9 @@ v.push(8); { let v = vec![1, 2, 3, 4]; - // do stuff with v + // 处理变量 v -} // <- v goes out of scope and is freed here +} // <- 这里 v 离开作用域并被丢弃 ``` 示例 8-4:展示 vector 和其元素于何处被丢弃 @@ -72,7 +72,12 @@ v.push(8); let v = vec![1, 2, 3, 4, 5]; let third: &i32 = &v[2]; -let third: Option<&i32> = v.get(2); +println!("The third element is {}", third); + +match v.get(2) { + Some(third) => println!("The third element is {}", third), + None => println!("There is no third element."), +} ``` 列表 8-5:使用索引语法或 `get` 方法来访问 vector 中的项 @@ -81,7 +86,7 @@ let third: Option<&i32> = v.get(2); Rust 有两个引用元素的方法的原因是程序可以选择如何处理当索引值在 vector 中没有对应值的情况。作为一个例子,让我们看看如果有一个有五个元素的 vector 接着尝试访问索引为 100 的元素时程序会如何处理,如示例 8-6 所示: -```rust,should_panic +```rust,should_panic,panics let v = vec![1, 2, 3, 4, 5]; let does_not_exist = &v[100]; @@ -90,20 +95,20 @@ let does_not_exist = v.get(100); 示例 8-6:尝试访问一个包含 5 个元素的 vector 的索引 100 处的元素 -当运行这段代码,你会发现对于第一个 `[]` 方法,当引用一个不存在的元素时 Rust 会造成 `panic!`。这个方法更适合当程序认为尝试访问超过 vector 结尾的元素是一个严重错误的情况,这时应该使程序崩溃。 +当运行这段代码,你会发现对于第一个 `[]` 方法,当引用一个不存在的元素时 Rust 会造成 panic。这个方法更适合当程序认为尝试访问超过 vector 结尾的元素是一个严重错误的情况,这时应该使程序崩溃。 当 `get` 方法被传递了一个数组外的索引时,它不会 panic 而是返回 `None`。当偶尔出现超过 vector 范围的访问属于正常情况的时候可以考虑使用它。接着你的代码可以有处理 `Some(&element)` 或 `None` 的逻辑,如第六章讨论的那样。例如,索引可能来源于用户输入的数字。如果它们不慎输入了一个过大的数字那么程序就会得到 `None` 值,你可以告诉用户当前 vector 元素的数量并再请求它们输入一个有效的值。这就比因为输入错误而使程序崩溃要友好的多! -#### 无效引用 +一旦程序获取了一个有效的引用,借用检查器将会执行所有权和借用规则(第四章讲到)来确保 vector 内容的这个引用和任何其他引用保持有效。回忆一下不能在相同作用域中同时存在可变和不可变引用的规则。这个规则适用于示例 8-7,当我们获取了 vector 的第一个元素的不可变引用并尝试在 vector 末尾增加一个元素的时候,这是行不通的: -一旦程序获取了一个有效的引用,借用检查器将会执行第四章讲到的所有权和借用规则来确保 vector 内容的这个引用和任何其他引用保持有效。回忆一下不能在相同作用域中同时存在可变和不可变引用的规则。这个规则适用于示例 8-7,当我们获取了 vector 的第一个元素的不可变引用并尝试在 vector 末尾增加一个元素的时候,这是行不通的: - -```rust,ignore +```rust,ignore,does_not_compile let mut v = vec![1, 2, 3, 4, 5]; let first = &v[0]; v.push(6); + +println!("The first element is: {}", first); ``` 示例 8-7:在拥有 vector 中项的引用的同时向其增加一个元素 @@ -112,19 +117,19 @@ v.push(6); ```text error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable - --> - | -4 | let first = &v[0]; - | - immutable borrow occurs here -5 | -6 | v.push(6); - | ^ mutable borrow occurs here -7 | -8 | } - | - immutable borrow ends here + --> src/main.rs:10:5 + | +8 | let first = &v[0]; + | - immutable borrow occurs here +9 | +10 | v.push(6); + | ^^^^^^^^^ mutable borrow occurs here +11 | +12 | println!("The first element is: {}", first); + | ----- borrow later used here ``` -示例 8-7 中的代码看起来应该能够运行:为什么第一个元素的引用会关心 vector 结尾的变化?不能这么做的原因是由于 vector 的工作方式。在 vector 的结尾增加新元素时,在没有足够空间将所有所有元素依次相邻存放的情况下,可能会要求分配新内存并将老的元素拷贝到新的空间中。这时,第一个元素的引用就指向了被释放的内存。借用规则阻止程序陷入这种状况。 +示例 8-7 中的代码看起来应该能够运行:为什么第一个元素的引用会关心 vector 结尾的变化?不能这么做的原因是由于 vector 的工作方式:在 vector 的结尾增加新元素时,在没有足够空间将所有所有元素依次相邻存放的情况下,可能会要求分配新内存并将老的元素拷贝到新的空间中。这时,第一个元素的引用就指向了被释放的内存。借用规则阻止程序陷入这种状况。 > 注意:关于 `Vec` 类型的更多实现细节,在 *https://doc.rust-lang.org/stable/nomicon/vec.html* 查看 “The Nomicon” @@ -152,7 +157,7 @@ for i in &mut v { 示例8-9:遍历 vector 中元素的可变引用 -为了修改可变引用所指向的值,在使用 `+=` 运算符之前必须使用解引用运算符(`*`)获取 `i` 中的值。 +为了修改可变引用所指向的值,在使用 `+=` 运算符之前必须使用解引用运算符(`*`)获取 `i` 中的值。第十五章会详细介绍 `*`。 ### 使用枚举来储存多种类型 diff --git a/src/ch08-02-strings.md b/src/ch08-02-strings.md index 570e870..dc46cba 100644 --- a/src/ch08-02-strings.md +++ b/src/ch08-02-strings.md @@ -1,10 +1,10 @@ ## 使用字符串存储 UTF-8 编码的文本 -> [ch08-02-strings.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch08-02-strings.md) +> [ch08-02-strings.md](https://github.com/rust-lang/book/blob/master/src/ch08-02-strings.md) >
-> commit d0e83220e083ef87880e6a04f030b90c9af9385b +> commit a86c1d315789b3ca13b20d50ad5005c62bdd9e37 -第四章已经讲过一些字符串的内容,不过现在让我们更深入地了解它。字符串是新晋 Rustacean 们通常会被困住的领域,这是由于三方面内容的结合:Rust 倾向于确保暴露出可能的错误,字符串是比很多程序员所想象的要更为复杂的数据结构,以及 UTF-8。所有这些结合起来对于来自其他语言背景的程序员就可能显得很困难了。 +第四章已经讲过一些字符串的内容,不过现在让我们更深入地了解它。字符串是新晋 Rustacean 们通常会被困住的领域,这是由于三方面理由的结合:Rust 倾向于确保暴露出可能的错误,字符串是比很多程序员所想象的要更为复杂的数据结构,以及 UTF-8。所有这些要素结合起来对于来自其他语言背景的程序员就可能显得很困难了。 在集合章节中讨论字符串的原因是,字符串就是作为字节的集合外加一些方法实现的,当这些字节被解释为文本时,这些方法提供了实用的功能。在这一部分,我们会讲到 `String` 中那些任何集合类型都有的操作,比如创建、更新和读取。也会讨论 `String` 与其他集合不一样的地方,例如索引` String` 是很复杂的,由于人和计算机理解 `String` 数据方式的不同。 @@ -26,16 +26,14 @@ let mut s = String::new(); 示例 8-11:新建一个空的 `String` -这新建了一个叫做 `s` 的空的字符串,接着我们可以向其中装载数据。 - -通常字符串会有初始数据,因为我们希望一开始就有这个字符串。为此,可以使用 `to_string` 方法,它能用于任何实现了 `Display` trait 的类型,字符串字面值也实现了它。示例 8-12 展示了两个例子。 +这新建了一个叫做 `s` 的空的字符串,接着我们可以向其中装载数据。通常字符串会有初始数据,因为我们希望一开始就有这个字符串。为此,可以使用 `to_string` 方法,它能用于任何实现了 `Display` trait 的类型,字符串字面值也实现了它。示例 8-12 展示了两个例子。 ```rust let data = "initial contents"; let s = data.to_string(); -// the method also works on a literal directly: +// 该方法也可直接用于字符串字面值: let s = "initial contents".to_string(); ``` @@ -71,13 +69,13 @@ let hello = String::from("Hola"); 示例 8-14:在字符串中储存不同语言的问候语 -所有这些都是有效的 `String`值。 +所有这些都是有效的 `String` 值。 ### 更新字符串 -`String` 的大小可以增长其内容也可以改变,就像可以放入更多数据来改变 `Vec` 的内容一样。另外,`String` 实现了 `+` 运算符作为连接运算符以便于使用。 +`String` 的大小可以增长其内容也可以改变,就像可以放入更多数据来改变 `Vec` 的内容一样。另外,可以方便的使用 `+` 运算符或 `format!` 宏来拼接 `String` 值。 -#### 使用 push 附加字符串 +#### 使用 `push_str` 和 `push` 附加字符串 可以通过 `push_str` 方法来附加字符串 slice,从而使 `String` 变长,如示例 8-15 所示。 @@ -101,7 +99,7 @@ println!("s2 is {}", s2); 如果 `push_str` 方法获取了 `s2` 的所有权,就不能在最后一行打印出其值了。好在代码如我们期望那样工作! -`push` 方法被定义为获取一个单独的字符作为参数,并附加到 `String` 中。示例 8-17 展示了使用 `push` 方法将字母 l 加入 `String` 的代码。 +`push` 方法被定义为获取一个单独的字符作为参数,并附加到 `String` 中。示例 8-17 展示了使用 `push` 方法将字母 *l* 加入 `String` 的代码。 ```rust let mut s = String::from("lo"); @@ -112,14 +110,14 @@ s.push('l'); 执行这些代码之后,`s` 将会包含 “lol”。 -#### 使用 + 运算符或 `format!` 宏连接字符串 +#### 使用 `+` 运算符或 `format!` 宏拼接字符串 通常你会希望将两个已知的字符串合并在一起。一种办法是像这样使用 `+` 运算符,如示例 8-18 所示。 ```rust let s1 = String::from("Hello, "); let s2 = String::from("world!"); -let s3 = s1 + &s2; // note s1 has been moved here and can no longer be used +let s3 = s1 + &s2; // 注意 s1 被移动了,不能继续使用 ``` 示例 8-18:使用 `+` 运算符将两个 `String` 值合并到一个新的 `String` 值中 @@ -132,11 +130,11 @@ fn add(self, s: &str) -> String { 这并不是标准库中实际的签名;标准库中的 `add` 使用泛型定义。这里我们看到的 `add` 的签名使用具体类型代替了泛型,这也正是当使用 `String` 值调用这个方法会发生的。第十章会讨论泛型。这个签名提供了理解 `+` 运算那微妙部分的线索。 -首先,`s2` 使用了 `&`,意味着我们使用第二个字符串的 **引用** 与第一个字符串相加。这是因为 `add` 函数的 `s` 参数:只能将 `&str` 和 `String` 相加,不能将两个 `String` 值相加。不过等一下——正如 `add` 的第二个参数所指定的,`&s2` 的类型是 `&String` 而不是 `&str`。那么为什么示例 8-18 还能编译呢? +首先,`s2` 使用了 `&`,意味着我们使用第二个字符串的 **引用** 与第一个字符串相加。这是因为 `add` 函数的 `s` 参数:只能将 `&str` 和 `String` 相加,不能将两个 `String` 值相加。不过等一下 —— 正如 `add` 的第二个参数所指定的,`&s2` 的类型是 `&String` 而不是 `&str`。那么为什么示例 8-18 还能编译呢? -之所以能够在 `add` 调用中使用 `&s2` 是因为 `&String` 可以被 **强转**(*coerced*)成 `&str`——当`add`函数被调用时,Rust 使用了一个被称为 **解引用强制多态**(*deref coercion*)的技术,你可以将其理解为它把 `&s2` 变成了 `&s2[..]`。第十五章会更深入的讨论解引用强制多态。因为 `add` 没有获取参数的所有权,所以 `s2` 在这个操作后仍然是有效的 `String`。 +之所以能够在 `add` 调用中使用 `&s2` 是因为 `&String` 可以被 **强转**(*coerced*)成 `&str`。当`add`函数被调用时,Rust 使用了一个被称为 **解引用强制多态**(*deref coercion*)的技术,你可以将其理解为它把 `&s2` 变成了 `&s2[..]`。第十五章会更深入的讨论解引用强制多态。因为 `add` 没有获取参数的所有权,所以 `s2` 在这个操作后仍然是有效的 `String`。 -其次,可以发现签名中 `add` 获取了 `self` 的所有权,因为 `self` **没有** 使用 `&`。这意味着上面例子中的 `s1` 的所有权将被移动到 `add` 调用中,之后就不再有效。所以虽然 `let s3 = s1 + &s2;` 看起来就像它会复制两个字符串并创建一个新的字符串,而实际上这个语句会获取 `s1` 的所有权,附加上从 `s2` 中拷贝的内容,并返回结果的所有权。换句话说,它看起来好像生成了很多拷贝不过实际上并没有:这个实现比拷贝要更高效。 +其次,可以发现签名中 `add` 获取了 `self` 的所有权,因为 `self` **没有** 使用 `&`。这意味着示例 8-18 中的 `s1` 的所有权将被移动到 `add` 调用中,之后就不再有效。所以虽然 `let s3 = s1 + &s2;` 看起来就像它会复制两个字符串并创建一个新的字符串,而实际上这个语句会获取 `s1` 的所有权,附加上从 `s2` 中拷贝的内容,并返回结果的所有权。换句话说,它看起来好像生成了很多拷贝不过实际上并没有:这个实现比拷贝要更高效。 如果想要级联多个字符串,`+` 的行为就显得笨重了: @@ -164,7 +162,7 @@ let s = format!("{}-{}-{}", s1, s2, s3); 在很多语言中,通过索引来引用字符串中的单独字符是有效且常见的操作。然而在 Rust 中,如果你尝试使用索引语法访问 `String` 的一部分,会出现一个错误。考虑一下如示例 8-19 中所示的无效代码。 -```rust,ignore +```rust,ignore,does_not_compile let s1 = String::from("hello"); let h = s1[0]; ``` @@ -193,7 +191,7 @@ error[E0277]: the trait bound `std::string::String: std::ops::Index<{integer}>` let len = String::from("Hola").len(); ``` -在这里,`len` 的值是 4 ,这意味着储存字符串 “Hola” 的 `Vec` 的长度是四个字节:这里每一个字母的 UTF-8 编码都占用一个字节。那下面这个例子又如何呢?(注意这个字符串中的首字母是西里尔字母的 Ze 而不是阿拉伯数字 3 ) +在这里,`len` 的值是 4 ,这意味着储存字符串 “Hola” 的 `Vec` 的长度是四个字节:这里每一个字母的 UTF-8 编码都占用一个字节。那下面这个例子又如何呢?(注意这个字符串中的首字母是西里尔字母的 Ze 而不是阿拉伯数字 3 。) ```rust let len = String::from("Здравствуйте").len(); @@ -201,7 +199,7 @@ let len = String::from("Здравствуйте").len(); 当问及这个字符是多长的时候有人可能会说是 12。然而,Rust 的回答是 24。这是使用 UTF-8 编码 “Здравствуйте” 所需要的字节数,这是因为每个 Unicode 标量值需要两个字节存储。因此一个字符串字节值的索引并不总是对应一个有效的 Unicode 标量值。作为演示,考虑如下无效的 Rust 代码: -```rust,ignore +```rust,ignore,does_not_compile let hello = "Здравствуйте"; let answer = &hello[0]; ``` @@ -291,8 +289,6 @@ for b in "नमस्ते".bytes() { ```text 224 164 -168 -224 // --snip-- 165 135 diff --git a/src/ch08-03-hash-maps.md b/src/ch08-03-hash-maps.md index 8213d07..9c86f8f 100644 --- a/src/ch08-03-hash-maps.md +++ b/src/ch08-03-hash-maps.md @@ -1,14 +1,14 @@ ## 哈希 map 储存键值对 -> [ch08-03-hash-maps.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch08-03-hash-maps.md) +> [ch08-03-hash-maps.md](https://github.com/rust-lang/book/blob/master/src/ch08-03-hash-maps.md) >
-> commit c2fd7b2d39c4130dd17bb99c101ac94af83d1a44 +> commit d073ece693e880b69412e645e4eabe99e74e7590 最后介绍的常用集合类型是 **哈希 map**(*hash map*)。`HashMap` 类型储存了一个键类型 `K` 对应一个值类型 `V` 的映射。它通过一个 **哈希函数**(*hashing function*)来实现映射,决定如何将键和值放入内存中。很多编程语言支持这种数据结构,不过通常有不同的名字:哈希、map、对象、哈希表或者关联数组,仅举几例。 -哈希 map 可以用于需要任何类型作为键来寻找数据的情况,而不是像 vector 那样通过索引。例如,在一个游戏中,你可以将每个团队的分数记录到哈希 map 中,其中键是队伍的名字而值是每个队伍的分数。给出一个队名,就能得到它们的得分。 +哈希 map 可以用于需要任何类型作为键来寻找数据的情况,而不是像 vector 那样通过索引。例如,在一个游戏中,你可以将每个团队的分数记录到哈希 map 中,其中键是队伍的名字而值是每个队伍的分数。给出一个队名,就能得到他们的得分。 -本章我们会介绍哈希 map 的基本 API,不过还有更多吸引人的功能隐藏于标准库中 `HashMap` 定义的函数中。请一如既往地查看标准库文档来了解更多信息。 +本章我们会介绍哈希 map 的基本 API,不过还有更多吸引人的功能隐藏于标准库中 `HashMap` 定义的函数中。一如既往请查看标准库文档来了解更多信息。 ### 新建一个哈希 map @@ -56,8 +56,8 @@ let field_value = String::from("Blue"); let mut map = HashMap::new(); map.insert(field_name, field_value); -// field_name and field_value are invalid at this point, try using them and -// see what compiler error you get! +// 这里 field_name 和 field_value 不再有效, +// 尝试使用它们看看会出现什么编译错误! ``` 示例 8-22:展示一旦键值对被插入后就为哈希 map 所拥有 @@ -178,7 +178,9 @@ println!("{:?}", map); ### 哈希函数 -`HashMap` 默认使用一种密码学安全的哈希函数,它可以抵抗拒绝服务(Denial of Service, DoS)攻击。然而这并不是可用的最快的算法,不过为了更高的安全性值得付出一些性能的代价。如果性能监测显示此哈希函数非常慢,以致于你无法接受,你可以指定一个不同的 *hasher* 来切换为其它函数。hasher 是一个实现了 `BuildHasher` trait 的类型。第十章会讨论 trait 和如何实现它们。你并不需要从头开始实现你自己的 hasher;crates.io 有其他人分享的实现了许多常用哈希算法的 hasher 的库。 +`HashMap` 默认使用一种 “密码学安全的”(“cryptographically strong” )[^siphash] 哈希函数,它可以抵抗拒绝服务(Denial of Service, DoS)攻击。然而这并不是可用的最快的算法,不过为了更高的安全性值得付出一些性能的代价。如果性能监测显示此哈希函数非常慢,以致于你无法接受,你可以指定一个不同的 *hasher* 来切换为其它函数。hasher 是一个实现了 `BuildHasher` trait 的类型。第十章会讨论 trait 和如何实现它们。你并不需要从头开始实现你自己的 hasher;[crates.io](https://crates.io) 有其他人分享的实现了许多常用哈希算法的 hasher 的库。 + +[^siphash]: [https://www.131002.net/siphash/siphash.pdf](https://www.131002.net/siphash/siphash.pdf) ## 总结 diff --git a/src/ch09-00-error-handling.md b/src/ch09-00-error-handling.md index b5e2429..985da6e 100644 --- a/src/ch09-00-error-handling.md +++ b/src/ch09-00-error-handling.md @@ -1,11 +1,11 @@ # 错误处理 -> [ch09-00-error-handling.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch09-00-error-handling.md) +> [ch09-00-error-handling.md](https://github.com/rust-lang/book/blob/master/src/ch09-00-error-handling.md) >
-> commit 7f0c806e746a133ab344cb2035d31f2a63fb6d79 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -Rust 对可靠性的执着也延伸到了错误处理。错误对于软件来说是不可避免的,所以 Rust 有很多特性来处理出现错误的情况。在很多情况下,Rust 要求你承认出错的可能性并在编译代码之前就采取行动。通过确保不会只有在将代码部署到生产环境之后才会发现错误来使得程序更可靠。 +Rust 对可靠性的执着也延伸到了错误处理。错误对于软件来说是不可避免的,所以 Rust 有很多特性来处理出现错误的情况。在很多情况下,Rust 要求你承认出错的可能性并在编译代码之前就采取行动。这些要求使得程序更为健壮,它们确保了你会在将代码部署到生产环境之前就发现错误并正确地处理它们! Rust 将错误组合成两个主要类别:**可恢复错误**(*recoverable*)和 **不可恢复错误**(*unrecoverable*)。可恢复错误通常代表向用户报告错误和重试操作是合理的情况,比如未找到文件。不可恢复错误通常是 bug 的同义词,比如尝试访问超过数组结尾的位置。 -大部分语言并不区分这两类错误,并采用类似异常这样方式统一处理他们。Rust 并没有异常。相反,对于可恢复错误有 `Result` 值,以及 `panic!`,它在遇到不可恢复错误时停止程序执行。这一章会首先介绍 `panic!` 调用,接着会讲到如何返回 `Result`。此外,我们将在决定是尝试从错误中恢复还是停止执行时探讨注意事项。 +大部分语言并不区分这两类错误,并采用类似异常这样方式统一处理他们。Rust 并没有异常。相反,对于可恢复错误有 `Result` 值,以及 `panic!`,它在遇到不可恢复错误时停止程序执行。这一章会首先介绍 `panic!` 调用,接着会讲到如何返回 `Result`。此外,我们将探讨决定是尝试从错误中恢复还是停止执行时的注意事项。 diff --git a/src/ch09-01-unrecoverable-errors-with-panic.md b/src/ch09-01-unrecoverable-errors-with-panic.md index 679a107..c2a9193 100644 --- a/src/ch09-01-unrecoverable-errors-with-panic.md +++ b/src/ch09-01-unrecoverable-errors-with-panic.md @@ -1,14 +1,14 @@ ## `panic!` 与不可恢复的错误 -> [ch09-01-unrecoverable-errors-with-panic.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch09-01-unrecoverable-errors-with-panic.md) +> [ch09-01-unrecoverable-errors-with-panic.md](https://github.com/rust-lang/book/blob/master/src/ch09-01-unrecoverable-errors-with-panic.md) >
-> commit 609909ec443042399858d1f679b0df1d6d0eba22 +> commit d073ece693e880b69412e645e4eabe99e74e7590 -突然有一天,糟糕的事情发生了,而你对此束手无策。对于这种情况,Rust 有 `panic!`宏。当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据,然后接着退出。出现这种情况的场景通常是检测到一些类型的 bug 而且程序员并不清楚该如何处理它。 +突然有一天,代码出问题了,而你对此束手无策。对于这种情况,Rust 有 `panic!`宏。当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据,然后接着退出。出现这种情况的场景通常是检测到一些类型的 bug 而且程序员并不清楚该如何处理它。 -> ### Panic 中的栈展开与终止 +> ### 对应 panic 时的栈展开或终止 > -> 当出现 `panic!` 时,程序默认会开始 **展开**(*unwinding*),这意味着 Rust 会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的过程有很多工作。另一种选择是直接 **终止**(*abort*),这会不清理数据就退出程序。那么程序所使用的内存需要由操作系统来清理。如果你需要项目的最终二进制文件越小越好,panic 时通过在 *Cargo.toml* 的 `[profile]` 部分增加 `panic = 'abort'`,可以由展开切换为终止。例如,如果你想要在release模式中 panic 时直接终止: +> 当出现 panic 时,程序默认会开始 **展开**(*unwinding*),这意味着 Rust 会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的过程有很多工作。另一种选择是直接 **终止**(*abort*),这会不清理数据就退出程序。那么程序所使用的内存需要由操作系统来清理。如果你需要项目的最终二进制文件越小越好,panic 时通过在 *Cargo.toml* 的 `[profile]` 部分增加 `panic = 'abort'`,可以由展开切换为终止。例如,如果你想要在release模式中 panic 时直接终止: > > ```toml > [profile.release] @@ -19,7 +19,7 @@ 文件名: src/main.rs -```rust,should_panic +```rust,should_panic,panics fn main() { panic!("crash and burn"); } @@ -36,7 +36,7 @@ thread 'main' panicked at 'crash and burn', src/main.rs:2:4 note: Run with `RUST_BACKTRACE=1` for a backtrace. ``` -最后三行包含 `panic!` 造成的错误信息。第一行显示了 panic 提供的信息并指明了源码中 panic 出现的位置:*src/main.rs:2:4* 表明这是 *src/main.rs* 文件的第二行第四个字符。 +最后两行包含 `panic!` 调用造成的错误信息。第一行显示了 panic 提供的信息并指明了源码中 panic 出现的位置:*src/main.rs:2:4* 表明这是 *src/main.rs* 文件的第二行第四个字符。 在这个例子中,被指明的那一行是我们代码的一部分,而且查看这一行的话就会发现 `panic!` 宏的调用。在其他情况下,`panic!` 可能会出现在我们的代码调用的代码中。错误信息报告的文件名和行号可能指向别人代码中的 `panic!` 宏调用,而不是我们代码中最终导致 `panic!` 的那一行。可以使用 `panic!` 被调用的函数的 backtrace 来寻找(我们代码中出问题的地方)。下面我们会详细介绍 backtrace 是什么。 @@ -46,7 +46,7 @@ note: Run with `RUST_BACKTRACE=1` for a backtrace. 文件名: src/main.rs -```rust,should_panic +```rust,should_panic,panics fn main() { let v = vec![1, 2, 3]; @@ -56,7 +56,7 @@ fn main() { 示例 9-1:尝试访问超越 vector 结尾的元素,这会造成 `panic!` -这里尝试访问 vector 的第一百个元素,不过它只有三个元素。这种情况下 Rust 会 panic。`[]` 应当返回一个元素,不过如果传递了一个无效索引,就没有可供 Rust 返回的正确的元素。 +这里尝试访问 vector 的第一百个元素(这里的索引是 99 因为索引从 0 开始),不过它只有三个元素。这种情况下 Rust 会 panic。`[]` 应当返回一个元素,不过如果传递了一个无效索引,就没有可供 Rust 返回的正确的元素。 这种情况下其他像 C 这样语言会尝试直接提供所要求的值,即便这可能不是你期望的:你会得到任何对应 vector 中这个元素的内存位置的值,甚至是这些内存并不属于 vector 的情况。这被称为 **缓冲区溢出**(*buffer overread*),并可能会导致安全漏洞,比如攻击者可以像这样操作索引来读取储存在数组后面不被允许的数据。 @@ -70,12 +70,11 @@ $ cargo run thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', /checkout/src/liballoc/vec.rs:1555:10 note: Run with `RUST_BACKTRACE=1` for a backtrace. -error: Process didn't exit successfully: `target/debug/panic` (exit code: 101) ``` -这指向了一个不是我们编写的文件,*libcollections/vec.rs*。这是标准库中 `Vec` 的实现。这是当对 vector `v` 使用 `[]` 时 *libcollections/vec.rs* 中会执行的代码,也是真正出现 `panic!` 的地方。 +这指向了一个不是我们编写的文件,*vec.rs*。这是标准库中 `Vec` 的实现。这是当对 vector `v` 使用 `[]` 时 *vec.rs* 中会执行的代码,也是真正出现 `panic!` 的地方。 -接下来的几行提醒我们可以设置 `RUST_BACKTRACE` 环境变量来得到一个 backtrace *backtrace* 是一个执行到目前位置所有被调用的函数的列表。Rust 的 backtrace 跟其他语言中的一样:阅读 backtrace 的关键是从头开始读直到发现你编写的文件。这就是问题的发源地。这一行往上是你的代码调用的代码;往下则是调用你的代码的代码。这些行可能包含核心 Rust 代码,标准库代码或用到的 crate 代码。让我们尝试获取一个 backtrace:示例 9-2 展示了与你看到类似的输出: +接下来的几行提醒我们可以设置 `RUST_BACKTRACE` 环境变量来得到一个 backtrace *backtrace* 是一个执行到目前位置所有被调用的函数的列表。Rust 的 backtrace 跟其他语言中的一样:阅读 backtrace 的关键是从头开始读直到发现你编写的文件。这就是问题的发源地。这一行往上是你的代码调用的代码;往下则是调用你的代码的代码。这些行可能包含核心 Rust 代码,标准库代码或用到的 crate 代码。让我们将 `RUST_BACKTRACE` 环境变量设置为任何不是 0 的值来获取 backtrace 看看。示例 9-2 展示了与你看到类似的输出: ```text $ RUST_BACKTRACE=1 cargo run @@ -121,8 +120,8 @@ stack backtrace: 示例 9-2:当设置 `RUST_BACKTRACE` 环境变量时 `panic!` 调用所生成的 backtrace 信息 -这里有大量的输出!你实际看到的输出可能因不同的操作系统和 Rust 版本而有所不同。为了获取带有这些信息的 backtrace,必须启用 debug 标识。当不使用 --release 参数运行 cargo build 或 cargo run 时 debug 标识会默认启用,这里便是如此。 +这里有大量的输出!你实际看到的输出可能因不同的操作系统和 Rust 版本而有所不同。为了获取带有这些信息的 backtrace,必须启用 debug 标识。当不使用 `--release` 参数运行 cargo build 或 cargo run 时 debug 标识会默认启用,就像这里一样。 -示例 9-2 的输出中,backtrace 的 11 行指向了我们项目中造成问题的行:*src/main.rs* 的第 4 行。如果你不希望程序 panic,第一个提到我们编写的代码行的位置是你应该开始调查的,以便查明是什么值如何在这个地方引起了 panic。在上面的例子中,我们故意编写会 panic 的代码来演示如何使用 backtrace,修复这个 panic 的方法就是不要尝试在一个只包含三个项的 vector 中请求索引是 100 的元素。当将来你的代码出现了 panic,你需要搞清楚在这特定的场景下代码中执行了什么操作和什么值导致了 panic,以及应当如何处理才能避免这个问题。 +示例 9-2 的输出中,backtrace 的 11 行指向了我们项目中造成问题的行:*src/main.rs* 的第 4 行。如果你不希望程序 panic,第一个提到我们编写的代码行的位置是你应该开始调查的,以便查明是什么值如何在这个地方引起了 panic。在示例 9-1 中,我们故意编写会 panic 的代码来演示如何使用 backtrace,修复这个 panic 的方法就是不要尝试在一个只包含三个项的 vector 中请求索引是 100 的元素。当将来你的代码出现了 panic,你需要搞清楚在这特定的场景下代码中执行了什么操作和什么值导致了 panic,以及应当如何处理才能避免这个问题。 -本章后面的小节“panic! 还是不 panic!”会再次回到 `panic!` 并讲到何时应该及何时不应该使用这个方式。接下来,我们来看看如何使用 `Result` 来从错误中恢复。 +本章后面的小节 “panic! 还是不 panic!”会再次回到 `panic!` 会回到 `panic!` 并讲解何时应该何时不应该使用 `panic!` 来处理错误情况。接下来,我们来看看如何使用 `Result` 来从错误中恢复。 diff --git a/src/ch09-02-recoverable-errors-with-result.md b/src/ch09-02-recoverable-errors-with-result.md index f8ae13d..a24743d 100644 --- a/src/ch09-02-recoverable-errors-with-result.md +++ b/src/ch09-02-recoverable-errors-with-result.md @@ -1,8 +1,8 @@ ## `Result` 与可恢复的错误 -> [ch09-02-recoverable-errors-with-result.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch09-02-recoverable-errors-with-result.md) +> [ch09-02-recoverable-errors-with-result.md](https://github.com/rust-lang/book/blob/master/src/ch09-02-recoverable-errors-with-result.md) >
-> commit 347a8bf6beaf34cef5c4e82c2171f498f081485e +> commit db53e2e3cdf77beac853df6f29db4b3b86ea598c 大部分错误并没有严重到需要程序完全停止执行。有时,一个函数会因为一个容易理解并做出反应的原因失败。例如,如果尝试打开一个文件不过由于文件并不存在而失败,此时我们可能想要创建这个文件而不是终止进程。 @@ -33,7 +33,7 @@ fn main() { 示例 9-3:打开文件 -如何知道 `File::open` 返回一个 `Result` 呢?我们可以查看标准库 API 文档,或者可以直接问编译器!如果给 `f` 某个我们知道 **不是** 函数返回值类型的类型注解,接着尝试编译代码,编译器会告诉我们类型不匹配。然后错误信息会告诉我们 `f` 的类型 **应该** 是什么。让我们试试!我们知道 `File::open` 的返回值不是 `u32` 类型的,所以将 `let f` 语句改为如下: +如何知道 `File::open` 返回一个 `Result` 呢?我们可以查看 [标准库 API 文档](https://doc.rust-lang.org/std/index.html),或者可以直接问编译器!如果给 `f` 某个我们知道 **不是** 函数返回值类型的类型注解,接着尝试编译代码,编译器会告诉我们类型不匹配。然后错误信息会告诉我们 `f` 的类型 **应该** 是什么。让我们试试!我们知道 `File::open` 的返回值不是 `u32` 类型的,所以将 `let f` 语句改为如下: ```rust,ignore let f: u32 = File::open("hello.txt"); @@ -99,6 +99,9 @@ Os { code: 2, message: "No such file or directory" } }', src/main.rs:9:12 文件名: src/main.rs + + ```rust,ignore use std::fs::File; use std::io::ErrorKind; @@ -108,22 +111,12 @@ fn main() { let f = match f { Ok(file) => file, - Err(ref error) if error.kind() == ErrorKind::NotFound => { - match File::create("hello.txt") { + Err(error) => match error.kind() { + ErrorKind::NotFound => match File::create("hello.txt") { Ok(fc) => fc, - Err(e) => { - panic!( - "Tried to create file but there was a problem: {:?}", - e - ) - }, - } - }, - Err(error) => { - panic!( - "There was a problem opening the file: {:?}", - error - ) + Err(e) => panic!("Tried to create file but there was a problem: {:?}", e), + }, + other_error => panic!("There was a problem opening the file: {:?}", other_error), }, }; } @@ -131,15 +124,36 @@ fn main() { 示例 9-5:使用不同的方式处理不同类型的错误 -`File::open` 返回的 `Err` 成员中的值类型 `io::Error`,它是一个标准库中提供的结构体。这个结构体有一个返回 `io::ErrorKind` 值的 `kind` 方法可供调用。`io::ErrorKind` 是一个标准库提供的枚举,它的成员对应 `io` 操作可能导致的不同错误类型。我们感兴趣的成员是 `ErrorKind::NotFound`,它代表尝试打开的文件并不存在。 +`File::open` 返回的 `Err` 成员中的值类型 `io::Error`,它是一个标准库中提供的结构体。这个结构体有一个返回 `io::ErrorKind` 值的 `kind` 方法可供调用。`io::ErrorKind` 是一个标准库提供的枚举,它的成员对应 `io` 操作可能导致的不同错误类型。我们感兴趣的成员是 `ErrorKind::NotFound`,它代表尝试打开的文件并不存在。所以 `match` 的 `f` 匹配,不过对于 `error.kind()` 还有一个内部 `match`。 + +我们希望在匹配守卫中检查的条件是 `error.kind()` 的返回值是 `ErrorKind`的 `NotFound` 成员。如果是,则尝试通过 `File::create` 创建文件。然而因为 `File::create` 也可能会失败,还需要增加一个内部 `match` 语句。当文件不能被打开,会打印出一个不同的错误信息。外部 `match` 的最后一个分支保持不变这样对任何除了文件不存在的错误会使程序 panic。 + +这里有好多 `match`!`match` 确实很强大,不过也非常的基础。第十三章我们会介绍闭包(closure)。`Result` 有很多接受闭包的方法,并采用 `match` 表达式实现。一个更老练的 Rustacean 可能会这么写: + +```rust,ignore +use std::fs::File; +use std::io::ErrorKind; -条件 `if error.kind() == ErrorKind::NotFound` 被称作 *match guard*:它是一个进一步完善 `match` 分支模式的额外的条件。这个条件必须为真才能使分支的代码被执行;否则,模式匹配会继续并考虑 `match` 中的下一个分支。模式中的 `ref` 是必须的,这样 `error` 就不会被移动到 guard 条件中而仅仅只是引用它。第十八章会详细解释为什么在模式中使用 `ref` 而不是 `&` 来获取一个引用。简而言之,在模式的上下文中,`&` 匹配一个引用并返回它的值,而 `ref` 匹配一个值并返回一个引用。 +fn main() { + let f = File::open("hello.txt").map_err(|error| { + if error.kind() == ErrorKind::NotFound { + File::create("hello.txt").unwrap_or_else(|error| { + panic!("Tried to create file but there was a problem: {:?}", error); + }) + } else { + panic!("There was a problem opening the file: {:?}", error); + } + }); +} +``` -在 match guard 中我们想要检查的条件是 `error.kind()` 是否是 `ErrorKind` 枚举的 `NotFound` 成员。如果是,尝试用 `File::create` 创建文件。然而 `File::create` 也可能会失败,还需要增加一个内部 `match` 语句。当文件不能被打开,会打印出一个不同的错误信息。外部 `match` 的最后一个分支保持不变这样对任何除了文件不存在的错误会使程序 panic。 +在阅读完第十三章后再回到这个例子,并查看标准库文档 `map_err` 和 `unwrap_or_else` 方法都做了什么操作。还有很多这类方法可以消除大量处理错误时嵌套的 `match` 表达式。 ### 失败时 panic 的简写:`unwrap` 和 `expect` -`match` 能够胜任它的工作,不过它可能有点冗长并且不总是能很好的表明其意图。`Result` 类型定义了很多辅助方法来处理各种情况。其中之一叫做 `unwrap`,它的实现就类似于示例 9-4 中的 `match` 语句。如果 `Result` 值是成员 `Ok`,`unwrap` 会返回 `Ok` 中的值。如果 `Result` 是成员 `Err`,`unwrap` 会为我们调用 `panic!`。 +`match` 能够胜任它的工作,不过它可能有点冗长并且不总是能很好的表明其意图。`Result` 类型定义了很多辅助方法来处理各种情况。其中之一叫做 `unwrap`,它的实现就类似于示例 9-4 中的 `match` 语句。如果 `Result` 值是成员 `Ok`,`unwrap` 会返回 `Ok` 中的值。如果 `Result` 是成员 `Err`,`unwrap` 会为我们调用 `panic!`。这里是一个实践 `unwrap` 的例子: + +文件名: src/main.rs ```rust,should_panic use std::fs::File; @@ -210,7 +224,7 @@ fn read_username_from_file() -> Result { 示例 9-6:一个函数使用 `match` 将错误返回给代码调用者 -首先让我们看看函数的返回值:`Result`。这意味着函数返回一个 `Result` 类型的值,其中泛型参数 `T` 的具体类型是 `String`,而 `E` 的具体类型是 `io::Error`。如果这个函数没有出任何错误成功返回,函数的调用者会收到一个包含 `String` 的 `Ok` 值————函数从文件中读取到的用户名。如果函数遇到任何错误,函数的调用者会收到一个 `Err` 值,它储存了一个包含更多这个问题相关信息的 `io::Error` 实例。这里选择 `io::Error` 作为函数的返回值是因为它正好是函数体中那两个可能会失败的操作的错误返回值:`File::open` 函数和 `read_to_string` 方法。 +首先让我们看看函数的返回值:`Result`。这意味着函数返回一个 `Result` 类型的值,其中泛型参数 `T` 的具体类型是 `String`,而 `E` 的具体类型是 `io::Error`。如果这个函数没有出任何错误成功返回,函数的调用者会收到一个包含 `String` 的 `Ok` 值 —— 函数从文件中读取到的用户名。如果函数遇到任何错误,函数的调用者会收到一个 `Err` 值,它储存了一个包含更多这个问题相关信息的 `io::Error` 实例。这里选择 `io::Error` 作为函数的返回值是因为它正好是函数体中那两个可能会失败的操作的错误返回值:`File::open` 函数和 `read_to_string` 方法。 函数体以 `File::open` 函数开头。接着使用 `match` 处理返回值 `Result`,类似于示例 9-4 中的 `match`,唯一的区别是当 `Err` 时不再调用 `panic!`,而是提早返回并将 `File::open` 返回的错误值作为函数的错误返回值传递给调用者。如果 `File::open` 成功了,我们将文件句柄储存在变量 `f` 中并继续。 @@ -243,7 +257,7 @@ fn read_username_from_file() -> Result { `Result` 值之后的 `?` 被定义为与示例 9-6 中定义的处理 `Result` 值的 `match` 表达式有着完全相同的工作方式。如果 `Result` 的值是 `Ok`,这个表达式将会返回 `Ok` 中的值而程序将继续执行。如果值是 `Err`,`Err` 中的值将作为整个函数的返回值,就好像使用了 `return` 关键字一样,这样错误值就被传播给了调用者。 -示例 9-6 中的 `match` 表达式与问号运算符所做的有一点不同:`?` 所使用的错误值被传递给了 `from` 函数,它定义于标准库的 `From` trait 中,其用来将错误从一种类型转换为另一种类型。到问号运算符调用 `from` 函数时,收到的错误类型被转换为定义为当前函数返回的错误类型。这在当一个函数返回一个错误类型来代表所有可能失败的方式时很有用,即使其可能会因很多种原因失败。只要每一个错误类型都实现了 `from` 函数来定义如将其转换为返回的错误类型,问号运算符会自动处理这些转换。 +示例 9-6 中的 `match` 表达式与问号运算符所做的有一点不同:`?` 所使用的错误值被传递给了 `from` 函数,它定义于标准库的 `From` trait 中,其用来将错误从一种类型转换为另一种类型。当 `?` 调用 `from` 函数时,收到的错误类型被转换为定义为当前函数返回的错误类型。这在当一个函数返回一个错误类型来代表所有可能失败的方式时很有用,即使其可能会因很多种原因失败。只要每一个错误类型都实现了 `from` 函数来定义如将其转换为返回的错误类型,`?` 会自动处理这些转换。 在示例 9-7 的上下文中,`File::open` 调用结尾的 `?` 将会把 `Ok` 中的值返回给变量 `f`。如果出现了错误,`?` 会提早返回整个函数并将一些 `Err` 值传播给调用者。同理也适用于 `read_to_string` 调用结尾的 `?`。 @@ -269,6 +283,23 @@ fn read_username_from_file() -> Result { 在 `s` 中创建新的 `String` 被放到了函数开头;这一部分没有变化。我们对 `File::open("hello.txt")?` 的结果直接链式调用了 `read_to_string`,而不再创建变量 `f`。仍然需要 `read_to_string` 调用结尾的 `?`,而且当 `File::open` 和 `read_to_string` 都成功没有失败时返回包含用户名 `s` 的 `Ok` 值。其功能再一次与示例 9-6 和示例 9-7 保持一致,不过这是一个与众不同且更符合工程学的写法。 +说到编写这个函数的不同方法,甚至还有一个更短的写法: + +文件名: src/main.rs + +```rust +use std::io; +use std::fs; + +fn read_username_from_file() -> Result { + fs::read_to_string("hello.txt") +} +``` + +示例 9-9: 使用 `fs::read_to_string` + +将文件读取到一个字符串是相当常见的操作,所以 Rust 提供了名为 `fs::read_to_string` 的函数,它会打开文件、新建一个 `String`、读取文件的内容,并将内容放入 `String`,接着返回它。当然,这样做就没有展示所有这些错误处理的机会了,所以我们最初就选择了艰苦的道路。 + ### `?` 只能被用于返回 `Result` 的函数 `?` 只能被用于返回值类型为 `Result` 的函数,因为他被定义为与示例 9-6 中的 `match` 表达式有着完全相同的工作方式。`match` 的 `return Err(e)` 部分要求返回值类型是 `Result`,所以函数的返回值必须是 `Result` 才能与这个 `return` 相兼容。 @@ -286,20 +317,31 @@ fn main() { 当编译这些代码,会得到如下错误信息: ```text -error[E0277]: the trait bound `(): std::ops::Try` is not satisfied +error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `std::ops::Try`) --> src/main.rs:4:13 | 4 | let f = File::open("hello.txt")?; - | ------------------------ - | | - | the `?` operator can only be used in a function that returns - `Result` (or another type that implements `std::ops::Try`) - | in this macro invocation + | ^^^^^^^^^^^^^^^^^^^^^^^^ cannot use the `?` operator in a function that returns `()` | = help: the trait `std::ops::Try` is not implemented for `()` = note: required by `std::ops::Try::from_error` ``` -错误指出只能在返回 `Result` 的函数中使用问号运算符。在不返回 `Result` 的函数中,当调用其他返回 `Result` 的函数时,需要使用 `match` 或 `Result` 的方法之一来处理,而不能用 `?` 将潜在的错误传播给调用者。 +错误指出只能在返回 `Result` 的函数中使用 `?`。在不返回 `Result` 的函数中,当调用其他返回 `Result` 的函数时,需要使用 `match` 或 `Result` 的方法之一来处理,而不能用 `?` 将潜在的错误传播给代码调用方。 + +不过 `main` 函数可以返回一个 `Result`: + +```rust,ignore +use std::error::Error; +use std::fs::File; + +fn main() -> Result<(), Box> { + let f = File::open("hello.txt")?; + + Ok(()) +} +``` + +`Box` 被称为 “trait 对象”(“trait object”),第十七章会介绍。目前可以理解 `Box` 为 “任何类型的错误”。 现在我们讨论过了调用 `panic!` 或返回 `Result` 的细节,是时候回到他们各自适合哪些场景的话题了。 diff --git a/src/ch09-03-to-panic-or-not-to-panic.md b/src/ch09-03-to-panic-or-not-to-panic.md index 56aed76..b038c37 100644 --- a/src/ch09-03-to-panic-or-not-to-panic.md +++ b/src/ch09-03-to-panic-or-not-to-panic.md @@ -1,12 +1,12 @@ ## `panic!` 还是不 `panic!` -> [ch09-03-to-panic-or-not-to-panic.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch09-03-to-panic-or-not-to-panic.md) +> [ch09-03-to-panic-or-not-to-panic.md](https://github.com/rust-lang/book/blob/master/src/ch09-03-to-panic-or-not-to-panic.md) >
-> commit 609909ec443042399858d1f679b0df1d6d0eba22 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 那么,该如何决定何时应该 `panic!` 以及何时应该返回 `Result` 呢?如果代码 panic,就没有恢复的可能。你可以选择对任何错误场景都调用 `panic!`,不管是否有可能恢复,不过这样就是你代替调用者决定了这是不可恢复的。选择返回 `Result` 值的话,就将选择权交给了调用者,而不是代替他们做出决定。调用者可能会选择以符合他们场景的方式尝试恢复,或者也可能干脆就认为 `Err` 是不可恢复的,所以他们也可能会调用 `panic!` 并将可恢复的错误变成了不可恢复的错误。因此返回 `Result` 是定义可能会失败的函数的一个好的默认选择。 -有一些情况 panic 比返回 `Result` 更为合适,不过他们并不常见。让我们讨论一下为何在示例、代码原型和测试中,以及那些人们认为不会失败而编译器不这么看的情况下, panic 是合适的,最后会总结一些在库代码中如何决定是否要 panic 的通用指导原则。 +有一些情况 panic 比返回 `Result` 更为合适,不过他们并不常见。让我们讨论一下为何在示例、代码原型和测试中,以及那些人们认为不会失败而编译器不这么看的情况下, panic 是合适的。章节最后会总结一些在库代码中如何决定是否要 panic 的通用指导原则。 ### 示例、代码原型和测试都非常适合 panic @@ -38,14 +38,12 @@ let home: IpAddr = "127.0.0.1".parse().unwrap(); 如果别人调用你的代码并传递了一个没有意义的值,最好的情况也许就是 `panic!` 并警告使用你的库的人他的代码中有 bug 以便他能在开发时就修复它。类似的,`panic!` 通常适合调用不能够控制的外部代码时,这时无法修复其返回的无效状态。 -无论代码编写的多么好,当有害状态是预期会出现时,返回 `Result` 仍要比调用 `panic!` 更为合适。这样的例子包括解析器接收到错误数据,或者 HTTP 请求返回一个表明触发了限流的状态。在这些例子中,应该通过返回 `Result` 来表明失败预期是可能的,这样将有害状态向上传播,调用者就可以决定该如何处理这个问题。使用 `panic!` 来处理这些情况就不是最好的选择。 +然而当错误预期会出现时,返回 `Result` 仍要比调用 `panic!` 更为合适。这样的例子包括解析器接收到错误数据,或者 HTTP 请求返回一个表明触发了限流的状态。在这些例子中,应该通过返回 `Result` 来表明失败预期是可能的,这样将有害状态向上传播,调用者就可以决定该如何处理这个问题。使用 `panic!` 来处理这些情况就不是最好的选择。 当代码对值进行操作时,应该首先验证值是有效的,并在其无效时 `panic!`。这主要是出于安全的原因:尝试操作无效数据会暴露代码漏洞,这就是标准库在尝试越界访问数组时会 `panic!` 的主要原因:尝试访问不属于当前数据结构的内存是一个常见的安全隐患。函数通常都遵循 **契约**(*contracts*):他们的行为只有在输入满足特定条件时才能得到保证。当违反契约时 panic 是有道理的,因为这通常代表调用方的 bug,而且这也不是那种你希望调用方必须处理的错误。事实上也没有合理的方式来恢复调用方的代码:调用方的 **程序员** 需要修复其代码。函数的契约,尤其是当违反它会造成 panic 的契约,应该在函数的 API 文档中得到解释。 虽然在所有函数中都拥有许多错误检查是冗长而烦人的。幸运的是,可以利用 Rust 的类型系统(以及编译器的类型检查)为你进行很多检查。如果函数有一个特定类型的参数,可以在知晓编译器已经确保其拥有一个有效值的前提下进行你的代码逻辑。例如,如果你使用了一个不同于 `Option` 的类型,而且程序期望它是 **有值** 的并且不是 **空值**。你的代码无需处理 `Some` 和 `None` 这两种情况,它只会有一种情况就是绝对会有一个值。尝试向函数传递空值的代码甚至根本不能编译,所以你的函数在运行时没有必要判空。另外一个例子是使用像 `u32` 这样的无符号整型,也会确保它永远不为负。 -### 创建自定义类型作为验证 - 让我们使用 Rust 类型系统的思想来进一步确保值的有效性,并尝试创建一个自定义类型以进行验证。回忆一下第二章的猜猜看游戏,我们的代码要求用户猜测一个 1 到 100 之间的数字,在将其与秘密数字做比较之前我们从未验证用户的猜测是位于这两个数字之间的,我们只验证它是否为正。在这种情况下,其影响并不是很严重:“Too high” 或 “Too low” 的输出仍然是正确的。但是这是一个很好的引导用户得出有效猜测的辅助,例如当用户猜测一个超出范围的数字或者输入字母时采取不同的行为。 一种实现方式是将猜测解析成 `i32` 而不仅仅是 `u32`,来默许输入负数,接着检查数字是否在范围内: @@ -73,15 +71,15 @@ loop { 然而,这并不是一个理想的解决方案:程序只处理 1 到 100 之间的值是绝对不可取的,而且如果有很多函数都有这样的要求,在每个函数中都有这样的检查将是非常冗余的(并可能潜在的影响性能)。 -相反我们可以创建一个新类型来将验证放入创建其实例的函数中,而不是到处重复这些检查。这样就可以安全的在函数签名中使用新类型并相信他们接收到的值。示例 9-9 中展示了一个定义 `Guess` 类型的方法,只有在 `new` 函数接收到 1 到 100 之间的值时才会创建 `Guess` 的实例: +相反我们可以创建一个新类型来将验证放入创建其实例的函数中,而不是到处重复这些检查。这样就可以安全的在函数签名中使用新类型并相信他们接收到的值。示例 9-10 中展示了一个定义 `Guess` 类型的方法,只有在 `new` 函数接收到 1 到 100 之间的值时才会创建 `Guess` 的实例: ```rust pub struct Guess { - value: u32, + value: i32, } impl Guess { - pub fn new(value: u32) -> Guess { + pub fn new(value: i32) -> Guess { if value < 1 || value > 100 { panic!("Guess value must be between 1 and 100, got {}.", value); } @@ -91,13 +89,13 @@ impl Guess { } } - pub fn value(&self) -> u32 { + pub fn value(&self) -> i32 { self.value } } ``` -示例 9-9:一个 `Guess` 类型,它只在值位于 1 和 100 之间时才继续 +示例 9-10:一个 `Guess` 类型,它只在值位于 1 和 100 之间时才继续 首先,我们定义了一个包含 `u32` 类型字段 `value` 的结构体 `Guess`。这里是储存猜测值的地方。 @@ -109,6 +107,6 @@ impl Guess { ## 总结 -Rust 的错误处理功能被设计为帮助你编写更加健壮的代码。`panic!` 宏代表一个程序无法处理的状态,并停止执行而不是使用无效或不正确的值继续处理。Rust 类型系统的 `Result` 枚举代表操作可能会在一种可以恢复的情况下失败。可以使用 `Result` 来告诉代码调用者他需要处理潜在的成功或失败。在适当的场景使用 `panic!` 和 `Result` 将会使你的代码在面对无处不在的错误时显得更加可靠。 +Rust 的错误处理功能被设计为帮助你编写更加健壮的代码。`panic!` 宏代表一个程序无法处理的状态,并停止执行而不是使用无效或不正确的值继续处理。Rust 类型系统的 `Result` 枚举代表操作可能会在一种可以恢复的情况下失败。可以使用 `Result` 来告诉代码调用者他需要处理潜在的成功或失败。在适当的场景使用 `panic!` 和 `Result` 将会使你的代码在面对不可避免的错误时显得更加可靠。 现在我们已经见识过了标准库中 `Option` 和 `Result` 泛型枚举的能力了,在下一章让我们聊聊泛型是如何工作的,以及如何在你的代码中使用他们。 diff --git a/src/ch10-00-generics.md b/src/ch10-00-generics.md index 21f81cf..d1f24f0 100644 --- a/src/ch10-00-generics.md +++ b/src/ch10-00-generics.md @@ -1,10 +1,10 @@ # 泛型、trait 和生命周期 -> [ch10-00-generics.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch10-00-generics.md) +> [ch10-00-generics.md](https://github.com/rust-lang/book/blob/master/src/ch10-00-generics.md) >
-> commit 9e8d1ed7a1732d9cc09606a9b62ee8838998502f +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -每一个编程语言都有高效的处理重复概念的工具;在 Rust 中其工具之一就是 **泛型**(*generics*)。泛型是具体类型或其他属性的抽象替代。我们可以表达泛型的属性,比如他们的行为或如何与其他泛型相关联,而不需要在编写和编译代码时知道他们在这里实际上代表什么。 +每一个编程语言都有高效的处理重复概念的工具.在 Rust 中其工具之一就是 **泛型**(*generics*)。泛型是具体类型或其他属性的抽象替代。我们可以表达泛型的属性,比如他们的行为或如何与其他泛型相关联,而不需要在编写和编译代码时知道他们在这里实际上代表什么。 同理为了编写一份可以用于多种具体值的代码,函数并不知道其参数为何值,这时就可以让函数获取泛型而不是像 `i32` 或 `String` 这样的具体值。我们已经使用过第六章的 `Option`,第八章的 `Vec` 和 `HashMap`,以及第九章的 `Result` 这些泛型了。本章会探索如何使用泛型定义我们自己的类型、函数和方法! @@ -45,7 +45,7 @@ fn main() { 如果需要在两个不同的列表中寻找最大值,我们可以重复示例 10-1 中的代码,这样程序中就会存在两段相同逻辑的代码,如示例 10-2 所示: -Filename: src/main.rs +文件名: src/main.rs ```rust fn main() { @@ -79,10 +79,6 @@ fn main() { 虽然代码能够执行,但是重复的代码是冗余且容易出错的,并且意味着当更新逻辑时需要修改多处地方的代码。 - - - 为了消除重复,我们可以创建一层抽象,在这个例子中将表现为一个获取任意整型列表作为参数并对其进行处理的函数。这将增加代码的简洁性并让我们将表达和推导寻找列表中最大值的这个概念与使用这个概念的特定位置相互独立。 在示例 10-3 的程序中将寻找最大值的代码提取到了一个叫做 `largest` 的函数中。这个程序可以找出两个不同数字列表的最大值,不过示例 10-1 中的代码只存在于一个位置: @@ -123,9 +119,9 @@ fn main() { 从示例 10-2 到示例 10-3 中涉及的机制经历了如下几步: -1. 我们注意到了重复代码。 -2. 我们将重复代码提取到了一个函数中,并在函数签名中指定了代码中的输入和返回值。 -3. 我们将重复代码的两个实例,改为调用函数。 +1. 找出重复代码。 +2. 将重复代码提取到了一个函数中,并在函数签名中指定了代码中的输入和返回值。 +3. 将重复代码的两个实例,改为调用函数。 在不同的场景使用不同的方式,我们也可以利用相同的步骤和泛型来减少重复代码。与函数体可以在抽象`list`而不是特定值上操作的方式相同,泛型允许代码对抽象类型进行操作。 diff --git a/src/ch10-01-syntax.md b/src/ch10-01-syntax.md index d4f1cf4..63b8df5 100644 --- a/src/ch10-01-syntax.md +++ b/src/ch10-01-syntax.md @@ -1,17 +1,16 @@ ## 泛型数据类型 -> [ch10-01-syntax.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch10-01-syntax.md) +> [ch10-01-syntax.md](https://github.com/rust-lang/book/blob/master/src/ch10-01-syntax.md) >
-> commit 9e8d1ed7a1732d9cc09606a9b62ee8838998502f +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -泛型用于为函数签名或结构体等创建定义,允许我们创建许多不同的具体数据类型。让我们看看如何使用泛型定义函数、结构体、枚举和方法,然后我们将讨论泛型如何影响代码性能。 +我们可以使用泛型为像函数签名或结构体这样的项创建定义,这样它们就可以用于多种不同的具体数据类型。让我们看看如何使用泛型定义函数、结构体、枚举和方法,然后我们将讨论泛型如何影响代码性能。 ### 在函数定义中使用泛型 -定义函数时可以在函数签名的参数数据类型和返回值中使用泛型。以这种方式编写的代码将更灵活并能向函数调用者提供更多功能,同时不引入重复代码。 - -回到 `largest` 函数上,示例 10-4 中展示了两个提供了相同的寻找 slice 中最大值功能的函数。第一个是从示例 10-3 中提取的寻找 slice 中 `i32` 最大值的函数。第二个函数寻找 slice 中 `char` 的最大值: +当使用泛型定义函数时,我们在函数签名中通常为参数和返回值指定数据类型的位置放置泛型。以这种方式编写的代码将更灵活并能向函数调用者提供更多功能,同时不引入重复代码。 +回到 `largest` 函数上,示例 10-4 中展示了两个提供了相同的寻找 slice 中最大值功能的函数。 文件名: src/main.rs @@ -57,13 +56,11 @@ fn main() { 示例 10-4:两个只在名称和签名中类型有所不同的函数 -这里 `largest_i32` 和 `largest_char` 有着完全相同的函数体,所以如果能够将这两个函数变成一个来减少重复就太好了。所幸通过引入一个泛型参数就能实现! - -为了参数化要定义的函数的签名中的类型,我们需要像给函数的值参数起名那样为这类型参数起一个名字。这里选择了名称 `T`。任何标识符都可以作为类型参数名,选择 `T` 是因为 Rust 的类型命名规范是骆驼命名法(CamelCase)。另外泛型类型参数的规范也倾向于简短,经常仅仅是一个字母。`T` 作为 “type” 的缩写是大部分 Rust 程序员的首选。 +`largest_i32` 函数是从示例 10-3 中提取的寻找 slice 中 `i32` 最大值的函数。`largest_char` 函数寻找 slice 中 `char` 的最大值:这两个函数有着相同的代码,所以让我们在一个单独的函数中引入泛型参数来消除重复。 -当需要在函数体中使用一个参数时,必须在函数签名中声明这个参数以便编译器能知道函数体中这个名称的意义。同理,当在函数签名中使用一个类型参数时,必须在使用它之前就声明它。类型参数声明位于函数名称与参数列表中间的尖括号中。 +为了参数化要定义的函数的签名中的类型,我们需要像给函数的值参数起名那样为这类型参数起一个名字。任何标识符都可以作为类型参数名。不过选择 `T` 是因为 Rust 的习惯是让变量名尽量短,通常就只有一个字母,同时 Rust 类型命名规范是骆驼命名法(CamelCase)。`T` 作为 “type” 的缩写是大部分 Rust 程序员的首选。 -我们将要定义的泛型版本的 `largest` 函数的签名看起来像这样: +当需要在函数体中使用一个参数时,必须在函数签名中声明这个参数以便编译器能知道函数体中这个名称的意义。同理,当在函数签名中使用一个类型参数时,必须在使用它之前就声明它。为了定义泛型版本的 `largest` 函数,类型参数声明位于函数名称与参数列表中间的尖括号 `<>` 中,像这样: ```rust,ignore fn largest(list: &[T]) -> T { @@ -71,11 +68,11 @@ fn largest(list: &[T]) -> T { 这可以理解为:函数 `largest` 有泛型类型 `T`。它有一个参数 `list`,它的类型是一个 `T` 值的 slice。`largest` 函数将会返回一个与 `T` 相同类型的值。 -示例 10-5 展示一个在签名中使用了泛型的统一的 `largest` 函数定义,并向我们展示了如何对 `i32` 值的 slice 或 `char` 值的 slice 调用 `largest` 函数。注意这些代码还不能编译! +示例 10-5 展示一个在签名中使用了泛型的统一的 `largest` 函数定义。该示例也向我们展示了如何对 `i32` 值的 slice 或 `char` 值的 slice 调用 `largest` 函数。注意这些代码还不能编译,不过本章稍后部分会修复错误。 文件名: src/main.rs -```rust,ignore +```rust,ignore,does_not_compile fn largest(list: &[T]) -> T { let mut largest = list[0]; @@ -107,22 +104,17 @@ fn main() { ```text error[E0369]: binary operation `>` cannot be applied to type `T` + --> src/main.rs:5:12 | 5 | if item > largest { - | ^^^^ + | ^^^^^^^^^^^^^^ | -note: an implementation of `std::cmp::PartialOrd` might be missing for `T` + = note: an implementation of `std::cmp::PartialOrd` might be missing for `T` ``` -注释中提到了 `std::cmp::PartialOrd`,这是一个 *trait*。下一部分会讲到 trait,不过简单来说,这个错误表明 `largest` 的函数体不能适用于 `T` 的所有可能的类型;因为在函数体需要比较 `T` 类型的值,不过它只能用于我们知道如何排序的类型。标准库中定义的 `std::cmp::PartialOrd` trait 可以实现类型的比较功能。在下一部分会再次回到 trait 并讲解如何为泛型指定一个 trait,不过让我们先把这个例子放在一边并探索其他那些可以使用泛型类型参数的地方。 +注释中提到了 `std::cmp::PartialOrd`,这是一个 *trait*。下一部分会讲到 trait。不过简单来说,这个错误表明 `largest` 的函数体不能适用于 `T` 的所有可能的类型。因为在函数体需要比较 `T` 类型的值,不过它只能用于我们知道如何排序的类型。为了开启比较功能,标准库中定义的 `std::cmp::PartialOrd` trait 可以实现类型的比较功能(查看附录 C 获取该 trait 的更多信息)。 - +标准库中定义的 `std::cmp::PartialOrd` trait 可以实现类型的比较功能。在 “trait bound” 部分会讲解如何指定泛型实现特定的 trait,不过让我们先探索其他使用泛型参数的方法。 ### 结构体定义中的泛型 @@ -146,11 +138,11 @@ fn main() { 其语法类似于函数定义中使用泛型。首先,必须在结构体名称后面的尖括号中声明泛型参数的名称。接着在结构体定义中可以指定具体数据类型的位置使用泛型类型。 -注意 `Point` 的定义中只使用了一个泛型类型,我们想要表达的是结构体 `Point` 对于一些类型 `T` 是泛型的,而且字段 `x` 和 `y` **都是** 相同类型的,无论它具体是何类型。如果尝试创建一个有不同类型值的 `Point` 的实例,像示例 10-7 中的代码就不能编译: +注意 `Point` 的定义中只使用了一个泛型类型,这个定义表明结构体 `Point` 对于一些类型 `T` 是泛型的,而且字段 `x` 和 `y` **都是** 相同类型的,无论它具体是何类型。如果尝试创建一个有不同类型值的 `Point` 的实例,像示例 10-7 中的代码就不能编译: 文件名: src/main.rs -```rust,ignore +```rust,ignore,does_not_compile struct Point { x: T, y: T, @@ -163,22 +155,20 @@ fn main() { 示例 10-7:字段 `x` 和 `y` 必须是相同类型,因为他们都有相同的泛型类型 `T` -尝试编译会得到如下错误: +在这个例子中,当把整型值 5 赋值给 `x` 时,就告诉了编译器这个 `Point` 实例中的泛型 `T` 是整型的。接着指定 `y` 为 4.0,它被定义为与 `x` 相同类型,就会得到一个像这样的类型不匹配错误: ```text error[E0308]: mismatched types - --> + --> src/main.rs:7:38 | 7 | let wont_work = Point { x: 5, y: 4.0 }; | ^^^ expected integral variable, found - floating-point variable +floating-point variable | = note: expected type `{integer}` - = note: found type `{float}` + found type `{float}` ``` -当我们将 5 赋值给 `x`,编译器就知道这个 `Point` 实例的泛型类型 `T` 是一个整型。接着我们将 `y` 指定为 4.0,而它被定义为与 `x` 有着相同的类型,所以出现了类型不匹配的错误。 - 如果想要定义一个 `x` 和 `y` 可以有不同类型且仍然是泛型的 `Point` 结构体,我们可以使用多个泛型类型参数。在示例 10-8 中,我们修改 `Point` 的定义为拥有两个泛型类型 `T` 和 `U`。其中字段 `x` 是 `T` 类型的,而字段 `y` 是 `U` 类型的: 文件名: src/main.rs @@ -200,9 +190,9 @@ fn main() { 现在所有这些 `Point` 实例都是被允许的了!你可以在定义中使用任意多的泛型类型参数,不过太多的话代码将难以阅读和理解。当你的代码中需要许多泛型类型时,它可能表明你的代码需要重组为更小的部分。 -### 枚举定义中的泛型数据类型 +### 枚举定义中的泛型 -类似于结构体,枚举也可以在其成员中存放泛型数据类型。第六章我们使用过了标准库提供的 `Option` 枚举,现在这个定义看起来就更容易理解了。让我们再看看: +类似于结构体,枚举也可以在其成员中存放泛型数据类型。第六章我们使用过了标准库提供的 `Option` 枚举,让我们再看看: ```rust enum Option { @@ -211,7 +201,7 @@ enum Option { } ``` -换句话说 `Option` 是一个拥有泛型 `T` 的枚举。它有两个成员:`Some`,它存放了一个类型 `T` 的值,和不存在任何值的`None`。标准库中只有这一个定义来支持创建任何具体类型的枚举值。“一个可能的值” 是一个比具体类型的值更抽象的概念,而 Rust 允许我们不引入重复代码就能表现抽象的概念。 +现在这个定义看起来就更容易理解了。如你所见 `Option` 是一个拥有泛型 `T` 的枚举,它有两个成员:`Some`,它存放了一个类型 `T` 的值,和不存在任何值的`None`。通过 `Option` 枚举可以表达有一个可能的值的抽象概念,同时因为 `Option` 是泛型的,无论这个可能的值是什么类型都可以使用这个抽象。 枚举也可以拥有多个泛型类型。第九章使用过的 `Result` 枚举定义就是一个这样的例子: @@ -226,9 +216,11 @@ enum Result { 当发现代码中有多个只有存放的值的类型有所不同的结构体或枚举定义时,你就应该像之前的函数定义中那样引入泛型类型来减少重复代码。 -### 方法定义中的枚举数据类型 +### 方法定义中的泛型 + +也可以在定义中使用泛型在结构体和枚举上实现方法(像第五章那样)。 -可以像第五章介绍的那样来为其定义中带有泛型的结构体或枚举实现方法。示例 10-9 中展示了示例 10-6 中定义的结构体 `Point`。接着我们在 `Point` 上定义了一个叫做 `x` 的方法来返回字段 `x` 中数据的引用: +可以像第五章介绍的那样来为其定义中带有泛型的结构体或枚举实现方法。示例 10-9 中展示了示例 10-6 中定义的结构体 `Point`,和在其上实现的名为 `x` 的方法。 文件名: src/main.rs @@ -253,7 +245,11 @@ fn main() { 示例 10-9:在 `Point` 结构体上实现方法 `x`,它返回 `T` 类型的字段 `x` 的引用 -注意必须在 `impl` 后面声明 `T`,这样就可以在 `Point` 上实现的方法中使用它了。在 `impl` 之后声明泛型 `T` ,这样 Rust 就知道 `Point` 的尖括号中的类型是泛型而不是具体类型。例如,可以选择为 `Point` 实例实现方法,而不是为泛型 `Point` 实例。示例 10-10 展示了一个没有在 `impl` 之后(的尖括号)声明泛型的例子,这里使用了一个具体类型,`f32`: +这里在 `Point` 上定义了一个叫做 `x` 的方法来返回字段 `x` 中数据的引用: + +注意必须在 `impl` 后面声明 `T`,这样就可以在 `Point` 上实现的方法中使用它了。在 `impl` 之后声明泛型 `T` ,这样 Rust 就知道 `Point` 的尖括号中的类型是泛型而不是具体类型。 + +例如,可以选择为 `Point` 实例实现方法,而不是为泛型 `Point` 实例。示例 10-10 展示了一个没有在 `impl` 之后(的尖括号)声明泛型的例子,这里使用了一个具体类型,`f32`: ```rust # struct Point { @@ -270,7 +266,7 @@ impl Point { 示例 10-10:构建一个只用于拥有泛型参数 `T` 的结构体的具体类型的 `impl` 块 -这段代码意味着 `Point` 类型会有一个方法 `distance_from_origin`,而其他 `T` 不是 `f32` 类型的 `Point` 实例则没有定义此方法。这个方法计算点实例与另一个点坐标之间的距离,它使用了只能用于浮点型的数学运算符。 +这段代码意味着 `Point` 类型会有一个方法 `distance_from_origin`,而其他 `T` 不是 `f32` 类型的 `Point` 实例则没有定义此方法。这个方法计算点实例与坐标 (0.0, 0.0) 之间的距离,并使用了只能用于浮点型的数学运算符。 结构体定义中的泛型类型参数并不总是与结构体方法签名中使用的泛型是同一类型。示例 10-11 中在示例 10-8 中的结构体 `Point` 上定义了一个方法 `mixup`。这个方法获取另一个 `Point` 作为参数,而它可能与调用 `mixup` 的 `self` 是不同的 `Point` 类型。这个方法用 `self` 的 `Point` 类型的 `x` 值(类型 `T`)和参数的 `Point` 类型的 `y` 值(类型 `W`)来创建一个新 `Point` 类型的实例: @@ -305,11 +301,11 @@ fn main() { 在 `main` 函数中,定义了一个有 `i32` 类型的 `x`(其值为 `5`)和 `f64` 的 `y`(其值为 `10.4`)的 `Point`。`p2` 则是一个有着字符串 slice 类型的 `x`(其值为 `"Hello"`)和 `char` 类型的 `y`(其值为`c`)的 `Point`。在 `p1` 上以 `p2` 作为参数调用 `mixup` 会返回一个 `p3`,它会有一个 `i32` 类型的 `x`,因为 `x` 来自 `p1`,并拥有一个 `char` 类型的 `y`,因为 `y` 来自 `p2`。`println!` 会打印出 `p3.x = 5, p3.y = c`。 -注意泛型参数 `T` 和 `U` 声明于 `impl` 之后,因为他们与结构体定义相对应。而泛型参数 `V` 和 `W` 声明于 `fn mixup` 之后,因为他们只是相对于方法本身的。 +这个例子的目的是展示一些泛型通过 `impl` 声明而另一些通过方法定义声明的情况。这里泛型参数 `T` 和 `U` 声明于 `impl` 之后,因为他们与结构体定义相对应。而泛型参数 `V` 和 `W` 声明于 `fn mixup` 之后,因为他们只是相对于方法本身的。 ### 泛型代码的性能 -在阅读本部分内容的同时,你可能会好奇使用泛型类型参数是否会有运行时消耗。好消息是:Rust 实现了泛型,使得使用泛型类型参数的代码相比使用具体类型并没有任何速度上的损失! +在阅读本部分内容的同时,你可能会好奇使用泛型类型参数是否会有运行时消耗。好消息是:Rust 实现了泛型,使得使用泛型类型参数的代码相比使用具体类型并没有任何速度上的损失。 Rust 通过在编译时进行泛型代码的 **单态化**(*monomorphization*)来保证效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。 From 539166bde6a928627d6cc35c02deb052d7f062c8 Mon Sep 17 00:00:00 2001 From: Chrislearn Young Date: Thu, 6 Dec 2018 11:34:04 +0800 Subject: [PATCH 19/49] =?UTF-8?q?=E7=A7=BB=E9=99=A4=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ch16-02-message-passing.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ch16-02-message-passing.md b/src/ch16-02-message-passing.md index 8836bbb..b7d2066 100644 --- a/src/ch16-02-message-passing.md +++ b/src/ch16-02-message-passing.md @@ -132,9 +132,7 @@ fn main() { 示例 16-9: 在我们已经发送到通道中后,尝试使用 `val` 引用 -这里尝试在通过 `tx.send` 发送 `val` 到通道中之后将其打印出来。允许这么做是一个坏主意:一旦将值发送到另一个线程后,那个线程可能会在我们再次使用它之前就将其修改或者丢弃。这会由于不一致或不存在的数据而导致错误或意外的结果。 - -这是一个坏主意:一旦将值发送到另一个线程后,那个线程可能会在我们再次使用它之前就将其修改或者丢弃。其他线程对值可能的修改会由于不一致或不存在的数据而导致错误或意外的结果。然而,尝试编译示例 16-9 的代码时,Rust 会给出一个错误: +这里尝试在通过 `tx.send` 发送 `val` 到通道中之后将其打印出来。允许这么做是一个坏主意:一旦将值发送到另一个线程后,那个线程可能会在我们再次使用它之前就将其修改或者丢弃。其他线程对值可能的修改会由于不一致或不存在的数据而导致错误或意外的结果。然而,尝试编译示例 16-9 的代码时,Rust 会给出一个错误: ```text error[E0382]: use of moved value: `val` From 9a9b361e06f2b6aaad3b5f9cd683a51b48879a81 Mon Sep 17 00:00:00 2001 From: KaiserY Date: Thu, 6 Dec 2018 21:32:44 +0800 Subject: [PATCH 20/49] check to ch12-06 --- src/ch10-02-traits.md | 259 +++++++++++------- src/ch10-03-lifetime-syntax.md | 250 ++++++++--------- src/ch11-00-testing.md | 10 +- src/ch11-01-writing-tests.md | 89 ++++-- src/ch11-02-running-tests.md | 8 +- src/ch11-03-test-organization.md | 16 +- src/ch12-00-an-io-project.md | 6 +- ...h12-01-accepting-command-line-arguments.md | 9 +- src/ch12-02-reading-a-file.md | 26 +- ...improving-error-handling-and-modularity.md | 48 ++-- ...2-04-testing-the-librarys-functionality.md | 41 ++- ...2-05-working-with-environment-variables.md | 51 ++-- ...-06-writing-to-stderr-instead-of-stdout.md | 6 +- 13 files changed, 436 insertions(+), 383 deletions(-) diff --git a/src/ch10-02-traits.md b/src/ch10-02-traits.md index 37e47d0..e8a2121 100644 --- a/src/ch10-02-traits.md +++ b/src/ch10-02-traits.md @@ -1,10 +1,10 @@ ## trait:定义共享的行为 -> [ch10-02-traits.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch10-02-traits.md) +> [ch10-02-traits.md](https://github.com/rust-lang/book/blob/master/src/ch10-02-traits.md) >
-> commit 131859023a0a6be67168d36dcdc8e2aa43f806fd +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -trait 允许我们进行另一种抽象:他们让我们可以抽象类型所通用的行为。*trait* 告诉 Rust 编译器某个特定类型拥有可能与其他类型共享的功能。在使用泛型类型参数的场景中,可以使用 *trait bounds* 在编译时指定泛型可以是任何实现了某个 trait 的类型,并由此在这个场景下拥有我们希望的功能。 +*trait* 告诉 Rust 编译器某个特定类型拥有可能与其他类型共享的功能。可以通过 trait 以一种抽象的方式定义共享的行为。可以使用 *trait bounds* 指定泛型是任何拥有特定行为的类型。 > 注意:*trait* 类似于其他语言中的常被称为 **接口**(*interfaces*)的功能,虽然有一些不同。 @@ -12,33 +12,35 @@ trait 允许我们进行另一种抽象:他们让我们可以抽象类型所 一个类型的行为由其可供调用的方法构成。如果可以对不同类型调用相同的方法的话,这些类型就可以共享相同的行为了。trait 定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必需的行为的集合。 -例如,这里有多个存放了不同类型和属性文本的结构体:结构体 `NewsArticle` 用于存放发生于世界各地的新闻故事,而结构体 `Tweet` 最多只能存放 140 个字符的内容,以及像是否转推或是否是对推友的回复这样的元数据。 +例如,这里有多个存放了不同类型和属性文本的结构体:结构体 `NewsArticle` 用于存放发生于世界各地的新闻故事,而结构体 `Tweet` 最多只能存放 280 个字符的内容,以及像是否转推或是否是对推友的回复这样的元数据。 -我们想要创建一个多媒体聚合库用来显示可能储存在 `NewsArticle` 或 `Tweet` 实例中的数据的总结。每一个结构体都需要的行为是他们是能够被总结的,这样的话就可以调用实例的 `summary` 方法来请求总结。示例 10-12 中展示了一个表现这个概念的 `Summarizable` trait 的定义: +我们想要创建一个多媒体聚合库用来显示可能储存在 `NewsArticle` 或 `Tweet` 实例中的数据的总结。每一个结构体都需要的行为是他们是能够被总结的,这样的话就可以调用实例的 `summarize` 方法来请求总结。示例 10-12 中展示了一个表现这个概念的 `Summary` trait 的定义: -文件名: lib.rs +文件名: src/lib.rs ```rust -pub trait Summarizable { - fn summary(&self) -> String; +pub trait Summary { + fn summarize(&self) -> String; } ``` -示例 10-12:`Summarizable` trait 定义,它包含由 `summary` 方法提供的行为 +示例 10-12:`Summarizable` trait 定义,它包含由 `summarize` 方法提供的行为 -使用 `trait` 关键字来声明一个 trait,后面是 trait 的名字,在这个例子中是 `Summarizable`。在大括号中声明描述实现这个 trait 的类型所需要的行为的方法签名,在这个例子中是 `fn summary(&self) -> String`。在方法签名后跟分号,而不是在大括号中提供其实现。接着每一个实现这个 trait 的类型都需要提供其自定义行为的方法体,编译器也会确保任何实现 `Summarizable` trait 的类型都拥有与这个签名的定义完全一致的 `summary` 方法。 +这里使用 `trait` 关键字来声明一个 trait,后面是 trait 的名字,在这个例子中是 `Summary`。在大括号中声明描述实现这个 trait 的类型所需要的行为的方法签名,在这个例子中是 `fn summarize(&self) -> String`。 -trait 体中可以有多个方法,一行一个方法签名且都以分号结尾。 +在方法签名后跟分号,而不是在大括号中提供其实现。接着每一个实现这个 trait 的类型都需要提供其自定义行为的方法体,编译器也会确保任何实现 `Summary` trait 的类型都拥有与这个签名的定义完全一致的 `summarize` 方法。 + +trait 体中可以有多个方法:一行一个方法签名且都以分号结尾。 ### 为类型实现 trait -现在我们定义了 `Summarizable` trait,接着就可以在多媒体聚合库中需要拥有这个行为的类型上实现它了。示例 10-13 中展示了 `NewsArticle` 结构体上 `Summarizable` trait 的一个实现,它使用标题、作者和创建的位置作为 `summary` 的返回值。对于 `Tweet` 结构体,我们选择将 `summary` 定义为用户名后跟推文的全部文本作为返回值,并假设推文内容已经被限制为 140 字符以内。 +现在我们定义了 `Summary` trait,接着就可以在多媒体聚合库中需要拥有这个行为的类型上实现它了。示例 10-13 中展示了 `NewsArticle` 结构体上 `Summary` trait 的一个实现,它使用标题、作者和创建的位置作为 `summarize` 的返回值。对于 `Tweet` 结构体,我们选择将 `summarize` 定义为用户名后跟推文的全部文本作为返回值,并假设推文内容已经被限制为 280 字符以内。 -文件名: lib.rs +文件名: src/lib.rs ```rust -# pub trait Summarizable { -# fn summary(&self) -> String; +# pub trait Summary { +# fn summarize(&self) -> String; # } # pub struct NewsArticle { @@ -48,8 +50,8 @@ pub struct NewsArticle { pub content: String, } -impl Summarizable for NewsArticle { - fn summary(&self) -> String { +impl Summary for NewsArticle { + fn summarize(&self) -> String { format!("{}, by {} ({})", self.headline, self.author, self.location) } } @@ -61,14 +63,14 @@ pub struct Tweet { pub retweet: bool, } -impl Summarizable for Tweet { - fn summary(&self) -> String { +impl Summary for Tweet { + fn summarize(&self) -> String { format!("{}: {}", self.username, self.content) } } ``` -示例 10-13:在 `NewsArticle` 和 `Tweet` 类型上实现 `Summarizable` trait +示例 10-13:在 `NewsArticle` 和 `Tweet` 类型上实现 `Summary` trait 在类型上实现 trait 类似于实现与 trait 无关的方法。区别在于 `impl` 关键字之后,我们提供需要实现 trait 的名称,接着是 `for` 和需要实现 trait 的类型的名称。在 `impl` 块中,使用 trait 定义中的方法签名,不过不再后跟分号,而是需要在大括号中编写函数体来为特定类型实现 trait 方法所拥有的行为。 @@ -82,66 +84,38 @@ let tweet = Tweet { retweet: false, }; -println!("1 new tweet: {}", tweet.summary()); +println!("1 new tweet: {}", tweet.summarize()); ``` 这会打印出 `1 new tweet: horse_ebooks: of course, as you probably already know, people`。 -注意因为示例 10-13 中我们在相同的 `lib.rs` 里定义了 `Summarizable` trait 和 `NewsArticle` 与 `Tweet` 类型,所以他们是位于同一作用域的。如果这个 `lib.rs` 是对应 `aggregator` crate 的,而别人想要利用我们 crate 的功能外加为其 `WeatherForecast` 结构体实现 `Summarizable` trait,在实现 `Summarizable` trait 之前他们首先就需要将其导入其作用域中,如示例 10-14 所示: - -文件名: lib.rs - -```rust,ignore -extern crate aggregator; +注意因为示例 10-13 中我们在相同的 *lib.rs* 里定义了 `Summary` trait 和 `NewsArticle` 与 `Tweet` 类型,所以他们是位于同一作用域的。如果这个 *lib.rs* 是对应 `aggregator` crate 的,而别人想要利用我们 crate 的功能为其自己的库作用域中的结构体实现 `Summary` trait。首先他们需要将 trait 引入作用域。这可以通过指定 `use aggregator::Summary;` 实现,这样就可以为其类型实现 `Summary` trait 了。`Summary` 还必须是公有 trait 使得其他 crate 可以实现它,这也是为什么实例 10-12 中将 `pub` 置于 `trait` 之前。 -use aggregator::Summarizable; +一个实现 trait 时需要注意的限制是只有要么 tait 或者类型是位于 crate 作用域本地时才能为其实现该 trait。例如,可以为像 `aggregator` crate 的 `Tweet` 这样的自定义类型实现如标准库中的 `Display` 这样 trait,因为 `Tweet` 类型位于 `aggregator` crate 本地。也可以在 `aggregator` crate 中为 `Vec` 实现 `Summary`,因为 `Summary` trait 位于 `aggregator` crate 本地。 -struct WeatherForecast { - high_temp: f64, - low_temp: f64, - chance_of_precipitation: f64, -} - -impl Summarizable for WeatherForecast { - fn summary(&self) -> String { - format!("The high will be {}, and the low will be {}. The chance of - precipitation is {}%.", self.high_temp, self.low_temp, - self.chance_of_precipitation) - } -} -``` - -示例 10-14:在另一个 crate 中将 `aggregator` crate 的 `Summarizable` trait 引入作用域 - -另外这段代码假设 `Summarizable` 是一个公有 trait,这是因为示例 10-12 中 `trait` 之前使用了 `pub` 关键字。 - -trait 实现的一个需要注意的限制是:只能在 trait 或对应类型位于我们 crate 本地的时候为其实现 trait。换句话说,不允许对外部类型实现外部 trait。例如,不能在 `Vec` 上实现 `Display` trait,因为 `Display` 和 `Vec` 都定义于标准库中。允许在像 `Tweet` 这样作为我们 `aggregator`crate 部分功能的自定义类型上实现标准库中的 trait `Display`。也允许在 `aggregator`crate 中为 `Vec` 实现 `Summarizable`,因为 `Summarizable` 定义于此。这个限制是我们称为 **孤儿规则**(*orphan rule*)的一部分,如果你感兴趣的可以在类型理论中找到它。简单来说,它被称为 orphan rule 是因为其父类型不存在。没有这条规则的话,两个 crate 可以分别对相同类型实现相同的 trait,因而这两个实现会相互冲突:Rust 将无从得知应该使用哪一个。因为 Rust 强制执行 orphan rule,其他人编写的代码不会破坏你代码,反之亦是如此。 +但是不能在外部类型上实现外部 trait。例如,不能在 `aggregator` crate 中为 `Vec` 实现 `Display` trait。因为 `Display` 和 `Vec` 都定义于标准库并不位于 `aggregator` crate 本地。这个限制是被称为 **相干性**(*coherence*) 的程序属性的一部分,或者更具体的说是 **孤儿规则**(*orphan rule*),其得名于不存在父类型。这条规则确保了其他人编写的代码不会破坏你代码,反之亦然。没有这条规则的话,两个 crate 可以分别对相同类型实现相同的 trait,而Rust 将无从得知应该使用哪一个实现。 ### 默认实现 有时为 trait 中的某些或全部方法提供默认的行为,而不是在每个类型的每个实现中都定义自己的行为是很有用的。这样当为某个特定类型实现 trait 时,可以选择保留或重载每个方法的默认行为。 -示例 10-15 中展示了如何为 `Summarize` trait 的 `summary` 方法指定一个默认的字符串值,而不是像示例 10-12 中那样只是定义方法签名: +示例 10-14 中展示了如何为 `Summary` trait 的 `summarize` 方法指定一个默认的字符串值,而不是像示例 10-12 中那样只是定义方法签名: -文件名: lib.rs +文件名: src/lib.rs ```rust -pub trait Summarizable { - fn summary(&self) -> String { +pub trait Summary { + fn summarize(&self) -> String { String::from("(Read more...)") } } ``` -示例 10-15:`Summarizable` trait 的定义,带有一个 `summary` 方法的默认实现 +示例 10-14:`Summary` trait 的定义,带有一个 `summarize` 方法的默认实现 -如果想要对 `NewsArticle` 实例使用这个默认实现,而不是像示例 10-13 中那样定义一个自己的实现,则可以指定一个空的 `impl` 块: - -```rust,ignore -impl Summarizable for NewsArticle {} -``` +如果想要对 `NewsArticle` 实例使用这个默认实现,而不是定义一个自己的实现,则可以通过 impl Summary for NewsArticle {} 指定一个空的 `impl` 块。 -即便选择不再直接为 `NewsArticle` 定义 `summary` 方法了,因为 `summary` 方法有一个默认实现而且 `NewsArticle` 被指定为实现了 `Summarizable` trait,我们仍然可以对 `NewsArticle` 的实例调用 `summary` 方法: +即便选择不再直接为 `NewsArticle` 定义 `summarize` 方法了,因为我们提供了一个默认实现而且 `NewsArticle` 被指定为实现了 `Summary` trait。为此我们仍然可以像这样对 `NewsArticle` 的实例调用 `summarize` 方法: ```rust,ignore let article = NewsArticle { @@ -152,36 +126,38 @@ let article = NewsArticle { hockey team in the NHL."), }; -println!("New article available! {}", article.summary()); +println!("New article available! {}", article.summarize()); ``` 这段代码会打印 `New article available! (Read more...)`。 +为 `summarize` 创建默认实现并不要求对示例 10-13 中 `Tweet` 上的 `Summary` 实现做任何改变。其原因是重载一个默认实现的语法与实现没有默认实现的 trait 方法一样。 + 将 `Summarizable` trait 改变为拥有默认 `summary` 实现并不要求对示例 10-13 中 `Tweet` 和示例 10-14 中 `WeatherForecast` 的 `Summarizable` 实现做任何改变:重载一个默认实现的语法与实现没有默认实现的 trait 方法时完全一样的。 -默认实现允许调用相同 trait 中的其他方法,哪怕这些方法没有默认实现。通过这种方法,trait 可以实现很多有用的功能而只需实现一小部分特定内容。我们可以选择让`Summarizable` trait 也拥有一个要求实现的`author_summary` 方法,接着 `summary` 方法则提供默认实现并调用 `author_summary` 方法: +默认实现允许调用相同 trait 中的其他方法,哪怕这些方法没有默认实现。如此,trait 可以实现很多有用的功能而只需实现一小部分特定内容。我们可以选择让`Summary` trait 也拥有一个要求实现的`summarize_author` 方法,接着 `summarize` 方法则提供默认实现并调用 `summarize_author` 方法: ```rust -pub trait Summarizable { - fn author_summary(&self) -> String; +pub trait Summary { + fn summarize_author(&self) -> String; - fn summary(&self) -> String { - format!("(Read more from {}...)", self.author_summary()) + fn summarize(&self) -> String { + format!("(Read more from {}...)", self.summarize_author()) } } ``` -为了使用这个版本的 `Summarizable`,只需在实现 trait 时定义 `author_summary` 即可: +为了使用这个版本的 `Summary`,只需在实现 trait 时定义 `summarize_author` 即可: ```rust,ignore -impl Summarizable for Tweet { - fn author_summary(&self) -> String { +impl Summary for Tweet { + fn summarize_author(&self) -> String { format!("@{}", self.username) } } ``` -一旦定义了 `author_summary`,我们就可以对 `Tweet` 结构体的实例调用 `summary` 了,而 `summary` 的默认实现会调用我们提供的 `author_summary` 定义。 +一旦定义了 `summarize_author`,我们就可以对 `Tweet` 结构体的实例调用 `summarize` 了,而 `summary` 的默认实现会调用我们提供的 `summarize_author` 定义。因为实现了 `summarize_author`,`Summary` trait 就提供了 `summarize` 方法的功能,且无需编写更多的代码。 ```rust,ignore let tweet = Tweet { @@ -191,37 +167,74 @@ let tweet = Tweet { retweet: false, }; -println!("1 new tweet: {}", tweet.summary()); +println!("1 new tweet: {}", tweet.summarize()); ``` 这会打印出 `1 new tweet: (Read more from @horse_ebooks...)`。 -注意在重载过的实现中调用默认实现是不可能的。 +注意无法从相同方法的重载实现中调用默认方法。 + +### trait 作为参数 + +知道了如何定义 trait 和在类型上实现这些 trait 之后,我们可以探索一下如何使用 trait 来接受多种不同类型的参数。 + +例如在示例 10-13 中为 `NewsArticle` 和 `Tweet` 类型实现了 `Summary` trait。我们可以定义一个函数 `notify` 来调用其参数 `item` 上的 `summarize` 方法,该参数为一些实现了 `Summary` trait 的方法。为此可以使用 ‘`impl Trait`’ 语法,像这样: + +```rust,ignore +pub fn notify(item: impl Summary) { + println!("Breaking news! {}", item.summarize()); +} +``` -### Trait Bounds +在 `notify` 函数体中,可以调用任何来自 `Summary` trait 的方法,比如 `summarize`。 -现在我们定义了 trait 并在类型上实现了这些 trait,也可以对泛型类型参数使用 trait。我们可以限制泛型不再适用于任何类型,编译器会确保其被限制为那些实现了特定 trait 的类型,由此泛型就会拥有我们希望其类型所拥有的功能。这被称为指定泛型的 *trait bounds*。 +#### Trait Bounds -例如在示例 10-13 中为 `NewsArticle` 和 `Tweet` 类型实现了 `Summarizable` trait。我们可以定义一个函数 `notify` 来调用 `summary` 方法,它拥有一个泛型类型 `T` 的参数 `item`。为了能够在 `item` 上调用 `summary` 而不出现错误,我们可以在 `T` 上使用 trait bounds 来指定 `item` 必须是实现了 `Summarizable` trait 的类型: +`impl Trait` 语法适用于短小的例子,它不过是一个较长形式的语法糖。这被称为 *trait bound*,这看起来像: ```rust,ignore -pub fn notify(item: T) { - println!("Breaking news! {}", item.summary()); +pub fn notify(item: T) { + println!("Breaking news! {}", item.summarize()); } ``` -trait bounds 连同泛型类型参数声明一同出现,位于尖括号中的冒号后面。由于 `T` 上的 trait bounds,我们可以传递任何 `NewsArticle` 或 `Tweet` 的实例来调用 `notify` 函数。示例 10-14 中使用我们 `aggregator` crate 的外部代码也可以传递一个 `WeatherForecast` 的实例来调用 `notify` 函数,因为 `WeatherForecast` 同样也实现了 `Summarizable`。使用任何其他类型,比如 `String` 或 `i32`,来调用 `notify` 的代码将不能编译,因为这些类型没有实现 `Summarizable`。 +这与之前的例子相同,不过稍微冗长了一些。trait bound 与泛型参数声明在一起,位于尖括号中分号的后面。因为 `T` 的 trait bound,我们可以传递任何 `NewsArticle` 或 `Tweet` 的实例调用 `notify`。用任何其他类型,比如 `String` 或 `i32`,调用该函数的代码将不能编译,因为这些类型没有实现 `Summary`。 -可以通过 `+` 来为泛型指定多个 trait bounds。如果我们需要能够在函数中使用 `T` 类型的显示格式的同时也能使用 `summary` 方法,则可以使用 trait bounds `T: Summarizable + Display`。这意味着 `T` 可以是任何实现了 `Summarizable` 和 `Display` 的类型。 +何时应该使用这种形式而不是 `impl Trait` 呢?虽然 `impl Trait` 适用于短小的例子,trait bound 则适用于更复杂的场景。例如,比如需要获取两个实现了 `Summary` 的类型: -对于拥有多个泛型类型参数的函数,每一个泛型都可以有其自己的 trait bounds。在函数名和参数列表之间的尖括号中指定很多的 trait bound 信息将是难以阅读的,所以有另外一个指定 trait bounds 的语法,它将其移动到函数签名后的 `where` 从句中。所以相比这样写: +```rust,ignore +pub fn notify(item1: impl Summary, item2: impl Summary) { +``` +这适用于 `item1` 和 `item2` 允许是不同类型的情况(只要它们都实现了 `Summary`)。不过如果你希望强制它们都是相同类型呢?这只有在使用 trait bound 时才有可能: + +```rust,ignore +pub fn notify(item1: T, item2: T) { +``` + +#### 通过 `+` 指定多个 trait + +如果 `notify` 需要显示 `item` 的格式化形式,同时也要使用 `summarize` 方法,那么 `item` 就需要同时实现两个不同的 trait:`Display` 和 `Summary`。这可以通过 `+` 语法实现: + +```rust,ignore +pub fn notify(item: impl Summary + Display) { +``` + +这个语法也适用于泛型的 trait bound: + +```rust,ignore +pub fn notify(item: T) { +``` + +#### 通过 `where` 简化代码 + +然而,使用过多的 trait bound 也有缺点。每个泛型有其自己的 trait bound,所以有多个泛型参数的函数在名称和参数列表之间会有很长的 trait bound 信息,这使得函数签名难以阅读。为此,Rust 有另一个在函数签名之后的 `where` 从句中指定 trait bound 的语法。所以除了这么写: ```rust,ignore fn some_function(t: T, u: U) -> i32 { ``` -我们也可以使用 `where` 从句: +还可以像这样使用 `where` 从句: ```rust,ignore fn some_function(t: T, u: U) -> i32 @@ -230,51 +243,97 @@ fn some_function(t: T, u: U) -> i32 { ``` -这就显得不那么杂乱,同时也使这个函数看起来更像没有很多 trait bounds 的函数。这时函数名、参数列表和返回值类型都离得很近。 +这个函数签名就显得不那么杂乱,函数名、参数列表和返回值类型都离得很近,看起来类似没有很多 trait bounds 的函数。 + +### 返回 trait + +也可以在返回值中使用 `impl Trait` 语法,来返回实现了某个 trait 的类型: + +```rust,ignore +fn returns_summarizable() -> impl Summary { + Tweet { + username: String::from("horse_ebooks"), + content: String::from("of course, as you probably already know, people"), + reply: false, + retweet: false, + } +} +``` + +这个签名表明,“我要返回某个实现了 `Summary` trait 的类型,但是不确定其具体的类型”。在例子中返回了一个 `Tweet`,不过调用方并不知情。 + +这有什么用呢?在第十三章中,我们会学些两个大量依赖 trait 的功能:闭包和迭代器。这些功能创建只有编译器知道的类型,或者是非常非常长的类型。`impl Trait` 允许你简单的说 “返回一个 `Iterator`” 而无需写出实际的冗长的类型。 + +不过这只适用于返回单一类型的情况。例如,这样就 **不行**: + +```rust,ignore,does_not_compile +fn returns_summarizable(switch: bool) -> impl Summary { + if switch { + NewsArticle { + headline: String::from("Penguins win the Stanley Cup Championship!"), + location: String::from("Pittsburgh, PA, USA"), + author: String::from("Iceburgh"), + content: String::from("The Pittsburgh Penguins once again are the best + hockey team in the NHL."), + } + } else { + Tweet { + username: String::from("horse_ebooks"), + content: String::from("of course, as you probably already know, people"), + reply: false, + retweet: false, + } + } +} +``` + +这里尝试返回 `NewsArticle` 或 `Tweet`。这不能编译,因为 `impl Trait` 工作方式的限制。为了编写这样的代码,你不得不等到第十七章的 “为使用不同类型的值而设计的 trait 对象” 部分。 ### 使用 trait bounds 来修复 `largest` 函数 -所以任何想要对泛型使用 trait 定义的行为的时候,都需要在泛型参数类型上指定 trait bounds。现在我们就可以修复示例 10-5 中那个使用泛型类型参数的 `largest` 函数定义了!当我们将其放置不管的时候,它会出现这个错误: +现在你知道了如何使用泛型参数 trait bound 来指定所需的行为。让我们回到实例 10-5 修复使用泛型类型参数的 `largest` 函数定义!最后尝试代时出现的错误是: ```text error[E0369]: binary operation `>` cannot be applied to type `T` + --> src/main.rs:5:12 | 5 | if item > largest { - | ^^^^ + | ^^^^^^^^^^^^^^ | -note: an implementation of `std::cmp::PartialOrd` might be missing for `T` + = note: an implementation of `std::cmp::PartialOrd` might be missing for `T` ``` -在 `largest` 函数体中我们想要使用大于运算符比较两个 `T` 类型的值。这个运算符被定义为标准库中 trait `std::cmp::PartialOrd` 的一个默认方法。所以为了能够使用大于运算符,需要在 `T` 的 trait bounds 中指定 `PartialOrd`,这样 `largest` 函数可以用于任何可以比较大小的类型的 slice。因为 `PartialOrd` 位于 prelude 中所以并不需要手动将其引入作用域。 +在 `largest` 函数体中我们想要使用大于运算符(`>`)比较两个 `T` 类型的值。这个运算符被定义为标准库中 trait `std::cmp::PartialOrd` 的一个默认方法。所以需要在 `T` 的 trait bound 中指定 `PartialOrd`,这样 `largest` 函数可以用于任何可以比较大小的类型的 slice。因为 `PartialOrd` 位于 prelude 中所以并不需要手动将其引入作用域。将 `largest` 的签名修改为如下: ```rust,ignore fn largest(list: &[T]) -> T { ``` -但是如果编译代码的话,会出现不同的错误: +但是如果编译代码的话,会出现一些不同的错误: ```text -error[E0508]: cannot move out of type `[T]`, a non-copy array - --> src/main.rs:4:23 +error[E0508]: cannot move out of type `[T]`, a non-copy slice + --> src/main.rs:2:23 | -4 | let mut largest = list[0]; - | ----------- ^^^^^^^ cannot move out of here - | | - | hint: to prevent move, use `ref largest` or `ref mut largest` +2 | let mut largest = list[0]; + | ^^^^^^^ + | | + | cannot move out of here + | help: consider using a reference instead: `&list[0]` error[E0507]: cannot move out of borrowed content - --> src/main.rs:6:9 + --> src/main.rs:4:9 | -6 | for &item in list.iter() { +4 | for &item in list.iter() { | ^---- | || | |hint: to prevent move, use `ref item` or `ref mut item` | cannot move out of borrowed content ``` -错误的核心是 `cannot move out of type [T], a non-copy array`,对于非泛型版本的 `largest` 函数,我们只尝试了寻找最大的 `i32` 和 `char`。正如第四章讨论过的,像 `i32` 和 `char` 这样的类型是已知大小的并可以储存在栈上,所以他们实现了 `Copy` trait。当我们将 `largest` 函数改成使用泛型后,现在 `list` 参数的类型就有可能是没有实现 `Copy` trait 的,这意味着我们可能不能将 `list[0]` 的值移动到 `largest` 变量中。 +错误的核心是 `cannot move out of type [T], a non-copy slice`,对于非泛型版本的 `largest` 函数,我们只尝试了寻找最大的 `i32` 和 `char`。正如第四章 “只在栈上的数据:拷贝” 部分讨论过的,像 `i32` 和 `char` 这样的类型是已知大小的并可以储存在栈上,所以他们实现了 `Copy` trait。当我们将 `largest` 函数改成使用泛型后,现在 `list` 参数的类型就有可能是没有实现 `Copy` trait 的。这意味着我们可能不能将 `list[0]` 的值移动到 `largest` 变量中,这导致了上面的错误。 -如果只想对实现了 `Copy` 的类型调用这些代码,可以在 `T` 的 trait bounds 中增加 `Copy`!示例 10-16 中展示了一个可以编译的泛型版本的 `largest` 函数的完整代码,只要传递给 `largest` 的 slice 值的类型实现了 `PartialOrd` 和 `Copy` 这两个 trait,例如 `i32` 和 `char`: +为了只对实现了 `Copy` 的类型调用这些代码,可以在 `T` 的 trait bounds 中增加 `Copy`!示例 10-15 中展示了一个可以编译的泛型版本的 `largest` 函数的完整代码,只要传递给 `largest` 的 slice 值的类型实现了 `PartialOrd` **和** `Copy` 这两个 trait,例如 `i32` 和 `char`: 文件名: src/main.rs @@ -304,13 +363,15 @@ fn main() { } ``` -示例 10-16:一个可以用于任何实现了 `PartialOrd` 和 `Copy` trait 的泛型的 `largest` 函数 +示例 10-15:一个可以用于任何实现了 `PartialOrd` 和 `Copy` trait 的泛型的 `largest` 函数 + +如果并不希望限制 `largest` 函数只能用于实现了 `Copy` trait 的类型,我们可以在 `T` 的 trait bounds 中指定 `Clone` 而不是 `Copy`。并克隆 slice 的每一个值使得 `largest` 函数拥有其所有权。使用 `clone` 函数意味着对于类似 `String` 这样拥有堆上数据的类型,会潜在的分配更多堆上空间,而堆分配在涉及大量数据时可能会相当缓慢。 -如果并不希望限制 `largest` 函数只能用于实现了 `Copy` trait 的类型,我们可以在 `T` 的 trait bounds 中指定 `Clone` 而不是 `Copy`,并克隆 slice 的每一个值使得 `largest` 函数拥有其所有权。但是使用 `clone` 函数潜在意味着更多的堆分配,而且堆分配在涉及大量数据时可能会相当缓慢。另一种 `largest` 的实现方式是返回 slice 中一个 `T` 值的引用。如果我们将函数返回值从 `T` 改为 `&T` 并改变函数体使其能够返回一个引用,我们将不需要任何 `Clone` 或 `Copy` 的 trait bounds 而且也不会有任何的堆分配。尝试自己实现这种替代解决方式吧! +另一种 `largest` 的实现方式是返回在 slice 中 `T` 值的引用。如果我们将函数返回值从 `T` 改为 `&T` 并改变函数体使其能够返回一个引用,我们将不需要任何 `Clone` 或 `Copy` 的 trait bounds 而且也不会有任何的堆分配。尝试自己实现这种替代解决方式吧! ### 使用 trait bound 有条件的实现方法 -通过使用带有 trait bound 的泛型 `impl` 块,可以有条件的只为实现了特定 trait 的类型实现方法。例如,示例 10-17 中的类型 `Pair` 总是实现了 `new` 方法,不过只有 `Pair` 内部的 `T` 类型实现了 `PartialOrd` trait 来允许比较和 `Display` trait 来启用打印,才会实现 `cmp_display`: +通过使用带有 trait bound 的泛型参数的 `impl` 块,可以有条件的只为实现了特定 trait 的类型实现方法。例如,示例 10-16 中的类型 `Pair` 总是实现了 `new` 方法,不过只有 `Pair` 内部的 `T` 类型实现了 `PartialOrd` trait 来允许比较 **和** `Display` trait 来启用打印,才会实现 `cmp_display` 方法: ```rust use std::fmt::Display; diff --git a/src/ch10-03-lifetime-syntax.md b/src/ch10-03-lifetime-syntax.md index 52e8eb5..a3674c1 100644 --- a/src/ch10-03-lifetime-syntax.md +++ b/src/ch10-03-lifetime-syntax.md @@ -1,20 +1,18 @@ ## 生命周期与引用有效性 -> [ch10-03-lifetime-syntax.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch10-03-lifetime-syntax.md) +> [ch10-03-lifetime-syntax.md](https://github.com/rust-lang/book/blob/master/src/ch10-03-lifetime-syntax.md) >
-> commit fa0e4403f8350287b034c5b64af752f647ebb5a2 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -当在第四章讨论引用时,我们遗漏了一个重要的细节:Rust 中的每一个引用都有其 **生命周期**(*lifetime*),也就是引用保持有效的作用域。大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以一些不同方式相关联的情况,所以 Rust 需要我们使用泛型生命周期参数来注明他们的关系,这样就能确保运行时实际使用的引用绝对是有效的。 +当在第四章讨论 “引用和借用” 部分时,我们遗漏了一个重要的细节:Rust 中的每一个引用都有其 **生命周期**(*lifetime*),也就是引用保持有效的作用域。大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以一些不同方式相关联的情况,所以 Rust 需要我们使用泛型生命周期参数来注明他们的关系,这样就能确保运行时实际使用的引用绝对是有效的。 -好吧,这有点不太寻常,而且也不同于其他语言中使用的工具。生命周期,从某种意义上说,是 Rust 最与众不同的功能。 - -生命周期是一个很广泛的话题,本章不可能涉及到它全部的内容,所以这里我们会讲到一些通常你可能会遇到的生命周期语法以便你熟悉这个概念。第十九章会包含生命周期所有功能的更高级的内容。 +生命周期的概念某种程度上说不同于其他语言中类似的工具,毫无疑问这是 Rust 最与众不同的功能。虽然本章不可能涉及到它全部的内容,我们会讲到一些通常你可能会遇到的生命周期语法以便你熟悉这个概念。查看第十九章 “高级生命周期” 部分了解更多的细节。 ### 生命周期避免了悬垂引用 -生命周期的主要目标是避免悬垂引用,它会导致程序引用了非预期引用的数据。考虑一下示例 10-18 中的程序,它有一个外部作用域和一个内部作用域,外部作用域声明了一个没有初值的变量 `r`,而内部作用域声明了一个初值为 5 的变量`x`。在内部作用域中,我们尝试将 `r` 的值设置为一个 `x` 的引用。接着在内部作用域结束后,尝试打印出 `r` 的值: +生命周期的主要目标是避免悬垂引用,它会导致程序引用了非预期引用的数据。考虑一下示例 10-17 中的程序,它有一个外部作用域和一个内部作用域. -```rust,ignore +```rust,ignore,does_not_compile { let r; @@ -27,16 +25,15 @@ } ``` -示例 10-18:尝试使用离开作用域的值的引用 +示例 10-17:尝试使用离开作用域的值的引用 -> #### 未初始化变量不能被使用 -> -> 接下来的一些例子中声明了没有初始值的变量,以便这些变量存在于外部作用域。这看起来好像和 Rust 不允许存在空值相冲突。然而这是可以的,如果我们尝试在给它一个值之前使用这个变量,会出现一个编译时错误。请自行尝试! +> 注意:示例 10-17、10-18 和 10-24 中声明了没有初始值的变量,所以这些变量存在于外部作用域。这乍看之下好像和 Rust 不允许存在空值相冲突。然而如果尝试在给它一个值之前使用这个变量,会出现一个编译时错误,这就说明了 Rust 确实不允许空值。 -当编译这段代码时会得到一个错误: +外部作用域声明了一个没有初值的变量 `r`,而内部作用域声明了一个初值为 5 的变量`x`。在内部作用域中,我们尝试将 `r` 的值设置为一个 `x` 的引用。接着在内部作用域结束后,尝试打印出 `r` 的值。这段代码不能编译因为 `r` 引用的值在尝试使用之前就离开了作用域。如下是错误信息: ```text -error: `x` does not live long enough +error[E0597]: `x` does not live long enough + --> src/main.rs:7:5 | 6 | r = &x; | - borrow occurs here @@ -47,51 +44,43 @@ error: `x` does not live long enough | - borrowed value needs to live until here ``` -变量 `x` 并没有 “存在的足够久”。为什么呢?好吧,`x` 在到达第 7 行的大括号的结束时就离开了作用域,这也是内部作用域的结尾。不过 `r` 在外部作用域也是有效的;作用域越大我们就说它 “存在的越久”。如果 Rust 允许这段代码工作,`r` 将会引用在 `x` 离开作用域时被释放的内存,这时尝试对 `r` 做任何操作都会不能正常工作。那么 Rust 是如何决定这段代码是不被允许的呢? +变量 `x` 并没有 “存在的足够久”。其原因是 `x` 在到达第 7 行内部作用域结束时就离开了作用域。不过 `r` 在外部作用域仍是有效的;作用域越大我们就说它 “存在的越久”。如果 Rust 允许这段代码工作,`r` 将会引用在 `x` 离开作用域时被释放的内存,这时尝试对 `r` 做任何操作都不能正常工作。那么 Rust 是如何决定这段代码是不被允许的呢?这得益于借用检查器。 #### 借用检查器 -编译器的这一部分叫做 **借用检查器**(*borrow checker*),它比较作用域来确保所有的借用都是有效的。示例 10-19 展示了与示例 10-18 相同的例子不过带有变量生命周期的注释: +Rust 编译器有一个 **借用检查器**(*borrow checker*),它比较作用域来确保所有的借用都是有效的。示例 10-18 展示了与示例 10-17 相同的例子不过带有变量生命周期的注释: -```rust,ignore +```rust,ignore,does_not_compile { - let r; // -------+-- 'a - // | - { // | - let x = 5; // -+-----+-- 'b - r = &x; // | | - } // -+ | - // | - println!("r: {}", r); // | -} // -------+ + let r; // ---------+-- 'a + // | + { // | + let x = 5; // -+-- 'b | + r = &x; // | | + } // -+ | + // | + println!("r: {}", r); // | +} // ---------+ ``` -示例 10-19:`r` 和 `x` 的生命周期注解,分别叫做 `'a` 和 `'b` +示例 10-18:`r` 和 `x` 的生命周期注解,分别叫做 `'a` 和 `'b` - - +这里将 `r` 的生命周期标记为 `'a` 并将 `x` 的生命周期标记为 `'b`。如你所见,内部的 `'b` 块要比外部的生命周期 `'a` 小得多。在编译时,Rust 比较这两个生命周期的大小,并发现 `r` 拥有生命周期 `'a`,不过它引用了一个拥有生命周期 `'b` 的对象。程序被拒绝编译,因为生命周期 `'b` 比生命周期 `'a` 要小:被引用的对象比它的引用者存在的时间更短。 -我们将 `r` 的生命周期标记为 `'a` 并将 `x` 的生命周期标记为 `'b`。如你所见,内部的 `'b` 块要比外部的生命周期 `'a` 小得多。在编译时,Rust 比较这两个生命周期的大小,并发现 `r` 拥有生命周期 `'a`,不过它引用了一个拥有生命周期 `'b` 的对象。程序被拒绝编译,因为生命周期 `'b` 比生命周期 `'a` 要小:被引用的对象比它的引用者存在的时间更短。 - -让我们看看示例 10-20 中这个并没有产生悬垂引用且可以正确编译的例子: +让我们看看示例 10-19 中这个并没有产生悬垂引用且可以正确编译的例子: ```rust { - let x = 5; // -----+-- 'b - // | - let r = &x; // --+--+-- 'a - // | | - println!("r: {}", r); // | | - // --+ | -} // -----+ + let x = 5; // ----------+-- 'b + // | + let r = &x; // --+-- 'a | + // | | + println!("r: {}", r); // | | + // --+ | +} // ----------+ ``` -示例 10-20:一个有效的引用,因为数据比引用有着更长的生命周期 +示例 10-19:一个有效的引用,因为数据比引用有着更长的生命周期 这里 `x` 拥有生命周期 `'b`,比 `'a` 要大。这就意味着 `r` 可以引用 `x`:Rust 知道 `r` 中的引用在 `x` 有效的时候也总是有效的。 @@ -99,7 +88,7 @@ looking arrows and labels? /Carol --> ### 函数中的泛型生命周期 -让我们来编写一个返回两个字符串 slice 中较长者的函数。我们希望能够通过传递两个字符串 slice 来调用这个函数,并希望返回一个字符串 slice。一旦我们实现了 `longest` 函数,示例 10-21 中的代码应该会打印出 `The longest string is abcd`: +让我们来编写一个返回两个字符串 slice 中较长者的函数。这个函数获取两个字符串 slice 并返回一个字符串 slice。一旦我们实现了 `longest` 函数,示例 10-20 中的代码应该会打印出 `The longest string is abcd`: 文件名: src/main.rs @@ -113,34 +102,15 @@ fn main() { } ``` -示例 10-21:`main` 函数调用 `longest` 函数来寻找两个字符串 slice 中较长的一个 - -注意函数期望获取字符串 slice(如第四章所讲到的这是引用)因为我们并不希望`longest` 函数获取其参数的所有权。我们希望函数能够接受 `String` 的 slice(也就是变量 `string1` 的类型)以及字符串字面值(也就是变量 `string2` 包含的值)。 - - - +示例 10-20:`main` 函数调用 `longest` 函数来寻找两个字符串 slice 中较长的一个 参考之前第四章中的 “字符串 slice 作为参数” 部分中更多关于为什么上面例子中的参数正符合我们期望的讨论。 -如果尝试像示例 10-22 中那样实现 `longest` 函数,它并不能编译: +如果尝试像示例 10-21 中那样实现 `longest` 函数,它并不能编译: 文件名: src/main.rs -```rust,ignore +```rust,ignore,does_not_compile fn longest(x: &str, y: &str) -> &str { if x.len() > y.len() { x @@ -150,43 +120,44 @@ fn longest(x: &str, y: &str) -> &str { } ``` -示例 10-22:一个 `longest` 函数的实现,它返回两个字符串 slice 中较长者,现在还不能编译 +示例 10-21:一个 `longest` 函数的实现,它返回两个字符串 slice 中较长者,现在还不能编译 -将会出现如下有关生命周期的错误: +相应地会出现如下有关生命周期的错误: ```text error[E0106]: missing lifetime specifier - | -1 | fn longest(x: &str, y: &str) -> &str { - | ^ expected lifetime parameter - | - = help: this function's return type contains a borrowed value, but the - signature does not say whether it is borrowed from `x` or `y` + --> src/main.rs:1:33 + | +1 | fn longest(x: &str, y: &str) -> &str { + | ^ expected lifetime parameter + | + = help: this function's return type contains a borrowed value, but the +signature does not say whether it is borrowed from `x` or `y` ``` -提示文本告诉我们返回值需要一个泛型生命周期参数,因为 Rust 并不知道将要返回的引用是指向 `x` 或 `y`。事实上我们也不知道,因为函数体中 `if` 块返回一个 `x` 的引用而 `else` 块返回一个 `y` 的引用。 +提示文本揭示了返回值需要一个泛型生命周期参数,因为 Rust 并不知道将要返回的引用是指向 `x` 或 `y`。事实上我们也不知道,因为函数体中 `if` 块返回一个 `x` 的引用而 `else` 块返回一个 `y` 的引用! -虽然我们定义了这个函数,但是并不知道传递给函数的具体值,所以也不知道到底是 `if` 还是 `else` 会被执行。我们也不知道传入的引用的具体生命周期,所以也就不能像示例 10-19 和 10-20 那样通过观察作用域来确定返回的引用是否总是有效。借用检查器自身同样也无法确定,因为它不知道 `x` 和 `y` 的生命周期是如何与返回值的生命周期相关联的。接下来我们将增加泛型生命周期参数来定义引用间的关系以便借用检查器可以进行分析。 +当我们定义这个函数的时候,并不知道传递给函数的具体值,所以也不知道到底是 `if` 还是 `else` 会被执行。我们也不知道传入的引用的具体生命周期,所以也就不能像示例 10-18 和 10-19 那样通过观察作用域来确定返回的引用是否总是有效。借用检查器自身同样也无法确定,因为它不知道 `x` 和 `y` 的生命周期是如何与返回值的生命周期相关联的。为了修复这个错误,我们将增加泛型生命周期参数来定义引用间的关系以便借用检查器可以进行分析。 ### 生命周期注解语法 -生命周期注解并不改变任何引用的生命周期的长短。与当函数签名中指定了泛型类型参数后就可以接受任何类型一样,当指定了泛型生命周期后函数也能接受任何生命周期的引用。生命周期注解所做的就是将多个引用的生命周期联系起来。 +生命周期注解并不改变任何引用的生命周期的长短。与当函数签名中指定了泛型类型参数后就可以接受任何类型一样,当指定了泛型生命周期后函数也能接受任何生命周期的引用。生命周期注解描述了多个引用生命周期相互的关系,而不影响其生命周期。 -生命周期注解有着一个不太常见的语法:生命周期参数名称必须以撇号(`'`)开头。生命周期参数的名称通常全是小写,而且类似于泛型类型,其名称通常非常短。`'a` 是大多数人默认使用的名称。生命周期参数注解位于引用的 `&` 之后,并有一个空格来将引用类型与生命周期注解分隔开。 +生命周期注解有着一个不太常见的语法:生命周期参数名称必须以撇号(`'`)开头,其名称通常全是小写,类似于泛型其名称非常短。`'a` 是大多数人默认使用的名称。生命周期参数注解位于引用的 `&` 之后,并有一个空格来将引用类型与生命周期注解分隔开。 这里有一些例子:我们有一个没有生命周期参数的 `i32` 的引用,一个有叫做 `'a` 的生命周期参数的 `i32` 的引用,和一个生命周期也是 `'a` 的 `i32` 的可变引用: ```rust,ignore -&i32 // a reference -&'a i32 // a reference with an explicit lifetime -&'a mut i32 // a mutable reference with an explicit lifetime +&i32 // 引用 +&'a i32 // 带有显式生命周期的引用 +&'a mut i32 // 带有显式生命周期的可变引用 ``` -单个的生命周期注解本身没有多少意义:生命周期注解告诉 Rust 多个引用的泛型生命周期参数如何相互联系。如果函数有一个生命周期 `'a` 的 `i32` 的引用的参数 `first`,还有另一个同样是生命周期 `'a` 的 `i32` 的引用的参数 `second`,这两个生命周期注解有相同的名称意味着 `first` 和 `second` 必须与这相同的泛型生命周期存在得一样久。 +单个的生命周期注解本身没有多少意义,因为生命周期注解告诉 Rust 多个引用的泛型生命周期参数如何相互联系的。例如如果函数有一个生命周期 `'a` 的 `i32` 的引用的参数 `first`。还有另一个同样是生命周期 `'a` 的 `i32` 的引用的参数 `second`。这两个生命周期注解意味着引用 `first` 和 `second` 必须与这泛型生命周期存在得一样久。 ### 函数签名中的生命周期注解 -来看看我们编写的 `longest` 函数的上下文中的生命周期。就像泛型类型参数,泛型生命周期参数需要声明在函数名和参数列表间的尖括号中。这里我们想要告诉 Rust 关于参数中的引用和返回值之间的限制是他们都必须拥有相同的生命周期,就像示例 10-23 中在每个引用中都加上了 `'a` 那样: +现在来看看 `longest` 函数的上下文中的生命周期。就像泛型类型参数,泛型生命周期参数需要声明在函数名和参数列表间的尖括号中。这里我们想要告诉 Rust 关于参数中的引用和返回值之间的限制是他们都必须拥有相同的生命周期,就像示例 10-22 中在每个引用中都加上了 `'a` 那样: 文件名: src/main.rs @@ -200,19 +171,17 @@ fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { } ``` -示例 10-23:`longest` 函数定义指定了签名中所有的引用必须有相同的生命周期 `'a` +示例 10-22:`longest` 函数定义指定了签名中所有的引用必须有相同的生命周期 `'a` -这段代码能够编译并会产生我们希望得到的示例 10-21 中的 `main` 函数的结果。 +这段代码能够编译并会产生我们希望得到的示例 10-20 中的 `main` 函数的结果。 -现在函数签名表明对于某些生命周期 `'a`,函数会获取两个参数,他们都是与生命周期 `'a` 存在的一样长的字符串 slice。函数会返回一个同样也与生命周期 `'a` 存在的一样长的字符串 slice。这就是我们告诉 Rust 需要其保证的契约。 +现在函数签名表明对于某些生命周期 `'a`,函数会获取两个参数,他们都是与生命周期 `'a` 存在的一样长的字符串 slice。函数会返回一个同样也与生命周期 `'a` 存在的一样长的字符串 slice。这就是我们告诉 Rust 需要其保证的契约。记住通过在函数签名中指定生命周期参数时,我们并没有改变任何传入后返回的值的生命周期。而是指出任何不遵守这个协议的传入值都将被借用检查器拒绝。注意 `longest` 函数并不需要知道 `x` 和 `y` 具体会存在多久,而只需要知道有某个可以被 `'a` 替代的作用域将会满足这个签名。 -通过在函数签名中指定生命周期参数,我们并没有改变任何传入后返回的值的生命周期,而是指出任何不遵守这个协议的传入值都将被借用检查器拒绝。这个函数并不知道(或需要知道)`x` 和 `y` 具体会存在多久,而只需要知道有某个可以被 `'a` 替代的作用域将会满足这个签名。 +当在函数中使用生命周期注解时,这些注解出现在函数签名中,而不存在于函数体中的任何代码中。这是因为 Rust 能够分析函数中代码而不需要任何协助,不过当函数引用或被函数之外的代码引用时,让 Rust 自身分析出参数或返回值的生命周期几乎是不可能的。这些生命周期在每次函数被调用时都可能不同。这也就是为什么我们需要手动标记生命周期。 -当在函数中使用生命周期注解时,这些注解出现在函数签名中,而不存在于函数体中的任何代码中。这是因为 Rust 能够分析函数中代码而不需要任何协助,不过当函数引用或被函数之外的代码引用时,参数或返回值的生命周期可能在每次函数被调用时都不同。这可能会产生惊人的消耗并且对于 Rust 来说通常是不可能分析的。在这种情况下,我们需要自己标注生命周期。 +当具体的引用被传递给 `longest` 时,被 `'a` 所替代的具体生命周期是 `x` 的作用域与 `y` 的作用域相重叠的那一部分。换一种说法就是泛型生命周期 `'a` 的具体生命周期等同于 `x` 和 `y` 的生命周期中较小的那一个。因为我们用相同的生命周期参数 `'a` 标注了返回的引用值,所以返回的引用值就能保证在 `x` 和 `y` 中较短的那个生命周期结束之前保持有效。 -当具体的引用被传递给 `longest` 时,被 `'a` 所替代的具体生命周期是 `x` 的作用域与 `y` 的作用域相重叠的那一部分。因为作用域总是嵌套的,所以换一种说法就是泛型生命周期 `'a` 的具体生命周期等同于 `x` 和 `y` 的生命周期中较小的那一个。因为我们用相同的生命周期参数 `'a` 标注了返回的引用值,所以返回的引用值就能保证在 `x` 和 `y` 中较短的那个生命周期结束之前保持有效。 - -让我们看看如何通过传递拥有不同具体生命周期的引用来限制 `longest` 函数的使用。示例 10-24 是一个应该在任何编程语言中都很直观的例子:`string1` 直到外部作用域结束都是有效的,`string2` 则在内部作用域中是有效的,而 `result` 则引用了一些直到内部作用域结束都是有效的值。借用检查器认可这些代码;它能够编译和运行,并打印出 `The longest string is long string is long`: +让我们看看如何通过传递拥有不同具体生命周期的引用来限制 `longest` 函数的使用。示例 10-23 是一个很直观的例子。 文件名: src/main.rs @@ -236,13 +205,15 @@ fn main() { } ``` -示例 10-24:通过拥有不同的具体生命周期的 `String` 值调用 `longest` 函数 +示例 10-23:通过拥有不同的具体生命周期的 `String` 值调用 `longest` 函数 + +在这个例子中,`string1` 直到外部作用域结束都是有效的,`string2` 则在内部作用域中是有效的,而 `result` 则引用了一些直到内部作用域结束都是有效的值。借用检查器认可这些代码;它能够编译和运行,并打印出 `The longest string is long string is long`。 -接下来,让我们尝试一个 `result` 的引用的生命周期肯定比两个参数的要短的例子。将 `result` 变量的声明从内部作用域中移动出来,但是将 `result` 和 `string2` 变量的赋值语句一同留在内部作用域里。接下来,我们将使用 `result` 的 `println!` 移动到内部作用域之外,就在其结束之后。注意示例 10-25 中的代码不能编译: +接下来,让我们尝试一个 `result` 的引用的生命周期肯定比两个参数的要短的例子。将 `result` 变量的声明从内部作用域中移动出来,但是将 `result` 和 `string2` 变量的赋值语句一同留在内部作用域里。接下来,我们将使用 `result` 的 `println!` 移动到内部作用域之外,就在其结束之后。注意示例 10-24 中的代码不能编译: 文件名: src/main.rs -```rust,ignore +```rust,ignore,does_not_compile fn main() { let string1 = String::from("long string is long"); let result; @@ -254,25 +225,26 @@ fn main() { } ``` -示例 10-25:在 `string2` 离开作用域之后使用 `result` 的尝试不能编译 +示例 10-24:在 `string2` 离开作用域之后使用 `result` 的尝试不能编译 如果尝试编译会出现如下错误: ```text -error: `string2` does not live long enough +error[E0597]: `string2` does not live long enough + --> src/main.rs:15:5 | -6 | result = longest(string1.as_str(), string2.as_str()); +14 | result = longest(string1.as_str(), string2.as_str()); | ------- borrow occurs here -7 | } +15 | } | ^ `string2` dropped here while still borrowed -8 | println!("The longest string is {}", result); -9 | } +16 | println!("The longest string is {}", result); +17 | } | - borrowed value needs to live until here ``` 错误表明为了保证 `println!` 中的 `result` 是有效的,`string2` 需要直到外部作用域结束都是有效的。Rust 知道这些是因为(`longest`)函数的参数和返回值都使用了相同的生命周期参数 `'a`。 -以人类的理解 `string1` 更长,因此 `result` 会包含指向 `string1` 的引用。因为 `string1` 尚未离开作用域,对于 `println!` 来说 `string1` 的引用仍然是有效的。然而,我们通过生命周期参数告诉 Rust 的是 `longest` 函数返回的引用的生命周期应该与传入参数的生命周期中较短那个保持一致。因此,借用检查器不允许示例 10-25 中的代码,因为它可能会存在无效的引用。 +以人类的理解 `string1` 更长,因此 `result` 会包含指向 `string1` 的引用。因为 `string1` 尚未离开作用域,对于 `println!` 来说 `string1` 的引用仍然是有效的。然而,我们通过生命周期参数告诉 Rust 的是 `longest` 函数返回的引用的生命周期应该与传入参数的生命周期中较短那个保持一致。因此,借用检查器不允许示例 10-24 中的代码,因为它可能会存在无效的引用。 请尝试更多采用不同的值和不同生命周期的引用作为 `longest` 函数的参数和返回值的实验。并在开始编译前猜想你的实验能否通过借用检查器,接着编译一下看看你的理解是否正确! @@ -294,7 +266,7 @@ fn longest<'a>(x: &'a str, y: &str) -> &'a str { 文件名: src/main.rs -```rust,ignore +```rust,ignore,does_not_compile fn longest<'a>(x: &str, y: &str) -> &'a str { let result = String::from("really long string"); result.as_str() @@ -304,18 +276,23 @@ fn longest<'a>(x: &str, y: &str) -> &'a str { 即便我们为返回值指定了生命周期参数 `'a`,这个实现却编译失败了,因为返回值的生命周期与参数完全没有关联。这里是会出现的错误信息: ```text -error: `result` does not live long enough +error[E0597]: `result` does not live long enough + --> src/main.rs:3:5 | 3 | result.as_str() | ^^^^^^ does not live long enough 4 | } | - borrowed value only lives until here | -note: borrowed value must be valid for the lifetime 'a as defined on the block -at 1:44... +note: borrowed value must be valid for the lifetime 'a as defined on the +function body at 1:1... + --> src/main.rs:1:1 | -1 | fn longest<'a>(x: &str, y: &str) -> &'a str { - | ^ +1 | / fn longest<'a>(x: &str, y: &str) -> &'a str { +2 | | let result = String::from("really long string"); +3 | | result.as_str() +4 | | } + | |_^ ``` 出现的问题是 `result` 在 `longest` 函数的结尾将离开作用域并被清理,而我们尝试从函数返回一个 `result` 的引用。无法指定生命周期参数来改变悬垂引用,而且 Rust 也不允许我们创建一个悬垂引用。在这种情况,最好的解决方案是返回一个有所有权的数据类型而不是一个引用,这样函数调用者就需要负责清理这个值了。 @@ -324,7 +301,7 @@ at 1:44... ### 结构体定义中的生命周期注解 -目前为止,我们只定义过有所有权类型的结构体。也可以定义存放引用的结构体,不过需要为结构体定义中的每一个引用添加生命周期注解。示例 10-26 中有一个存放了一个字符串 slice 的结构体 `ImportantExcerpt`: +目前为止,我们只定义过有所有权类型的结构体。也可以定义存放引用的结构体,不过需要为结构体定义中的每一个引用添加生命周期注解。示例 10-25 中有一个存放了一个字符串 slice 的结构体 `ImportantExcerpt`: 文件名: src/main.rs @@ -342,15 +319,15 @@ fn main() { } ``` -示例 10-26:一个存放引用的结构体,所以其定义需要生命周期注解 +示例 10-25:一个存放引用的结构体,所以其定义需要生命周期注解 -这个结构体有一个字段,`part`,它存放了一个字符串 slice,这是一个引用。类似于泛型参数类型,必须在结构体名称后面的尖括号中声明泛型生命周期参数,以便在结构体定义中使用生命周期参数。 +这个结构体有一个字段,`part`,它存放了一个字符串 slice,这是一个引用。类似于泛型参数类型,必须在结构体名称后面的尖括号中声明泛型生命周期参数,以便在结构体定义中使用生命周期参数。这个注解意味着 `ImportantExcerpt` 的示例不能比其 `part` 字段中的引用存在的更久。 -这里的 `main` 函数创建了一个 `ImportantExcerpt` 的实例,它存放了变量 `novel` 所拥有的 `String` 的第一个句子的引用。 +这里的 `main` 函数创建了一个 `ImportantExcerpt` 的实例,它存放了变量 `novel` 所拥有的 `String` 的第一个句子的引用。`novel` 的数据在 `ImportantExcerpt` 实例创建之前就存在。另外,直到 `ImportantExcerpt` 离开作用域之后 `novel` 都不会离开作用域,所以 `ImportantExcerpt` 实例中的引用是有效的 ### 生命周期省略(Lifetime Elision) -在这一部分,我们知道了每一个引用都有一个生命周期,而且需要为使用了引用的函数或结构体指定生命周期。然而,第四章的 “字符串 slice” 部分有一个函数,我们在示例 10-27 中再次展示出来,它没有生命周期注解却能成功编译: +在这一部分,我们知道了每一个引用都有一个生命周期,而且需要为使用了引用的函数或结构体指定生命周期。然而,第四章的示例 4-9 中有一个函数,我们在示例 10-26 中再次展示出来,它没有生命周期注解却能成功编译: 文件名: src/lib.rs @@ -368,9 +345,9 @@ fn first_word(s: &str) -> &str { } ``` -示例 10-27:第四章定义了一个没有使用生命周期注解的函数,即便其参数和返回值都是引用 +示例 10-27:示例 4-9 定义了一个没有使用生命周期注解的函数,即便其参数和返回值都是引用 -这个函数没有生命周期注解却能编译是由于一些历史原因:在早期 pre-1.0 版本的 Rust 中,这的确是不能编译的。每一个引用都必须有明确的生命周期。那时的函数签名将会写成这样: +这个函数没有生命周期注解却能编译是由于一些历史原因:在早期版本(pre-1.0)的 Rust 中,这的确是不能编译的。每一个引用都必须有明确的生命周期。那时的函数签名将会写成这样: ```rust,ignore fn first_word<'a>(s: &'a str) -> &'a str { @@ -384,23 +361,25 @@ fn first_word<'a>(s: &'a str) -> &'a str { 省略规则并不提供完整的推断:如果 Rust 在明确遵守这些规则的前提下变量的生命周期仍然是模棱两可的话,它不会猜测剩余引用的生命周期应该是什么。在这种情况,编译器会给出一个错误,这可以通过增加对应引用之间相联系的生命周期注解来解决。 -首先,介绍一些定义:函数或方法的参数的生命周期被称为 **输入生命周期**(*input lifetimes*),而返回值的生命周期被称为 **输出生命周期**(*output lifetimes*)。 +函数或方法的参数的生命周期被称为 **输入生命周期**(*input lifetimes*),而返回值的生命周期被称为 **输出生命周期**(*output lifetimes*)。 + +编译器采用三条规则来判断引用何时不需要明确的注解。第一条规则适用于输入生命周期,后两条规则适用于输出生命周期。如果编译器检查完这三条规则后仍然存在没有计算出生命周期的引用,编译器将会停止并生成错误。 -现在介绍编译器用于判断引用何时不需要明确生命周期注解的规则。第一条规则适用于输入生命周期,后两条规则适用于输出生命周期。如果编译器检查完这三条规则后仍然存在没有计算出生命周期的引用,编译器将会停止并生成错误。 +这些规则适用于 `fn` 定义,以及 `impl` 块。 -1. 每一个是引用的参数都有它自己的生命周期参数。换句话说就是,有一个引用参数的函数有一个生命周期参数:`fn foo<'a>(x: &'a i32)`,有两个引用参数的函数有两个不同的生命周期参数,`fn foo<'a, 'b>(x: &'a i32, y: &'b i32)`,依此类推。 +第一条规则是每一个是引用的参数都有它自己的生命周期参数。换句话说就是,有一个引用参数的函数有一个生命周期参数:`fn foo<'a>(x: &'a i32)`,有两个引用参数的函数有两个不同的生命周期参数,`fn foo<'a, 'b>(x: &'a i32, y: &'b i32)`,依此类推。 -2. 如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数:`fn foo<'a>(x: &'a i32) -> &'a i32`。 +第二条规则是如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数:`fn foo<'a>(x: &'a i32) -> &'a i32`。 -3. 如果方法有多个输入生命周期参数,不过其中之一因为方法的缘故为 `&self` 或 `&mut self`,那么 `self` 的生命周期被赋给所有输出生命周期参数。这使得方法编写起来更简洁。 +第三条规则是如果方法有多个输入生命周期参数,不过其中之一因为方法的缘故为 `&self` 或 `&mut self`,那么 `self` 的生命周期被赋给所有输出生命周期参数。这使得方法更容易读写,因为只需更少的符号。 -假设我们自己就是编译器并来计算示例 10-25 `first_word` 函数的签名中的引用的生命周期。开始时签名中的引用并没有关联任何生命周期: +假设我们自己就是编译器。并应用这些规则来计算示例 10-26 中 `first_word` 函数签名中的引用的生命周期。开始时签名中的引用并没有关联任何生命周期: ```rust,ignore fn first_word(s: &str) -> &str { ``` -接着我们(作为编译器)应用第一条规则,也就是每个引用参数都有其自己的生命周期。我们像往常一样称之为 `'a`,所以现在签名看起来像这样: +接着编译器应用第一条规则,也就是每个引用参数都有其自己的生命周期。我们像往常一样称之为 `'a`,所以现在签名看起来像这样: ```rust,ignore fn first_word<'a>(s: &'a str) -> &str { @@ -414,7 +393,7 @@ fn first_word<'a>(s: &'a str) -> &'a str { 现在这个函数签名中的所有引用都有了生命周期,如此编译器可以继续它的分析而无须程序员标记这个函数签名中的生命周期。 -让我们再看看另一个例子,这次我们从示例 10-22 中没有生命周期参数的 `longest` 函数开始: +让我们再看看另一个例子,这次我们从示例 10-21 中没有生命周期参数的 `longest` 函数开始: ```rust,ignore fn longest(x: &str, y: &str) -> &str { @@ -426,25 +405,17 @@ fn longest(x: &str, y: &str) -> &str { fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str { ``` -再来应用第二条规则,它并不适用因为存在多于一个输入生命周期。再来看第三条规则,它同样也不适用因为没有 `self` 参数。然后我们就没有更多规则了,不过还没有计算出返回值的类型的生命周期。这就是为什么在编译示例 10-22 的代码时会出现错误的原因:编译器使用所有已知的生命周期省略规则,不过仍不能计算出签名中所有引用的生命周期。 +再来应用第二条规则,它并不适用因为存在多于一个输入生命周期。再来看第三条规则,它同样也不适用因为没有 `self` 参数。然后我们就没有更多规则了,不过还没有计算出返回值的类型的生命周期。这就是为什么在编译示例 10-21 的代码时会出现错误的原因:编译器使用所有已知的生命周期省略规则,不过仍不能计算出签名中所有引用的生命周期。 因为第三条规则真正能够适用的就只有方法签名,现在就让我们看看那种情况中的生命周期,并看看为什么这条规则意味着我们经常不需要在方法签名中标注生命周期。 ### 方法定义中的生命周期注解 - - - -当为带有生命周期的结构体实现方法时,其语法依然类似示例 10-11 中展示的泛型类型参数的语法:声明和使用生命周期参数的位置依赖于生命周期参数是否同结构体字段或方法参数和返回值相关。 +当为带有生命周期的结构体实现方法时,其语法依然类似示例 10-11 中展示的泛型类型参数的语法。声明和使用生命周期参数的位置依赖于生命周期参数是否同结构体字段或方法参数和返回值相关。 (实现方法时)结构体字段的生命周期必须总是在 `impl` 关键字之后声明并在结构体名称之后被使用,因为这些生命周期是结构体类型的一部分。 -`impl` 块里的方法签名中,引用可能与结构体字段中的引用相关联,也可能是独立的。另外,生命周期省略规则也经常让我们无需在方法签名中使用生命周期注解。让我们看看一些使用示例 10-26 中定义的结构体 `ImportantExcerpt` 的例子。 +`impl` 块里的方法签名中,引用可能与结构体字段中的引用相关联,也可能是独立的。另外,生命周期省略规则也经常让我们无需在方法签名中使用生命周期注解。让我们看看一些使用示例 10-25 中定义的结构体 `ImportantExcerpt` 的例子。 首先,这里有一个方法 `level`。其唯一的参数是 `self` 的引用,而且返回值只是一个 `i32`,并不引用任何值: @@ -481,7 +452,7 @@ impl<'a> ImportantExcerpt<'a> { ### 静态生命周期 -这里有 **一种** 特殊的生命周期值得讨论:`'static`。`'static` 生命周期存活于整个程序期间。所有的字符串字面值都拥有 `'static` 生命周期,我们也可以选择像下面这样标注出来: +这里有一种特殊的生命周期值得讨论:`'static`,其生命周期存活于整个程序期间。所有的字符串字面值都拥有 `'static` 生命周期,我们也可以选择像下面这样标注出来: ```rust let s: &'static str = "I have a static lifetime."; @@ -489,10 +460,7 @@ let s: &'static str = "I have a static lifetime."; 这个字符串的文本被直接储存在程序的二进制文件中而这个文件总是可用的。因此所有的字符串字面值都是 `'static` 的。 - - - -你可能在错误信息的帮助文本中见过使用 `'static` 生命周期的建议,不过将引用指定为 `'static` 之前,思考一下这个引用是否真的在整个程序的生命周期里都有效(或者哪怕你希望它一直有效,如果可能的话)。大部分情况,代码中的问题是尝试创建一个悬垂引用或者可用的生命周期不匹配,请解决这些问题而不是指定一个 `'static` 的生命周期。 +你可能在错误信息的帮助文本中见过使用 `'static` 生命周期的建议,不过将引用指定为 `'static` 之前,思考一下这个引用是否真的在整个程序的生命周期里都有效。你可能会考虑希望它一直有效,如果可能的话。大部分情况,代码中的问题是尝试创建一个悬垂引用或者可用的生命周期不匹配,请解决这些问题而不是指定一个 `'static` 的生命周期。 ### 结合泛型类型参数、trait bounds 和生命周期 @@ -513,7 +481,7 @@ fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a st } ``` -这个是示例 10-23 中那个返回两个字符串 slice 中较长者的 `longest` 函数,不过带有一个额外的参数 `ann`。`ann` 的类型是泛型 `T`,它可以被放入任何实现了 `where` 从句中指定的 `Display` trait 的类型。这个额外的参数会在函数比较字符串 slice 的长度之前被打印出来,这也就是为什么 `Display` trait bound 是必须的。因为生命周期也是泛型,所以生命周期参数 `'a` 和泛型类型参数 `T` 都位于函数名后的同一尖括号列表中。 +这个是示例 10-22 中那个返回两个字符串 slice 中较长者的 `longest` 函数,不过带有一个额外的参数 `ann`。`ann` 的类型是泛型 `T`,它可以被放入任何实现了 `where` 从句中指定的 `Display` trait 的类型。这个额外的参数会在函数比较字符串 slice 的长度之前被打印出来,这也就是为什么 `Display` trait bound 是必须的。因为生命周期也是泛型,所以生命周期参数 `'a` 和泛型类型参数 `T` 都位于函数名后的同一尖括号列表中。 ## 总结 diff --git a/src/ch11-00-testing.md b/src/ch11-00-testing.md index 96de28c..f57beeb 100644 --- a/src/ch11-00-testing.md +++ b/src/ch11-00-testing.md @@ -1,8 +1,8 @@ -# 测试 +# 编写自动化测试 -> [ch11-00-testing.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch11-00-testing.md) +> [ch11-00-testing.md](https://github.com/rust-lang/book/blob/master/src/ch11-00-testing.md) >
-> commit 4464eab0892297b83db7134b7ace12762a89b389 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f > Program testing can be a very effective way to show the presence of bugs, but it is hopelessly inadequate for showing their absence. > @@ -12,7 +12,9 @@ > > Edsger W. Dijkstra,【谦卑的程序员】(1972) -这并不意味着我们不该尽可能测试软件!程序的正确性意味着代码如我们期望的那样运行。Rust 是一个相当注重正确性的编程语言,不过正确性是一个难以证明的复杂主题。Rust 的类型系统在此问题上下了很大的功夫,不过它不可能捕获所有种类的错误。为此,Rust 也在语言本身包含了编写软件测试的支持。 +Edsger W. Dijkstra 在其 1972 年的文章【谦卑的程序员】(“The Humble Programmer”)中说到 “软件测试是证明 bug 存在的有效方法,而证明其不存在时则显得令人绝望的不足。”(“Program testing can be a very effective way to show the presence of bugs, but it is hopelessly inadequate for showing their absence.”)这并不意味着我们不该尽可能地测试软件! + +程序的正确性意味着代码如我们期望的那样运行。Rust 是一个相当注重正确性的编程语言,不过正确性是一个难以证明的复杂主题。Rust 的类型系统在此问题上下了很大的功夫,不过它不可能捕获所有种类的错误。为此,Rust 也在语言本身包含了编写软件测试的支持。 例如,我们可以编写一个叫做 `add_two` 的将传递给它的值加二的函数。它的签名有一个整型参数并返回一个整型值。当实现和编译这个函数时,Rust 会进行所有目前我们已经见过的类型检查和借用检查,例如,这些检查会确保我们不会传递 `String` 或无效的引用给这个函数。Rust 所 **不能** 检查的是这个函数是否会准确的完成我们期望的工作:返回参数加二后的值,而不是比如说参数加 10 或减 50 的值!这也就是测试出场的地方。 diff --git a/src/ch11-01-writing-tests.md b/src/ch11-01-writing-tests.md index 7aba6cb..18cfe25 100644 --- a/src/ch11-01-writing-tests.md +++ b/src/ch11-01-writing-tests.md @@ -1,8 +1,8 @@ ## 如何编写测试 -> [ch11-01-writing-tests.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch11-01-writing-tests.md) +> [ch11-01-writing-tests.md](https://github.com/rust-lang/book/blob/master/src/ch11-01-writing-tests.md) >
-> commit 4464eab0892297b83db7134b7ace12762a89b389 +> commit 820ac357f6cf0e866e5a8e7a9c57dd3e17e9f8ca Rust 中的测试函数是用来验证非测试代码是否按照期望的方式运行的。测试函数体通常执行如下三种操作: @@ -19,21 +19,23 @@ Rust 中的测试函数是用来验证非测试代码是否按照期望的方式 第七章当使用 Cargo 新建一个库项目时,它会自动为我们生成一个测试模块和一个测试函数。这有助于我们开始编写测试,因为这样每次开始新项目时不必去查找测试函数的具体结构和语法了。当然你也可以额外增加任意多的测试函数以及测试模块! -为了理清测试是如何工作的,我们将通过观察那些自动生成的测试模版——尽管它们实际上没有测试任何代码。接着,我们会写一些真正的测试,调用我们编写的代码并断言他们的行为的正确性。 +我们会通过实验那些自动生成的测试模版而不是实际编写测试代码来探索测试如何工作的一些方面。接着,我们会写一些真正的测试,调用我们编写的代码并断言他们的行为的正确性。 让我们创建一个新的库项目 `adder`: ```text -$ cargo new adder +$ cargo new adder --lib Created library `adder` project $ cd adder ``` + adder 库中 `src/lib.rs` 的内容应该看起来如示例 11-1 所示: 文件名: src/lib.rs ```rust +# fn main() {} #[cfg(test)] mod tests { #[test] @@ -75,15 +77,18 @@ Cargo 编译并运行了测试。在 `Compiling`、`Finished` 和 `Running` 这 因为之前我们并没有将任何测试标记为忽略,所以摘要中会显示 `0 ignored`。我们也没有过滤需要运行的测试,所以摘要中会显示`0 filtered out`。在下一部分 “控制测试如何运行” 会讨论忽略和过滤测试。 -`0 measured` 统计是针对性能测试的。性能测试(benchmark tests)在编写本书时,仍只能用于 Rust 开发版(nightly Rust)。请查看第一章来了解更多 Rust 开发版的信息。 +`0 measured` 统计是针对性能测试的。性能测试(benchmark tests)在编写本书时,仍只能用于 Rust 开发版(nightly Rust)。请查看 [性能测试的文档][bench] 了解更多。 + +[bench]: https://doc.rust-lang.org/unstable-book/library-features/test.html -测试输出中以 `Doc-tests adder` 开头的这一部分是所有文档测试的结果。我们现在并没有任何文档测试,不过 Rust 会编译任何在 API 文档中的代码示例。这个功能帮助我们使文档和代码保持同步!在第十四章的 “文档注释” 部分会讲到如何编写文档测试。现在我们将忽略 `Doc-tests` 部分的输出。 +测试输出的中以 `Doc-tests adder` 开头的这一部分是所有文档测试的结果。我们现在并没有任何文档测试,不过 Rust 会编译任何在 API 文档中的代码示例。这个功能帮助我们使文档和代码保持同步!在第十四章的 “文档注释” 部分会讲到如何编写文档测试。现在我们将忽略 `Doc-tests` 部分的输出。 让我们改变测试的名称并看看这如何改变测试的输出。给 `it_works` 函数起个不同的名字,比如 `exploration`,像这样: 文件名: src/lib.rs ```rust +# fn main() {} #[cfg(test)] mod tests { #[test] @@ -106,7 +111,8 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out 文件名: src/lib.rs -```rust +```rust,panics +# fn main() {} #[cfg(test)] mod tests { #[test] @@ -123,7 +129,6 @@ mod tests { 示例 11-3:增加第二个因调用了 `panic!` 而失败的测试 - 再次 `cargo test` 运行测试。输出应该看起来像示例 11-4,它表明 `exploration` 测试通过了而 `another` 失败了: ```text @@ -162,6 +167,7 @@ error: test failed 文件名: src/lib.rs ```rust +# fn main() {} #[derive(Debug)] pub struct Rectangle { length: u32, @@ -182,6 +188,7 @@ impl Rectangle { 文件名: src/lib.rs ```rust +# fn main() {} #[cfg(test)] mod tests { use super::*; @@ -198,7 +205,7 @@ mod tests { 示例 11-6:一个 `can_hold` 的测试,检查一个较大的矩形确实能放得下一个较小的矩形 -注意在 `tests` 模块中新增加了一行:`use super::*;`。`tests` 是一个普通的模块,它遵循第七章 “私有性规则” 部分介绍的可见性规则。因为这是一个内部模块,要测试外部模块中的代码,需要将其引入到内部模块的作用域中。这里选择使用全局导入,以便在 `tests` 模块中使用所有在外部模块定义的内容。 +注意在 `tests` 模块中新增加了一行:`use super::*;`。`tests` 是一个普通的模块,它遵循第七章 “私有性规则” 部分介绍的可见性规则。因为这是一个内部模块,要测试外部模块中的代码,需要将其引入到内部模块的作用域中。这里选择使用 glob 全局导入,以便在 `tests` 模块中使用所有在外部模块定义的内容。 我们将测试命名为 `larger_can_hold_smaller`,并创建所需的两个 `Rectangle` 实例。接着调用 `assert!` 宏并传递 `larger.can_hold(&smaller)` 调用的结果作为参数。这个表达式预期会返回 `true`,所以测试应该通过。让我们拭目以待! @@ -214,6 +221,7 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out 文件名: src/lib.rs ```rust +# fn main() {} #[cfg(test)] mod tests { use super::*; @@ -245,7 +253,8 @@ test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out 两个通过的测试!现在让我们看看如果引入一个 bug 的话测试结果会发生什么。将 `can_hold` 方法中比较长度时本应使用大于号的地方改成小于号: -```rust +```rust,not_desired_behavior +# fn main() {} # #[derive(Debug)] # pub struct Rectangle { # length: u32, @@ -284,13 +293,14 @@ test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out ### 使用 `assert_eq!` 和 `assert_ne!` 宏来测试相等 -测试功能的一个常用方法是将需要测试代码的值与期望值做比较,并检查是否相等。可以通过向 `assert!` 宏传递一个使用 `==` 运算符的表达式来做到。不过这个操作实在是太常见了,以至于标准库提供了一对宏来更方便的处理这些操作:`assert_eq!` 和 `assert_ne!`。这两个宏分别比较两个值是相等还是不相等。当断言失败时他们也会打印出这两个值具体是什么,以便于观察测试 **为什么** 失败,而 `assert!` 只会打印出它从 `==` 表达式中得到了 `false` 值,而不是导致 `false` 的两个值。 +测试功能的一个常用方法是将需要测试代码的值与期望值做比较,并检查是否相等。可以通过向 `assert!` 宏传递一个使用 `==` 运算符的表达式来做到。不过这个操作实在是太常见了,以至于标准库提供了一对宏来更方便的处理这些操作 —— `assert_eq!` 和 `assert_ne!`。这两个宏分别比较两个值是相等还是不相等。当断言失败时他们也会打印出这两个值具体是什么,以便于观察测试 **为什么** 失败,而 `assert!` 只会打印出它从 `==` 表达式中得到了 `false` 值,而不是导致 `false` 的两个值。 -示例 11-7 中,让我们编写一个对其参数加二并返回结果的函数 `add_two`。接着使用 `assert_eq!` 宏测试这个函数: +示例 11-7 中,让我们编写一个对其参数加二并返回结果的函数 `add_two`。接着使用 `assert_eq!` 宏测试这个函数。 文件名: src/lib.rs ```rust +# fn main() {} pub fn add_two(a: i32) -> i32 { a + 2 } @@ -321,7 +331,8 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out 在代码中引入一个 bug 来看看使用 `assert_eq!` 的测试失败是什么样的。修改 `add_two` 函数的实现使其加 3: -```rust +```rust,not_desired_behavior +# fn main() {} pub fn add_two(a: i32) -> i32 { a + 3 } @@ -353,17 +364,18 @@ test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out `assert_ne!` 宏在传递给它的两个值不相等时通过,而在相等时失败。在代码按预期运行,我们不确定值 **会** 是什么,不过能确定值绝对 **不会** 是什么的时候,这个宏最有用处。例如,如果一个函数保证会以某种方式改变其输出,不过这种改变方式是由运行测试时是星期几来决定的,这时最好的断言可能就是函数的输出不等于其输入。 -`assert_eq!` 和 `assert_ne!` 宏在底层分别使用了 `==` 和 `!=`。当断言失败时,这些宏会使用调试格式打印出其参数,这意味着被比较的值必需实现了 `PartialEq` 和 `Debug` trait。所有的基本类型和大部分标准库类型都实现了这些 trait。对于自定义的结构体和枚举,需要实现 `PartialEq` 才能断言他们的值是否相等。需要实现 `Debug` 才能在断言失败时打印他们的值。因为这两个 trait 都是派生 trait,如第五章示例 5-12 所提到的,通常可以直接在结构体或枚举上添加 `#[derive(PartialEq, Debug)]` 注解。附录 C 中有更多关于这些和其他派生 trait 的详细信息。 +`assert_eq!` 和 `assert_ne!` 宏在底层分别使用了 `==` 和 `!=`。当断言失败时,这些宏会使用调试格式打印出其参数,这意味着被比较的值必需实现了 `PartialEq` 和 `Debug` trait。所有的基本类型和大部分标准库类型都实现了这些 trait。对于自定义的结构体和枚举,需要实现 `PartialEq` 才能断言他们的值是否相等。需要实现 `Debug` 才能在断言失败时打印他们的值。因为这两个 trait 都是派生 trait,如第五章示例 5-12 所提到的,通常可以直接在结构体或枚举上添加 `#[derive(PartialEq, Debug)]` 注解。附录 C “可派生 trait” 中有更多关于这些和其他派生 trait 的详细信息。 ### 自定义失败信息 -你也可以向 `assert!`、`assert_eq!` 和 `assert_ne!` 宏传递一个可选的失败信息参数,可以在测试失败时将自定义失败信息一同打印出来。任何在 `assert!` 的一个必需参数和 `assert_eq!` 和 `assert_ne!` 的两个必需参数之后指定的参数都会传递给 `format!` 宏(在第八章的“使用 `+` 运算符或 `format!` 宏连接字符串”部分讨论过),所以可以传递一个包含 `{}` 占位符的格式字符串和需要放入占位符的值。自定义信息有助于记录断言的意义;当测试失败时就能更好的理解代码出了什么问题。 +你也可以向 `assert!`、`assert_eq!` 和 `assert_ne!` 宏传递一个可选的失败信息参数,可以在测试失败时将自定义失败信息一同打印出来。任何在 `assert!` 的一个必需参数和 `assert_eq!` 和 `assert_ne!` 的两个必需参数之后指定的参数都会传递给 `format!` 宏(在第八章的“使用 `+` 运算符或 `format!` 宏拼接字符串”部分讨论过),所以可以传递一个包含 `{}` 占位符的格式字符串和需要放入占位符的值。自定义信息有助于记录断言的意义;当测试失败时就能更好的理解代码出了什么问题。 例如,比如说有一个根据人名进行问候的函数,而我们希望测试将传递给函数的人名显示在输出中: 文件名: src/lib.rs ```rust +# fn main() {} pub fn greeting(name: &str) -> String { format!("Hello {}!", name) } @@ -384,7 +396,8 @@ mod tests { 让我们通过将 `greeting` 改为不包含 `name` 来在代码中引入一个 bug 来测试失败时是怎样的: -```rust +```rust,not_desired_behavior +# fn main() {} pub fn greeting(name: &str) -> String { String::from("Hello!") } @@ -407,7 +420,7 @@ failures: tests::greeting_contains_name ``` -这仅仅告诉了我们断言失败了和失败的行号。一个更有用的失败信息应该打印出 `greeting` 函数的值。让我们为测试函数增加一个自定义失败信息参数:带占位符的格式字符串,以及 `greeting` 函数的值: +结果仅仅告诉了我们断言失败了和失败的行号。一个更有用的失败信息应该打印出 `greeting` 函数的值。让我们为测试函数增加一个自定义失败信息参数:带占位符的格式字符串,以及 `greeting` 函数的值: ```rust,ignore #[test] @@ -422,7 +435,6 @@ fn greeting_contains_name() { 现在如果再次运行测试,将会看到更有价值的信息: - ```text ---- tests::greeting_contains_name stdout ---- thread 'tests::greeting_contains_name' panicked at 'Greeting did not @@ -434,7 +446,7 @@ note: Run with `RUST_BACKTRACE=1` for a backtrace. ### 使用 `should_panic` 检查 panic -除了检查代码是否返回期望的正确的值之外,检查代码是否按照期望处理错误也是很重要的。例如,考虑第九章示例 9-9 创建的 `Guess` 类型。其他使用 `Guess` 的代码都是基于 `Guess` 实例仅有的值范围在 1 到 100 的前提。可以编写一个测试来确保创建一个超出范围的值的 `Guess` 实例会 panic。 +除了检查代码是否返回期望的正确的值之外,检查代码是否按照期望处理错误也是很重要的。例如,考虑第九章示例 9-10 创建的 `Guess` 类型。其他使用 `Guess` 的代码都是基于 `Guess` 实例仅有的值范围在 1 到 100 的前提。可以编写一个测试来确保创建一个超出范围的值的 `Guess` 实例会 panic。 可以通过对函数增加另一个属性 `should_panic` 来实现这些。这个属性在函数中的代码 panic 时会通过,而在其中的代码没有 panic 时失败。 @@ -443,12 +455,13 @@ note: Run with `RUST_BACKTRACE=1` for a backtrace. 文件名: src/lib.rs ```rust +# fn main() {} pub struct Guess { - value: u32, + value: i32, } impl Guess { - pub fn new(value: u32) -> Guess { + pub fn new(value: i32) -> Guess { if value < 1 || value > 100 { panic!("Guess value must be between 1 and 100, got {}.", value); } @@ -484,15 +497,16 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out 看起来不错!现在在代码中引入 bug,移除 `new` 函数在值大于 100 时会 panic 的条件: -```rust +```rust,not_desired_behavior +# fn main() {} # pub struct Guess { -# value: u32, +# value: i32, # } # // --snip-- impl Guess { - pub fn new(value: u32) -> Guess { + pub fn new(value: i32) -> Guess { if value < 1 { panic!("Guess value must be between 1 and 100, got {}.", value); } @@ -525,14 +539,15 @@ test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out 文件名: src/lib.rs ```rust +# fn main() {} # pub struct Guess { -# value: u32, +# value: i32, # } # // --snip-- impl Guess { - pub fn new(value: u32) -> Guess { + pub fn new(value: i32) -> Guess { if value < 1 { panic!("Guess value must be greater than or equal to 1, got {}.", value); @@ -565,7 +580,7 @@ mod tests { 为了观察带有 `expected` 信息的 `should_panic` 测试失败时会发生什么,让我们再次引入一个 bug,将 `if value < 1` 和 `else if value > 100` 的代码块对换: -```rust,ignore +```rust,ignore,not_desired_behavior if value < 1 { panic!("Guess value must be less than or equal to 100, got {}.", value); } else if value > 100 { @@ -596,4 +611,24 @@ test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out 失败信息表明测试确实如期望 panic 了,不过 panic 信息中并没有包含 `expected` 信息 `'Guess value must be less than or equal to 100'`。而我们得到的 panic 信息是 `'Guess value must be greater than or equal to 1, got 200.'`。这样就可以开始寻找 bug 在哪了! +### 将 `Result` 用于测试 + +目前为止,我们编写的测试在失败时就会 panic。也可以使用 `Result` 编写测试!这里是第一个例子采用了 Result: + +```rust +#[cfg(test)] +mod tests { + #[test] + fn it_works() -> Result<(), String> { + if 2 + 2 == 4 { + Ok(()) + } else { + Err(String::from("two plus two does not equal four")) + } + } +} +``` + +这里我们将 `it_works` 改为返回 Result。同时在函数体中,在成功时返回 `Ok(())` 而不是 `assert_eq!`,而失败时返回带有 `String` 的 `Err`。跟之前一样,这个测试可能成功或失败,不过不再通过 panic,可以通过 `Result` 来判断结果。为此不能在对这些函数使用 `#[should_panic]`;而是应该返回 `Err`! + 现在你知道了几种编写测试的方法,让我们看看运行测试时会发生什么,和可以用于 `cargo test` 的不同选项。 diff --git a/src/ch11-02-running-tests.md b/src/ch11-02-running-tests.md index 1de61e2..9b32b0a 100644 --- a/src/ch11-02-running-tests.md +++ b/src/ch11-02-running-tests.md @@ -1,8 +1,8 @@ ## 控制测试如何运行 -> [ch11-02-running-tests.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch11-02-running-tests.md) +> [ch11-02-running-tests.md](https://github.com/rust-lang/book/blob/master/src/ch11-02-running-tests.md) >
-> commit 550c8ea6f74060ff1f7b67e7e1878c4da121682d +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 就像 `cargo run` 会编译代码并运行生成的二进制文件一样,`cargo test` 在测试模式下编译代码并运行生成的测试二进制文件。可以指定命令行参数来改变 `cargo test` 的默认行为。例如,`cargo test` 生成的二进制文件的默认行为是并行的运行所有测试,并截获测试运行过程中产生的输出,阻止他们被显示出来,使得阅读测试结果相关的内容变得更容易。 @@ -20,7 +20,7 @@ $ cargo test -- --test-threads=1 ``` -这里将测试线程设置为 1,告诉程序不要使用任何并行机制。这也会比并行运行花费更多时间,不过在有共享的状态时,测试就不会潜在的相互干扰了。 +这里将测试线程设置为 `1`,告诉程序不要使用任何并行机制。这也会比并行运行花费更多时间,不过在有共享的状态时,测试就不会潜在的相互干扰了。 ### 显示函数输出 @@ -30,7 +30,7 @@ $ cargo test -- --test-threads=1 文件名: src/lib.rs -```rust +```rust,panics fn prints_and_returns_10(a: i32) -> i32 { println!("I got the value {}", a); 10 diff --git a/src/ch11-03-test-organization.md b/src/ch11-03-test-organization.md index 7903bab..ea13021 100644 --- a/src/ch11-03-test-organization.md +++ b/src/ch11-03-test-organization.md @@ -1,8 +1,8 @@ ## 测试的组织结构 -> [ch11-03-test-organization.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch11-03-test-organization.md) +> [ch11-03-test-organization.md](https://github.com/rust-lang/book/blob/master/src/ch11-03-test-organization.md) >
-> commit b3eddb8edc0c3f83647143673d18efac0a44083a +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 本章一开始就提到,测试是一个复杂的概念,而且不同的开发者也采用不同的技术和组织。Rust 社区倾向于根据测试的两个主要分类来考虑问题:**单元测试**(*unit tests*)与 **集成测试**(*integration tests*)。单元测试倾向于更小而更集中,在隔离的环境中一次测试一个模块,或者是测试私有接口。而集成测试对于你的库来说则完全是外部的。它们与其他外部代码一样,通过相同的方式使用你的代码,只测试公有接口而且每个测试都有可能会测试多个模块。 @@ -16,7 +16,7 @@ 测试模块的 `#[cfg(test)]` 注解告诉 Rust 只在执行 `cargo test` 时才编译和运行测试代码,而在运行 `cargo build` 时不这么做。这在只希望构建库的时候可以节省编译时间,并且因为它们并没有包含测试,所以能减少编译产生的文件的大小。与之对应的集成测试因为位于另一个文件夹,所以它们并不需要 `#[cfg(test)]` 注解。然而单元测试位于与源码相同的文件中,所以你需要使用 `#[cfg(test)]` 来指定他们不应该被包含进编译结果中。 -还记得本章第一部分新建的 `adder` 项目吗?Cargo 为我们生成了如下代码: +回忆本章第一部分新建的 `adder` 项目吗,Cargo 为我们生成了如下代码: 文件名: src/lib.rs @@ -39,6 +39,8 @@ mod tests { 文件名: src/lib.rs ```rust +# fn main() {} + pub fn add_two(a: i32) -> i32 { internal_adder(a, 2) } @@ -75,7 +77,7 @@ mod tests { 文件名: tests/integration_test.rs ```rust,ignore -extern crate adder; +use adder; #[test] fn it_adds_two() { @@ -147,7 +149,7 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ```rust pub fn setup() { - // setup code specific to your library's tests would go here + // 编写特定库测试所需的代码 } ``` @@ -188,7 +190,7 @@ test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out 文件名: tests/integration_test.rs ```rust,ignore -extern crate adder; +use adder; mod common; @@ -199,7 +201,7 @@ fn it_adds_two() { } ``` -注意 `mod common;` 声明与示例 7-4 中展示的模块声明相同。接着在测试函数中就可以调用 `common::setup()` 了。 +注意 `mod common;` 声明与示例 7-25 中展示的模块声明相同。接着在测试函数中就可以调用 `common::setup()` 了。 #### 二进制 crate 的集成测试 diff --git a/src/ch12-00-an-io-project.md b/src/ch12-00-an-io-project.md index bd640b6..df607a3 100644 --- a/src/ch12-00-an-io-project.md +++ b/src/ch12-00-an-io-project.md @@ -1,12 +1,12 @@ # 一个 I/O 项目:构建一个命令行程序 -> [ch12-00-an-io-project.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-00-an-io-project.md) +> [ch12-00-an-io-project.md](https://github.com/rust-lang/book/blob/master/src/ch12-00-an-io-project.md) >
-> commit 97e60b3cb623d4a5b85419212b085ade8a11cbe1 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 本章既是一个目前所学的很多技能的概括,也是一个更多标准库功能的探索。我们将构建一个与文件和命令行输入/输出交互的命令行工具来练习现在一些你已经掌握的 Rust 技能。 -Rust 的运行速度、安全性、**单二进制文件** 输出和跨平台支持使其成为创建命令行程序的绝佳选择,所以我们的项目将创建一个我们自己版本的经典命令行工具:`grep`。grep 是 “**G**lobally search a **R**egular **E**xpression and **P**rint.” 的首字母缩写。`grep` 最简单的使用场景是在特定文件中搜索指定字符串。为此,`grep` 获取一个文件名和一个字符串作为参数,接着读取文件并找到其中包含字符串参数的行,然后打印出这些行。 +Rust 的运行速度、安全性、单二进制文件输出和跨平台支持使其成为创建命令行程序的绝佳选择,所以我们的项目将创建一个我们自己版本的经典命令行工具:`grep`。grep 是 “**G**lobally search a **R**egular **E**xpression and **P**rint.” 的首字母缩写。`grep` 最简单的使用场景是在特定文件中搜索指定字符串。为此,`grep` 获取一个文件名和一个字符串作为参数,接着读取文件并找到其中包含字符串参数的行,然后打印出这些行。 在这个过程中,我们会展示如何让我们的命令行工具利用很多命令行工具中用到的终端功能。读取环境变量来使得用户可以配置工具的行为。打印到标准错误控制流(`stderr`) 而不是标准输出(`stdout`),例如这样用户可以选择将成功输出重定向到文件中的同时仍然在屏幕上显示错误信息。 diff --git a/src/ch12-01-accepting-command-line-arguments.md b/src/ch12-01-accepting-command-line-arguments.md index f8b20ab..35c1ca6 100644 --- a/src/ch12-01-accepting-command-line-arguments.md +++ b/src/ch12-01-accepting-command-line-arguments.md @@ -1,13 +1,13 @@ ## 接受命令行参数 -> [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) +> [ch12-01-accepting-command-line-arguments.md](https://github.com/rust-lang/book/blob/master/src/ch12-01-accepting-command-line-arguments.md) >
-> commit 97e60b3cb623d4a5b85419212b085ade8a11cbe1 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -一如之前使用 `cargo new` 新建一个项目,我们称之为 `minigrep` 以便与可能已经安装在系统上的`grep`工具相区别: +一如既往使用 `cargo new` 新建一个项目,我们称之为 `minigrep` 以便与可能已经安装在系统上的 `grep` 工具相区别: ```text -$ cargo new --bin minigrep +$ cargo new minigrep Created binary (application) `minigrep` project $ cd minigrep ``` @@ -37,7 +37,6 @@ fn main() { } ``` - 示例 12-1:将命令行参数收集到一个 vector 中并打印出来 首先使用 `use` 语句来将 `std::env` 模块引入作用域以便可以使用它的 `args` 函数。注意 `std::env::args` 函数被嵌套进了两层模块中。正如第七章讲到的,当所需函数嵌套了多于一层模块时,通常将父模块引入作用域,而不是其自身。这便于我们利用 `std::env` 中的其他函数。这比增加了 `use std::env::args;` 后仅仅使用 `args` 调用函数要更明确一些,因为 `args` 容易被错认成一个定义于当前模块的函数。 diff --git a/src/ch12-02-reading-a-file.md b/src/ch12-02-reading-a-file.md index 7eba5e5..d39a93e 100644 --- a/src/ch12-02-reading-a-file.md +++ b/src/ch12-02-reading-a-file.md @@ -1,18 +1,18 @@ ## 读取文件 -> [ch12-02-reading-a-file.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-02-reading-a-file.md) +> [ch12-02-reading-a-file.md](https://github.com/rust-lang/book/blob/master/src/ch12-02-reading-a-file.md) >
-> commit 97e60b3cb623d4a5b85419212b085ade8a11cbe1 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -接下来我们将读取由命令行文件名参数指定的文件。首先,需要一个用来测试的示例文件——用来确保 `minigrep` 正常工作的最好的文件是拥有多行少量文本且有一些重复单词的文件。示例 12-3 是一首艾米莉·狄金森(Emily Dickinson)的诗,它正适合这个工作!在项目根目录创建一个文件 `poem.txt`,并输入诗 "I'm nobody! Who are you?": +现在我们要增加读取由 `filename` 命令行参数指定的文件的功能。首先,需要一个用来测试的示例文件:用来确保 `minigrep` 正常工作的最好的文件是拥有多行少量文本且有一些重复单词的文件。示例 12-3 是一首艾米莉·狄金森(Emily Dickinson)的诗,它正适合这个工作!在项目根目录创建一个文件 `poem.txt`,并输入诗 "I'm nobody! Who are you?": 文件名: poem.txt ```text -I’m nobody! Who are you? +I'm nobody! Who are you? Are you nobody, too? -Then there’s a pair of us — don’t tell! -They’d banish us, you know. +Then there's a pair of us - don't tell! +They'd banish us, you know. How dreary to be somebody! How public, like a frog @@ -28,8 +28,7 @@ To an admiring bog! ```rust,should_panic use std::env; -use std::fs::File; -use std::io::prelude::*; +use std::fs; fn main() { # let args: Vec = env::args().collect(); @@ -41,11 +40,8 @@ fn main() { // --snip-- println!("In file {}", filename); - let mut f = File::open(filename).expect("file not found"); - - let mut contents = String::new(); - f.read_to_string(&mut contents) - .expect("something went wrong reading the file"); + let contents = fs::read_to_string(filename) + .expect("Something went wrong reading the file"); println!("With text:\n{}", contents); } @@ -53,9 +49,9 @@ fn main() { 示例 12-4:读取第二个参数所指定的文件内容 -首先,我们增加了更多的 `use` 语句来引入标准库中的相关部分:需要 `std::fs::File` 来处理文件,而 `std::io::prelude::*` 则包含许多对于 I/O(包括文件 I/O)有帮助的 trait。类似于 Rust 有一个通用的 prelude 来自动引入特定内容,`std::io` 也有其自己的 prelude 来引入处理 I/O 时所需的通用内容。不同于默认的 prelude,必须显式 `use` 位于 `std::io` 中的 prelude。 +首先,我们增加了更多的 `use` 语句来引入标准库中的相关部分:需要 `std::fs` 来处理文件。 -在 `main` 中,我们增加了三点内容:第一,通过传递变量 `filename` 的值调用 `File::open` 函数来获取文件的可变句柄。创建了叫做 `contents` 的变量并将其设置为一个可变的,空的 `String`。它将会存放之后读取的文件的内容。第三,对文件句柄调用 `read_to_string` 并传递 `contents` 的可变引用作为参数。 +在 `main` 中新增了一行语句:`fs::read_to_string` 接受 `filename`,打开文件,接着返回包含其内容的 `Result`。 在这些代码之后,我们再次增加了临时的 `println!` 打印出读取文件后 `contents` 的值,这样就可以检查目前为止的程序能否工作。 diff --git a/src/ch12-03-improving-error-handling-and-modularity.md b/src/ch12-03-improving-error-handling-and-modularity.md index bffb8d9..873f38c 100644 --- a/src/ch12-03-improving-error-handling-and-modularity.md +++ b/src/ch12-03-improving-error-handling-and-modularity.md @@ -1,8 +1,8 @@ ## 重构改进模块性和错误处理 -> [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) +> [ch12-03-improving-error-handling-and-modularity.md](https://github.com/rust-lang/book/blob/master/src/ch12-03-improving-error-handling-and-modularity.md) >
-> commit c1fb695e6c9091c9a5145320498ef80a649af33c +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 为了改善我们的程序这里有四个问题需要修复,而且他们都与程序的组织方式和如何处理潜在错误有关。 @@ -12,7 +12,7 @@ 第三个问题是如果打开文件失败我们使用 `expect` 来打印出错误信息,不过这个错误信息只是说 `file not found`。除了缺少文件之外还有很多打开文件可能失败的方式:例如,文件可能存在,不过可能没有打开它的权限。如果我们现在就出于这种情况,打印出的 `file not found` 错误信息就给了用户错误的建议! -第四,我们不停的使用 `expect` 来处理不同的错误,如果用户没有指定足够的参数来运行程序,他们会从 Rust 得到 "index out of bounds" 错误,而这并不能明确的解释问题。如果所有的错误处理都位于一处这样将来的维护者在需要修改错误处理逻辑时就只需要考虑这一处代码。将所有的错误处理都放在一处也有助于确保我们打印的错误信息对终端用户来说是有意义的。 +第四,我们不停的使用 `expect` 来处理不同的错误,如果用户没有指定足够的参数来运行程序,他们会从 Rust 得到 `index out of bounds` 错误,而这并不能明确的解释问题。如果所有的错误处理都位于一处这样将来的维护者在需要修改错误处理逻辑时就只需要考虑这一处代码。将所有的错误处理都放在一处也有助于确保我们打印的错误信息对终端用户来说是有意义的。 让我们通过重构项目来解决这些问题。 @@ -24,6 +24,7 @@ 2. 当命令行解析逻辑比较小时,可以保留在 *main.rs* 中。 3. 当命令行解析开始变得复杂时,也同样将其从 *main.rs* 提取到 *lib.rs* 中。 4. 经过这些过程之后保留在 `main` 函数中的责任应该被限制为: + * 使用参数值调用命令行解析逻辑 * 设置任何其他的配置 * 调用 *lib.rs* 中的 `run` 函数 @@ -74,7 +75,7 @@ fn parse_config(args: &[String]) -> (&str, &str) { ```rust,should_panic # use std::env; -# use std::fs::File; +# use std::fs; # fn main() { let args: Vec = env::args().collect(); @@ -84,7 +85,8 @@ fn main() { println!("Searching for {}", config.query); println!("In file {}", config.filename); - let mut f = File::open(config.filename).expect("file not found"); + let contents = fs::read_to_string(config.filename) + .expect("Something went wrong reading the file"); // --snip-- } @@ -174,7 +176,7 @@ note: Run with `RUST_BACKTRACE=1` for a backtrace. #### 改善错误信息 -在示例 12-8 中,在 `new` 函数中增加了一个检查在访问索引 1 和 2 之前检查 slice 是否足够长。如果 slice 不够长,我们使用一个更好的错误信息 panic 而不是 `index out of bounds` 信息: +在示例 12-8 中,在 `new` 函数中增加了一个检查在访问索引 `1` 和 `2` 之前检查 slice 是否足够长。如果 slice 不够长,我们使用一个更好的错误信息 panic 而不是 `index out of bounds` 信息: 文件名: src/main.rs @@ -189,7 +191,7 @@ fn new(args: &[String]) -> Config { 示例 12-8:增加一个参数数量检查 -这类似于示例 9-9 中的 `Guess::new` 函数,那里如果 `value` 参数超出了有效值的范围就调用 `panic!`。不同于检查值的范围,这里检查 `args` 的长度至少是 3,而函数的剩余部分则可以在假设这个条件成立的基础上运行。如果 +这类似于示例 9-9 中的 `Guess::new` 函数,那里如果 `value` 参数超出了有效值的范围就调用 `panic!`。不同于检查值的范围,这里检查 `args` 的长度至少是 `3`,而函数的剩余部分则可以在假设这个条件成立的基础上运行。如果 `args` 少于 3 个项,则这个条件将为真,并调用 `panic!` 立即终止程序。 有了 `new` 中这几行额外的代码,再次不带任何参数运行程序并看看现在错误看起来像什么: @@ -203,13 +205,13 @@ thread 'main' panicked at 'not enough arguments', src/main.rs:30:12 note: Run with `RUST_BACKTRACE=1` for a backtrace. ``` -这个输出就好多了,现在有了一个合理的错误信息。然而,还是有一堆额外的信息我们不希望提供给用户。所以在这里使用示例 9-9 中的技术可能不是最好的;正如第九章所讲到的一样,`panic!` 的调用更趋向于程序上的问题而不是使用上的问题。相反我们可以使用第九章学习的另一个技术:返回一个可以表明成功或错误的 `Result`。 +这个输出就好多了,现在有了一个合理的错误信息。然而,还是有一堆额外的信息我们不希望提供给用户。所以在这里使用示例 9-9 中的技术可能不是最好的;正如第九章所讲到的一样,`panic!` 的调用更趋向于程序上的问题而不是使用上的问题。相反我们可以使用第九章学习的另一个技术 —— 返回一个可以表明成功或错误的 `Result`。 #### 从 `new` 中返回 `Result` 而不是调用 `panic!` 我们可以选择返回一个 `Result` 值,它在成功时会包含一个 `Config` 的实例,而在错误时会描述问题。当 `Config::new` 与 `main` 交流时,可以使用 `Result` 类型来表明这里存在问题。接着修改 `main` 将 `Err` 成员转换为对用户更友好的错误,而不是 `panic!` 调用产生的关于 `thread 'main'` 和 `RUST_BACKTRACE` 的文本。 -示例 12-9 展示了为了返回 `Result` 在 `Config::new` 的返回值和函数体中所需的改变: +示例 12-9 展示了为了返回 `Result` 在 `Config::new` 的返回值和函数体中所需的改变。注意这还不能编译,直到下一个示例同时也更新了 `main` 之后。 文件名: src/main.rs @@ -291,10 +293,7 @@ fn main() { } fn run(config: Config) { - let mut f = File::open(config.filename).expect("file not found"); - - let mut contents = String::new(); - f.read_to_string(&mut contents) + let contents = fs::read_to_string(config.filename) .expect("something went wrong reading the file"); println!("With text:\n{}", contents); @@ -318,11 +317,8 @@ use std::error::Error; // --snip-- -fn run(config: Config) -> Result<(), Box> { - let mut f = File::open(config.filename)?; - - let mut contents = String::new(); - f.read_to_string(&mut contents)?; +fn run(config: Config) -> Result<(), Box> { + let contents = fs::read_to_string(config.filename)?; println!("With text:\n{}", contents); @@ -334,9 +330,9 @@ fn run(config: Config) -> Result<(), Box> { 这里我们做出了三个明显的修改。首先,将 `run` 函数的返回类型变为 `Result<(), Box>`。之前这个函数返回 unit 类型 `()`,现在它仍然保持作为 `Ok` 时的返回值。 -对于错误类型,使用了 **trait 对象** `Box`(在开头使用了 `use` 语句将 `std::error::Error` 引入作用域)。第十七章会涉及 trait 对象。目前只需知道 `Box` 意味着函数会返回实现了 `Error` trait 的类型,不过无需指定具体将会返回的值的类型。这提供了在不同的错误场景可能有不同类型的错误返回值的灵活性。 +对于错误类型,使用了 **trait 对象** `Box`(在开头使用了 `use` 语句将 `std::error::Error` 引入作用域)。第十七章会涉及 trait 对象。目前只需知道 `Box` 意味着函数会返回实现了 `Error` trait 的类型,不过无需指定具体将会返回的值的类型。这提供了在不同的错误场景可能有不同类型的错误返回值的灵活性。这也就是 `dyn`,它是 “动态”(“dynamic”)的缩写。 -第二个改变是去掉了 `expect` 调用并替换为第九章讲到的 `?`。不同于遇到错误就 `panic!`,这会从函数中返回错误值并让调用者来处理它。 +第二个改变是去掉了 `expect` 调用并替换为第九章讲到的 `?`。不同于遇到错误就 `panic!`,`?` 会从函数中返回错误值并让调用者来处理它。 第三个修改是现在成功时这个函数会返回一个 `Ok` 值。因为 `run` 函数签名中声明成功类型返回值是 `()`,这意味着需要将 unit 类型值包装进 `Ok` 值中。`Ok(())` 一开始看起来有点奇怪,不过这样使用 `()` 是表明我们调用 `run` 只是为了它的副作用的惯用方式;它并没有返回什么有意义的值。 @@ -351,7 +347,7 @@ warning: unused `std::result::Result` which must be used = note: #[warn(unused_must_use)] on by default ``` -Rust 提示我们的代码忽略了 `Result` 值,它可能表明这里存在一个错误。虽然我们没有检查这里是否有一个错误,而编译器提醒我们这里应该有一些错误处理代码!现在就让我们修正他们。 +Rust 提示我们的代码忽略了 `Result` 值,它可能表明这里存在一个错误。虽然我们没有检查这里是否有一个错误,而编译器提醒我们这里应该有一些错误处理代码!现在就让我们修正这个问题。 #### 处理 `main` 中 `run` 返回的错误 @@ -395,8 +391,7 @@ fn main() { ```rust,ignore use std::error::Error; -use std::fs::File; -use std::io::prelude::*; +use std::fs; pub struct Config { pub query: String, @@ -409,25 +404,24 @@ impl Config { } } -pub fn run(config: Config) -> Result<(), Box> { +pub fn run(config: Config) -> Result<(), Box> { // --snip-- } ``` 示例 12-13:将 `Config` 和 `run` 移动到 *src/lib.rs* -这里使用了公有的 `pub`:在 `Config`、其字段和其 `new` 方法,以及 `run` 函数上。现在我们有了一个拥有可以测试的公有 API 的库 crate 了。 +这里使用了公有的 `pub` 关键字:在 `Config`、其字段和其 `new` 方法,以及 `run` 函数上。现在我们有了一个拥有可以测试的公有 API 的库 crate 了。 现在需要在 *src/main.rs* 中将移动到 *src/lib.rs* 的代码引入二进制 crate 的作用域中,如示例 12-14 所示: Filename: src/main.rs ```rust,ignore -extern crate minigrep; - use std::env; use std::process; +use minigrep; use minigrep::Config; fn main() { diff --git a/src/ch12-04-testing-the-librarys-functionality.md b/src/ch12-04-testing-the-librarys-functionality.md index 903aaf5..ea9ab3c 100644 --- a/src/ch12-04-testing-the-librarys-functionality.md +++ b/src/ch12-04-testing-the-librarys-functionality.md @@ -1,8 +1,8 @@ ## 采用测试驱动开发完善库的功能 -> [ch12-04-testing-the-librarys-functionality.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-04-testing-the-librarys-functionality.md) +> [ch12-04-testing-the-librarys-functionality.md](https://github.com/rust-lang/book/blob/master/src/ch12-04-testing-the-librarys-functionality.md) >
-> commit 1fe78a83f37ecc69b840fdc8dcfc727f88a3a3d4 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 现在我们将逻辑提取到了 *src/lib.rs* 并将所有的参数解析和错误处理留在了 *src/main.rs* 中,为代码的核心功能编写测试将更加容易。我们可以直接使用多种参数调用函数并检查返回值而无需从命令行运行二进制文件了。如果你愿意的话,请自行为 `Config::new` 和 `run` 函数的功能编写一些测试。 @@ -29,7 +29,7 @@ # } # #[cfg(test)] -mod test { +mod tests { use super::*; #[test] @@ -50,14 +50,14 @@ Pick three."; 示例 12-15:创建一个我们期望的 `search` 函数的失败测试 -这里选择使用 "duct" 作为这个测试中需要搜索的字符串。用来搜索的文本有三行,其中只有一行包含 "duct"。我们断言 `search` 函数的返回值只包含期望的那一行。 +这里选择使用 `"duct"` 作为这个测试中需要搜索的字符串。用来搜索的文本有三行,其中只有一行包含 `"duct"`。我们断言 `search` 函数的返回值只包含期望的那一行。 -我们还不能运行这个测试并看到它失败,因为它甚至都还不能编译!我们将增加足够的代码来使其能够编译:一个总是会返回空 vector 的 `search` 函数定义,如示例 12-16 所示。然后这个测试应该能够编译并因为空 vector 并不匹配一个包含一行 `"safe, fast, productive."` 的 vector 而失败。 +我们还不能运行这个测试并看到它失败,因为它甚至都还不能编译:`search` 函数还不存在呢!我们将增加足够的代码来使其能够编译:一个总是会返回空 vector 的 `search` 函数定义,如示例 12-16 所示。然后这个测试应该能够编译并因为空 vector 并不匹配一个包含一行 `"safe, fast, productive."` 的 vector 而失败。 文件名: src/lib.rs ```rust -pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { +fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { vec![] } ``` @@ -74,7 +74,7 @@ pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { error[E0106]: missing lifetime specifier --> src/lib.rs:5:51 | -5 | pub fn search(query: &str, contents: &str) -> Vec<&str> { +5 | fn search(query: &str, contents: &str) -> Vec<&str> { | ^ expected lifetime parameter | @@ -84,7 +84,7 @@ parameter Rust 不可能知道我们需要的是哪一个参数,所以需要告诉它。因为参数 `contents` 包含了所有的文本而且我们希望返回匹配的那部分文本,所以我们知道 `contents` 是应该要使用生命周期语法来与返回值相关联的参数。 -其他语言中并不需要你在函数签名中将参数与返回值相关联,所以这么做可能仍然感觉有些陌生,随着时间的推移这将会变得越来越容易。你可能想要将这个例子与第十章中生命周期语法部分做对比。 +其他语言中并不需要你在函数签名中将参数与返回值相关联。所以这么做可能仍然感觉有些陌生,随着时间的推移这将会变得越来越容易。你可能想要将这个例子与第十章中生命 “生命周期与引用有效性” 部分做对比。 现在运行测试: @@ -96,12 +96,12 @@ $ cargo test Running target/debug/deps/minigrep-abcabcabc running 1 test -test test::one_result ... FAILED +test tests::one_result ... FAILED failures: ----- test::one_result stdout ---- - thread 'test::one_result' panicked at 'assertion failed: `(left == +---- tests::one_result stdout ---- + thread 'tests::one_result' panicked at 'assertion failed: `(left == right)` left: `["safe, fast, productive."]`, right: `[]`)', src/lib.rs:48:8 @@ -109,7 +109,7 @@ note: Run with `RUST_BACKTRACE=1` for a backtrace. failures: - test::one_result + tests::one_result test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out @@ -137,7 +137,7 @@ Rust 有一个有助于一行一行遍历字符串的方法,出于方便它被 文件名: src/lib.rs ```rust,ignore -pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { +fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { for line in contents.lines() { // do something with line } @@ -146,7 +146,7 @@ pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { 示例 12-17:遍历 `contents` 的每一行 -`lines` 方法返回一个迭代器。第十三章会深入了解迭代器,不过我们已经在示例 3-4 中见过使用迭代器的方法了,在那里使用了一个 `for` 循环和迭代器在一个集合的每一项上运行了一些代码。 +`lines` 方法返回一个迭代器。第十三章会深入了解迭代器,不过我们已经在示例 3-5 中见过使用迭代器的方法了,在那里使用了一个 `for` 循环和迭代器在一个集合的每一项上运行了一些代码。 #### 用查询字符串搜索每一行 @@ -155,7 +155,7 @@ pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { 文件名: src/lib.rs ```rust,ignore -pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { +fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { for line in contents.lines() { if line.contains(query) { // do something with line @@ -173,7 +173,7 @@ pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { 文件名: src/lib.rs ```rust,ignore -pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { +fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { let mut results = Vec::new(); for line in contents.lines() { @@ -194,7 +194,7 @@ pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { $ cargo test --snip-- running 1 test -test test::one_result ... ok +test tests::one_result ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ``` @@ -210,11 +210,8 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out 文件名: src/lib.rs ```rust,ignore -pub fn run(config: Config) -> Result<(), Box> { - let mut f = File::open(config.filename)?; - - let mut contents = String::new(); - f.read_to_string(&mut contents)?; +pub fn run(config: Config) -> Result<(), Box> { + let contents = fs::read_to_string(config.filename)?; for line in search(&config.query, &contents) { println!("{}", line); diff --git a/src/ch12-05-working-with-environment-variables.md b/src/ch12-05-working-with-environment-variables.md index 26283e9..40daf8a 100644 --- a/src/ch12-05-working-with-environment-variables.md +++ b/src/ch12-05-working-with-environment-variables.md @@ -1,8 +1,8 @@ ## 处理环境变量 -> [ch12-05-working-with-environment-variables.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-05-working-with-environment-variables.md) +> [ch12-05-working-with-environment-variables.md](https://github.com/rust-lang/book/blob/master/src/ch12-05-working-with-environment-variables.md) >
-> commit 1fe78a83f37ecc69b840fdc8dcfc727f88a3a3d4 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 我们将增加一个额外的功能来改进 `minigrep`:一个通过环境变量启用的大小写不敏感搜索的选项。可以将其设计为一个命令行参数并要求用户每次需要时都加上它,不过相反我们将使用环境变量。这允许用户设置环境变量一次之后在整个终端会话中所有的搜索都将是大小写不敏感的。 @@ -14,7 +14,7 @@ ```rust #[cfg(test)] -mod test { +mod tests { use super::*; #[test] @@ -51,9 +51,9 @@ Trust me."; 示例 12-20:为准备添加的大小写不敏感函数新增失败测试 -注意我们也改变了老测试中 `contents` 的值。还新增了一个含有文本 "Duct tape" 的行,它有一个大写的 D,这在大小写敏感搜索时不应该匹配 "duct"。我们修改这个测试以确保不会意外破坏已经实现的大小写敏感搜索功能;这个测试现在应该能通过并在处理大小写不敏感搜索时应该能一直通过。 +注意我们也改变了老测试中 `contents` 的值。还新增了一个含有文本 `"Duct tape."` 的行,它有一个大写的 D,这在大小写敏感搜索时不应该匹配 "duct"。我们修改这个测试以确保不会意外破坏已经实现的大小写敏感搜索功能;这个测试现在应该能通过并在处理大小写不敏感搜索时应该能一直通过。 -大小写 **不敏感** 搜索的新测试使用 "rUsT" 作为其查询字符串。在我们将要增加的 `search_case_insensitive` 函数中,“rUsT” 查询应该包含 “Rust:” 包含一个大写的 R 还有 “Trust me.” 这两行,即便他们与查询的大小写都不同。这个测试现在会编译失败因为还没有定义 `search_case_insensitive` 函数。请随意增加一个总是返回空 vector 的骨架实现,正如示例 12-16 中 `search` 函数为了使测试编译并失败时所做的那样。 +大小写 **不敏感** 搜索的新测试使用 `"rUsT"` 作为其查询字符串。在我们将要增加的 `search_case_insensitive` 函数中,`"rUsT"` 查询应该包含带有一个大写 R 的 `"Rust:"` 还有 `"Trust me."` 这两行,即便他们与查询的大小写都不同。这个测试现在会编译失败因为还没有定义 `search_case_insensitive` 函数。请随意增加一个总是返回空 vector 的骨架实现,正如示例 12-16 中 `search` 函数为了使测试编译并失败时所做的那样。 ### 实现 `search_case_insensitive` 函数 @@ -78,9 +78,9 @@ fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { 示例 12-21:定义 `search_case_insensitive` 函数,它在比较查询和每一行之前将他们都转换为小写 -首先我们将 `query` 字符串转换为小写,并将其覆盖到同名的变量中。对查询字符串调用 `to_lowercase` 是必需的,这样不管用户的查询是 “rust”、“RUST”、“Rust” 或者 “rUsT”,我们都将其当作 “rust” 处理并对大小写不敏感。 +首先我们将 `query` 字符串转换为小写,并将其覆盖到同名的变量中。对查询字符串调用 `to_lowercase` 是必需的,这样不管用户的查询是 `"rust"`、`"RUST"`、`"Rust"` 或者 `"rUsT"`,我们都将其当作 `"rust"` 处理并对大小写不敏感。 -注意 `query` 现在是一个 `String` 而不是字符串 slice,因为调用 `to_lowercase` 是在创建新数据,而不是引用现有数据。如果查询字符串是 “rUsT”,这个字符串 slice 并不包含可供我们使用的小写的 “u” 或 “t”,所以必需分配一个包含 “rust” 的新 `String`。现在当我们将 `query` 作为一个参数传递给 `contains` 方法时,需要增加一个 & 因为 `contains` 的签名被定义为获取一个字符串 slice。 +注意 `query` 现在是一个 `String` 而不是字符串 slice,因为调用 `to_lowercase` 是在创建新数据,而不是引用现有数据。如果查询字符串是 `"rUsT"`,这个字符串 slice 并不包含可供我们使用的小写的 `u` 或 `t`,所以必需分配一个包含 `"rust"` 的新 `String`。现在当我们将 `query` 作为一个参数传递给 `contains` 方法时,需要增加一个 & 因为 `contains` 的签名被定义为获取一个字符串 slice。 接下来在检查每个 `line` 是否包含 `search` 之前增加了一个 `to_lowercase` 调用将他们都变为小写。现在我们将 `line` 和 `query` 都转换成了小写,这样就可以不管查询的大小写进行匹配了。 @@ -88,13 +88,13 @@ fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { ```text running 2 tests -test test::case_insensitive ... ok -test test::case_sensitive ... ok +test tests::case_insensitive ... ok +test tests::case_sensitive ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ``` -好的!现在,让我们在 `run` 函数中实际调用新 `search_case_insensitive` 函数。首先,我们将在 `Config` 结构体中增加一个配置项来切换大小写敏感和大小写不敏感搜索: +好的!现在,让我们在 `run` 函数中实际调用新 `search_case_insensitive` 函数。首先,我们将在 `Config` 结构体中增加一个配置项来切换大小写敏感和大小写不敏感搜索。增加这些字段会导致编译错误,因为我们还没有在任何地方初始化这些字段: 文件名: src/lib.rs @@ -112,7 +112,7 @@ pub struct Config { ```rust # use std::error::Error; -# use std::fs::File; +# use std::fs::{self, File}; # use std::io::prelude::*; # # fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { @@ -123,17 +123,14 @@ pub struct Config { # vec![] # } # -# struct Config { +# pub struct Config { # query: String, # filename: String, # case_sensitive: bool, # } # -pub fn run(config: Config) -> Result<(), Box> { - let mut f = File::open(config.filename)?; - - let mut contents = String::new(); - f.read_to_string(&mut contents)?; +pub fn run(config: Config) -> Result<(), Box> { + let contents = fs::read_to_string(config.filename)?; let results = if config.case_sensitive { search(&config.query, &contents) @@ -189,7 +186,7 @@ impl Config { 我们将变量 `case_sensitive` 的值传递给 `Config` 实例,这样 `run` 函数可以读取其值并决定是否调用 `search` 或者示例 12-22 中实现的 `search_case_insensitive`。 -让我们试一试吧!首先不设置环境变量并使用查询 “to” 运行程序,这应该会匹配任何全小写的单词 “to” 的行: +让我们试一试吧!首先不设置环境变量并使用查询 `to` 运行程序,这应该会匹配任何全小写的单词 “to” 的行: ```text $ cargo run to poem.txt @@ -200,7 +197,16 @@ Are you nobody, too? How dreary to be somebody! ``` -看起来程序仍然能够工作!现在将 `CASE_INSENSITIVE` 设置为 1 并仍使用相同的查询 “to”,这回应该得到包含可能有大写字母的 “to” 的行: +看起来程序仍然能够工作!现在将 `CASE_INSENSITIVE` 设置为 `1` 并仍使用相同的查询 `to`。 + +如果你使用 PowerShell,则需要用两句命令而不是一句来设置环境变量并运行程序: + +```text +$ $env:CASE_INSENSITIVE=1 +$ cargo run to poem.txt +``` + +这回应该得到包含可能有大写字母的 “to” 的行: ```text $ CASE_INSENSITIVE=1 cargo run to poem.txt @@ -212,13 +218,6 @@ To tell your name the livelong day To an admiring bog! ``` -如果你使用 PowerShell,则需要用两句命令而不是一句来设置环境变量并运行程序: - -```text -$ $env:CASE_INSENSITIVE=1 -$ cargo run to poem.txt -``` - 好极了,我们也得到了包含 “To” 的行!现在 `minigrep` 程序可以通过环境变量控制进行大小写不敏感搜索了。现在你知道了如何管理由命令行参数或环境变量设置的选项了! 一些程序允许对相同配置同时使用参数 **和** 环境变量。在这种情况下,程序来决定参数和环境变量的优先级。作为一个留给你的测试,尝试通过一个命令行参数或一个环境变量来控制大小写不敏感搜索。并在运行程序时遇到矛盾值时决定命令行参数和环境变量的优先级。 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 3decb28..3481d95 100644 --- a/src/ch12-06-writing-to-stderr-instead-of-stdout.md +++ b/src/ch12-06-writing-to-stderr-instead-of-stdout.md @@ -1,8 +1,8 @@ ## 将错误信息输出到标准错误而不是标准输出 -> [ch12-06-writing-to-stderr-instead-of-stdout.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-06-writing-to-stderr-instead-of-stdout.md) +> [ch12-06-writing-to-stderr-instead-of-stdout.md](https://github.com/rust-lang/book/blob/master/src/ch12-06-writing-to-stderr-instead-of-stdout.md) >
-> commit 1fe78a83f37ecc69b840fdc8dcfc727f88a3a3d4 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 目前为止,我们将所有的输出都 `println!` 到了终端。大部分终端都提供了两种输出:**标准输出**(*standard output*,`stdout`)对应通用信息,**标准错误**(*standard error*,`stderr`)则用于错误信息。这种区别允许用户选择将程序正常输出定向到一个文件中并仍将错误信息打印到屏幕上。 @@ -60,7 +60,7 @@ $ cargo run > output.txt Problem parsing arguments: not enough arguments ``` -现在我们看到了屏幕上的错误信息,同时 `output.txt` 里什么也没有,这正是命令行程序所期望的行为。 +现在我们看到了屏幕上的错误信息,同时 *output.txt* 里什么也没有,这正是命令行程序所期望的行为。 如果使用不会造成错误的参数再次运行程序,不过仍然将标准输出重定向到一个文件,像这样: From 2915a218b8317622f3659ef8a5ae84d32228f853 Mon Sep 17 00:00:00 2001 From: KaiserY Date: Thu, 6 Dec 2018 21:49:12 +0800 Subject: [PATCH 21/49] Update README.md --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b02a942..c2d7540 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,13 @@ ## 状态 -还在施工中。大部分章节已经可以阅读。具体状态请参见官方 [projects](https://github.com/rust-lang/book/projects/1),`Frozen` 之后的内容应该较为稳定。 +目前正向 2018 edtion 过渡,目前官方仓库状已经不再分版本提供源码了,故本仓库准备与官方保持一致。 -每章翻译开头都带有官方链接和 commit hash,若发现与官方不一致,欢迎 Issue 或 PR :) +PS: + +* 对照源码位置:https://github.com/rust-lang/book/tree/master/src +* 官方仓库用于阅读的网页仍是之前的版本,与新版区别不大,最大的区别在于 2018 edtion 对模块功能做出较大修改,第七章基本重写。 +* 每章翻译开头都带有官方链接和 commit hash,若发现与官方不一致,欢迎 Issue 或 PR :) ## 静态页面构建与文档撰写 @@ -49,4 +53,4 @@ vuepress dev ./src 本翻译加速查看站点[上海站点http://rustdoc.saigao.fun](http://rustdoc.saigao.fun) -[GitBook.com](https://www.gitbook.com/) 地址:[https://www.gitbook.com/book/kaisery/trpl-zh-cn/details](https://www.gitbook.com/book/kaisery/trpl-zh-cn/details) +[GitBook.com](https://www.gitbook.com/) 地址:[https://legacy.gitbook.com/book/kaisery/trpl-zh-cn/details](https://legacy.gitbook.com/book/kaisery/trpl-zh-cn/details) From b318e03625f7c6247c7b1ebbed55881eb0eb3076 Mon Sep 17 00:00:00 2001 From: yl2014 <842237798@qq.com> Date: Thu, 6 Dec 2018 22:19:42 +0800 Subject: [PATCH 22/49] =?UTF-8?q?fix(ch07-02):=20=E9=94=99=E5=88=AB?= =?UTF-8?q?=E5=AD=97&=E8=AF=AD=E5=8F=A5=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ch07-02-modules-and-use-to-control-scope-and-privacy.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ch07-02-modules-and-use-to-control-scope-and-privacy.md b/src/ch07-02-modules-and-use-to-control-scope-and-privacy.md index 8c9efae..6e0dff4 100644 --- a/src/ch07-02-modules-and-use-to-control-scope-and-privacy.md +++ b/src/ch07-02-modules-and-use-to-control-scope-and-privacy.md @@ -367,11 +367,11 @@ fn main() { } ``` -示例 7-13: 使用 `use` 将模块引入作用域并使用绝对路ing来缩短在模块中调用项所必须的路径 +示例 7-13: 使用 `use` 将模块引入作用域并使用绝对路径来缩短在模块中调用项所必须的路径 当指定 `use` 后以 `self` 开头的相对路径在未来可能不是必须的;这是一个开发者正在尽力消除的语言中的不一致。 -选择使用 `use` 和指定绝对路径,可以使得当调用项的代码移动到模块树的不同位置,不过定义的代码没有移动的情况的代码更新变得更为轻松,这与示例 7-10 中同时移动的情况相对。例如,如果我们决定采用示例 7-13 的代码,将 `main` 函数的行为提取到函数 `clarinet_trio` 中,并将该函数移动到模块 `performance_group` 中,这时 `use` 所指定的路径无需变化,如示例 7-15 所示。 +如果调用项目的代码移动到模块树的不同位置但是定义项目的代码却没有,那么选择使用 `use` 指定绝对路径可以使更新更轻松。,这与示例 7-10 中同时移动的情况相对。例如,如果我们决定采用示例 7-13 的代码,将 `main` 函数的行为提取到函数 `clarinet_trio` 中,并将该函数移动到模块 `performance_group` 中,这时 `use` 所指定的路径无需变化,如示例 7-15 所示。 文件名: src/main.rs From a5e82bc6d8c3584bc833863e8719be16e7f3c14b Mon Sep 17 00:00:00 2001 From: yl2014 <842237798@qq.com> Date: Thu, 6 Dec 2018 22:22:17 +0800 Subject: [PATCH 23/49] =?UTF-8?q?fix(ch07-02):=20=E5=8E=BB=E6=8E=89?= =?UTF-8?q?=E5=A4=9A=E4=BD=99=E6=A0=87=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ch07-02-modules-and-use-to-control-scope-and-privacy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ch07-02-modules-and-use-to-control-scope-and-privacy.md b/src/ch07-02-modules-and-use-to-control-scope-and-privacy.md index 6e0dff4..c3a3437 100644 --- a/src/ch07-02-modules-and-use-to-control-scope-and-privacy.md +++ b/src/ch07-02-modules-and-use-to-control-scope-and-privacy.md @@ -371,7 +371,7 @@ fn main() { 当指定 `use` 后以 `self` 开头的相对路径在未来可能不是必须的;这是一个开发者正在尽力消除的语言中的不一致。 -如果调用项目的代码移动到模块树的不同位置但是定义项目的代码却没有,那么选择使用 `use` 指定绝对路径可以使更新更轻松。,这与示例 7-10 中同时移动的情况相对。例如,如果我们决定采用示例 7-13 的代码,将 `main` 函数的行为提取到函数 `clarinet_trio` 中,并将该函数移动到模块 `performance_group` 中,这时 `use` 所指定的路径无需变化,如示例 7-15 所示。 +如果调用项目的代码移动到模块树的不同位置但是定义项目的代码却没有,那么选择使用 `use` 指定绝对路径可以使更新更轻松,这与示例 7-10 中同时移动的情况相对。例如,如果我们决定采用示例 7-13 的代码,将 `main` 函数的行为提取到函数 `clarinet_trio` 中,并将该函数移动到模块 `performance_group` 中,这时 `use` 所指定的路径无需变化,如示例 7-15 所示。 文件名: src/main.rs From 820474537f54dd9608620ee93b4df90734833204 Mon Sep 17 00:00:00 2001 From: KaiserY Date: Thu, 6 Dec 2018 22:53:14 +0800 Subject: [PATCH 24/49] ferris technology --- .travis.yml | 2 +- book.toml | 4 ++++ ferris.css | 33 +++++++++++++++++++++++++++++++++ ferris.js | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 ferris.css create mode 100644 ferris.js diff --git a/.travis.yml b/.travis.yml index 1ea0074..9f7930e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ dist: trusty language: rust cache: cargo rust: - - nightly + - beta # Change this to stable when Rust 1.31.0 is out branches: only: - master diff --git a/book.toml b/book.toml index f34accd..f3bcb0e 100644 --- a/book.toml +++ b/book.toml @@ -5,3 +5,7 @@ description = "Rust 程序设计语言 简体中文版" [build-dir] destination = "mdbook" + +[output.html] +additional-css = ["ferris.css"] +additional-js = ["ferris.js"] \ No newline at end of file diff --git a/ferris.css b/ferris.css new file mode 100644 index 0000000..b856d47 --- /dev/null +++ b/ferris.css @@ -0,0 +1,33 @@ +body.light .does_not_compile, +body.light .panics, +body.light .not_desired_behavior, +body.rust .does_not_compile, +body.rust .panics, +body.rust .not_desired_behavior { + background: #fff1f1; +} + +body.coal .does_not_compile, +body.coal .panics, +body.coal .not_desired_behavior, +body.navy .does_not_compile, +body.navy .panics, +body.navy .not_desired_behavior, +body.ayu .does_not_compile, +body.ayu .panics, +body.ayu .not_desired_behavior { + background: #501f21; +} + +.ferris { + position: absolute; + z-index: 99; + right: 5px; + top: 30px; + width: 10%; + height: auto; +} + +.ferris-explain { + width: 100px; +} \ No newline at end of file diff --git a/ferris.js b/ferris.js new file mode 100644 index 0000000..5d014f3 --- /dev/null +++ b/ferris.js @@ -0,0 +1,51 @@ +var ferrisTypes = [ + { + attr: 'does_not_compile', + title: 'This code does not compile!' + }, + { + attr: 'panics', + title: 'This code panics!' + }, + { + attr: 'unsafe', + title: 'This code block contains unsafe code.' + }, + { + attr: 'not_desired_behavior', + title: 'This code does not produce the desired behavior.' + } + ] + + document.addEventListener('DOMContentLoaded', () => { + for (var ferrisType of ferrisTypes) { + attachFerrises(ferrisType) + } + }) + + function attachFerrises (type) { + var elements = document.getElementsByClassName(type.attr) + + for (var codeBlock of elements) { + var lines = codeBlock.textContent.split(/\r|\r\n|\n/).length - 1; + + if (lines >= 4) { + attachFerris(codeBlock, type) + } + } + } + + function attachFerris (element, type) { + var a = document.createElement('a') + a.setAttribute('href', 'ch00-00-introduction.html#ferris') + a.setAttribute('target', '_blank') + + var img = document.createElement('img') + img.setAttribute('src', 'img/ferris/' + type.attr + '.svg') + img.setAttribute('title', type.title) + img.className = 'ferris' + + a.appendChild(img) + + element.parentElement.insertBefore(a, element) + } \ No newline at end of file From d11ba9796e682c083962bfddf28cff7e48e77290 Mon Sep 17 00:00:00 2001 From: KaiserY Date: Thu, 6 Dec 2018 23:31:27 +0800 Subject: [PATCH 25/49] check to ch13-04 --- src/ch13-00-functional-features.md | 10 ++++++---- src/ch13-01-closures.md | 24 ++++++++++++------------ src/ch13-02-iterators.md | 25 +++++++++++++------------ src/ch13-03-improving-our-io-project.md | 21 ++++++++++----------- src/ch13-04-performance.md | 4 ++-- 5 files changed, 43 insertions(+), 41 deletions(-) diff --git a/src/ch13-00-functional-features.md b/src/ch13-00-functional-features.md index 6de9f6e..e9ae891 100644 --- a/src/ch13-00-functional-features.md +++ b/src/ch13-00-functional-features.md @@ -1,16 +1,18 @@ # Rust 中的函数式语言功能:迭代器与闭包 -> [ch13-00-functional-features.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch13-00-functional-features.md) +> [ch13-00-functional-features.md](https://github.com/rust-lang/book/blob/master/src/ch13-00-functional-features.md) >
-> commit 2bcb126815a381acc3d46b0d6fc382cb4c98fbc5 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -Rust 的设计灵感来源于很多现存的语言和技术。其中一个显著的影响就是 **函数式编程**(*functional programming*)。函数式编程风格通常包含将函数作为参数值或其他函数的返回值、将函数赋值给变量以供之后执行等等。我们不会在这里讨论函数式编程是或不是什么的问题,而是展示 Rust 的一些在功能上与其他被认为是函数式语言类似的特性。 +Rust 的设计灵感来源于很多现存的语言和技术。其中一个显著的影响就是 **函数式编程**(*functional programming*)。函数式编程风格通常包含将函数作为参数值或其他函数的返回值、将函数赋值给变量以供之后执行等等。 + +本章我们不会讨论函数式编程是或不是什么的问题,而是展示 Rust 的一些在功能上与其他被认为是函数式语言类似的特性。 更具体的,我们将要涉及: * **闭包**(*Closures*),一个可以储存在变量里的类似函数的结构 * **迭代器**(*Iterators*),一种处理元素序列的方式 * 如何使用这些功能来改进第十二章的 I/O 项目。 -* 这两个功能的性能。(**剧透高能:** 他们的速度超乎你的想象!) +* 这两个功能的性能。(**剧透警告:** 他们的速度超乎你的想象!) 还有其它受函数式风格影响的 Rust 功能,比如模式匹配和枚举,这些已经在其他章节中讲到过了。掌握闭包和迭代器则是编写符合语言风格的高性能 Rust 代码的重要一环,所以我们将专门用一整章来讲解他们。 diff --git a/src/ch13-01-closures.md b/src/ch13-01-closures.md index 712c300..1914347 100644 --- a/src/ch13-01-closures.md +++ b/src/ch13-01-closures.md @@ -1,8 +1,8 @@ ## 闭包:可以捕获环境的匿名函数 -> [ch13-01-closures.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch13-01-closures.md) +> [ch13-01-closures.md](https://github.com/rust-lang/book/blob/master/src/ch13-01-closures.md) >
-> commit bdcba470e4a71d16242b1727bbf99b300c194c46 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f Rust 的 **闭包**(*closures*)是可以保存进变量或作为参数传递给其他函数的匿名函数。可以在一个地方创建闭包,然后在不同的上下文中执行闭包运算。不同于函数,闭包允许捕获调用者作用域中的值。我们将展示闭包的这些功能如何复用代码和自定义行为。 @@ -27,14 +27,14 @@ fn simulated_expensive_calculation(intensity: u32) -> u32 { } ``` -示例 13-1:一个用来代替假象计算的函数,它大约会执行两秒钟 +示例 13-1:一个用来代替假想计算的函数,它大约会执行两秒钟 接下来,`main` 函数中将会包含本例的健身 app 中的重要部分。这代表当用户请求健身计划时 app 会调用的代码。因为与 app 前端的交互与闭包的使用并不相关,所以我们将硬编码代表程序输入的值并打印输出。 所需的输入有这些: -* **一个来自用户的 intensity 数字**,请求健身计划时指定,它代表用户喜好低强度还是高强度健身。 -* **一个随机数**,其会在健身计划中生成变化。 +* 一个来自用户的 intensity 数字,请求健身计划时指定,它代表用户喜好低强度还是高强度健身。 +* 一个随机数,其会在健身计划中生成变化。 程序的输出将会是建议的锻炼计划。示例 13-2 展示了我们将要使用的 `main` 函数: @@ -178,7 +178,7 @@ let expensive_closure = |num| { 闭包定义是 `expensive_closure` 赋值的 `=` 之后的部分。闭包的定义以一对竖线(`|`)开始,在竖线中指定闭包的参数;之所以选择这个语法是因为它与 Smalltalk 和 Ruby 的闭包定义类似。这个闭包有一个参数 `num`;如果有多于一个参数,可以使用逗号分隔,比如 `|param1, param2|`。 -参数之后是存放闭包体的大括号 ———— 如果闭包体只有一行则大括号是可以省略的。大括号之后闭包的结尾,需要用于 `let` 语句的分号。闭包体的最后一行(`num`)返回的值将是调用闭包时返回的值,因为最后一行没有分号;正如函数体中的一样。 +参数之后是存放闭包体的大括号 —— 如果闭包体只有一行则大括号是可以省略的。大括号之后闭包的结尾,需要用于 `let` 语句的分号。闭包体的最后一行(`num`)返回的值将是调用闭包时返回的值,因为最后一行没有分号;正如函数体中的一样。 注意这个 `let` 语句意味着 `expensive_closure` 包含一个匿名函数的 **定义**,不是调用匿名函数的 **返回值**。回忆一下使用闭包的原因是我们需要在一个位置定义代码,储存代码,并在之后的位置实际调用它;期望调用的代码现在储存在 `expensive_closure` 中。 @@ -223,7 +223,7 @@ fn generate_workout(intensity: u32, random_number: u32) { 现在耗时的计算只在一个地方被调用,并只会在需要结果的时候执行改代码。 -然而,我们又重新引入了示例 13-3 中的问题:仍然在第一个 `if` 块中调用了闭包两次,这会调用慢计算两次并使用户多等待一倍的时间。可以通过在 `if` 块中创建一个本地变量存放闭包调用的结果来解决这个问题,不过正因为使用了闭包还有另一个解决方案。稍后会回到这个方案上;首先讨论一下为何闭包定义中和所涉及的 trait 中没有类型注解。 +然而,我们又重新引入了示例 13-3 中的问题:仍然在第一个 `if` 块中调用了闭包两次,这会调用慢计算两次并使用户多等待一倍的时间。可以通过在 `if` 块中创建一个本地变量存放闭包调用的结果来解决这个问题,不过正因为使用了闭包还有另一个解决方案。稍后会回到这个方案上。首先讨论一下为何闭包定义中和所涉及的 trait 中没有类型注解。 ### 闭包类型推断和注解 @@ -265,7 +265,7 @@ let add_one_v4 = |x| x + 1 ; 文件名: src/main.rs -```rust,ignore +```rust,ignore,does_not_compile let example_closure = |x| x; let s = example_closure(String::from("hello")); @@ -446,7 +446,7 @@ fn generate_workout(intensity: u32, random_number: u32) { 第一个问题是 `Cacher` 实例假设对于 `value` 方法的任何 `arg` 参数值总是会返回相同的值。也就是说,这个 `Cacher` 的测试会失败: -```rust,ignore +```rust,ignore,panics #[test] fn call_with_different_values() { let mut c = Cacher::new(|a| a); @@ -472,7 +472,7 @@ thread 'call_with_different_values' panicked at 'assertion failed: `(left == rig 尝试修改 `Cacher` 存放一个哈希 map 而不是单独一个值。哈希 map 的 key 将是传递进来的 `arg` 值,而 value 则是对应 key 调用闭包的结果值。相比之前检查 `self.value` 直接是 `Some` 还是 `None` 值,现在 `value` 会在哈希 map 中寻找 `arg`,如果存在就返回它。如果不存在,`Cacher` 会调用闭包并将结果值保存在哈希 map 对应 `arg` 值的位置。 -当前 `Cacher` 实现的另一个问题是它的应用被限制为只接受获取一个 `u32` 值并返回一个 `u32` 值的闭包。比如说,我们可能需要能够缓存一个获取字符串 slice 并返回 `usize` 值的闭包的结果。请尝试引入更多泛型参数来增加 `Cacher` 功能的灵活性。 +当前 `Cacher` 实现的第二个问题是它的应用被限制为只接受获取一个 `u32` 值并返回一个 `u32` 值的闭包。比如说,我们可能需要能够缓存一个获取字符串 slice 并返回 `usize` 值的闭包的结果。请尝试引入更多泛型参数来增加 `Cacher` 功能的灵活性。 ### 闭包会捕获其环境 @@ -502,7 +502,7 @@ fn main() { 文件名: src/main.rs -```rust,ignore +```rust,ignore,does_not_compile fn main() { let x = 4; @@ -543,7 +543,7 @@ error[E0434]: can't capture dynamic environment in a fn item; use the || { ... 文件名: src/main.rs -```rust,ignore +```rust,ignore,does_not_compile fn main() { let x = vec![1, 2, 3]; diff --git a/src/ch13-02-iterators.md b/src/ch13-02-iterators.md index 7f6668c..9a18967 100644 --- a/src/ch13-02-iterators.md +++ b/src/ch13-02-iterators.md @@ -1,8 +1,8 @@ ## 使用迭代器处理元素序列 -> [ch13-02-iterators.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch13-02-iterators.md) +> [ch13-02-iterators.md](https://github.com/rust-lang/book/blob/master/src/ch13-02-iterators.md) >
-> commit ceb31210263d49994bbf09456a35a135da690f24 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 迭代器模式允许你对一个项的序列进行某些处理。**迭代器**(*iterator*)负责遍历序列中的每一项和决定序列何时结束的逻辑。当使用迭代器时,我们无需重新实现这些逻辑。 @@ -16,7 +16,7 @@ let v1_iter = v1.iter(); 示例 13-13:创建一个迭代器 -创建迭代器之后,可以选择用多种方式利用它。在示例 3-4 中,我们使用迭代器和 `for` 循环在每一个项上执行了一些代码,不过直到现在我们掩盖了 `iter` 调用做了什么。 +一旦创建迭代器之后,可以选择用多种方式利用它。在示例 3-4 中,我们使用迭代器和 `for` 循环在每一个项上执行了一些代码,虽然直到现在为止我们一直掩盖了 `iter` 调用做了什么。 示例 13-14 中的例子将迭代器的创建和 `for` 循环中的使用分开。迭代器被储存在 `v1_iter` 变量中,而这时没有进行迭代。一旦 `for` 循环开始使用 `v1_iter`,接着迭代器中的每一个元素被用于循环的一次迭代,这会打印出其每一个值: @@ -34,7 +34,7 @@ for val in v1_iter { 在标准库中没有提供迭代器的语言中,我们可能会使用一个从 0 开始的索引变量,使用这个变量索引 vector 中的值,并循环增加其值直到达到 vector 的元素数量。 -迭代器为我们处理了所有这些逻辑,这减少了重复代码并潜在的消除了混乱。另外,迭代器的实现方式提供了对多种不同的序列使用相同逻辑的灵活性,而不仅仅是像 vector 这样可索引的数据结构.让我们看看迭代器是如何做到这些的。 +迭代器为我们处理了所有这些逻辑,这减少了重复代码并消除了潜在的混乱。另外,迭代器的实现方式提供了对多种不同的序列使用相同逻辑的灵活性,而不仅仅是像 vector 这样可索引的数据结构.让我们看看迭代器是如何做到这些的。 ### `Iterator` trait 和 `next` 方法 @@ -46,17 +46,19 @@ trait Iterator { fn next(&mut self) -> Option; - // methods with default implementations elided + // 此处省略了方法的默认实现 } ``` 注意这里有一下我们还未讲到的新语法:`type Item` 和 `Self::Item`,他们定义了 trait 的 **关联类型**(*associated type*)。第十九章会深入讲解关联类型,不过现在只需知道这段代码表明实现 `Iterator` trait 要求同时定义一个 `Item` 类型,这个 `Item` 类型被用作 `next` 方法的返回值类型。换句话说,`Item` 类型将是迭代器返回元素的类型。 -`next` 是 `Iterator` 实现者被要求定义的唯一方法。`next` 一次返回迭代器中的一个项,封装在 `Some` 中,当迭代器结束时,它返回 `None`。如果你希望的话可以直接调用迭代器的 `next` 方法;示例 13-15 有一个测试展示了重复调用由 vector 创建的迭代器的 `next` 方法所得到的值: +`next` 是 `Iterator` 实现者被要求定义的唯一方法。`next` 一次返回迭代器中的一个项,封装在 `Some` 中,当迭代器结束时,它返回 `None`。 + +可以直接调用迭代器的 `next` 方法;示例 13-15 有一个测试展示了重复调用由 vector 创建的迭代器的 `next` 方法所得到的值: 文件名: src/lib.rs -```rust,test_harness +```rust #[test] fn iterator_demonstration() { let v1 = vec![1, 2, 3]; @@ -109,7 +111,7 @@ fn iterator_sum() { 文件名: src/main.rs -```rust +```rust,not_desired_behavior let v1: Vec = vec![1, 2, 3]; v1.iter().map(|x| x + 1); @@ -134,7 +136,7 @@ and do nothing unless consumed 为了修复这个警告并消费迭代器获取有用的结果,我们将使用第十二章简要讲到的 `collect` 方法。这个方法消费迭代器并将结果收集到一个数据结构中。 -在示例 13-18 中,我们将遍历由 `map` 调用生成的迭代器的结果收集到一个 vector 中,它将会含有原始 vector 中每个元素加一的结果: +在示例 13-18 中,我们将遍历由 `map` 调用生成的迭代器的结果收集到一个 vector 中,它将会含有原始 vector 中每个元素加 1 的结果: 文件名: src/main.rs @@ -158,7 +160,7 @@ assert_eq!(v2, vec![2, 3, 4]); 文件名: src/lib.rs -```rust,test_harness +```rust #[derive(PartialEq, Debug)] struct Shoe { size: u32, @@ -209,7 +211,6 @@ fn filters_by_size() { 示例 13-20 有一个 `Counter` 结构体定义和一个创建 `Counter` 实例的关联函数 `new`: - 文件名: src/lib.rs ```rust @@ -350,4 +351,4 @@ fn using_other_iterator_trait_methods() { 注意 `zip` 只产生四对值;理论上第五对值 `(5, None)` 从未被产生,因为 `zip` 在任一输入迭代器返回 `None` 时也返回 `None`。 -所有这些方法调用都是可能的,因为我们通过指定 `next` 如何工作来实现 `Iterator` trait 而标准库则提供其他调用 `next` 的默认方法实现。 +所有这些方法调用都是可能的,因为我们指定了 `next` 方法如何工作,而标准库则提供了其它调用 `next` 的方法的默认实现。 diff --git a/src/ch13-03-improving-our-io-project.md b/src/ch13-03-improving-our-io-project.md index 0b63745..626a17d 100644 --- a/src/ch13-03-improving-our-io-project.md +++ b/src/ch13-03-improving-our-io-project.md @@ -1,14 +1,14 @@ ## 改进 I/O 项目 -> [ch13-03-improving-our-io-project.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch13-03-improving-our-io-project.md) +> [ch13-03-improving-our-io-project.md](https://github.com/rust-lang/book/blob/master/src/ch13-03-improving-our-io-project.md) >
-> commit 2bcb126815a381acc3d46b0d6fc382cb4c98fbc5 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 有了这些关于迭代器的新知识,我们可以使用迭代器来改进第十二章中 I/O 项目的实现来使得代码更简洁明了。让我们看看迭代器如何能够改进 `Config::new` 函数和 `search` 函数的实现。 ### 使用迭代器并去掉 `clone` -在示例 12-6 中,我们增加了一些代码获取一个 `String` slice 并创建一个 `Config` 结构体的实例,他们索引 slice 中的值并克隆这些值以便 `Config` 结构体可以拥有这些值。在示例 13-24 中原原本本的重现了第十二章结尾示例 12-23 中 `Config::new` 函数的实现: +在示例 12-6 中,我们增加了一些代码获取一个 `String` slice 并创建一个 `Config` 结构体的实例,他们索引 slice 中的值并克隆这些值以便 `Config` 结构体可以拥有这些值。在示例 13-24 中重现了第十二章结尾示例 12-23 中 `Config::new` 函数的实现: 文件名: src/lib.rs @@ -58,9 +58,7 @@ fn main() { } ``` -我们会修改第十二章结尾示例 12-24 中的 `main` 函数的开头为示例 13-25 中的代码。直到同时更新 `Config::new` 这些代码还不能编译: - -将他们改为如示例 13-25 所示: +我们会修改第十二章结尾示例 12-24 中的 `main` 函数的开头为示例 13-25 中的代码。直到同步更新 `Config::new` 之前这些代码还不能编译: 文件名: src/main.rs @@ -100,6 +98,7 @@ impl Config { 文件名: src/lib.rs ```rust +# fn main() {} # use std::env; # # struct Config { @@ -129,13 +128,13 @@ impl Config { } ``` -示例 13-27:修改 `Config::new` 的函数体为使用迭代器方法 +示例 13-27:修改 `Config::new` 的函数体来使用迭代器方法 请记住 `env::args` 返回值的第一个值是程序的名称。我们希望忽略它并获取下一个值,所以首先调用 `next` 并不对返回值做任何操作。之后对希望放入 `Config` 中字段 `query` 调用 `next`。如果 `next` 返回 `Some`,使用 `match` 来提取其值。如果它返回 `None`,则意味着没有提供足够的参数并通过 `Err` 值提早返回。对 `filename` 值进行同样的操作。 ### 使用迭代器适配器来使代码更简明 -I/O 项目中其他可以利用迭代器优势的地方位于 `search` 函数,在示例 13-28 中重现了第十二章结尾示例 12-19 中此函数的定义: +I/O 项目中其他可以利用迭代器优势的地方位于 `search` 函数,示例 13-28 中重现了第十二章结尾示例 12-19 中此函数的定义: 文件名: src/lib.rs @@ -153,9 +152,9 @@ pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { } ``` -示例 13-28:第十二章结尾 `search` 函数的定义 +示例 13-28:示例 12-19 中 `search` 函数的定义 -可以通过使用迭代器适配器方法来编写更短的代码。这也避免了一个可变的中间 `results` vector 的使用。函数式编程风格倾向于最小化可变状态的数量来使代码更简洁。去掉可变状态可能会使得将来进行并行搜索的增强变得更容易,因为我们不必管理 `results` vector 的并发访问。示例 13-29 展示了该变化: +可以通过使用迭代器适配器方法来编写更简明的代码。这也避免了一个可变的中间 `results` vector 的使用。函数式编程风格倾向于最小化可变状态的数量来使代码更简洁。去掉可变状态可能会使得将来进行并行搜索的增强变得更容易,因为我们不必管理 `results` vector 的并发访问。示例 13-29 展示了该变化: 文件名: src/lib.rs @@ -169,7 +168,7 @@ pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { 示例 13-29:在 `search` 函数实现中使用迭代器适配器 -回忆 `search` 函数的目的是返回所有 `contents` 中包含 `query` 的行。类似于示例 13-19 中的 `filter` 例子,可以使用 `filter` 适配器只保留 `line.contains(query)` 为真的那些行。接着使用 `collect` 将匹配行收集到另一个 vector 中。这样就容易多了!请随意对 `search_case_insensitive` 函数做出同样的使用迭代器方法的修改。 +回忆 `search` 函数的目的是返回所有 `contents` 中包含 `query` 的行。类似于示例 13-19 中的 `filter` 例子,可以使用 `filter` 适配器只保留 `line.contains(query)` 返回 `true` 的那些行。接着使用 `collect` 将匹配行收集到另一个 vector 中。这样就容易多了!请随意对 `search_case_insensitive` 函数做出同样的使用迭代器方法的修改。 接下来的逻辑问题就是在代码中应该选择哪种风格:示例 13-28 中的原始实现,或者是示例 13-29 中使用迭代器的版本。大部分 Rust 程序员倾向于使用迭代器风格。开始这有点难以理解,不过一旦你对不同迭代器的工作方式有了感觉之后,迭代器可能会更容易理解。相比摆弄不同的循环并创建新 vector,(迭代器)代码则更关注循环的目的。这抽象出了那些老生常谈的代码,这样就更容易看清代码所特有的概念,比如迭代器中每个元素必须面对的过滤条件。 diff --git a/src/ch13-04-performance.md b/src/ch13-04-performance.md index de39e1d..28e95ba 100644 --- a/src/ch13-04-performance.md +++ b/src/ch13-04-performance.md @@ -1,8 +1,8 @@ ## 性能对比:循环 VS 迭代器 -> [ch13-04-performance.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch13-04-performance.md) +> [ch13-04-performance.md](https://github.com/rust-lang/book/blob/master/src/ch13-04-performance.md) >
-> commit 2bcb126815a381acc3d46b0d6fc382cb4c98fbc5 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 为了决定使用哪个实现,我们需要知道哪个版本的 `search` 函数更快:直接使用 `for` 循环的版本还是使用迭代器的版本。 From 19e97f6c36afa47493d26b648d26ad7e6a146953 Mon Sep 17 00:00:00 2001 From: KaiserY Date: Fri, 7 Dec 2018 13:59:02 +0800 Subject: [PATCH 26/49] check to ch14-05 --- src/ch14-00-more-about-cargo.md | 8 ++-- src/ch14-01-release-profiles.md | 10 ++--- src/ch14-02-publishing-to-crates-io.md | 52 +++++++++++++------------- src/ch14-03-cargo-workspaces.md | 22 ++++++----- src/ch14-04-installing-binaries.md | 6 +-- src/ch14-05-extending-cargo.md | 4 +- 6 files changed, 51 insertions(+), 51 deletions(-) diff --git a/src/ch14-00-more-about-cargo.md b/src/ch14-00-more-about-cargo.md index f9d54ae..781ec68 100644 --- a/src/ch14-00-more-about-cargo.md +++ b/src/ch14-00-more-about-cargo.md @@ -1,10 +1,10 @@ # 进一步认识 Cargo 和 Crates.io -> [ch14-00-more-about-cargo.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch14-00-more-about-cargo.md) +> [ch14-00-more-about-cargo.md](https://github.com/rust-lang/book/blob/master/src/ch14-00-more-about-cargo.md) >
-> commit ff93f82ff63ade5a352d9ccc430945d4ec804cdf +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -目前为止我们只使用过 Cargo 构建、运行和测试代码的最基本功能,不过它还可以做到更多。这里我们将了解一些 Cargo 其他更为高级的功能,他们将展示如何: +目前为止我们只使用过 Cargo 构建、运行和测试代码这些最基本的功能,不过它还可以做到更多。本章会讨论 Cargo 其他一些更为高级的功能,我们将展示如何: * 使用发布配置来自定义构建 * 将库发布到 [crates.io](https://crates.io) @@ -12,4 +12,4 @@ * 从 [crates.io](https://crates.io) 安装二进制文件 * 使用自定义的命令来扩展 Cargo -相比本章能够涉及的工作 Cargo 甚至还可以做到更多,关于其功能的全部解释,请查看 [其文档](http://doc.rust-lang.org/cargo/) \ No newline at end of file +相比本章能够涉及的工作 Cargo 甚至还可以做到更多,关于其功能的全部解释,请查看 [文档](http://doc.rust-lang.org/cargo/) \ No newline at end of file diff --git a/src/ch14-01-release-profiles.md b/src/ch14-01-release-profiles.md index d772793..cf71ddd 100644 --- a/src/ch14-01-release-profiles.md +++ b/src/ch14-01-release-profiles.md @@ -1,14 +1,14 @@ ## 采用发布配置自定义构建 -> [ch14-01-release-profiles.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch14-01-release-profiles.md) +> [ch14-01-release-profiles.md](https://github.com/rust-lang/book/blob/master/src/ch14-01-release-profiles.md) >
-> commit ff93f82ff63ade5a352d9ccc430945d4ec804cdf +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -在 Rust 中 **发布配置**(*release profiles*)是预定义的、可定制的带有不同选项的配置,他们允许程序员更多的控制代码编译的多种选项。每一个配置都彼此相互独立。 +在 Rust 中 **发布配置**(*release profiles*)是预定义的、可定制的带有不同选项的配置,他们允许程序员更灵活地控制代码编译的多种选项。每一个配置都彼此相互独立。 Cargo 有两个主要的配置:运行 `cargo build` 时采用的 `dev` 配置和运行 `cargo build --release` 的 `release` 配置。`dev` 配置被定义为开发时的好的默认配置,`release` 配置则有着良好的发布构建的默认配置。 -我们应该很熟悉这些配置名称因为他们出现在构建的输出中,这会展示构建所使用的配置: +这些配置名称可能很眼熟,因为它们出现在构建的输出中: ```text $ cargo build @@ -19,7 +19,7 @@ $ cargo build --release 构建输出中的 `dev` 和 `release` 表明编译器在使用不同的配置。 -Cargo 对每一个配置都有默认设置,当项目的 *Cargo.toml* 文件中没有任何 `[profile.*]` 部分的时候。通过增加任何希望定制的配置对应的 `[profile.*]` 部分,我们可以选择覆盖任意默认设置的子集。例如,如下是 `dev` 和 `release` 配置的 `opt-level` 设置的默认值: +当项目的 *Cargo.toml* 文件中没有任何 `[profile.*]` 部分的时候,Cargo 会对每一个配置都采用默认设置。通过增加任何希望定制的配置对应的 `[profile.*]` 部分,我们可以选择覆盖任意默认设置的子集。例如,如下是 `dev` 和 `release` 配置的 `opt-level` 设置的默认值: 文件名: Cargo.toml diff --git a/src/ch14-02-publishing-to-crates-io.md b/src/ch14-02-publishing-to-crates-io.md index 2e3c0fd..07a2093 100644 --- a/src/ch14-02-publishing-to-crates-io.md +++ b/src/ch14-02-publishing-to-crates-io.md @@ -1,24 +1,23 @@ ## 将 crate 发布到 Crates.io -> [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) +> [ch14-02-publishing-to-crates-io.md](https://github.com/rust-lang/book/blob/master/src/ch14-02-publishing-to-crates-io.md) >
-> commit ff93f82ff63ade5a352d9ccc430945d4ec804cdf +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 我们曾经在项目中使用 [crates.io](https://crates.io) 上的包作为依赖,不过你也可以通过发布自己的包来向它人分享代码。[crates.io](https://crates.io) 用来分发包的源代码,所以它主要托管开源代码。 Rust 和 Cargo 有一些帮助它人更方便找到和使用你发布的包的功能。我们将介绍一些这样的功能,接着讲到如何发布一个包。 - ### 编写有用的文档注释 -准确的包文档有助于其他用户理解如何以及何时使用他们,所以花一些时间编写文档是值得的。第三章中我们讨论了如何使用 `//` 注释 Rust 代码。Rust 也有特定的用于文档的注释类型,通常被称为 **文档注释**(*documentation comments*),他们会生成 HTML 文档。这些 HTML 展示公有 API 文档注释的内容,他们意在让对库感兴趣的程序员理解如何 **使用** 这个 crate,而不是它是如何被 **实现** 的。 +准确的包文档有助于其他用户理解如何以及何时使用他们,所以花一些时间编写文档是值得的。第三章中我们讨论了如何使用两斜杠 `//` 注释 Rust 代码。Rust 也有特定的用于文档的注释类型,通常被称为 **文档注释**(*documentation comments*),他们会生成 HTML 文档。这些 HTML 展示公有 API 文档注释的内容,他们意在让对库感兴趣的程序员理解如何 **使用** 这个 crate,而不是它是如何被 **实现** 的。 -文档注释使用 `///` 而不是 `//` 并支持 Markdown 注解来格式化文本。文档注释就位于需要文档的项的之前。示例 14-1 展示了一个 `my_crate` crate 中 `add_one` 函数的文档注释: +文档注释使用三斜杠 `///` 而不是两斜杆并支持 Markdown 注解来格式化文本。文档注释就位于需要文档的项的之前。示例 14-1 展示了一个 `my_crate` crate 中 `add_one` 函数的文档注释: 文件名: src/lib.rs ```rust,ignore -/// Adds one to the number given. +/// 将给定的数字加一 /// /// # Examples /// @@ -34,7 +33,7 @@ pub fn add_one(x: i32) -> i32 { 示例 14-1:一个函数的文档注释 -这里,我们提供了一个 `add_one` 函数工作的描述,接着开始了一个标题为 “Examples” 的部分,和展示如何使用 `add_one` 函数的代码。可以运行 `cargo doc` 来生成这个文档注释的 HTML 文档。这个命令运行由 Rust 分发的工具 `rustdoc` 并将生成的 HTML 文档放入 *target/doc* 目录。 +这里,我们提供了一个 `add_one` 函数工作的描述,接着开始了一个标题为 `Examples` 的部分,和展示如何使用 `add_one` 函数的代码。可以运行 `cargo doc` 来生成这个文档注释的 HTML 文档。这个命令运行由 Rust 分发的工具 `rustdoc` 并将生成的 HTML 文档放入 *target/doc* 目录。 为了方便起见,运行 `cargo doc --open` 会构建当前 crate 文档(同时还有所有 crate 依赖的文档)的 HTML 并在浏览器中打开。导航到 `add_one` 函数将会发现文档注释的文本是如何渲染的,如图 14-1 所示: @@ -78,10 +77,10 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ```rust,ignore //! # My Crate //! -//! `my_crate` is a collection of utilities to make performing certain -//! calculations more convenient. +//! `my_crate` 是一个使得特定计算更方便的 +//! 工具集合 -/// Adds one to the number given. +/// 将给定的数字加一。 // --snip-- ``` @@ -99,30 +98,30 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ### 使用 `pub use` 导出合适的公有 API -第七章介绍了如何使用 `mod` 关键字来将代码组织进模块中,如何使用 `pub` 关键字将项变为公有,和如何使用 `use` 关键字将项引入作用域。然而对你开发来说很有道理的结果可能对用户来说就不太方便了。你可能希望将结构组织进有多个层次的层级中,不过想要使用被定义在很深层级中的类型的人可能很难发现这些类型是否存在。他们也可能会厌烦 `use my_crate::some_module::another_module::UsefulType;` 而不是 `use my_crate::UsefulType;` 来使用类型。 +第七章介绍了如何使用 `mod` 关键字来将代码组织进模块中,如何使用 `pub` 关键字将项变为公有,和如何使用 `use` 关键字将项引入作用域。然而对你开发来说很有道理的结果可能对用户来说就不太方便了。你可能希望将结构组织进有多个层次的层级中,不过想要使用被定义在很深层级中的类型的人可能很难发现这些类型是否存在。他们也可能会厌烦使用 `use my_crate::some_module::another_module::UsefulType;` 而不是 `use my_crate::UsefulType;` 来使用类型。 公有 API 的结构是你发布 crate 时主要需要考虑的。crate 用户没有你那么熟悉其结构,并且如果模块层级过大他们可能会难以找到所需的部分。 好消息是,如果结果对于用户来说 **不是** 很方便,你也无需重新安排内部组织:你可以选择使用 `pub use` 重导出(re-export)项来使公有结构不同于私有结构。重导出获取位于一个位置的公有项并将其公开到另一个位置,好像它就定义在这个新位置一样。 -例如,假设我们创建了一个模块化了充满艺术化气息的库 `art`。在这个库中是一个包含两个枚举 `PrimaryColor` 和 `SecondaryColor` 的模块 `kinds`,以及一个包含函数 `mix` 的模块 `utils`,如示例 14-3 所示: +例如,假设我们创建了一个模块化的充满艺术化气息的库 `art`。在这个库中是一个包含两个枚举 `PrimaryColor` 和 `SecondaryColor` 的模块 `kinds`,以及一个包含函数 `mix` 的模块 `utils`,如示例 14-3 所示: 文件名: src/lib.rs ```rust,ignore //! # Art //! -//! A library for modeling artistic concepts. +//! 一个模块化的充满艺术化气息的库。 pub mod kinds { - /// The primary colors according to the RYB color model. + /// 采用 RGB 色彩模式的主要颜色。 pub enum PrimaryColor { Red, Yellow, Blue, } - /// The secondary colors according to the RYB color model. + /// 采用 RGB 色彩模式的次要颜色。 pub enum SecondaryColor { Orange, Green, @@ -133,8 +132,8 @@ pub mod kinds { pub mod utils { use kinds::*; - /// Combines two primary colors in equal amounts to create - /// a secondary color. + /// 等量的混合两个主要颜色 + /// 来创建一个次要颜色。 pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor { // --snip-- } @@ -149,15 +148,13 @@ pub mod utils { 图 14-3:包含 `kinds` 和 `utils` 模块的库 `art` 的文档首页 -注意 `PrimaryColor` 和 `SecondaryColor` 类型没有在首页中列出,`mix` 函数也是。必须点击 `kinds` 或 `utils` 才能看到他们。 +注意 `PrimaryColor` 和 `SecondaryColor` 类型没有在首页中列出,`mix` 函数也没有。必须点击 `kinds` 或 `utils` 才能看到他们。 另一个依赖这个库的 crate 需要 `use` 语句来导入 `art` 中的项,这包含指定其当前定义的模块结构。示例 14-4 展示了一个使用 `art` crate 中 `PrimaryColor` 和 `mix` 项的 crate 的例子: 文件名: src/main.rs ```rust,ignore -extern crate art; - use art::kinds::PrimaryColor; use art::utils::mix; @@ -179,7 +176,7 @@ fn main() { ```rust,ignore //! # Art //! -//! A library for modeling artistic concepts. +//! 一个模块化的充满艺术化气息的库。 pub use kinds::PrimaryColor; pub use kinds::SecondaryColor; @@ -202,13 +199,11 @@ pub mod utils { 图 14-10:`art` 文档的首页,这里列出了重导出的项 -`art` crate 的用户仍然可以看见和选择使用示例 14-3 中的内部结构,或者可以使用示例 14-4 中更为方便的结构,如示例 14-6 所示: +`art` crate 的用户仍然可以看见和选择使用示例 14-4 中的内部结构,或者可以使用示例 14-5 中更为方便的结构,如示例 14-6 所示: 文件名: src/main.rs ```rust,ignore -extern crate art; - use art::PrimaryColor; use art::mix; @@ -225,7 +220,7 @@ fn main() { ### 创建 Crates.io 账号 -在你可以发布任何 crate 之前,需要在 [crates.io](https://crates.io) 上注册账号并获取一个 API token。为此,访问位于 [crates.io](https://crates.io) 的首页并使用 GitHub 账号登陆————目前 GitHub 账号是必须的,不过将来该网站可能会支持其他创建账号的方法。一旦登陆之后,查看位于 [https://crates.io/me/](https://crates.io/me/) 的账户设置页面并获取 API token。接着使用该 API token 运行 `cargo login` 命令,像这样: +在你可以发布任何 crate 之前,需要在 [crates.io](https://crates.io) 上注册账号并获取一个 API token。为此,访问位于 [crates.io](https://crates.io) 的首页并使用 GitHub 账号登陆。(目前 GitHub 账号是必须的,不过将来该网站可能会支持其他创建账号的方法)一旦登陆之后,查看位于 [https://crates.io/me/](https://crates.io/me/) 的账户设置页面并获取 API token。接着使用该 API token 运行 `cargo login` 命令,像这样: ```text $ cargo login abcdefghijklmnopqrstuvwxyz012345 @@ -259,7 +254,10 @@ error: api errors: missing or empty metadata fields: description, license. 这是因为我们缺少一些关键信息:关于该 crate 用途的描述和用户可能在何种条款下使用该 crate 的 license。为了修正这个错误,需要在 *Cargo.toml* 中引入这些信息。 -描述通常是一两句话,因为它会出现在 crate 的搜索结果中和 crate 页面里。对于 `license` 字段,你需要一个 **license 标识符值**(*license identifier value*)。Linux 基金会位于 *http://spdx.org/licenses/* 的 Software Package Data Exchange (SPDX) 列出了可以使用的标识符。例如,为了指定 crate 使用 MIT License,增加 `MIT` 标识符: +描述通常是一两句话,因为它会出现在 crate 的搜索结果中和 crate 页面里。对于 `license` 字段,你需要一个 **license 标识符值**(*license identifier value*)。[Linux 基金会 的 Software Package Data +Exchange (SPDX)][spdx] 列出了可以使用的标识符。例如,为了指定 crate 使用 MIT License,增加 `MIT` 标识符: + +[spdx]: http://spdx.org/licenses/ 文件名: Cargo.toml @@ -271,7 +269,7 @@ license = "MIT" 如果你希望使用不存在于 SPDX 的 license,则需要将 license 文本放入一个文件,将该文件包含进项目中,接着使用 `license-file` 来指定文件名而不是使用 `license` 字段。 -关于项目所适用的 license 指导超出了本书的范畴。很多 Rust 社区成员选择与 Rust 自身相同的 license,这是一个双许可的 `MIT OR Apache-2.0` ———— 这展示了也可以通过 `OR` 来分隔来为项目指定多个 license 标识符。 +关于项目所适用的 license 指导超出了本书的范畴。很多 Rust 社区成员选择与 Rust 自身相同的 license,这是一个双许可的 `MIT OR Apache-2.0`。这个实践展示了也可以通过 `OR` 分隔为项目指定多个 license 标识符。 那么,有了唯一的名称、版本号、由 `cargo new` 新建项目时增加的作者信息、描述和所选择的 license,已经准备好发布的项目的 *Cargo.toml* 文件可能看起来像这样: diff --git a/src/ch14-03-cargo-workspaces.md b/src/ch14-03-cargo-workspaces.md index da5fb44..3ca037b 100644 --- a/src/ch14-03-cargo-workspaces.md +++ b/src/ch14-03-cargo-workspaces.md @@ -1,12 +1,14 @@ ## Cargo 工作空间 -> [ch14-03-cargo-workspaces.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch14-03-cargo-workspaces.md) +> [ch14-03-cargo-workspaces.md](https://github.com/rust-lang/book/blob/master/src/ch14-03-cargo-workspaces.md) >
-> commit a59537604248f2970e0831d5ead9f6fac2cdef84 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 第十二章中,我们构建一个包含二进制 crate 和库 crate 的包。你可能会发现,随着项目开发的深入,库 crate 持续增大,而你希望将其进一步拆分成多个库 crate。对于这种情况,Cargo 提供了一个叫 **工作空间**(*workspaces*)的功能,它可以帮助我们管理多个相关的协同开发的包。 -**工作空间** 是一系列共享同样的 *Cargo.lock* 和输出目录的包。让我们使用工作空间创建一个项目,这里采用常见的代码这样就可以关注工作空间的结构了。有多种组织工作空间的方式;我们将展示一个常用方法。我们的工作空间有一个二进制项目和两个库。二进制项目会提供作为命令行工具的主要功能,它会依赖另两个库。一个库会提供 `add_one` 方法而第二个会提供 `add_two` 方法。这三个 crate 将会是相同工作空间的一部分。让我们以新建工作空间目录开始: +### 创建工作空间 + +**工作空间** 是一系列共享同样的 *Cargo.lock* 和输出目录的包。让我们使用工作空间创建一个项目 —— 这里采用常见的代码以便可以关注工作空间的结构。有多种组织工作空间的方式;我们将展示一个常用方法。我们的工作空间有一个二进制项目和两个库。二进制项目会提供主要功能,并会依赖另两个库。一个库会提供 `add_one` 方法而第二个会提供 `add_two` 方法。这三个 crate 将会是相同工作空间的一部分。让我们以新建工作空间目录开始: ```text $ mkdir add @@ -28,7 +30,7 @@ members = [ 接下来,在 *add* 目录运行 `cargo new` 新建 `adder` 二进制 crate: ```text -$ cargo new --bin adder +$ cargo new adder Created binary (application) `adder` project ``` @@ -64,7 +66,7 @@ members = [ 接着新生成一个叫做 `add-one` 的库: ```text -$ cargo new add-one +$ cargo new add-one --lib Created library `add-one` project ``` @@ -111,7 +113,7 @@ add-one = { path = "../add-one" } 文件名: adder/src/main.rs ```rust,ignore -extern crate add_one; +use add_one; fn main() { let num = 10; @@ -154,7 +156,7 @@ Hello, world! 10 plus one is 11! rand = "0.3.14" ``` -现在就可以在 *add-one/src/lib.rs* 中增加 `extern crate rand;` 了,接着在 *add* 目录运行 `cargo build` 构建整个工作空间就会引入并编译 `rand` crate: +现在就可以在 *add-one/src/lib.rs* 中增加 `use rand;` 了,接着在 *add* 目录运行 `cargo build` 构建整个工作空间就会引入并编译 `rand` crate: ```text $ cargo build @@ -167,7 +169,7 @@ $ cargo build Finished dev [unoptimized + debuginfo] target(s) in 10.18 secs ``` -现在顶级的 *Cargo.lock* 包含了 `add-one` 的 `rand` 依赖的信息。然而,即使 `rand` 被用于工作空间的某处,也不能在其他 crate 中使用它,除非也在他们的 *Cargo.toml* 中加入 `rand`。例如,如果在顶级的 `adder` crate 的 *adder/src/main.rs* 中增加 `extern crate rand;`,会得到一个错误: +现在顶级的 *Cargo.lock* 包含了 `add-one` 的 `rand` 依赖的信息。然而,即使 `rand` 被用于工作空间的某处,也不能在其他 crate 中使用它,除非也在他们的 *Cargo.toml* 中加入 `rand`。例如,如果在顶级的 `adder` crate 的 *adder/src/main.rs* 中增加 `use rand;`,会得到一个错误: ```text $ cargo build @@ -176,7 +178,7 @@ error: use of unstable library feature 'rand': use `rand` from crates.io (see issue #27703) --> adder/src/main.rs:1:1 | -1 | extern crate rand; +1 | use rand; ``` 为了修复这个错误,修改顶级 `adder` crate 的 *Cargo.toml* 来表明 `rand` 也是这个 crate 的依赖。构建 `adder` crate 会将 `rand` 加入到 *Cargo.lock* 中 `adder` 的依赖列表中,但是这并不会下载 `rand` 的额外拷贝。Cargo 确保了工作空间中任何使用 `rand` 的 crate 都采用相同的版本。在整个工作空间中使用相同版本的 `rand` 节省了空间,因为这样就无需多个拷贝并确保了工作空间中的 crate 将是相互兼容的。 @@ -257,4 +259,4 @@ test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out 现在尝试以类似 `add-one` crate 的方式向工作空间增加 `add-two` crate 来作为更多的练习! -随着项目增长,考虑使用工作空间:每一个更小的组件比一大块代码要容易理解。将 crate 保持在工作空间中更易于协调他们的改变,如果他们一起运行并经常需要同时被修改的话。 +随着项目增长,考虑使用工作空间:每一个更小的组件比一大块代码要容易理解。如果它们经常需要同时被修改的话,将 crate 保持在工作空间中更易于协调他们的改变。 diff --git a/src/ch14-04-installing-binaries.md b/src/ch14-04-installing-binaries.md index 9f956fe..fa93f9b 100644 --- a/src/ch14-04-installing-binaries.md +++ b/src/ch14-04-installing-binaries.md @@ -1,10 +1,10 @@ ## 使用 `cargo install` 从 Crates.io 安装二进制文件 -> [ch14-04-installing-binaries.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch14-04-installing-binaries.md) +> [ch14-04-installing-binaries.md](https://github.com/rust-lang/book/blob/master/src/ch14-04-installing-binaries.md) >
-> commit ff93f82ff63ade5a352d9ccc430945d4ec804cdf +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -`cargo install` 命令用于在本地安装和使用二进制 crate。它并不打算替换系统中的包;它意在作为一个方便 Rust 开发者们安装其他人已经在 [crates.io](https://crates.io) 上共享的工具的手段。只有拥有二进制目标文件的包能够被安装。二进制目标文件是在 crate 有 *src/main.rs* 或者其他指定为二进制文件时所创建的可执行程序,这不同于自身不能执行但适合包含在其他程序中的库目标文件。通常 crate 的 *README* 文件中有该 crate 是库、二进制目标还是两者都是的信息。 +`cargo install` 命令用于在本地安装和使用二进制 crate。它并不打算替换系统中的包;它意在作为一个方便 Rust 开发者们安装其他人已经在 [crates.io](https://crates.io) 上共享的工具的手段。只有拥有二进制目标文件的包能够被安装。**二进制目标** 文件是在 crate 有 *src/main.rs* 或者其他指定为二进制文件时所创建的可执行程序,这不同于自身不能执行但适合包含在其他程序中的库目标文件。通常 crate 的 *README* 文件中有该 crate 是库、二进制目标还是两者都是的信息。 所有来自 `cargo install` 的二进制文件都安装到 Rust 安装根目录的 *bin* 文件夹中。如果你使用 *rustup.rs* 安装的 Rust 且没有自定义任何配置,这将是 `$HOME/.cargo/bin`。确保将这个目录添加到 `$PATH` 环境变量中就能够运行通过 `cargo install` 安装的程序了。 diff --git a/src/ch14-05-extending-cargo.md b/src/ch14-05-extending-cargo.md index 26946b8..1e88eb1 100644 --- a/src/ch14-05-extending-cargo.md +++ b/src/ch14-05-extending-cargo.md @@ -1,8 +1,8 @@ ## Cargo 自定义扩展命令 -> [ch14-05-extending-cargo.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch14-05-extending-cargo.md) +> [ch14-05-extending-cargo.md](https://github.com/rust-lang/book/blob/master/src/ch14-05-extending-cargo.md) >
-> commit ff93f82ff63ade5a352d9ccc430945d4ec804cdf +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f Cargo 被设计为可以通过新的子命令而无须修改 Cargo 自身来进行扩展。如果 `$PATH` 中有类似 `cargo-something` 的二进制文件,就可以通过 `cargo something` 来像 Cargo 子命令一样运行它。像这样的自定义命令也可以运行 `cargo --list` 来展示出来。能够通过 `cargo install` 向 Cargo 安装扩展并可以如内建 Cargo 工具那样运行他们是 Cargo 设计上的一个非常方便的优点! From 3dc99108daf8ee9d7917e0662f650e0dbd02bfd7 Mon Sep 17 00:00:00 2001 From: Zhao Sizhe Date: Fri, 7 Dec 2018 22:25:01 +0800 Subject: [PATCH 27/49] improve translations & fix typo --- ...kages-and-crates-for-making-libraries-and-executables.md | 2 +- src/ch07-02-modules-and-use-to-control-scope-and-privacy.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ch07-01-packages-and-crates-for-making-libraries-and-executables.md b/src/ch07-01-packages-and-crates-for-making-libraries-and-executables.md index 4a82060..db08964 100644 --- a/src/ch07-01-packages-and-crates-for-making-libraries-and-executables.md +++ b/src/ch07-01-packages-and-crates-for-making-libraries-and-executables.md @@ -8,7 +8,7 @@ * *crate* 是一个二进制或库项目。 * **crate 根**(*crate root*)是一个用来描述如何构建 crate 的文件。 -* **包** 带有 *Cargo.toml* 文件用以描述如何构建一个或多个 crate。一个包中至多可以有一个库项目。 +* 带有 *Cargo.toml* 文件的 **包** 用以描述如何构建一个或多个 crate。一个包中至多可以有一个库项目。 所以当运行 `cargo new` 时是在创建一个包: diff --git a/src/ch07-02-modules-and-use-to-control-scope-and-privacy.md b/src/ch07-02-modules-and-use-to-control-scope-and-privacy.md index c3a3437..44c1ce5 100644 --- a/src/ch07-02-modules-and-use-to-control-scope-and-privacy.md +++ b/src/ch07-02-modules-and-use-to-control-scope-and-privacy.md @@ -146,7 +146,7 @@ error[E0603]: module `instrument` is private 之前我们讨论到模块的语法和组织代码的用途。Rust 采用模块还有另一个原因:模块是 Rust 中的 **私有性边界**(*privacy boundary*)。如果你希望函数或结构体是私有的,将其放入模块。私有性规则有如下: * 所有项(函数、方法、结构体、枚举、模块和常量)默认是私有的。 -* 可以使用 `pub` 关键字使项变为共有。 +* 可以使用 `pub` 关键字使项变为公有。 * 不允许使用定义于当前模块的子模块中的私有代码。 * 允许使用任何定义于父模块或当前模块中的代码。 @@ -154,7 +154,7 @@ error[E0603]: module `instrument` is private ### 使用 `pub` 关键字使项变为公有 -示例 7-5 中的错误说 `instrument` 模块使私有的。让我们使用 `pub` 关键字标记 `instrument` 模块使其可以在 `main` 函数中使用。这些改变如示例 7-6 所示,它仍然不能编译,不过会产生一个不同的错误: +示例 7-5 中的错误说 `instrument` 模块是私有的。让我们使用 `pub` 关键字标记 `instrument` 模块使其可以在 `main` 函数中使用。这些改变如示例 7-6 所示,它仍然不能编译,不过会产生一个不同的错误: 文件名: src/main.rs @@ -696,6 +696,6 @@ pub fn clarinet() { ## 总结 -Rust 提供了将包组织进 crate、将 crate 组织进模块和通过指定绝对或相对路径从一个模块引用另一个模块中定义的项的方式。可以通过 `use` 语句将路径引入作用域,这样在多次使用时可以使用更短的路径。模块定义的代码默认时私有的,不过可以选择增加 `pub` 关键字使其定义变为公有。 +Rust 提供了将包组织进 crate、将 crate 组织进模块和通过指定绝对或相对路径从一个模块引用另一个模块中定义的项的方式。可以通过 `use` 语句将路径引入作用域,这样在多次使用时可以使用更短的路径。模块定义的代码默认是私有的,不过可以选择增加 `pub` 关键字使其定义变为公有。 接下来,让我们看看一些标准库提供的集合数据类型,你可以利用它们编写出漂亮整洁的代码。 \ No newline at end of file From 6b3c71d89ed27ff030dace9c4d87cc2e9bf4a487 Mon Sep 17 00:00:00 2001 From: KaiserY Date: Fri, 7 Dec 2018 23:48:24 +0800 Subject: [PATCH 28/49] check to ch15-06 --- src/ch15-00-smart-pointers.md | 48 +------ src/ch15-01-box.md | 113 ++++------------ src/ch15-02-deref.md | 135 +++++-------------- src/ch15-03-drop.md | 20 ++- src/ch15-04-rc.md | 65 +++------- src/ch15-05-interior-mutability.md | 107 +++++---------- src/ch15-06-reference-cycles.md | 201 +++++++++-------------------- 7 files changed, 185 insertions(+), 504 deletions(-) diff --git a/src/ch15-00-smart-pointers.md b/src/ch15-00-smart-pointers.md index cb11eb4..3251288 100644 --- a/src/ch15-00-smart-pointers.md +++ b/src/ch15-00-smart-pointers.md @@ -1,65 +1,25 @@ # 智能指针 -> [ch15-00-smart-pointers.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch15-00-smart-pointers.md) +> [ch15-00-smart-pointers.md](https://github.com/rust-lang/book/blob/master/src/ch15-00-smart-pointers.md) >
-> commit 68267b982a226fa252e9afa1a5029396ccf5fa03 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -**指针** (*pointer*)是一个包含内存地址的变量的通用概念。这个地址引用,或 “指向”(points at)一些其他数据。Rust 中最常见的指针是第四章介绍的 **引用**(*reference*)。引用以 `&` 符号为标志并借用了他们所指向的值。除了引用数据它们没有任何其他特殊功能。它们也没有任何额外开销,所以应用的最多。 +**指针** (*pointer*)是一个包含内存地址的变量的通用概念。这个地址引用,或 “指向”(points at)一些其他数据。Rust 中最常见的指针是第四章介绍的 **引用**(*reference*)。引用以 `&` 符号为标志并借用了他们所指向的值。除了引用数据没有任何其他特殊功能。它们也没有任何额外开销,所以应用的最多。 另一方面,**智能指针**(*smart pointers*)是一类数据结构,他们的表现类似指针,但是也拥有额外的元数据和功能。智能指针的概念并不为 Rust 所独有;其起源于 C++ 并存在于其他语言中。Rust 标准库中不同的智能指针提供了多于引用的额外功能。本章将会探索的一个例子便是 **引用计数** (*reference counting*)智能指针类型,其允许数据有多个所有者。引用计数智能指针记录总共有多少个所有者,并当没有任何所有者时负责清理数据。 - - - - - - 在 Rust 中,普通引用和智能指针的一个额外的区别是引用是一类只借用数据的指针;相反大部分情况,智能指针 **拥有** 他们指向的数据。 实际上本书中已经出现过一些智能指针,比如第八章的 `String` 和 `Vec`,虽然当时我们并不这么称呼它们。这些类型都属于智能指针因为它们拥有一些数据并允许你修改它们。它们也带有元数据(比如他们的容量)和额外的功能或保证(`String` 的数据总是有效的 UTF-8 编码)。 - - - 智能指针通常使用结构体实现。智能指针区别于常规结构体的显著特性在于其实现了 `Deref` 和 `Drop` trait。`Deref` trait 允许智能指针结构体实例表现的像引用一样,这样就可以编写既用于引用又用于智能指针的代码。`Drop` trait 允许我们自定义当智能指针离开作用域时运行的代码。本章会讨论这些 trait 以及为什么对于智能指针来说他们很重要。 考虑到智能指针是一个在 Rust 经常被使用的通用设计模式,本章并不会覆盖所有现存的智能指针。很多库都有自己的智能指针而你也可以编写属于你自己的智能指针。这里将会讲到的是来自标准库中最常用的一些: - - - * `Box`,用于在堆上分配值 * `Rc`,一个引用计数类型,其数据可以有多个所有者 * `Ref` 和 `RefMut`,通过 `RefCell` 访问,一个在运行时而不是在编译时执行借用规则的类型。 - - - -同时我们会涉及 **内部可变性**(*interior mutability*)模式,这时不可变类型暴露出改变其内部值的 API。我们也会讨论 **引用循环**(*reference cycles*)会如何泄露内存,以及如何避免。 +另外我们会涉及 **内部可变性**(*interior mutability*)模式,这时不可变类型暴露出改变其内部值的 API。我们也会讨论 **引用循环**(*reference cycles*)会如何泄露内存,以及如何避免。 让我们开始吧! diff --git a/src/ch15-01-box.md b/src/ch15-01-box.md index 0c5703d..272bf89 100644 --- a/src/ch15-01-box.md +++ b/src/ch15-01-box.md @@ -1,23 +1,18 @@ ## `Box` 在堆上存储数据,并且可确定大小 -> [ch15-01-box.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch15-01-box.md) +> [ch15-01-box.md](https://github.com/rust-lang/book/blob/master/src/ch15-01-box.md) >
-> commit 0905e41f7387b60865e6eac744e31a7f7b46edf5 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 最简单直接的智能指针是 *box*,其类型是 `Box`。 box 允许你将一个值放在堆上而不是栈上。留在栈上的则是指向堆数据的指针。如果你想回顾一下栈与堆的区别请参考第四章。 - - - -除了数据被储存在堆上而不是栈上之外,box 没有性能损失,不过也没有很多额外的功能。他们多用于如下场景: +除了数据被储存在堆上而不是栈上之外,box 没有性能损失。不过也没有很多额外的功能。它们多用于如下场景: - 当有一个在编译时未知大小的类型,而又想要在需要确切大小的上下文中使用这个类型值的时候 - 当有大量数据并希望在确保数据不被拷贝的情况下转移所有权的时候 - 当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候 -我们将在本部分的余下内容中展示第一种应用场景。作为对另外两个情况更详细的说明:在第二种情况中,转移大量数据的所有权可能会花费很长的时间,因为数据在栈上进行了拷贝。为了改善这种情况下的性能,可以通过 box 将这些数据储存在堆上。接着,只有少量的指针数据在栈上被拷贝。第三种情况被称为 **trait 对象**(*trait object*),第十七章刚好有一整个部分专门讲解这个主题。所以这里所学的内容会在第十七章再次用上! +我们会在 “box 允许创建递归类型” 部分展示第一种场景。在第二种情况中,转移大量数据的所有权可能会花费很长的时间,因为数据在栈上进行了拷贝。为了改善这种情况下的性能,可以通过 box 将这些数据储存在堆上。接着,只有少量的指针数据在栈上被拷贝。第三种情况被称为 **trait 对象**(*trait object*),第十七章刚好有一整个部分 “为使用不同类型的值而设计的 trait 对象” 专门讲解这个主题。所以这里所学的内容会在第十七章再次用上! ### 使用 `Box` 在堆上储存数据 @@ -40,62 +35,27 @@ fn main() { 将一个单独的值存放在堆上并不是很有意义,所以像示例 15-1 这样单独使用 box 并不常见。将像单个 `i32` 这样的值储存在栈上,也就是其默认存放的地方在大部分使用场景中更为合适。让我们看看一个不使用 box 时无法定义的类型的例子。 - - - ### box 允许创建递归类型 - - - - - Rust 需要在编译时知道类型占用多少空间。一种无法在编译时知道大小的类型是 **递归类型**(*recursive type*),其值的一部分可以是相同类型的另一个值。这种值的嵌套理论上可以无限的进行下去,所以 Rust 不知道递归类型需要多少空间。不过 box 有一个已知的大小,所以通过在循环类型定义中插入 box,就可以创建递归类型了。 让我们探索一下 *cons list*,一个函数式编程语言中的常见类型,来展示这个(递归类型)概念。除了递归之外,我们将要定义的 cons list 类型是很直白的,所以这个例子中的概念在任何遇到更为复杂的涉及到递归类型的场景时都很实用。 - - - -cons list 是一个每一项都包含两个部分的列表:当前项的值和下一项。其最后一项值包含一个叫做 `Nil` 的值并没有下一项。 - -> #### cons list 的更多内容 -> -> *cons list* 是一个来源于 Lisp 编程语言及其方言的数据结构。在 Lisp 中,`cons` 函数(“construct function" 的缩写)利用两个参数来构造一个新的列表,他们通常是一个单独的值和另一个列表。 -> -> cons 函数的概念涉及到更通用的函数式编程术语;“将 x 与 y 连接” 通常意味着构建一个新的容器而将 x 的元素放在新容器的开头,其后则是容器 y 的元素。 -> -> cons list 通过递归调用 `cons` 函数产生。代表递归的终止条件(base case)的规范名称是 `Nil`,它宣布列表的终止。注意这不同于第六章中的 “null” 或 “nil” 的概念,他们代表无效或缺失的值。 +#### cons list 的更多内容 -注意虽然函数式编程语言经常使用 cons list,但是它并不是一个 Rust 中常见的类型。大部分在 Rust 中需要列表的时候,`Vec` 是一个更好的选择。其他更为复杂的递归数据类型 **确实** 在 Rust 的很多场景中很有用,不过通过以 cons list 作为开始,我们可以探索如何使用 box 毫不费力的定义一个递归数据类型。 +*cons list* 是一个来源于 Lisp 编程语言及其方言的数据结构。在 Lisp 中,`cons` 函数(“construct function" 的缩写)利用两个参数来构造一个新的列表,他们通常是一个单独的值和另一个列表。 + +cons 函数的概念涉及到更通用的函数式编程术语;“将 *x* 与 *y* 连接” 通常意味着构建一个新的容器而将 *x* 的元素放在新容器的开头,其后则是容器 *y* 的元素。 - - +cons list 的每一项都包含两个元素:当前项的值和下一项。其最后一项值包含一个叫做 `Nil` 的值并没有下一项。cons list 通过递归调用 `cons` 函数产生。代表递归的终止条件(base case)的规范名称是 `Nil`,它宣布列表的终止。注意这不同于第六章中的 “null” 或 “nil” 的概念,他们代表无效或缺失的值。 + +注意虽然函数式编程语言经常使用 cons list,但是它并不是一个 Rust 中常见的类型。大部分在 Rust 中需要列表的时候,`Vec` 是一个更好的选择。其他更为复杂的递归数据类型 **确实** 在 Rust 的很多场景中很有用,不过通过以 cons list 作为开始,我们可以探索如何使用 box 毫不费力的定义一个递归数据类型。 示例 15-2 包含一个 cons list 的枚举定义。注意这还不能编译因为这个类型没有已知的大小,之后我们会展示: 文件名: src/main.rs -```rust,ignore +```rust,ignore,does_not_compile enum List { Cons(i32, List), Nil, @@ -104,13 +64,7 @@ enum List { 示例 15-2:第一次尝试定义一个代表 `i32` 值的 cons list 数据结构的枚举 -> 注意:出于示例的需要我们选择实现一个只存放 `i32` 值的 cons list。也可以用泛型实现它,正如第十章讲到的,来定义一个可以存放任何类型值的 cons list 类型。 - - - +> 注意:出于示例的需要我们选择实现一个只存放 `i32` 值的 cons list。也可以用泛型,正如第十章讲到的,来定义一个可以存放任何类型值的 cons list 类型。 使用这个 cons list 来储存列表 `1, 2, 3` 将看起来如示例 15-3 所示: @@ -128,11 +82,11 @@ fn main() { 第一个 `Cons` 储存了 `1` 和另一个 `List` 值。这个 `List` 是另一个包含 `2` 的 `Cons` 值和下一个 `List` 值。接着又有另一个存放了 `3` 的 `Cons` 值和最后一个值为 `Nil` 的 `List`,非递归成员代表了列表的结尾。 -如果尝试编译上面的代码,会得到如示例 15-4 所示的错误: +如果尝试编译示例 15-3 的代码,会得到如示例 15-4 所示的错误: ```text error[E0072]: recursive type `List` has infinite size - --> + --> src/main.rs:1:1 | 1 | enum List { | ^^^^^^^^^ recursive type has infinite size @@ -145,14 +99,6 @@ error[E0072]: recursive type `List` has infinite size 示例 15-4:尝试定义一个递归枚举时得到的错误 - - - 这个错误表明这个类型 “有无限的大小”。其原因是 `List` 的一个成员被定义为是递归的:它直接存放了另一个相同类型的值。这意味着 Rust 无法计算为了存放 `List` 值到底需要多少空间。让我们一点一点来看:首先了解一下 Rust 如何决定需要多少空间来存放一个非递归类型。 ### 计算非递归类型的大小 @@ -170,28 +116,26 @@ enum Message { 当 Rust 需要知道要为 `Message` 值分配多少空间时,它可以检查每一个成员并发现 `Message::Quit` 并不需要任何空间,`Message::Move` 需要足够储存两个 `i32` 值的空间,依此类推。因此,`Message` 值所需的空间等于储存其最大成员的空间大小。 -与此相对当 Rust 编译器检查像示例 15-2 中的 `List` 这样的递归类型时会发生什么呢。编译器尝试计算出储存一个 `List` 枚举需要多少内存,并开始检查 `Cons` 成员,那么 `Cons` 需要的空间等于 `i32` 的大小加上 `List` 的大小。为了计算 `List` 需要多少内存,它检查其成员,从 `Cons` 成员开始。`Cons`成员储存了一个 `i32` 值和一个`List`值,这样的计算将无限进行下去,如图 15-5 所示: +与此相对当 Rust 编译器检查像示例 15-2 中的 `List` 这样的递归类型时会发生什么呢。编译器尝试计算出储存一个 `List` 枚举需要多少内存,并开始检查 `Cons` 成员,那么 `Cons` 需要的空间等于 `i32` 的大小加上 `List` 的大小。为了计算 `List` 需要多少内存,它检查其成员,从 `Cons` 成员开始。`Cons`成员储存了一个 `i32` 值和一个`List`值,这样的计算将无限进行下去,如图 15-1 所示: An infinite Cons list -图 15-5:一个包含无限个 `Cons` 成员的无限 `List` +图 15-1:一个包含无限个 `Cons` 成员的无限 `List` ### 使用 `Box` 给递归类型一个已知的大小 Rust 无法计算出要为定义为递归的类型分配多少空间,所以编译器给出了示例 15-4 中的错误。这个错误也包括了有用的建议: ```text -= help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to - make `List` representable + = help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to + make `List` representable ``` 在建议中,“indirection” 意味着不同于直接储存一个值,我们将间接的储存一个指向值的指针。 -因为 `Box` 是一个指针,我们总是知道它需要多少空间:指针的大小并不会根据其指向的数据量而改变。 - -所以可以将 `Box` 放入 `Cons` 成员中而不是直接存放另一个 `List` 值。`Box` 会指向另一个位于堆上的 `List` 值,而不是存放在 `Cons` 成员中。从概念上讲,我们仍然有一个通过在其中 “存放” 其他列表创建的列表,不过现在实现这个概念的方式更像是一个项挨着另一项,而不是一项包含另一项。 +因为 `Box` 是一个指针,我们总是知道它需要多少空间:指针的大小并不会根据其指向的数据量而改变。这意味着可以将 `Box` 放入 `Cons` 成员中而不是直接存放另一个 `List` 值。`Box` 会指向另一个位于堆上的 `List` 值,而不是存放在 `Cons` 成员中。从概念上讲,我们仍然有一个通过在其中 “存放” 其他列表创建的列表,不过现在实现这个概念的方式更像是一个项挨着另一项,而不是一项包含另一项。 -我们可以修改示例 15-2 中 `List` 枚举的定义和示例 15-3 中对 `List` 的应用,如示例 15-6 所示,这是可以编译的: +我们可以修改示例 15-2 中 `List` 枚举的定义和示例 15-3 中对 `List` 的应用,如示例 15-65 所示,这是可以编译的: 文件名: src/main.rs @@ -211,21 +155,14 @@ fn main() { } ``` -示例 15-6:为了拥有已知大小而使用 `Box` 的 `List` 定义 +示例 15-5:为了拥有已知大小而使用 `Box` 的 `List` 定义 -`Cons` 成员将会需要一个 `i32` 的大小加上储存 box 指针数据的空间。`Nil` 成员不储存值,所以它比 `Cons` 成员需要更少的空间。现在我们知道了任何 `List` 值最多需要一个 `i32` 加上 box 指针数据的大小。通过使用 box ,打破了这无限递归的连锁,这样编译器就能够计算出储存 `List` 值需要的大小了。图 15-7 展示了现在 `Cons` 成员看起来像什么: +`Cons` 成员将会需要一个 `i32` 的大小加上储存 box 指针数据的空间。`Nil` 成员不储存值,所以它比 `Cons` 成员需要更少的空间。现在我们知道了任何 `List` 值最多需要一个 `i32` 加上 box 指针数据的大小。通过使用 box ,打破了这无限递归的连锁,这样编译器就能够计算出储存 `List` 值需要的大小了。图 15-2 展示了现在 `Cons` 成员看起来像什么: A finite Cons list -图 15-7:因为 `Cons` 存放一个 `Box` 所以 `List` 不是无限大小的了 +图 15-2:因为 `Cons` 存放一个 `Box` 所以 `List` 不是无限大小的了 -box 只提供了间接存储和堆分配;他们并没有任何其他特殊的功能,比如我们将会见到的其他智能指针。他们也没有这些特殊功能带来的性能损失,所以他们可以用于像 cons list 这样间接存储是唯一所需功能的场景。我们还将在第十七章看到 box 的更多应用场景。 +box 只提供了间接存储和堆分配;他们并没有任何其他特殊的功能,比如我们将会见到的其他智能指针。它们也没有这些特殊功能带来的性能损失,所以他们可以用于像 cons list 这样间接存储是唯一所需功能的场景。我们还将在第十七章看到 box 的更多应用场景。 `Box` 类型是一个智能指针,因为它实现了 `Deref` trait,它允许 `Box` 值被当作引用对待。当 `Box` 值离开作用域时,由于 `Box` 类型 `Drop` trait 的实现,box 所指向的堆数据也会被清除。让我们更详细的探索一下这两个 trait;这些 trait 在本章余下讨论的其他智能指针所提供的功能中将会更为重要。 - - - diff --git a/src/ch15-02-deref.md b/src/ch15-02-deref.md index 27aecf0..ef79c4a 100644 --- a/src/ch15-02-deref.md +++ b/src/ch15-02-deref.md @@ -1,49 +1,18 @@ ## 通过 `Deref` trait 将智能指针当作常规引用处理 -> [ch15-02-deref.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch15-02-deref.md) +> [ch15-02-deref.md](https://github.com/rust-lang/book/blob/master/src/ch15-02-deref.md) >
-> commit d06a6a181fd61704cbf7feb55bc61d518c6469f9 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -实现 `Deref` trait 允许我们重载 **解引用运算符**(*dereference operator*)`*`(与乘法运算符或 glob 运算符相区别)。通过这种方式实现 `Deref` trait 可以被当作常规引用来对待,可以编写操作引用的代码并用于智能指针。 +实现 `Deref` trait 允许我们重载 **解引用运算符**(*dereference operator*)`*`(与乘法运算符或 glob 运算符相区别)。通过这种方式实现 `Deref` trait 的智能指针可以被当作常规引用来对待,可以编写操作引用的代码并用于智能指针。 - - +让我们首先看看解引用运算符如何处理常规引用,接着尝试定义我们自己的类似 `Box` 的类型并看看为何解引用运算符不能像引用一样工作。我们会探索如何实现 `Deref` trait 使得智能指针以类似引用的方式工作变为可能。最后,我们会讨论 Rust 的 **解引用强制多态**(*deref coercions*)功能和它是如何一同处理引用或智能指针的。 - - +> 我们将要构建的 `MyBox` 类型与真正的 `Box` 有一个巨大的区别:我们的版本不会在堆上储存数据。这个例子重点关注 Deref`,所以其数据实际存放在何处相比其类似指针的行为来说不算重要。 -让我们首先看看 `*` 如何处理引用,接着尝试定义我们自己的类 `Box` 类型并看看为何 `*` 不能像引用一样工作。我们会探索如何实现 `Deref` trait 使得智能指针以类似引用的方式工作变为可能。最后,我们会讨论 Rust 的 **解引用强制多态**(*deref coercions*)功能和它是如何一同处理引用或智能指针的。 +### 通过解引用运算符追踪指针的值 -### 通过 `*` 追踪指针的值 - - - - - - - -常规引用是一个指针类型,一种理解指针的方式是将其看成指向储存在其他某处值的箭头。在示例 15-8 中,创建了一个 `i32` 值的引用接着使用解引用运算符来跟踪所引用的数据: - - - - - - +常规引用是一个指针类型,一种理解指针的方式是将其看成指向储存在其他某处值的箭头。在示例 15-6 中,创建了一个 `i32` 值的引用接着使用解引用运算符来跟踪所引用的数据: 文件名: src/main.rs @@ -57,7 +26,7 @@ fn main() { } ``` -示例 15-8:使用解引用运算符来跟踪 `i32` 值的引用 +示例 15-6:使用解引用运算符来跟踪 `i32` 值的引用 变量 `x` 存放了一个 `i32` 值 `5`。`y` 等于 `x` 的一个引用。可以断言 `x` 等于 `5`。然而,如果希望对 `y` 的值做出断言,必须使用 `*y` 来追踪引用所指向的值(也就是 **解引用**)。一旦解引用了 `y`,就可以访问 `y` 所指向的整型值并可以与 `5` 做比较。 @@ -75,11 +44,11 @@ not satisfied `{integer}` ``` -不允许比较数字的引用与数字,因为它们是不同的类型。必须使用 `*` 追踪引用所指向的值。 +不允许比较数字的引用与数字,因为它们是不同的类型。必须使用解引用运算符追踪引用所指向的值。 ### 像引用一样使用 `Box` -可以重写示例 15-8 中的代码,使用 `Box` 来代替引用,解引用运算符也一样能工作,如示例 15-9 所示: +可以使用 `Box` 代替引用来重写示例 15-6 中的代码,解引用运算符也一样能工作,如示例 15-7 所示: 文件名: src/main.rs @@ -93,15 +62,15 @@ fn main() { } ``` -示例 15-9:在 `Box` 上使用解引用运算符 +示例 15-7:在 `Box` 上使用解引用运算符 -相比示例 15-8 唯一修改的地方就是将 `y` 设置为一个指向 `x` 值的 box 实例,而不是指向 `x` 值的引用。在最后的断言中,可以使用解引用运算符以 `y` 为引用时相同的方式追踪 box 的指针。让我们通过实现自己的 box 类型来探索 `Box` 能这么做有何特殊之处。 +示例 15-7 相比示例 15-6 唯一不同的地方就是将 `y` 设置为一个指向 `x` 值的 box 实例,而不是指向 `x` 值的引用。在最后的断言中,可以使用解引用运算符以 `y` 为引用时相同的方式追踪 box 的指针。接下来让我们通过实现自己的 box 类型来探索 `Box` 能这么做有何特殊之处。 ### 自定义智能指针 为了体会默认智能指针的行为不同于引用,让我们创建一个类似于标准库提供的 `Box` 类型的智能指针。接着会学习如何增加使用解引用运算符的功能。 -从根本上说,`Box` 被定义为包含一个元素的元组结构体,所以示例 15-10 以相同的方式定义了 `MyBox` 类型。我们还定义了 `new` 函数来对应定义于 `Box` 的 `new` 函数: +从根本上说,`Box` 被定义为包含一个元素的元组结构体,所以示例 15-8 以相同的方式定义了 `MyBox` 类型。我们还定义了 `new` 函数来对应定义于 `Box` 的 `new` 函数: 文件名: src/main.rs @@ -115,15 +84,15 @@ impl MyBox { } ``` -示例 15-10:定义 `MyBox` 类型 +示例 15-8:定义 `MyBox` 类型 -这里定义了一个结构体 `MyBox` 并声明了一个泛型 `T`,因为我们希望其可以存放任何类型的值。`MyBox` 是一个包含 `T` 类型元素的元组结构体。`MyBox::new` 函数获取一个 `T` 类型的参数并返回一个存放传入值的 `MyBox` 实例。 +这里定义了一个结构体 `MyBox` 并声明了一个泛型参数 `T`,因为我们希望其可以存放任何类型的值。`MyBox` 是一个包含 `T` 类型元素的元组结构体。`MyBox::new` 函数获取一个 `T` 类型的参数并返回一个存放传入值的 `MyBox` 实例。 -尝试将示例 15-9 中的代码加入示例 15-10 中并修改 `main` 使用我们定义的 `MyBox` 类型代替 `Box`。示例 15-11 中的代码不能编译,因为 Rust 不知道如何解引用 `MyBox`: +尝试将示例 15-7 中的代码加入示例 15-8 中并修改 `main` 使用我们定义的 `MyBox` 类型代替 `Box`。示例 15-9 中的代码不能编译,因为 Rust 不知道如何解引用 `MyBox`: 文件名: src/main.rs -```rust,ignore +```rust,ignore,does_not_compile fn main() { let x = 5; let y = MyBox::new(x); @@ -133,23 +102,23 @@ fn main() { } ``` -示例 15-11:尝试以使用引用和 `Box` 相同的方式使用 `MyBox` +示例 15-9:尝试以使用引用和 `Box` 相同的方式使用 `MyBox` 得到的编译错误是: ```text -error: type `MyBox<{integer}>` cannot be dereferenced +error[E0614]: type `MyBox<{integer}>` cannot be dereferenced --> src/main.rs:14:19 | 14 | assert_eq!(5, *y); | ^^ ``` -`MyBox` 类型不能解引用我们并没有为其实现这个功能。为了启用 `*` 运算符的解引用功能,可以实现 `Deref` trait。 +`MyBox` 类型不能解引用我们并没有为其实现这个功能。为了启用 `*` 运算符的解引用功能,需要实现 `Deref` trait。 -### 实现 `Deref` trait 定义如何像引用一样对待某类型 +### 通过实现 `Deref` trait 将某类型像引用一样处理 -如第十章所讨论的,为了实现 trait,需要提供 trait 所需的方法实现。`Deref` trait,由标准库提供,要求实现名为 `deref` 的方法,其借用 `self` 并返回一个内部数据的引用。示例 15-12 包含定义于 `MyBox` 之上的 `Deref` 实现: +如第十章所讨论的,为了实现 trait,需要提供 trait 所需的方法实现。`Deref` trait,由标准库提供,要求实现名为 `deref` 的方法,其借用 `self` 并返回一个内部数据的引用。示例 15-10 包含定义于 `MyBox` 之上的 `Deref` 实现: 文件名: src/main.rs @@ -166,49 +135,34 @@ impl Deref for MyBox { } ``` -示例 15-12:`MyBox` 上的 `Deref` 实现 +示例 15-10:`MyBox` 上的 `Deref` 实现 `type Target = T;` 语法定义了用于此 trait 的关联类型。关联类型是一个稍有不同的定义泛型参数的方式,现在还无需过多的担心它;第十九章会详细介绍。 - - +`deref` 方法体中写入了 `&self.0`,这样 `deref` 返回了我希望通过 `*` 运算符访问的值的引用。示例 15-9 中的 `main` 函数中对 `MyBox` 值的 `*` 调用现在可以编译并能通过断言了! -`deref` 方法体中写入了 `&self.0`,这样 `deref` 返回了我希望通过 `*` 运算符访问的值的引用。示例 15-11 中的 `main` 函数中对 `MyBox` 值的 `*` 调用现在可以编译并能通过断言了! +没有 `Deref` trait 的话,编译器只会解引用 `&` 引用类型。`deref` 方法向编译器提供了获取任何实现了 `Deref` trait 的类型的值并调用这个类型的 `deref` 方法来获取一个它知道如何解引用的 `&` 引用的能力。 -没有 `Deref` trait 的话,编译器可以解引用的只有 `&` 引用类型;有了 `Deref` trait 之后,对任何实现 `Deref` trait 的类型,编译器都能(通过解引用的形式)从其获取一个值。只要调用这个类型的 `deref` 方法,编译器就可以得到一个 `&` 引用,再对 `&` 引用进行解引用对它来说就是熟悉的操作了。 - -当我们在示例 15-11 中输入 `*y` 时,Rust 事实上在底层运行了如下代码: +当我们在示例 15-9 中输入 `*y` 时,Rust 事实上在底层运行了如下代码: ```rust,ignore *(y.deref()) ``` - - - Rust 将 `*` 运算符替换为先调用 `deref` 方法再进行直接引用的操作,如此我们便不用担心是不是还需要手动调用 `deref` 方法了。Rust 的这个特性可以让我们写出行为一致的代码,无论是面对的是常规引用还是实现了 `Deref` 的类型。 `deref` 方法返回值的引用,以及 `*(y.deref())` 括号外边的普通解引用仍为必须的原因在于所有权。如果 `deref` 方法直接返回值而不是值的引用,其值(的所有权)将被移出 `self`。在这里以及大部分使用解引用运算符的情况下我们并不希望获取 `MyBox` 内部值的所有权。 -注意,每次当我们在代码中使用 `*` 时, `*` 运算符都被替换成了先调用 `deref` 方法再接着使用 `*` 解引用的操作,且只会发生一次,不会对 `*` 操作符无限递归替换,解引用出上面 `i32` 类型的值就停止了,这个值与示例 15-11 中 `assert_eq!` 的 `5` 相匹配。 +注意,每次当我们在代码中使用 `*` 时, `*` 运算符都被替换成了先调用 `deref` 方法再接着使用 `*` 解引用的操作,且只会发生一次,不会对 `*` 操作符无限递归替换,解引用出上面 `i32` 类型的值就停止了,这个值与示例 15-9 中 `assert_eq!` 的 `5` 相匹配。 ### 函数和方法的隐式解引用强制多态 - - - **解引用强制多态**(*deref coercions*)是 Rust 表现在函数或方法传参上的一种便利。其将实现了 `Deref` 的类型的引用转换为原始类型通过 `Deref` 所能够转换的类型的引用。当这种特定类型的引用作为实参传递给和形参类型不同的函数或方法时,解引用强制多态将自动发生。这时会有一系列的 `deref` 方法被调用,把我们提供的类型转换成了参数所需的类型。 解引用强制多态的加入使得 Rust 程序员编写函数和方法调用时无需增加过多显式使用 `&` 和 `*` 的引用和解引用。这个功能也使得我们可以编写更多同时作用于引用或智能指针的代码。 -作为展示解引用强制多态的实例,让我们使用示例 15-10 中定义的 `MyBox`,以及示例 15-12 中增加的 `Deref` 实现。示例 15-13 展示了一个有着字符串 slice 参数的函数定义: +作为展示解引用强制多态的实例,让我们使用示例 15-8 中定义的 `MyBox`,以及示例 15-10 中增加的 `Deref` 实现。示例 15-11 展示了一个有着字符串 slice 参数的函数定义: 文件名: src/main.rs @@ -218,9 +172,9 @@ fn hello(name: &str) { } ``` -示例 15-13:`hello` 函数有着 `&str` 类型的参数 `name` +示例 15-11:`hello` 函数有着 `&str` 类型的参数 `name` -可以使用字符串 slice 作为参数调用 `hello` 函数,比如 `hello("Rust");`。解引用强制多态使得用 `MyBox` 类型值的引用调用 `hello` 成为可能,如示例 15-14 所示: +可以使用字符串 slice 作为参数调用 `hello` 函数,比如 `hello("Rust");`。解引用强制多态使得用 `MyBox` 类型值的引用调用 `hello` 成为可能,如示例 15-12 所示: 文件名: src/main.rs @@ -253,11 +207,11 @@ fn main() { } ``` -示例 15-14:因为解引用强制多态,使用 `MyBox` 的引用调用 `hello` 是可行的 +示例 15-12:因为解引用强制多态,使用 `MyBox` 的引用调用 `hello` 是可行的 -这里使用 `&m` 调用 `hello` 函数,其为 `MyBox` 值的引用。因为示例 15-12 中在 `MyBox` 上实现了 `Deref` trait,Rust 可以通过 `deref` 调用将 `&MyBox` 变为 `&String`。标准库中提供了 `String` 上的 `Deref` 实现,其会返回字符串 slice,这可以在 `Deref` 的 API 文档中看到。Rust 再次调用 `deref` 将 `&String` 变为 `&str`,这就符合 `hello` 函数的定义了。 +这里使用 `&m` 调用 `hello` 函数,其为 `MyBox` 值的引用。因为示例 15-10 中在 `MyBox` 上实现了 `Deref` trait,Rust 可以通过 `deref` 调用将 `&MyBox` 变为 `&String`。标准库中提供了 `String` 上的 `Deref` 实现,其会返回字符串 slice,这可以在 `Deref` 的 API 文档中看到。Rust 再次调用 `deref` 将 `&String` 变为 `&str`,这就符合 `hello` 函数的定义了。 -如果 Rust 没有实现解引用强制多态,为了使用 `&MyBox` 类型的值调用 `hello`,则不得不编写示例 15-15 中的代码来代替示例 15-14: +如果 Rust 没有实现解引用强制多态,为了使用 `&MyBox` 类型的值调用 `hello`,则不得不编写示例 15-13 中的代码来代替示例 15-12: 文件名: src/main.rs @@ -290,7 +244,7 @@ fn main() { } ``` -示例 15-15:如果 Rust 没有解引用强制多态则必须编写的代码 +示例 15-13:如果 Rust 没有解引用强制多态则必须编写的代码 `(*m)` 将 `MyBox` 解引用为 `String`。接着 `&` 和 `[..]` 获取了整个 `String` 的字符串 slice 来匹配 `hello` 的签名。没有解引用强制多态所有这些符号混在一起将更难以读写和理解。解引用强制多态使得 Rust 自动的帮我们处理这些转换。 @@ -298,33 +252,14 @@ fn main() { ### 解引用强制多态如何与可变性交互 - - - 类似于如何使用 `Deref` trait 重载不可变引用的 `*` 运算符,Rust 提供了 `DerefMut` trait 用于重载可变引用的 `*` 运算符。 Rust 在发现类型和 trait 实现满足三种情况时会进行解引用强制多态: - - - * 当 `T: Deref` 时从 `&T` 到 `&U`。 * 当 `T: DerefMut` 时从 `&mut T` 到 `&mut U`。 * 当 `T: Deref` 时从 `&mut T` 到 `&U`。 头两个情况除了可变性之外是相同的:第一种情况表明如果有一个 `&T`,而 `T` 实现了返回 `U` 类型的 `Deref`,则可以直接得到 `&U`。第二种情况表明对于可变引用也有着相同的行为。 -最后一个情况有些微妙:Rust 也会将可变引用强转为不可变引用。但是反之是 **不可能** 的:不可变引用永远也不能强转为可变引用。因为根据借用规则,如果有一个可变引用,其必须是这些数据的唯一引用(否则程序将无法编译)。将一个可变引用转换为不可变引用永远也不会打破借用规则。将不可变引用转换为可变引用则需要数据只能有一个不可变引用,而借用规则无法保证这一点。因此,Rust 无法假设将不可变引用转换为可变引用是可能的。 - - - +第三个情况有些微妙:Rust 也会将可变引用强转为不可变引用。但是反之是 **不可能** 的:不可变引用永远也不能强转为可变引用。因为根据借用规则,如果有一个可变引用,其必须是这些数据的唯一引用(否则程序将无法编译)。将一个可变引用转换为不可变引用永远也不会打破借用规则。将不可变引用转换为可变引用则需要数据只能有一个不可变引用,而借用规则无法保证这一点。因此,Rust 无法假设将不可变引用转换为可变引用是可能的。 diff --git a/src/ch15-03-drop.md b/src/ch15-03-drop.md index ba62d57..633ff3e 100644 --- a/src/ch15-03-drop.md +++ b/src/ch15-03-drop.md @@ -1,18 +1,16 @@ ## 使用 `Drop` Trait 运行清理代码 -> [ch15-03-drop.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch15-03-drop.md) +> [ch15-03-drop.md](https://github.com/rust-lang/book/blob/master/src/ch15-03-drop.md) >
-> commit 6060440d67759b7c8627b4d97cb69576057f5fa6 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -对于智能指针模式来说另一个重要的 trait 是 `Drop`。`Drop` 允许我们在值要离开作用域时执行一些代码。可以为任何类型提供 `Drop` trait 的实现,同时所指定的代码被用于释放类似于文件或网络连接的资源。我们在智能指针上下文中讨论 `Drop` 是因为其功能几乎总是用于实现智能指针。例如,`Box` 自定义了 `Drop` 用来释放 box 所指向的堆空间。 +对于智能指针模式来说第二个重要的 trait 是 `Drop`,其允许我们在值要离开作用域时执行一些代码。可以为任何类型提供 `Drop` trait 的实现,同时所指定的代码被用于释放类似于文件或网络连接的资源。我们在智能指针上下文中讨论 `Drop` 是因为其功能几乎总是用于实现智能指针。例如,`Box` 自定义了 `Drop` 用来释放 box 所指向的堆空间。 -在其他一些语言中,我们不得不记住在每次使用完智能指针实例后调用清理内存或资源的代码。如果忘记的话,运行代码的系统可能会因为负荷过重而崩溃。在 Rust 中,可以指定一些代码应该在值离开作用域时被执行,而编译器会自动插入这些代码。于是我们就不需要在程序中到处编写在实例结束时清理这些变量的代码——而且还不会泄露资源了。 - -这意味着无需记住在所有处理完这些类型实例后调用清理代码,而仍然不会泄露资源! +在其他一些语言中,我们不得不记住在每次使用完智能指针实例后调用清理内存或资源的代码。如果忘记的话,运行代码的系统可能会因为负荷过重而崩溃。在 Rust 中,可以指定一些代码应该在值离开作用域时被执行,而编译器会自动插入这些代码。于是我们就不需要在程序中到处编写在实例结束时清理这些变量的代码 —— 而且还不会泄露资源。 指定在值离开作用域时应该执行的代码的方式是实现 `Drop` trait。`Drop` trait 要求实现一个叫做 `drop` 的方法,它获取一个 `self` 的可变引用。为了能够看出 Rust 何时调用 `drop`,让我们暂时使用 `println!` 语句实现 `drop`。 -示例 15-16 展示了唯一定制功能就是当其实例离开作用域时打印出 `Dropping CustomSmartPointer!` 的结构体 `CustomSmartPointer`。这会演示 Rust 何时运行 `drop` 函数: +示例 15-14 展示了唯一定制功能就是当其实例离开作用域时打印出 `Dropping CustomSmartPointer!` 的结构体 `CustomSmartPointer`。这会演示 Rust 何时运行 `drop` 函数: 文件名: src/main.rs @@ -52,13 +50,13 @@ Dropping CustomSmartPointer with data `my stuff`! #### 通过 `std::mem::drop` 提早丢弃值 -不幸的是,我们并不能直截了当的禁用 `drop` 这个功能。通常也不需要禁用 `drop` ;整个 `Drop` trait 存在的意义在于其是自动处理的。然而,有时你可能需要提早清理某个值。一个例子是当使用智能指针管理锁时;你可能希望强制运行 `drop` 方法来释放锁以便作用域中的其他代码可以获取锁。Rust 并不允许我们主动调用 `Drop` trait 的 `drop` 方法;当我们希望在作用域结束之前就释放变量的话,我们应该使用的是由标准库提供的 `std::mem::drop`。 +不幸的是,我们并不能直截了当的禁用 `drop` 这个功能。通常也不需要禁用 `drop` ;整个 `Drop` trait 存在的意义在于其是自动处理的。然而,有时你可能需要提早清理某个值。一个例子是当使用智能指针管理锁时;你可能希望强制运行 `drop` 方法来释放锁以便作用域中的其他代码可以获取锁。Rust 并不允许我们主动调用 `Drop` trait 的 `drop` 方法;当我们希望在作用域结束之前就强制释放变量的话,我们应该使用的是由标准库提供的 `std::mem::drop`。 如果我们像是示例 15-14 那样尝试调用 `Drop` trait 的 `drop` 方法,就会得到像示例 15-15 那样的编译错误: 文件名: src/main.rs -```rust,ignore +```rust,ignore,does_not_compile fn main() { let c = CustomSmartPointer { data: String::from("some data") }; println!("CustomSmartPointer created."); @@ -83,7 +81,7 @@ error[E0040]: explicit use of destructor method Rust 不允许我们显式调用 `drop` 因为 Rust 仍然会在 `main` 的结尾对值自动调用 `drop`,这会导致一个 **double free** 错误,因为 Rust 会尝试清理相同的值两次。 -因为不能禁用当值离开作用域时自动插入的 `drop`,并且不能显示调用 `drop`,如果我们需要提早清理值,可以使用 `std::mem::drop` 函数。 +因为不能禁用当值离开作用域时自动插入的 `drop`,并且不能显示调用 `drop`,如果我们需要强制提早清理值,可以使用 `std::mem::drop` 函数。 `std::mem::drop` 函数不同于 `Drop` trait 中的 `drop` 方法。可以通过传递希望提早强制丢弃的值作为参数。`std::mem::drop` 位于 prelude,所以我们可以修改示例 15-15 中的 `main` 来调用 `drop` 函数如示例 15-16 所示: @@ -124,6 +122,4 @@ CustomSmartPointer dropped before the end of main. 我们也无需担心意外的清理掉仍在使用的值,这会造成编译器错误:所有权系统确保引用总是有效的,也会确保 `drop` 只会在值不再被使用时被调用一次。 -使用 `Drop` trait 实现指定的代码在很多方面都使得清理值变得方便和安全:比如可以使用它来创建我们自己的内存分配器!通过`Drop` trait 和 Rust 所有权系统,就无需担心之后清理代码,因为 Rust 会自动考虑这些问题。如果代码在值仍被使用时就清理它会出现编译错误,因为所有权系统确保了引用总是有效的,这也就保证了`drop`只会在值不再被使用时被调用一次。 - 现在我们学习了 `Box` 和一些智能指针的特性,让我们聊聊一些其他标准库中定义的智能指针。 \ No newline at end of file diff --git a/src/ch15-04-rc.md b/src/ch15-04-rc.md index 5f86fd5..f885a7a 100644 --- a/src/ch15-04-rc.md +++ b/src/ch15-04-rc.md @@ -1,15 +1,11 @@ ## `Rc` 引用计数智能指针 -> [ch15-04-rc.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch15-04-rc.md) +> [ch15-04-rc.md](https://github.com/rust-lang/book/blob/master/src/ch15-04-rc.md) >
-> commit 071b97540bca12fd416d2ea7a2daa5d3e9c74400 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 大部分情况下所有权是非常明确的:可以准确的知道哪个变量拥有某个值。然而,有些情况单个值可能会有多个所有者。例如,在图数据结构中,多个边可能指向相同的结点,而这个结点从概念上讲为所有指向它的边所拥有。结点直到没有任何边指向它之前都不应该被清理。 - - - 为了启用多所有权,Rust 有一个叫做 `Rc` 的类型。其名称为 **引用计数**(*reference counting*)的缩写。引用计数意味着记录一个值引用的数量来知晓这个值是否仍在被使用。如果某个值有零个引用,就代表没有任何有效引用并可以被清理。 可以将其想象为客厅中的电视。当一个人进来看电视时,他打开电视。其他人也可以进来看电视。当最后一个人离开房间时,他关掉电视因为它不再被使用了。如果某人在其他人还在看的时候就关掉了电视,正在看电视的人肯定会抓狂的! @@ -20,19 +16,19 @@ ### 使用 `Rc` 共享数据 -让我们回到示例 15-6 中使用 `Box` 定义 cons list 的例子。这一次,我们希望创建两个共享第三个列表所有权的列表,其概念将会看起来如图 15-19 所示: +让我们回到示例 15-5 中使用 `Box` 定义 cons list 的例子。这一次,我们希望创建两个共享第三个列表所有权的列表,其概念将会看起来如图 15-3 所示: Two lists that share ownership of a third list -图 15-19: 两个列表, `b` 和 `c`, 共享第三个列表 `a` 的所有权 +图 15-3: 两个列表, `b` 和 `c`, 共享第三个列表 `a` 的所有权 列表 `a` 包含 5 之后是 10,之后是另两个列表:`b` 从 3 开始而 `c` 从 4 开始。`b` 和 `c` 会接上包含 5 和 10 的列表 `a`。换句话说,这两个列表会尝试共享第一个列表所包含的 5 和 10。 -尝试使用 `Box` 定义的 `List` 并实现不能工作,如示例 15-20 所示: +尝试使用 `Box` 定义的 `List` 并实现不能工作,如示例 15-17 所示: 文件名: src/main.rs -```rust,ignore +```rust,ignore,does_not_compile enum List { Cons(i32, Box), Nil, @@ -49,7 +45,7 @@ fn main() { } ``` -示例 15-20: 展示不能用两个 `Box` 的列表尝试共享第三个列表的所有权 +示例 15-17: 展示不能用两个 `Box` 的列表尝试共享第三个列表的所有权 编译会得出如下错误: @@ -62,21 +58,15 @@ error[E0382]: use of moved value: `a` 13 | let c = Cons(4, Box::new(a)); | ^ value used here after move | - = note: move occurs because `a` has type `List`, which does not - implement the `Copy` trait + = note: move occurs because `a` has type `List`, which does not implement + the `Copy` trait ``` `Cons` 成员拥有其储存的数据,所以当创建 `b` 列表时,`a` 被移动进了 `b` 这样 `b` 就拥有了 `a`。接着当再次尝使用 `a` 创建 `c` 时,这不被允许因为 `a` 的所有权已经被移动。 可以改变 `Cons` 的定义来存放一个引用,不过接着必须指定生命周期参数。通过指定生命周期参数,表明列表中的每一个元素都至少与列表本身存在的一样久。例如,借用检查器不会允许 `let a = Cons(10, &Nil);` 编译,因为临时值 `Nil` 会在 `a` 获取其引用之前就被丢弃了。 -相反,我们修改 `List` 的定义为使用 `Rc` 代替 `Box`,如列表 15-21 所示。现在每一个 `Cons` 变量都包含一个值和一个指向 `List` 的 `Rc`。当创建 `b` 时,不同于获取 `a` 的所有权,这里会克隆 `a` 所包含的 `Rc`,这会将引用计数从 1 增加到 2 并允许 `a` 和 `b` 共享 `Rc` 中数据的所有权。创建 `c` 时也会克隆 `a`,这会将引用计数从 2 增加为 3。每次调用 `Rc::clone`,`Rc` 中数据的引用计数都会增加,直到有零个引用之前其数据都不会被清理: - - - +相反,我们修改 `List` 的定义为使用 `Rc` 代替 `Box`,如列表 15-18 所示。现在每一个 `Cons` 变量都包含一个值和一个指向 `List` 的 `Rc`。当创建 `b` 时,不同于获取 `a` 的所有权,这里会克隆 `a` 所包含的 `Rc`,这会将引用计数从 1 增加到 2 并允许 `a` 和 `b` 共享 `Rc` 中数据的所有权。创建 `c` 时也会克隆 `a`,这会将引用计数从 2 增加为 3。每次调用 `Rc::clone`,`Rc` 中数据的引用计数都会增加,直到有零个引用之前其数据都不会被清理。 文件名: src/main.rs @@ -96,30 +86,17 @@ fn main() { } ``` -示例 15-21: 使用 `Rc` 定义的 `List` +示例 15-18: 使用 `Rc` 定义的 `List` -需要为 `Rc` 增加`use`语句因为它不在 prelude 中。在 `main` 中创建了存放 5 和 10 的列表并将其存放在 `a` 的新的 `Rc` 中。接着当创建 `b` 和 `c` 时,调用 `Rc::clone` 函数并传递 `a` 中 `Rc` 的引用作为参数。 +需要使用 `use` 语句将 `Rc` 引入作用域因为它不在 prelude 中。在 `main` 中创建了存放 5 和 10 的列表并将其存放在 `a` 的新的 `Rc` 中。接着当创建 `b` 和 `c` 时,调用 `Rc::clone` 函数并传递 `a` 中 `Rc` 的引用作为参数。 -也可以调用 `a.clone()` 而不是 `Rc::clone(&a)`,不过在这里 Rust 的习惯是使用 `Rc::clone`。`Rc::clone` 的实现并不像大部分类型的 `clone` 实现那样对所有数据进行深拷贝。`Rc::clone` 只会增加引用计数,这并不会花费多少时间。深拷贝可能会花费很长时间,所以通过使用 `Rc::clone` 进行引用计数,可以明显的区别可能会对运行时性能有巨大影响的深拷贝和不分配内存的对运行时性能影响相对较小的增加引用计数拷贝。 +也可以调用 `a.clone()` 而不是 `Rc::clone(&a)`,不过在这里 Rust 的习惯是使用 `Rc::clone`。`Rc::clone` 的实现并不像大部分类型的 `clone` 实现那样对所有数据进行深拷贝。`Rc::clone` 只会增加引用计数,这并不会花费多少时间。深拷贝可能会花费很长时间。通过使用 `Rc::clone` 进行引用计数,可以明显的区别深拷贝类的克隆和增加引用计数类的克隆。当查找代码中的性能问题时,只需考虑神拷贝类克隆而无需考虑 `Rc::clone` 调用。 ### 克隆 `Rc` 会增加引用计数 -让我们修改示例 15-21 的代码以便观察创建和丢弃 `a` 中 `Rc` 的引用时引用计数的变化。 - - - +让我们修改示例 15-18 的代码以便观察创建和丢弃 `a` 中 `Rc` 的引用时引用计数的变化。 -在示例 15-22 中,修改了 `main` 以便将列表 `c` 置于内部作用域中,这样就可以观察当 `c` 离开作用域时引用计数如何变化。在程序中每个引用计数变化的点,会打印出引用计数,其值可以通过调用 `Rc::strong_count` 函数获得。在本章稍后的部分讨论避免引用循环时会解释为何这个函数叫做 `strong_count` 而不是 `count`。 - - - +在示例 15-19 中,修改了 `main` 以便将列表 `c` 置于内部作用域中,这样就可以观察当 `c` 离开作用域时引用计数如何变化。 文件名: src/main.rs @@ -145,9 +122,11 @@ fn main() { } ``` -示例 15-22:打印出引用计数 +示例 15-19:打印出引用计数 + +在程序中每个引用计数变化的点,会打印出引用计数,其值可以通过调用 `Rc::strong_count` 函数获得。这个函数叫做 `strong_count` 而不是 `count` 是因为 `Rc` 也有 `weak_count`;在 “避免引用循环” 部分会讲解 `weak_count` 的用途。 -这会打印出: +这段代码会打印出: ```text count after creating a = 1 @@ -156,11 +135,7 @@ count after creating c = 3 count after c goes out of scope = 2 ``` - - - -我们能够看到 `a` 中 `Rc` 的初始引用计数为一,接着每次调用 `clone`,计数会增加一。当 `c` 离开作用域时,计数减一。不必像调用 `Rc::clone` 增加引用计数那样调用一个函数来减少计数;`Drop` trait 的实现当 `Rc` 值离开作用域时自动减少引用计数。 +我们能够看到 `a` 中 `Rc` 的初始引用计数为一,接着每次调用 `clone`,计数会增加一。当 `c` 离开作用域时,计数减一。不必像调用 `Rc::clone` 增加引用计数那样调用一个函数来减少计数;`Drop` trait 的实现当 `Rc` 值离开作用域时自动减少引用计数。 从这个例子我们所不能看到的是在 `main` 的结尾当 `b` 然后是 `a` 离开作用域时,此处计数会是 0,同时 `Rc` 被完全清理。使用 `Rc` 允许一个值有多个所有者,引用计数则确保只要任何所有者依然存在其值也保持有效。 diff --git a/src/ch15-05-interior-mutability.md b/src/ch15-05-interior-mutability.md index 05354a9..d169bcc 100644 --- a/src/ch15-05-interior-mutability.md +++ b/src/ch15-05-interior-mutability.md @@ -1,24 +1,12 @@ ## `RefCell` 和内部可变性模式 -> [ch15-05-interior-mutability.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch15-05-interior-mutability.md) +> [ch15-05-interior-mutability.md](https://github.com/rust-lang/book/blob/master/src/ch15-05-interior-mutability.md) >
-> commit 54169ef43f57847913ebec7e021c1267663a5d12 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f - - + - - - -**内部可变性**(*Interior mutability*)是 Rust 中的一个设计模式,它允许你即使在有不可变引用时改变数据,这通常是借用规则所不允许的。为此,该模式在数据结构中使用 `unsafe` 代码来模糊 Rust 通常的可变性和借用规则。我们还未讲到不安全代码;第十九章会学习它们。当可以确保代码在运行时会遵守借用规则,即使编译器不能保证的情况,可以选择使用那些运用内部可变性模式的类型。所涉及的 `unsafe` 代码将被封装进安全的 API 中,而外部类型仍然是不可变的。 +**内部可变性**(*Interior mutability*)是 Rust 中的一个设计模式,它允许你即使在有不可变引用时改变数据,这通常是借用规则所不允许的。为了改变数据,该模式在数据结构中使用 `unsafe` 代码来模糊 Rust 通常的可变性和借用规则。我们还未讲到不安全代码;第十九章会学习它们。当可以确保代码在运行时会遵守借用规则,即使编译器不能保证的情况,可以选择使用那些运用内部可变性模式的类型。所涉及的 `unsafe` 代码将被封装进安全的 API 中,而外部类型仍然是不可变的。 让我们通过遵循内部可变性模式的 `RefCell` 类型来开始探索。 @@ -26,46 +14,32 @@ pattern. /Carol --> 不同于 `Rc`,`RefCell` 代表其数据的唯一的所有权。那么是什么让 `RefCell` 不同于像 `Box` 这样的类型呢?回忆一下第四章所学的借用规则: -1. 在任意给定时间,**只能** 拥有如下中的一个: - * 一个可变引用。 - * 任意数量的不可变引用。 +1. 在任意给定时间,只能拥有一个可变引用或任意数量的不可变引用 **之一**(而不是全部)。 2. 引用必须总是有效的。 -对于引用和 `Box`,借用规则的不可变性作用于编译时。对于 `RefCell`,这些不可变性作用于 **运行时**。对于引用,如果违反这些规则,会得到一个编译错误。而对于`RefCell`,违反这些规则会 `panic!`。 - - - +对于引用和 `Box`,借用规则的不可变性作用于编译时。对于 `RefCell`,这些不可变性作用于 **运行时**。对于引用,如果违反这些规则,会得到一个编译错误。而对于 `RefCell`,如果违反这些规则程序会 panic 并退出。 -在编译时检查借用规则的好处是这些错误将在开发过程的早期被捕获同时对没有运行时性能影响,因为所有的分析都提前完成了。为此,在编译时检查借用规则是大部分情况的最佳选择,这也正是其为何是 Rust 的默认行为。 +在编译时检查借用规则的优势是这些错误将在开发过程的早期被捕获同时对没有运行时性能影响,因为所有的分析都提前完成了。为此,在编译时检查借用规则是大部分情况的最佳选择,这也正是其为何是 Rust 的默认行为。 -相反在运行时检查借用规则的好处是特定内存安全的场景是允许的,而它们在编译时检查中是不允许的。静态分析,正如 Rust 编译器,是天生保守的。代码的一些属性则不可能通过分析代码发现:其中最著名的就是 [停机问题(Halting Problem)](https://zh.wikipedia.org/wiki/%E5%81%9C%E6%9C%BA%E9%97%AE%E9%A2%98),这超出了本书的范畴,不过如果你感兴趣的话这是一个值得研究的有趣主题。 +相反在运行时检查借用规则的好处则是允许出现特定内存安全的场景,而它们在编译时检查中是不允许的。静态分析,正如 Rust 编译器,是天生保守的。代码的一些属性则不可能通过分析代码发现:其中最著名的就是 [停机问题(Halting Problem)](https://zh.wikipedia.org/wiki/%E5%81%9C%E6%9C%BA%E9%97%AE%E9%A2%98),这超出了本书的范畴,不过如果你感兴趣的话这是一个值得研究的有趣主题。 - - - -因为一些分析是不可能的,如果 Rust 编译器不能通过所有权规则编译,它可能会拒绝一个正确的程序;从这种角度考虑它是保守的。如果 Rust 接受不正确的程序,那么人们也就不会相信 Rust 所做的保证了。然而,如果 Rust 拒绝正确的程序,会给程序员带来不便,但不会带来灾难。`RefCell` 正是用于当你确信代码遵守借用规则,而编译器不能理解和确定的时候。 +因为一些分析是不可能的,如果 Rust 编译器不能通过所有权规则编译,它可能会拒绝一个正确的程序;从这种角度考虑它是保守的。如果 Rust 接受不正确的程序,那么用户也就不会相信 Rust 所做的保证了。然而,如果 Rust 拒绝正确的程序,虽然会给程序员带来不便,但不会带来灾难。`RefCell` 正是用于当你确信代码遵守借用规则,而编译器不能理解和确定的时候。 类似于 `Rc`,`RefCell` 只能用于单线程场景。如果尝试在多线程上下文中使用`RefCell`,会得到一个编译错误。第十六章会介绍如何在多线程程序中使用 `RefCell` 的功能。 - - - 如下为选择 `Box`,`Rc` 或 `RefCell` 的理由: -- `Rc` 允许相同数据有多个所有者;`Box` 和 `RefCell` 有单一所有者。 -- `Box` 允许在编译时执行不可变(或可变)借用检查;`Rc`仅允许在编译时执行不可变借用检查;`RefCell` 允许在运行时执行不可变(或可变)借用检查。 -- 因为 `RefCell` 允许在运行时执行可变借用检查,所以我们可以在即便 `RefCell` 自身是不可变的情况下修改其内部的值。 +* `Rc` 允许相同数据有多个所有者;`Box` 和 `RefCell` 有单一所有者。 +* `Box` 允许在编译时执行不可变或可变借用检查;`Rc`仅允许在编译时执行不可变借用检查;`RefCell` 允许在运行时执行不可变或可变借用检查。 +* 因为 `RefCell` 允许在运行时执行可变借用检查,所以我们可以在即便 `RefCell` 自身是不可变的情况下修改其内部的值。 -最后一个理由便是指 **内部可变性** 模式。让我们看看何时内部可变性是有用的,并讨论这是如何成为可能的。 +在不可变值内部改变值就是 **内部可变性** 模式。让我们看看何时内部可变性是有用的,并讨论这是如何成为可能的。 ### 内部可变性:不可变值的可变借用 借用规则的一个推论是当有一个不可变值时,不能可变的借用它。例如,如下代码不能编译: -```rust,ignore +```rust,ignore,does_not_compile fn main() { let x = 5; let y = &mut x; @@ -96,7 +70,7 @@ error[E0596]: cannot borrow immutable local variable `x` as mutable 如下是一个我们想要测试的场景:我们在编写一个记录某个值与最大值的差距的库,并根据当前值与最大值的差距来发送消息。例如,这个库可以用于记录用户所允许的 API 调用数量限额。 -该库只提供记录与最大值的差距,以及何种情况发送什么消息的功能。使用此库的程序则期望提供实际发送消息的机制:程序可以选择记录一条消息、发送 email、发送短信等等。库本身无需知道这些细节;只需实现其提供的 `Messenger` trait 即可。示例 15-23 展示了库代码: +该库只提供记录与最大值的差距,以及何种情况发送什么消息的功能。使用此库的程序则期望提供实际发送消息的机制:程序可以选择记录一条消息、发送 email、发送短信等等。库本身无需知道这些细节;只需实现其提供的 `Messenger` trait 即可。示例 15-20 展示了库代码: 文件名: src/lib.rs @@ -141,11 +115,11 @@ impl<'a, T> LimitTracker<'a, T> 这些代码中一个重要部分是拥有一个方法 `send` 的 `Messenger` trait,其获取一个 `self` 的不可变引用和文本信息。这是我们的 mock 对象所需要拥有的接口。另一个重要的部分是我们需要测试 `LimitTracker` 的 `set_value` 方法的行为。可以改变传递的 `value` 参数的值,不过 `set_value` 并没有返回任何可供断言的值。也就是说,如果使用某个实现了 `Messenger` trait 的值和特定的 `max` 创建 `LimitTracker`,当传递不同 `value` 值时,消息发送者应被告知发送合适的消息。 -我们所需的 mock 对象是,调用 `send` 不同于实际发送 email 或短息,其只记录信息被通知要发送了。可以新建一个 mock 对象示例,用其创建 `LimitTracker`,调用 `LimitTracker` 的 `set_value` 方法,然后检查 mock 对象是否有我们期望的消息。示例 15-24 展示了一个如此尝试的 mock 对象实现,不过借用检查器并不允许: +我们所需的 mock 对象是,调用 `send` 不同于实际发送 email 或短息,其只记录信息被通知要发送了。可以新建一个 mock 对象示例,用其创建 `LimitTracker`,调用 `LimitTracker` 的 `set_value` 方法,然后检查 mock 对象是否有我们期望的消息。示例 15-21 展示了一个如此尝试的 mock 对象实现,不过借用检查器并不允许: 文件名: src/lib.rs -```rust +```rust,does_not_compile #[cfg(test)] mod tests { use super::*; @@ -178,7 +152,7 @@ mod tests { } ``` -示例 15-24:尝试实现 `MockMessenger`,借用检查器并不允许 +示例 15-21:尝试实现 `MockMessenger`,借用检查器不允许这么做 测试代码定义了一个 `MockMessenger` 结构体,其 `sent_messages` 字段为一个 `String` 值的 `Vec` 用来记录被告知发送的消息。我们还定义了一个关联函数 `new` 以便于新建从空消息列表开始的 `MockMessenger` 值。接着为 `MockMessenger` 实现 `Messenger` trait 这样就可以为 `LimitTracker` 提供一个 `MockMessenger`。在 `send` 方法的定义中,获取传入的消息作为参数并储存在 `MockMessenger` 的 `sent_messages` 列表中。 @@ -188,17 +162,17 @@ mod tests { ```text error[E0596]: cannot borrow immutable field `self.sent_messages` as mutable - --> src/lib.rs:46:13 + --> src/lib.rs:52:13 | -45 | fn send(&self, message: &str) { +51 | fn send(&self, message: &str) { | ----- use `&mut self` here to make mutable -46 | self.sent_messages.push(String::from(message)); +52 | self.sent_messages.push(String::from(message)); | ^^^^^^^^^^^^^^^^^^ cannot mutably borrow immutable field ``` 不能修改 `MockMessenger` 来记录消息,因为 `send` 方法获取 `self` 的不可变引用。我们也不能参考错误文本的建议使用 `&mut self` 替代,因为这样 `send` 的签名就不符合 `Messenger` trait 定义中的签名了(请随意尝试如此修改并看看会出现什么错误信息)。 -这正是内部可变性的用武之地!我们将通过 `RefCell` 来储存 `sent_messages`,然而 `send` 将能够修改 `sent_messages` 并储存消息。示例 15-25 展示了代码: +这正是内部可变性的用武之地!我们将通过 `RefCell` 来储存 `sent_messages`,然而 `send` 将能够修改 `sent_messages` 并储存消息。示例 15-22 展示了代码: 文件名: src/lib.rs @@ -236,7 +210,7 @@ mod tests { } ``` -示例 15-25:使用 `RefCell` 能够在外部值被认为是不可变的情况下修改内部值 +示例 15-22:使用 `RefCell` 能够在外部值被认为是不可变的情况下修改内部值 现在 `sent_messages` 字段的类型是 `RefCell>` 而不是 `Vec`。在 `new` 函数中新建了一个 `RefCell` 示例替代空 vector。 @@ -246,21 +220,17 @@ mod tests { 现在我们见识了如何使用 `RefCell`,让我们研究一下它怎样工作的! -### `RefCell` 在运行时检查借用规则 +### `RefCell` 在运行时记录借用 当创建不可变和可变引用时,我们分别使用 `&` 和 `&mut` 语法。对于 `RefCell` 来说,则是 `borrow` 和 `borrow_mut` 方法,这属于 `RefCell` 安全 API 的一部分。`borrow` 方法返回 `Ref` 类型的智能指针,`borrow_mut` 方法返回 `RefMut` 类型的智能指针。这两个类型都实现了 `Deref` 所以可以当作常规引用对待。 - - - -`RefCell` 记录当前有多少个活动的 `Ref` 和 `RefMut` 智能指针。每次调用 `borrow`,`RefCell` 将活动的不可变借用计数加一。当 `Ref` 值离开作用域时,不可变借用计数减一。就像编译时借用规则一样,`RefCell` 在任何时候只允许有多个不可变借用或一个可变借用。 +`RefCell` 记录当前有多少个活动的 `Ref` 和 `RefMut` 智能指针。每次调用 `borrow`,`RefCell` 将活动的不可变借用计数加一。当 `Ref` 值离开作用域时,不可变借用计数减一。就像编译时借用规则一样,`RefCell` 在任何时候只允许有多个不可变借用或一个可变借用。 -如果我们尝试违反这些规则,相比引用时的编译时错误,`RefCell` 的实现会在运行时 `panic!`。示例 15-26 展示了对示例 15-25 中 `send` 实现的修改,这里我们故意尝试在相同作用域创建两个可变借用以便演示 `RefCell` 不允许我们在运行时这么做: +如果我们尝试违反这些规则,相比引用时的编译时错误,`RefCell` 的实现会在运行时 `panic!`。示例 15-23 展示了对示例 15-22 中 `send` 实现的修改,这里我们故意尝试在相同作用域创建两个可变借用以便演示 `RefCell` 不允许我们在运行时这么做: 文件名: src/lib.rs -```rust,ignore +```rust,ignore,panics impl Messenger for MockMessenger { fn send(&self, message: &str) { let mut one_borrow = self.sent_messages.borrow_mut(); @@ -274,27 +244,24 @@ impl Messenger for MockMessenger { 示例 15-26:在同一作用域中创建两个可变引用并观察 `RefCell` panic -这里为 `borrow_mut` 返回的 `RefMut` 智能指针创建了 `one_borrow` 变量。接着用相同的方式在变量 `two_borrow` 创建了另一个可变借用。这会在相同作用域中创建一个可变引用,这是不允许的,如果运行库的测试,编译时不会有任何错误,不过测试会失败: +这里为 `borrow_mut` 返回的 `RefMut` 智能指针创建了 `one_borrow` 变量。接着用相同的方式在变量 `two_borrow` 创建了另一个可变借用。这会在相同作用域中创建一个可变引用,这是不允许的。当运行库的测试时,示例 15-23 编译时不会有任何错误,不过测试会失败: ```text ---- tests::it_sends_an_over_75_percent_warning_message stdout ---- thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at - 'already borrowed: BorrowMutError', src/libcore/result.rs:906:4 +'already borrowed: BorrowMutError', src/libcore/result.rs:906:4 note: Run with `RUST_BACKTRACE=1` for a backtrace. ``` -可以看到代码 panic 和信息`already borrowed: BorrowMutError`。这也就是 `RefCell` 如何在运行时处理违反借用规则的情况。 +注意代码 panic 和信息 `already borrowed: BorrowMutError`。这也就是 `RefCell` 如何在运行时处理违反借用规则的情况。 -在运行时捕获借用错误而不是编译时意味着将会在开发过程的后期才会发现错误 ———— 甚至有可能发布到生产环境才发现。还会因为在运行时而不是编译时记录借用而导致少量的运行时性能惩罚。然而,使用 `RefCell` 使得在只允许不可变值的上下文中编写修改自身以记录消息的 mock 对象成为可能。虽然有取舍,但是我们可以选择使用 `RefCell` 来获得比常规引用所能提供的更多的功能。 +在运行时捕获借用错误而不是编译时意味着将会在开发过程的后期才会发现错误,甚至有可能发布到生产环境才发现。还会因为在运行时而不是编译时记录借用而导致少量的运行时性能惩罚。然而,使用 `RefCell` 使得在只允许不可变值的上下文中编写修改自身以记录消息的 mock 对象成为可能。虽然有取舍,但是我们可以选择使用 `RefCell` 来获得比常规引用所能提供的更多的功能。 ### 结合 `Rc` 和 `RefCell` 来拥有多个可变数据所有者 `RefCell` 的一个常见用法是与 `Rc` 结合。回忆一下 `Rc` 允许对相同数据有多个所有者,不过只能提供数据的不可变访问。如果有一个储存了 `RefCell` 的 `Rc` 的话,就可以得到有多个所有者 **并且** 可以修改的值了! - - - -例如,回忆示例 15-13 的 cons list 的例子中使用 `Rc` 使得多个列表共享另一个列表的所有权。因为 `Rc` 只存放不可变值,所以一旦创建了这些列表值后就不能修改。让我们加入 `RefCell` 来获得修改列表中值的能力。示例 15-27 展示了通过在 `Cons` 定义中使用 `RefCell`,我们就允许修改所有列表中的值了: +例如,回忆示例 15-18 的 cons list 的例子中使用 `Rc` 使得多个列表共享另一个列表的所有权。因为 `Rc` 只存放不可变值,所以一旦创建了这些列表值后就不能修改。让我们加入 `RefCell` 来获得修改列表中值的能力。示例 15-24 展示了通过在 `Cons` 定义中使用 `RefCell`,我们就允许修改所有列表中的值了: 文件名: src/main.rs @@ -325,17 +292,13 @@ fn main() { } ``` -示例 15-27:使用 `Rc>` 创建可以修改的 `List` +示例 15-24:使用 `Rc>` 创建可以修改的 `List` 这里创建了一个 `Rc` 实例并储存在变量 `value` 中以便之后直接访问。接着在 `a` 中用包含 `value` 的 `Cons` 成员创建了一个 `List`。需要克隆 `value` 以便 `a` 和 `value` 都能拥有其内部值 `5` 的所有权,而不是将所有权从 `value` 移动到 `a` 或者让 `a` 借用 `value`。 - - - -我们将列表 `a` 封装进了 `Rc` 这样当创建列表 `b` 和 `c` 时,他们都可以引用 `a`,正如示例 15-13 一样。 +我们将列表 `a` 封装进了 `Rc` 这样当创建列表 `b` 和 `c` 时,他们都可以引用 `a`,正如示例 15-18 一样。 -一旦创建了列表 `a`、`b` 和 `c`,我们将 `value` 的值加 10。为此对 `value` 调用了 `borrow_mut`,这里使用了第五章讨论的自定解引用功能(“`->`运算符到哪去了?”)来解引用 `Rc` 以获取其内部的 `RefCell` 值。`borrow_mut` 方法返回 `RefMut` 智能指针,可以对其使用解引用运算符并修改其内部值。 +一旦创建了列表 `a`、`b` 和 `c`,我们将 `value` 的值加 10。为此对 `value` 调用了 `borrow_mut`,这里使用了第五章讨论的自动解引用功能(“`->` 运算符到哪去了?” 部分)来解引用 `Rc` 以获取其内部的 `RefCell` 值。`borrow_mut` 方法返回 `RefMut` 智能指针,可以对其使用解引用运算符并修改其内部值。 当我们打印出 `a`、`b` 和 `c` 时,可以看到他们都拥有修改后的值 15 而不是 5: diff --git a/src/ch15-06-reference-cycles.md b/src/ch15-06-reference-cycles.md index 49fc038..f948b4d 100644 --- a/src/ch15-06-reference-cycles.md +++ b/src/ch15-06-reference-cycles.md @@ -1,18 +1,22 @@ ## 引用循环与内存泄漏 -> [ch15-06-reference-cycles.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch15-06-reference-cycles.md) +> [ch15-06-reference-cycles.md](https://github.com/rust-lang/book/blob/master/src/ch15-06-reference-cycles.md) >
-> commit cd7d9bcfb099c224439db0ba3b02956d9843864d +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -Rust 的内存安全保证使其 **难以** 意外的制造永远也不会被清理的内存(被称为 **内存泄露**(*memory leak*)),但并不是不可能。完全的避免内存泄露并不是同在编译时拒绝数据竞争一样为 Rust 的保证之一,这意味着内存泄露在 Rust 被认为是内存安全的。这一点可以通过 `Rc` 和 `RefCell` 看出:有可能会创建个个项之间相互引用的引用。这会造成内存泄露,因为每一项的引用计数将永远也到不了 0,其值也永远也不会被丢弃。 +Rust 的内存安全保证使其难以意外的制造永远也不会被清理的内存(被称为 **内存泄露**(*memory leak*)),但并不是不可能。完全的避免内存泄露并不是同在编译时拒绝数据竞争一样为 Rust 的保证之一,这意味着内存泄露在 Rust 被认为是内存安全的。这一点可以通过 `Rc` 和 `RefCell` 看出:有可能会创建个个项之间相互引用的引用。这会造成内存泄露,因为每一项的引用计数将永远也到不了 0,其值也永远也不会被丢弃。 ### 制造引用循环 -让我们看看引用循环是如何发生的以及如何避免它。以示例 15-28 中的 `List` 枚举和 `tail` 方法的定义开始: +让我们看看引用循环是如何发生的以及如何避免它。以示例 15-25 中的 `List` 枚举和 `tail` 方法的定义开始: 文件名: src/main.rs -```rust,ignore + + +```rust +# fn main() {} use std::rc::Rc; use std::cell::RefCell; use List::{Cons, Nil}; @@ -25,33 +29,19 @@ enum List { impl List { fn tail(&self) -> Option<&RefCell>> { - match *self { - Cons(_, ref item) => Some(item), + match self { + Cons(_, item) => Some(item), Nil => None, } } } ``` -示例:一个存放 `RefCell` 的 cons list 定义,这样可以修改 `Cons` 成员所引用的数据 - -这里采用了示例 15-6 中 `List` 定义的另一种变体。现在 `Cons` 成员的第二个元素是 `RefCell>`,这意味着不同于像示例 15-19 那样能够修改 `i32` 的值,我们希望能够修改 `Cons` 成员所指向的 `List`。这里还增加了一个 `tail` 方法来方便我们在有 `Cons` 成员的时候访问其第二项。 +示例 15-25: 一个存放 `RefCell` 的 cons list 定义,这样可以修改 `Cons` 成员所引用的数据 - - +这里采用了示例 15-25 中 `List` 定义的另一种变体。现在 `Cons` 成员的第二个元素是 `RefCell>`,这意味着不同于像示例 15-24 那样能够修改 `i32` 的值,我们希望能够修改 `Cons` 成员所指向的 `List`。这里还增加了一个 `tail` 方法来方便我们在有 `Cons` 成员的时候访问其第二项。 -在示例 15-29 中增加了一个 `main` 函数,其使用了示例 15-28 中的定义。这些代码在 `a` 中创建了一个列表,一个指向 `a` 中列表的 `b` 列表,接着修改 `b` 中的列表指向 `a` 中的列表,这会创建一个引用循环。在这个过程的多个位置有 `println!` 语句展示引用计数。 - - - +在示例 15-26 中增加了一个 `main` 函数,其使用了示例 15-25 中的定义。这些代码在 `a` 中创建了一个列表,一个指向 `a` 中列表的 `b` 列表,接着修改 `b` 中的列表指向 `a` 中的列表,这会创建一个引用循环。在这个过程的多个位置有 `println!` 语句展示引用计数。 Filename: src/main.rs @@ -67,8 +57,8 @@ file --> # # impl List { # fn tail(&self) -> Option<&RefCell>> { -# match *self { -# Cons(_, ref item) => Some(item), +# match self { +# Cons(_, item) => Some(item), # Nil => None, # } # } @@ -93,18 +83,17 @@ fn main() { println!("b rc count after changing a = {}", Rc::strong_count(&b)); println!("a rc count after changing a = {}", Rc::strong_count(&a)); - // Uncomment the next line to see that we have a cycle; it will - // overflow the stack + // 取消如下行的注释来观察引用循环; + // 这会导致栈溢出 // println!("a next item = {:?}", a.tail()); } ``` -示例 15-29:创建一个引用循环:两个`List` 值互相指向彼此 - -这里在变量 `a` 中创建了一个 `Rc` 实例来存放初值为 `5, Nil` 的 `List` 值。接着在变量 `b` 中创建了存放包含值 10 和指向列表 `a` 的 `List` 的另一个 `Rc` 实例。 +示例 15-26:创建一个引用循环:两个 `List` 值互相指向彼此 -最后,修改 `a` 使其指向 `b` 而不是 `Nil`,这就创建了一个循环。为此需要使用 `tail` 方法获取 `a` 中 `RefCell` 的引用,并放入变量 `link` 中。接着使用 `RefCell` 的 `borrow_mut` 方法将其值从存放 `Nil` 的 `Rc` 修改为 `b` 中的 `Rc`。 +这里在变量 `a` 中创建了一个 `Rc` 实例来存放初值为 `5, Nil` 的 `List` 值。接着在变量 `b` 中创建了存放包含值 10 和指向列表 `a` 的 `List` 的另一个 `Rc` 实例。 +最后,修改 `a` 使其指向 `b` 而不是 `Nil`,这就创建了一个循环。为此需要使用 `tail` 方法获取 `a` 中 `RefCell>` 的引用,并放入变量 `link` 中。接着使用 `RefCell>` 的 `borrow_mut` 方法将其值从存放 `Nil` 的 `Rc` 修改为 `b` 中的 `Rc`。 如果保持最后的 `println!` 行注释并运行代码,会得到如下输出: @@ -118,74 +107,29 @@ b rc count after changing a = 2 a rc count after changing a = 2 ``` -可以看到将 `a` 修改为指向 `b` 之后,`a` 和 `b` 中都有的 `Rc` 实例的引用计数为 2。在 `main` 的结尾,Rust 会尝试首先丢弃 `b`,这会使 `a` 和 `b` 中 `Rc` 实例的引用计数减一。 +可以看到将 `a` 修改为指向 `b` 之后,`a` 和 `b` 中都有的 `Rc` 实例的引用计数为 2。在 `main` 的结尾,Rust 会尝试首先丢弃 `b`,这会使 `a` 和 `b` 中 `Rc` 实例的引用计数减 1。 - - - - - - -然而,因为 `a` 仍然引用 `b` 中的 `Rc`,`Rc` 的引用计数是 1 而不是 0,所以 `Rc` 在堆上的内存不会被丢弃。其内存会因为引用计数为 1 而永远停留。 - -为了更形象的展示,我们创建了一个如图 15-30 所示的引用循环: +然而,因为 `a` 仍然引用 `b` 中的 `Rc`,`Rc` 的引用计数是 1 而不是 0,所以 `Rc` 在堆上的内存不会被丢弃。其内存会因为引用计数为 1 而永远停留。为了更形象的展示,我们创建了一个如图 15-4 所示的引用循环: Reference cycle of lists -图 15-30: 列表 `a` 和 `b` 彼此互相指向形成引用循环 +图 15-4: 列表 `a` 和 `b` 彼此互相指向形成引用循环 如果取消最后 `println!` 的注释并运行程序,Rust 会尝试打印出 `a` 指向 `b` 指向 `a` 这样的循环直到栈溢出。 - - - 这个特定的例子中,创建了引用循环之后程序立刻就结束了。这个循环的结果并不可怕。如果在更为复杂的程序中并在循环里分配了很多内存并占有很长时间,这个程序会使用多于它所需要的内存,并有可能压垮系统并造成没有内存可供使用。 -创建引用循环并不容易,但也不是不可能。如果你有包含`Rc`的`RefCell`值或类似的嵌套结合了内部可变性和引用计数的类型,请务必小心确保你没有形成一个引用循环;你无法指望 Rust 帮你捕获它们。创建引用循环是一个程序上的逻辑 bug,你应该使用自动化测试、代码评审和其他软件开发最佳实践来使其最小化。 +创建引用循环并不容易,但也不是不可能。如果你有包含 `Rc` 的 `RefCell` 值或类似的嵌套结合了内部可变性和引用计数的类型,请务必小心确保你没有形成一个引用循环;你无法指望 Rust 帮你捕获它们。创建引用循环是一个程序上的逻辑 bug,你应该使用自动化测试、代码评审和其他软件开发最佳实践来使其最小化。 - - - -另一个解决方案是重新组织数据结构使得一些引用有所有权而另一些则没有。如此,循环将由一些有所有权的关系和一些没有所有权的关系,而只有所有权关系才影响值是否被丢弃。在示例 15-28 中,我们总是希望 `Cons` 成员拥有其列表,所以重新组织数据结构是不可能的。让我们看看一个由父结点和子结点构成的图的例子,观察何时无所有权关系是一个好的避免引用循环的方法。 +另一个解决方案是重新组织数据结构使得一些引用有所有权而另一些则没有。如此,循环将由一些有所有权的关系和一些没有所有权的关系,而只有所有权关系才影响值是否被丢弃。在示例 15-25 中,我们总是希望 `Cons` 成员拥有其列表,所以重新组织数据结构是不可能的。让我们看看一个由父结点和子结点构成的图的例子,观察何时无所有权关系是一个好的避免引用循环的方法。 ### 避免引用循环:将 `Rc` 变为 `Weak` -到目前为止,我们已经展示了调用 `Rc::clone` 会增加 `Rc` 实例的 `strong_count`,和 `Rc` 实例只在其 `strong_count` 为 0 时才会被清理。也可以通过调用 `Rc::downgrade` 并传递 `Rc` 实例的引用来创建其值的 **弱引用**(*weak reference*)。调用 `Rc::downgrade` 时会得到 `Weak` 类型的智能指针。不同于将 `Rc` 实例的 `strong_count` 加一,调用 `Rc::downgrade` 会将 `weak_count` 加一。`Rc` 类型使用 `weak_count` 来记录其存在多少个 `Weak` 引用,类似于 `strong_count`。其区别在于 `weak_count` 无需计数为 0 就能使 `Rc` 实例被清理。 - - - +到目前为止,我们已经展示了调用 `Rc::clone` 会增加 `Rc` 实例的 `strong_count`,和 `Rc` 实例只在其 `strong_count` 为 0 时才会被清理。也可以通过调用 `Rc::downgrade` 并传递 `Rc` 实例的引用来创建其值的 **弱引用**(*weak reference*)。调用 `Rc::downgrade` 时会得到 `Weak` 类型的智能指针。不同于将 `Rc` 实例的 `strong_count` 加一,调用 `Rc::downgrade` 会将 `weak_count` 加一。`Rc` 类型使用 `weak_count` 来记录其存在多少个 `Weak` 引用,类似于 `strong_count`。其区别在于 `weak_count` 无需计数为 0 就能使 `Rc` 实例被清理。 -强引用代表如何共享 `Rc` 实例的引用。弱引用并不代表所有权关系。他们不会造成引用循环,因为任何引入了弱引用的循环一旦所涉及的强引用计数为 0 就会被打破。 +强引用代表如何共享 `Rc` 实例的所有权。弱引用并不代表所有权关系。他们不会造成引用循环,因为任何引入了弱引用的循环一旦所涉及的强引用计数为 0 就会被打破。 - - - -因为 `Weak` 引用的值可能已经被丢弃了,为了使用 `Weak` 所指向的值,我们必须确保其值仍然有效。为此可以调用 `Weak` 实例的 `upgrade` 方法,这会返回 `Option>`。如果 `Rc` 值还未被丢弃则结果是 `Some`,如果 `Rc` 已经被丢弃则结果是 `None`。因为 `upgrade` 返回一个 `Option`,我们确信 Rust 会处理 `Some` 和 `None`的情况,并且不会有一个无效的指针。 +因为 `Weak` 引用的值可能已经被丢弃了,为了使用 `Weak` 所指向的值,我们必须确保其值仍然有效。为此可以调用 `Weak` 实例的 `upgrade` 方法,这会返回 `Option>`。如果 `Rc` 值还未被丢弃则结果是 `Some`,如果 `Rc` 已经被丢弃则结果是 `None`。因为 `upgrade` 返回一个 `Option`,我们确信 Rust 会处理 `Some` 和 `None`的情况,并且不会有一个无效的指针。 作为一个例子,不同于使用一个某项只知道其下一项的列表,我们会创建一个某项知道其子项 **和** 父项的树形结构。 @@ -206,9 +150,9 @@ struct Node { } ``` -我们希望能够 `Node` 拥有其子结点,同时也希望变量可以拥有每个结点以便可以直接访问他们。为此 `Vec` 的项的类型被定义为 `Rc`。我们还希望能改其他结点的子结点,所以 `children` 中 `Vec` 被放进了 `RefCell`。 +我们希望能够 `Node` 拥有其子结点,同时也希望通过变量来共享所有权,以便可以直接访问树种的每一个 `Node`。为此 `Vec` 的项的类型被定义为 `Rc`。我们还希望能改其他结点的子结点,所以 `children` 中 `Vec>` 被放进了 `RefCell`。 -接下来,使用此结构体定义来创建一个叫做 `leaf` 的带有值 3 且没有子结点的 `Node` 实例,和另一个带有值 5 并以 `leaf` 作为子结点的实例 `branch`,如示例 15-31 所示: +接下来,使用此结构体定义来创建一个叫做 `leaf` 的带有值 3 且没有子结点的 `Node` 实例,和另一个带有值 5 并以 `leaf` 作为子结点的实例 `branch`,如示例 15-27 所示: 文件名: src/main.rs @@ -235,9 +179,9 @@ fn main() { } ``` -示例 15-31:创建没有子结点的 `leaf` 结点和以 `leaf` 作为子结点的 `branch` 结点 +示例 15-27:创建没有子结点的 `leaf` 结点和以 `leaf` 作为子结点的 `branch` 结点 -这里克隆了 `leaf` 中的 `Rc` 并储存在了 `branch` 中,这意味着 `leaf` 中的 `Node` 现在有两个所有者:`leaf`和`branch`。可以通过 `branch.children` 从 `branch` 中获得 `leaf`,不过无法从 `leaf` 到 `branch`。`leaf` 没有到 `branch` 的引用且并不知道他们相互关联。我们希望 `leaf` 知道 `branch` 是其父结点。 +这里克隆了 `leaf` 中的 `Rc` 并储存在了 `branch` 中,这意味着 `leaf` 中的 `Node` 现在有两个所有者:`leaf`和`branch`。可以通过 `branch.children` 从 `branch` 中获得 `leaf`,不过无法从 `leaf` 到 `branch`。`leaf` 没有到 `branch` 的引用且并不知道他们相互关联。我们希望 `leaf` 知道 `branch` 是其父结点。稍后会这么做。 #### 增加从子到父的引用 @@ -245,12 +189,7 @@ fn main() { 现在换一种方式思考这个关系,父结点应该拥有其子结点:如果父结点被丢弃了,其子结点也应该被丢弃。然而子结点不应该拥有其父结点:如果丢弃子结点,其父结点应该依然存在。这正是弱引用的例子! -所以 `parent` 使用 `Weak` 类型而不是 `Rc`,具体来说是 `RefCell>`。现在 `Node` 结构体定义看起来像这样: - - - +所以 `parent` 使用 `Weak` 类型而不是 `Rc`,具体来说是 `RefCell>`。现在 `Node` 结构体定义看起来像这样: 文件名: src/main.rs @@ -266,19 +205,7 @@ struct Node { } ``` - - - -这样,一个结点就能够引用其父结点,但不拥有其父结点。在示例 15-32 中,我们更新 `main` 来使用新定义以便 `leaf` 结点可以引用其父结点: - - - +这样,一个结点就能够引用其父结点,但不拥有其父结点。在示例 15-28 中,我们更新 `main` 来使用新定义以便 `leaf` 结点可以通过 `branch` 引用其父结点: 文件名: src/main.rs @@ -314,13 +241,9 @@ fn main() { } ``` -示例 15-32:一个 `leaf` 结点,其拥有指向其父结点 `branch` 的 `Weak` 引用 - - - +示例 15-28:一个 `leaf` 结点,其拥有指向其父结点 `branch` 的 `Weak` 引用 -创建 `leaf` 结点类似于示例 15-31 中如何创建 `leaf` 结点的,除了 `parent` 字段有所不同:`leaf` 开始时没有父结点,所以我们新建了一个空的 `Weak` 引用实例。 +创建 `leaf` 结点类似于示例 15-27 中如何创建 `leaf` 结点的,除了 `parent` 字段有所不同:`leaf` 开始时没有父结点,所以我们新建了一个空的 `Weak` 引用实例。 此时,当尝试使用 `upgrade` 方法获取 `leaf` 的父结点引用时,会得到一个 `None` 值。如第一个 `println!` 输出所示: @@ -328,18 +251,9 @@ talk it through --> leaf parent = None ``` - - +当创建 `branch` 结点时,其也会新建一个 `Weak` 引用,因为 `branch` 并没有父结点。`leaf` 仍然作为 `branch` 的一个子结点。一旦在 `branch` 中有了 `Node` 实例,就可以修改 `leaf` 使其拥有指向父结点的 `Weak` 引用。这里使用了 `leaf` 中 `parent` 字段里的 `RefCell>` 的 `borrow_mut` 方法,接着使用了 `Rc::downgrade` 函数来从 `branch` 中的 `Rc` 值创建了一个指向 `branch` 的 `Weak` 引用。 -当创建 `branch` 结点时,其也会新建一个 `Weak` 引用,因为 `branch` 并没有父结点。`leaf` 仍然作为 `branch` 的一个子结点。一旦在 `branch` 中有了 `Node` 实例,就可以修改 `leaf` 使其拥有指向父结点的 `Weak` 引用。这里使用了 `leaf` 中 `parent` 字段里的 `RefCell` 的 `borrow_mut` 方法,接着使用了 `Rc::downgrade` 函数来从 `branch` 中的 `Rc` 值创建了一个指向 `branch` 的 `Weak` 引用。 - - - - -当再次打印出 `leaf` 的父结点时,这一次将会得到存放了 `branch` 的 `Some` 值:现在 `leaf` 可以访问其父结点了!当打印出 `leaf` 时,我们也避免了如示例 15-29 中最终会导致栈溢出的循环:`Weak` 引用被打印为 `(Weak)`: +当再次打印出 `leaf` 的父结点时,这一次将会得到存放了 `branch` 的 `Some` 值:现在 `leaf` 可以访问其父结点了!当打印出 `leaf` 时,我们也避免了如示例 15-26 中最终会导致栈溢出的循环:`Weak` 引用被打印为 `(Weak)`: ```text leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) }, @@ -351,11 +265,21 @@ children: RefCell { value: [] } }] } }) #### 可视化 `strong_count` 和 `weak_count` 的改变 -让我们通过创建了一个新的内部作用域并将 `branch` 的创建放入其中,来观察 `Rc` 实例的 `strong_count` 和 `weak_count` 值的变化。这会展示当 `branch` 创建和离开作用域被丢弃时会发生什么。这些修改如示例 15-33 所示: +让我们通过创建了一个新的内部作用域并将 `branch` 的创建放入其中,来观察 `Rc` 实例的 `strong_count` 和 `weak_count` 值的变化。这会展示当 `branch` 创建和离开作用域被丢弃时会发生什么。这些修改如示例 15-29 所示: 文件名: src/main.rs -```rust,ignore +```rust +# use std::rc::{Rc, Weak}; +# use std::cell::RefCell; +# +# #[derive(Debug)] +# struct Node { +# value: i32, +# parent: RefCell>, +# children: RefCell>>, +# } +# fn main() { let leaf = Rc::new(Node { value: 3, @@ -375,6 +299,7 @@ fn main() { parent: RefCell::new(Weak::new()), children: RefCell::new(vec![Rc::clone(&leaf)]), }); + *leaf.parent.borrow_mut() = Rc::downgrade(&branch); println!( @@ -399,25 +324,15 @@ fn main() { } ``` -示例 15-33:在内部作用域创建 `branch` 并检查其强弱引用计数 - -一旦创建了 `leaf`,其 `Rc` 的强引用计数为 1,弱引用计数为 0。在内部作用域中创建了 `branch` 并与 `leaf` 相关联,此时 `branch` 中 `Rc` 的强引用计数为 1,弱引用计数为 1(因为 `leaf.parent` 通过 `Weak` 指向 `branch`)。这里 `leaf` 的强引用计数为 2,因为现在 `branch` 的 `branch.children` 中储存了 `leaf` 的 `Rc` 的拷贝,不过弱引用计数仍然为 0。 - -当内部作用域结束时,`branch` 离开作用域,其强引用计数减少为 0,所以其 `Node` 被丢弃。来自 `leaf.parent` 的弱引用计数 1 与 `Node` 是否被丢弃无关,所以并没有产生任何内存泄露! +示例 15-29:在内部作用域创建 `branch` 并检查其强弱引用计数 -如果在内部作用域结束后尝试访问 `leaf` 的父结点,会再次得到 `None`。在程序的结尾,`leaf` 中 `Rc` 的强引用计数为 1,弱引用计数为 0,因为现在 `leaf` 又是 `Rc` 唯一的引用了。 +一旦创建了 `leaf`,其 `Rc` 的强引用计数为 1,弱引用计数为 0。在内部作用域中创建了 `branch` 并与 `leaf` 相关联,此时 `branch` 中 `Rc` 的强引用计数为 1,弱引用计数为 1(因为 `leaf.parent` 通过 `Weak` 指向 `branch`)。这里 `leaf` 的强引用计数为 2,因为现在 `branch` 的 `branch.children` 中储存了 `leaf` 的 `Rc` 的拷贝,不过弱引用计数仍然为 0。 - - +当内部作用域结束时,`branch` 离开作用域,`Rc` 的强引用计数减少为 0,所以其 `Node` 被丢弃。来自 `leaf.parent` 的弱引用计数 1 与 `Node` 是否被丢弃无关,所以并没有产生任何内存泄露! -所有这些管理计数和值的逻辑都内建于 `Rc` 和 `Weak` 以及它们的 `Drop` trait 实现中。通过在 `Node` 定义中指定从子结点到父结点的关系为一个`Weak`引用,就能够拥有父结点和子结点之间的双向引用而不会造成引用循环和内存泄露。 +如果在内部作用域结束后尝试访问 `leaf` 的父结点,会再次得到 `None`。在程序的结尾,`leaf` 中 `Rc` 的强引用计数为 1,弱引用计数为 0,因为现在 `leaf` 又是 `Rc` 唯一的引用了。 - - +所有这些管理计数和值的逻辑都内建于 `Rc` 和 `Weak` 以及它们的 `Drop` trait 实现中。通过在 `Node` 定义中指定从子结点到父结点的关系为一个`Weak`引用,就能够拥有父结点和子结点之间的双向引用而不会造成引用循环和内存泄露。 ## 总结 @@ -429,4 +344,4 @@ like that to the start of the Weak section? --> [“The Nomicon”]: https://doc.rust-lang.org/stable/nomicon/ -接下来,让我们谈谈 Rust 的并发。我们还会学习到一些新的对并发有帮助的智能指针。 +接下来,让我们谈谈 Rust 的并发。届时甚至还会学习到一些新的对并发有帮助的智能指针。 From 2269bfc767f6fff82b6d435c77c86a1e90819c15 Mon Sep 17 00:00:00 2001 From: KaiserY Date: Sat, 8 Dec 2018 00:46:04 +0800 Subject: [PATCH 29/49] check to ch16-04 --- src/ch16-00-concurrency.md | 6 +- src/ch16-01-threads.md | 19 +++---- src/ch16-02-message-passing.md | 22 ++------ src/ch16-03-shared-state.md | 56 +++++++++++-------- ...04-extensible-concurrency-sync-and-send.md | 4 +- 5 files changed, 52 insertions(+), 55 deletions(-) diff --git a/src/ch16-00-concurrency.md b/src/ch16-00-concurrency.md index 944f515..84779e7 100644 --- a/src/ch16-00-concurrency.md +++ b/src/ch16-00-concurrency.md @@ -1,14 +1,14 @@ # 无畏并发 -> [ch16-00-concurrency.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch16-00-concurrency.md) +> [ch16-00-concurrency.md](https://github.com/rust-lang/book/blob/master/src/ch16-00-concurrency.md) >
-> commit 90406bd5a4cd4447b46cd7e03d33f34a651e9bb7 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 安全并高效的处理并发编程是 Rust 的另一个主要目标。**并发编程**(*Concurrent programming*),代表程序的不同部分相互独立的执行,而 **并行编程**(*parallel programming*)代表程序不同部分于同时执行,这两个概念随着计算机越来越多的利用多处理器的优势时显得愈发重要。由于历史原因,在此类上下文中编程一直是困难且容易出错的:Rust 希望能改变这一点。 起初,Rust 团队认为确保内存安全和防止并发问题是两个分别需要不同方法应对的挑战。随着时间的推移,团队发现所有权和类型系统是一系列解决内存安全 **和** 并发问题的强有力的工具!通过改进所有权和类型检查,Rust 很多并发错误都是 **编译时** 错误,而非运行时错误。因此,相比花费大量时间尝试重现运行时并发 bug 出现的特定情况,Rust 会拒绝编译不正确的代码并提供解释问题的错误信息。因此,你可以在开发时而不是不慎部署到生产环境后修复代码。我们给 Rust 的这一部分起了一个绰号 **无畏并发**(*fearless concurrency*)。无畏并发令你的代码免于出现诡异的 bug 并可以轻松重构且无需担心会引入新的 bug。 -> 注意:出于简洁的考虑,我们将很多问题归为并发,而不是更准确的区分并发和(或)并行。如果这是一本专注于并发和/或并行的书,我们肯定会更加精确的。对于本章,当我们谈到并发时,请自行脑内替换为并发和(或)并行。 +> 注意:出于简洁的考虑,我们将很多问题归类为 **并发**,而不是更准确的区分 **并发和(或)并行**。如果这是一本专注于并发和/或并行的书,我们肯定会更加精确的。对于本章,当我们谈到 **并发** 时,请自行脑内替换为 **并发和(或)并行**。 很多语言所提供的处理并发问题的解决方法都非常有特色。例如,Erlang 有着优雅的消息传递并发功能,但只有模糊不清的在线程间共享状态的方法。对于高级语言来说,只实现可能解决方案的子集是一个合理的策略,因为高级语言所许诺的价值来源于牺牲一些控制来换取抽象。然而对于底层语言则期望提供在任何给定的情况下有着最高的性能且对硬件有更少的抽象。因此,Rust 提供了多种工具,以符合实际情况和需求的方式来为问题建模。 diff --git a/src/ch16-01-threads.md b/src/ch16-01-threads.md index ce3edf0..54c2de9 100644 --- a/src/ch16-01-threads.md +++ b/src/ch16-01-threads.md @@ -1,8 +1,8 @@ ## 使用线程同时运行代码 -> [ch16-01-threads.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch16-01-threads.md) +> [ch16-01-threads.md](https://github.com/rust-lang/book/blob/master/src/ch16-00-concurrency.md) >
-> commit 90406bd5a4cd4447b46cd7e03d33f34a651e9bb7 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 在大部分现代操作系统中,执行中程序的代码运行于一个 **进程**(*process*)中,操作系统则负责管理多个进程。在程序内部,也可以拥有多个同时运行的独立部分。这个运行这些独立部分的功能被称为 **线程**(*threads*)。 @@ -16,9 +16,9 @@ Rust 尝试缓和使用线程的负面影响。不过在多线程上下文中编 编程语言有一些不同的方法来实现线程。很多操作系统提供了创建新线程的 API。这种由编程语言调用操作系统 API 创建线程的模模型有时被称为 *1:1*,一个 OS 线程对应一个语言线程。 -很多编程语言提供了自己特殊的线程实现。编程语言提供的线程被称为 **绿色**(*green*)线程,使用绿色线程的语言会在不同数量的 OS 线程中执行它们。为此,绿色线程模式被称为 *M:N* 模型:`M` 个绿色线程对应 `N` 个 OS 线程,这里 `M` 和 `N` 不必相同。 +很多编程语言提供了自己特殊的线程实现。编程语言提供的线程被称为 **绿色**(*green*)线程,使用绿色线程的语言会在不同数量的 OS 线程的上下文中执行它们。为此,绿色线程模式被称为 *M:N* 模型:`M` 个绿色线程对应 `N` 个 OS 线程,这里 `M` 和 `N` 不必相同。 -每一个模型都有其优势和取舍。对于 Rust 来说最重要的取舍是运行时支持。运行时是一个令人迷惑的概念,其在不同上下文中可能有不同的含义。 +每一个模型都有其优势和取舍。对于 Rust 来说最重要的取舍是运行时支持。**运行时**(*Runtime*)是一个令人迷惑的概念,其在不同上下文中可能有不同的含义。 在当前上下文中,**运行时** 代表二进制文件中包含的由语言自身提供的代码。这些代码根据语言的不同可大可小,不过任何非汇编语言都会有一定数量的运行时代码。为此,通常人们说一个语言 “没有运行时”,一般意味着 “小运行时”。更小的运行时拥有更少的功能不过其优势在于更小的二进制输出,这使其易于在更多上下文中与其他语言相结合。虽然很多语言觉得增加运行时来换取更多功能没有什么问题,但是 Rust 需要做到几乎没有运行时,同时为了保持高性能必需能够调用 C 语言,这点也是不能妥协的。 @@ -69,7 +69,7 @@ hi number 5 from the spawned thread! `thread::sleep` 调用强制线程停止执行一小段时间,这会允许其他不同的线程运行。这些线程可能会轮流运行,不过并不保证如此:这依赖操作系统如何调度线程。在这里,主线程首先打印,即便新创建线程的打印语句位于程序的开头。甚至即便我们告诉新建的线程打印直到 `i` 等于 9 ,它在主线程结束之前也只打印到了 5。 -如果你只看到了主线程的输出,或没有出现重叠打印的现象,尝试增加 range 的数值来增加操作系统切换线程的机会。 +如果运行代码只看到了主线程的输出,或没有出现重叠打印的现象,尝试增加 range 的数值来增加操作系统切换线程的机会。 #### 使用 `join` 等待所有线程结束 @@ -171,15 +171,13 @@ hi number 4 from the main thread! `move` 闭包,我们曾在第十三章简要的提到过,其经常与 `thread::spawn` 一起使用,因为它允许我们在一个线程中使用另一个线程的数据。 -第十三章讲到 “如果我们希望强制闭包获取其使用的环境值的所有权,可以在参数列表前使用 `move` 关键字。这个技巧在将闭包传递给新线程以便将数据移动到新线程中时最为实用。” - -现在我们正在创建新线程,所以让我们讨论一下在闭包中获取环境值吧。 +第十三章讲到如果可以在参数列表前使用 `move` 关键字强制闭包获取其使用的环境值的所有权。这个技巧在将闭包传递给新线程以便将数据移动到新线程中时最为实用。 注意示例 16-1 中传递给 `thread::spawn` 的闭包并没有任何参数:并没有在新建线程代码中使用任何主线程的数据。为了在新建线程中使用来自于主线程的数据,需要新建线程的闭包获取它需要的值。示例 16-3 展示了一个尝试在主线程中创建一个 vector 并用于新建线程的例子,不过这么写还不能工作,如下所示: 文件名: src/main.rs -```rust,ignore +```rust,ignore,does_not_compile use std::thread; fn main() { @@ -220,7 +218,7 @@ Rust 会 **推断** 如何捕获 `v`,因为 `println!` 只需要 `v` 的引用 文件名: src/main.rs -```rust,ignore +```rust,ignore,does_not_compile use std::thread; fn main() { @@ -240,7 +238,6 @@ fn main() { 假如这段代码能正常运行的话,则新建线程则可能会立刻被转移到后台并完全没有机会运行。新建线程内部有一个 `v` 的引用,不过主线程立刻就使用第十五章讨论的 `drop` 丢弃了 `v`。接着当新建线程开始执行,`v` 已不再有效,所以其引用也是无效的。噢,这太糟了! - 为了修复示例 16-3 的编译错误,我们可以听取错误信息的建议: ```text diff --git a/src/ch16-02-message-passing.md b/src/ch16-02-message-passing.md index b7d2066..1c13865 100644 --- a/src/ch16-02-message-passing.md +++ b/src/ch16-02-message-passing.md @@ -1,28 +1,20 @@ ## 使用消息传递在线程间传送数据 -> [ch16-02-message-passing.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch16-02-message-passing.md) +> [ch16-02-message-passing.md](https://github.com/rust-lang/book/blob/master/src/ch16-02-message-passing.md) >
-> commit 90406bd5a4cd4447b46cd7e03d33f34a651e9bb7 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -一个人气正在上升的确保安全并发的方式是 **消息传递**(*message passing*),这里线程或 actor 通过发送包含数据的消息来相互沟通。这个思想来源于 Go 编程语言文档中的口号: - -> Do not communicate by sharing memory; instead, share memory by -> communicating. -> -> 不要共享内存来通讯;而是要通讯来共享内存。 -> -> --[Effective Go](http://golang.org/doc/effective_go.html) +一个人气正在上升的确保安全并发的方式是 **消息传递**(*message passing*),这里线程或 actor 通过发送包含数据的消息来相互沟通。这个思想来源于 [Go 编程语言文档中](http://golang.org/doc/effective_go.html) 的口号:“不要共享内存来通讯;而是要通讯来共享内存。”(“Do not communicate by +sharing memory; instead, share memory by communicating.”) Rust 中一个实现消息传递并发的主要工具是 **通道**(*channel*),一个 Rust 标准库提供了其实现的编程概念。你可以将其想象为一个水流的通道,比如河流或小溪。如果你将诸如橡皮鸭或小船之类的东西放入其中,它们会顺流而下到达下游。 - 编程中的通道有两部分组成,一个发送者(transmitter)和一个接收者(receiver)。发送者一端位于上游位置,在这里可以将橡皮鸭放入河中,接收者部分则位于下游,橡皮鸭最终会漂流至此。代码中的一部分调用发送者的方法以及希望发送的数据,另一部分则检查接收端收到到达的消息。当发送者或接收者任一被丢弃时可以认为通道被 **关闭**(*closed*)了 这里,我们将开发一个程序,它会在一个线程生成值向通道发送,而在另一个线程会接收值并打印出来。这里会通过通道在线程间发送简单值来演示这个功能。一旦你熟悉了这项技术,就能使用通道来实现聊天系统或利用很多线程进行分布式计算并将部分计算结果发送给一个线程进行聚合。 首先,在示例 16-6 中,创建了一个通道但没有做任何事。注意这还不能编译,因为 Rust 不知道我们想要在通道中发送什么类型: - 文件名: src/main.rs ```rust @@ -38,7 +30,6 @@ fn main() { 这里使用 `mpsc::channel` 函数创建一个新的通道;`mpsc` 是 **多个生产者,单个消费者**(*multiple producer, single consumer*)的缩写。简而言之,Rust 标准库实现通道的方式意味着一个通道可以有多个产生值的 **发送**(*sending*)端,但只能有一个消费这些值的 **接收**(*receiving*)端。想象一下多条小河小溪最终汇聚成大河:所有通过这些小河发出的东西最后都会来到大河的下游。目前我们以单个生产者开始,但是当示例可以工作后会增加多个生产者。 - `mpsc::channel` 函数返回一个元组:第一个元素是发送端,而第二个元素是接收端。由于历史原因,`tx` 和 `rx` 通常作为 **发送者**(*transmitter*)和 **接收者**(*receiver*)的缩写,所以这就是我们将用来绑定这两端变量的名字。这里使用了一个 `let` 语句和模式来解构了此元组;第十八章会讨论 `let` 语句中的模式和解构。如此使用 `let` 语句是一个方便提取 `mpsc::channel` 返回的元组中一部分的手段。 @@ -108,11 +99,11 @@ Got: hi 所有权规则在消息传递中扮演了重要角色,其有助于我们编写安全的并发代码。在并发编程中避免错误是在整个 Rust 程序中必须思考所有权所换来的一大优势。 -现在让我们做一个试验来看看通道与所有权如何一同协作以避免产生问题:我们将尝试在新建线程中的通道中发送完 `val` 值 **之后** 再使用它。尝试编译示例 16-9 中的代码: +现在让我们做一个试验来看看通道与所有权如何一同协作以避免产生问题:我们将尝试在新建线程中的通道中发送完 `val` 值 **之后** 再使用它。尝试编译示例 16-9 中的代码并看看为何这是不允许的: 文件名: src/main.rs -```rust,ignore +```rust,ignore,does_not_compile use std::thread; use std::sync::mpsc; @@ -206,7 +197,6 @@ Got: thread 文件名: src/main.rs - ```rust # use std::thread; # use std::sync::mpsc; diff --git a/src/ch16-03-shared-state.md b/src/ch16-03-shared-state.md index f67faaa..5a237da 100644 --- a/src/ch16-03-shared-state.md +++ b/src/ch16-03-shared-state.md @@ -1,8 +1,8 @@ ## 共享状态并发 -> [ch16-03-shared-state.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch16-03-shared-state.md) +> [ch16-03-shared-state.md](https://github.com/rust-lang/book/blob/master/src/ch16-03-shared-state.md) >
-> commit 90406bd5a4cd4447b46cd7e03d33f34a651e9bb7 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 虽然消息传递是一个很好的处理并发的方式,但并不是唯一一个。再一次思考一下 Go 编程语言文档中口号的这一部分:“通过共享内存通讯”: @@ -10,7 +10,7 @@ > > 通过共享内存通讯看起来如何?除此之外,为何消息传递的拥护者并不使用它并反其道而行之呢? -在某种程度上,任何编程语言中的通道都类似于单所有权,因为一旦将一个值传送到通道中,将无法再使用这个值。共享内存类似于多所有权:多个线程可以同时访问相同的内存位置。第十五章介绍了智能指针如何使得多所有权成为可能,然而这会增加额外的复杂性,因为需要以某种方式管理这些不同的所有者。作为一个例子,让我们看看互斥器,一个更为常见的共享内存并发原语。 +在某种程度上,任何编程语言中的通道都类似于单所有权,因为一旦将一个值传送到通道中,将无法再使用这个值。共享内存类似于多所有权:多个线程可以同时访问相同的内存位置。第十五章介绍了智能指针如何使得多所有权成为可能,然而这会增加额外的复杂性,因为需要以某种方式管理这些不同的所有者。Rust 的类型系统和所有权规则极大的协助了正确地管理这些所有权。作为一个例子,让我们看看互斥器,一个更为常见的共享内存并发原语。 ### 互斥器一次只允许一个线程访问数据 @@ -52,7 +52,6 @@ fn main() { 如果另一个线程拥有锁,并且那个线程 panic 了,则 `lock` 调用会失败。在这种情况下,没人能够再获取锁,所以这里选择 `unwrap` 并在遇到这种情况时使线程 panic。 - 一旦获取了锁,就可以将返回值(在这里是`num`)视为一个其内部数据的可变引用了。类型系统确保了我们在使用 `m` 中的值之前获取锁:`Mutex` 并不是一个 `i32`,所以 **必须** 获取锁才能使用这个 `i32` 值。我们是不会忘记这么做的,因为反之类型系统不允许访问内部的 `i32` 值。 正如你所怀疑的,`Mutex` 是一个智能指针。更准确的说,`lock` 调用 **返回** 一个叫做 `MutexGuard` 的智能指针。这个智能指针实现了 `Deref` 来指向其内部数据;其也提供了一个 `Drop` 实现当 `MutexGuard` 离开作用域时自动释放锁,这正发生于示例 16-12 内部作用域的结尾。为此,我们不会冒忘记释放锁并阻塞互斥器为其它线程所用的风险,因为锁的释放是自动发生的。 @@ -65,7 +64,7 @@ fn main() { 文件名: src/main.rs -```rust,ignore +```rust,ignore,does_not_compile use std::sync::Mutex; use std::thread; @@ -94,11 +93,10 @@ fn main() { 这里创建了一个 `counter` 变量来存放内含 `i32` 的 `Mutex`,类似示例 16-12 那样。接下来遍历 range 创建了 10 个线程。使用了 `thread::spawn` 并对所有线程使用了相同的闭包:他们每一个都将调用 `lock` 方法来获取 `Mutex` 上的锁,接着将互斥器中的值加一。当一个线程结束执行,`num` 会离开闭包作用域并释放锁,这样另一个线程就可以获取它了。 -在主线程中,我们像示例 16-2 那样收集了所有的 join 句柄,调用它们的 `join` 方法来确保所有线程都会结束。之后,主线程会获取锁并打印出程序的结果。 +在主线程中,我们像示例 16-2 那样收集了所有的 join 句柄,调用它们的 `join` 方法来确保所有线程都会结束。这时,主线程会获取锁并打印出程序的结果。 之前提示过这个例子不能编译,让我们看看为什么! - ```text error[E0382]: capture of moved value: `counter` --> src/main.rs:10:27 @@ -130,20 +128,34 @@ error: aborting due to 2 previous errors 让我们简化程序来进行分析。不同于在 `for` 循环中创建 10 个线程,仅仅创建两个线程来观察发生了什么。将示例 16-13 中第一个 `for` 循环替换为如下代码: -```rust,ignore -let handle = thread::spawn(move || { - let mut num = counter.lock().unwrap(); +```rust,ignore,does_not_compile +use std::sync::Mutex; +use std::thread; + +fn main() { + let counter = Mutex::new(0); + let mut handles = vec![]; + + let handle = thread::spawn(move || { + let mut num = counter.lock().unwrap(); + + *num += 1; + }); + handles.push(handle); - *num += 1; -}); -handles.push(handle); + let handle2 = thread::spawn(move || { + let mut num2 = counter.lock().unwrap(); -let handle2 = thread::spawn(move || { - let mut num2 = counter.lock().unwrap(); + *num2 += 1; + }); + handles.push(handle2); + + for handle in handles { + handle.join().unwrap(); + } - *num2 += 1; -}); -handles.push(handle2); + println!("Result: {}", *counter.lock().unwrap()); +} ``` 这里创建了两个线程并将用于第二个线程的变量名改为 `handle2` 和 `num2`。这一次当运行代码时,编译会给出如下错误: @@ -184,7 +196,7 @@ error: aborting due to 2 previous errors 文件名: src/main.rs -```rust,ignore +```rust,ignore,does_not_compile use std::rc::Rc; use std::sync::Mutex; use std::thread; @@ -218,8 +230,7 @@ fn main() { ```text error[E0277]: the trait bound `std::rc::Rc>: std::marker::Send` is not satisfied in `[closure@src/main.rs:11:36: -15:10 -counter:std::rc::Rc>]` +15:10 counter:std::rc::Rc>]` --> src/main.rs:11:22 | 11 | let handle = thread::spawn(move || { @@ -230,8 +241,7 @@ cannot be sent between threads safely counter:std::rc::Rc>]`, the trait `std::marker::Send` is not implemented for `std::rc::Rc>` = note: required because it appears within the type -`[closure@src/main.rs:11:36: 15:10 -counter:std::rc::Rc>]` +`[closure@src/main.rs:11:36: 15:10 counter:std::rc::Rc>]` = note: required by `std::thread::spawn` ``` diff --git a/src/ch16-04-extensible-concurrency-sync-and-send.md b/src/ch16-04-extensible-concurrency-sync-and-send.md index a03773d..cd32151 100644 --- a/src/ch16-04-extensible-concurrency-sync-and-send.md +++ b/src/ch16-04-extensible-concurrency-sync-and-send.md @@ -1,8 +1,8 @@ ## 使用 `Sync` 和 `Send` trait 的可扩展并发 -> [ch16-04-extensible-concurrency-sync-and-send.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch16-04-extensible-concurrency-sync-and-send.md) +> [ch16-04-extensible-concurrency-sync-and-send.md](https://github.com/rust-lang/book/blob/master/src/ch16-04-extensible-concurrency-sync-and-send.md) >
-> commit 90406bd5a4cd4447b46cd7e03d33f34a651e9bb7 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f Rust 的并发模型中一个有趣的方面是:语言本身对并发知之 **甚少**。我们之前讨论的几乎所有内容,都属于标准库,而不是语言本身的内容。由于不需要语言提供并发相关的基础设施,并发方案不受标准库或语言所限:我们可以编写自己的或使用别人编写的并发功能。 From ed13bd6748ce0e292bf7e2ced58caa9fbf2dda04 Mon Sep 17 00:00:00 2001 From: KaiserY Date: Sat, 8 Dec 2018 14:16:23 +0800 Subject: [PATCH 30/49] check to ch17-03 --- README.md | 3 +- src/ch09-03-to-panic-or-not-to-panic.md | 2 +- src/ch17-00-oop.md | 11 +-- src/ch17-01-what-is-oo.md | 37 +++------ src/ch17-02-trait-objects.md | 101 +++++++---------------- src/ch17-03-oo-design-patterns.md | 102 +++++++++--------------- 6 files changed, 83 insertions(+), 173 deletions(-) diff --git a/README.md b/README.md index c2d7540..bb6ebe3 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,11 @@ ## 状态 -目前正向 2018 edtion 过渡,目前官方仓库状已经不再分版本提供源码了,故本仓库准备与官方保持一致。 +目前正向 2018 edtion 过渡,目前官方仓库状已经不再分版本提供源码了,故本仓库准备与官方保持一致。已更新到第十七章。 PS: * 对照源码位置:https://github.com/rust-lang/book/tree/master/src -* 官方仓库用于阅读的网页仍是之前的版本,与新版区别不大,最大的区别在于 2018 edtion 对模块功能做出较大修改,第七章基本重写。 * 每章翻译开头都带有官方链接和 commit hash,若发现与官方不一致,欢迎 Issue 或 PR :) ## 静态页面构建与文档撰写 diff --git a/src/ch09-03-to-panic-or-not-to-panic.md b/src/ch09-03-to-panic-or-not-to-panic.md index b038c37..a703cc8 100644 --- a/src/ch09-03-to-panic-or-not-to-panic.md +++ b/src/ch09-03-to-panic-or-not-to-panic.md @@ -16,7 +16,7 @@ 如果方法调用在测试中失败了,我们希望这个测试都失败,即便这个方法并不是需要测试的功能。因为 `panic!` 是测试如何被标记为失败的,调用 `unwrap` 或 `expect` 就是应该发生的事情。 -### 当你比编译器知道更多的情况 +### 当我们比编译器知道更多的情况 当你有一些其他的逻辑来确保 `Result` 会是 `Ok` 值时,调用 `unwrap` 也是合适的,虽然编译器无法理解这种逻辑。你仍然需要处理一个 `Result` 值:即使在你的特定情况下逻辑上是不可能的,你所调用的任何操作仍然有可能失败。如果通过人工检查代码来确保永远也不会出现 `Err` 值,那么调用 `unwrap` 也是完全可以接受的,这里是一个例子: diff --git a/src/ch17-00-oop.md b/src/ch17-00-oop.md index edd137f..93c73cf 100644 --- a/src/ch17-00-oop.md +++ b/src/ch17-00-oop.md @@ -1,12 +1,7 @@ # Rust 的面向对象特性 -> [ch17-00-oop.md][ch17-00] +> [ch17-00-oop.md](https://github.com/rust-lang/book/blob/master/src/ch17-00-oop.md) >
-> commit 07b0ca8c829af09d60ab4eb9e69584b6f4a96f60 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -[ch17-00]: https://github.com/rust-lang/book/blob/master/second-edition/src/ch17-00-oop.md -[commit]: https://github.com/rust-lang/book/commit/07b0ca8c829af09d60ab4eb9e69584b6f4a96f60 - -面向对象编程(Object-Oriented Programming,OOP) - -面向对象编程(Object-Oriented Programming,OOP)是一种起源于 20 世纪 60 年代的 Simula 编程语言的模式化编程方式,然后在 90 年代随着 C++ 语言开始流行。关于 OOP 是什么有很多相互矛盾的定义,在一些定义下,Rust 是面向对象的;在其他定义下,Rust 不是。在本章节中,我们会探索一些被普遍认为是面向对象的特性和这些特性是如何体现在 Rust 语言习惯中的。接着会展示如何在 Rust 中实现面向对象设计模式,并讨论这么做与利用 Rust 自身的一些优势实现的方案相比有什么取舍。 +面向对象编程(Object-Oriented Programming,OOP)是一种模式化编程方式。对象(Object)来源于 20 世纪 60 年代的 Simula 编程语言。这些对象影响了 Alan Kay 的编程架构中对象之间的消息传递。他在 1967 年创造了 **面向对象编程** 这个术语来描述这种架构。关于 OOP 是什么有很多相互矛盾的定义;在一些定义下,Rust 是面向对象的;在其他定义下,Rust 不是。在本章节中,我们会探索一些被普遍认为是面向对象的特性和这些特性是如何体现在 Rust 语言习惯中的。接着会展示如何在 Rust 中实现面向对象设计模式,并讨论这么做与利用 Rust 自身的一些优势实现的方案相比有什么取舍。 diff --git a/src/ch17-01-what-is-oo.md b/src/ch17-01-what-is-oo.md index 79ac7c6..ec35577 100644 --- a/src/ch17-01-what-is-oo.md +++ b/src/ch17-01-what-is-oo.md @@ -1,30 +1,26 @@ -## 什么是面向对象? +## 面向对象语言的特征 -> [ch17-01-what-is-oo.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch17-01-what-is-oo.md) +> [ch17-01-what-is-oo.md](https://github.com/rust-lang/book/blob/master/src/ch17-01-what-is-oo.md) >
-> commit e7df3050309924827ff828ddc668a8667652d2fe +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 关于一个语言被称为面向对象所需的功能,在编程社区内并未达成一致意见。Rust 被很多不同的编程范式影响,包括面向对象编程;比如第十三章提到了来自函数式编程的特性。面向对象编程语言所共享的一些特性往往是对象、封装和继承。让我们看一下这每一个概念的含义以及 Rust 是否支持他们。 ### 对象包含数据和行为 - - - -`Design Patterns: Elements of Reusable Object-Oriented Software` 这本书被俗称为 `The Gang of Four book`,是面向对象编程模式的目录。它这样定义面向对象编程: +Erich Gamma、Richard Helm、Ralph Johnson和 John Vlissides(Addison-Wesley Professional, 1994)的 *Design Patterns: Elements of Reusable Object-Oriented Software* 被俗称为 `The Gang of Four book`,是面向对象编程模式的目录。它这样定义面向对象编程: +> Object-oriented programs are made up of objects. An *object* packages both +> data and the procedures that operate on that data. The procedures are +> typically called *methods* or *operations*. +> > 面向对象的程序是由对象组成的。一个 **对象** 包含数据和操作这些数据的过程。这些过程通常被称为 **方法** 或 **操作**。 -在这个定义下,Rust 是面向对象的:结构体和枚举包含数据而 impl 块提供了在结构体和枚举之上的方法。虽然带有方法的结构体和枚举并不被 **称为** 对象,但是他们提供了与对象相同的功能,参考 Gang of Four 中对象的定义。 +在这个定义下,Rust 是面向对象的:结构体和枚举包含数据而 `impl` 块提供了在结构体和枚举之上的方法。虽然带有方法的结构体和枚举并不被 **称为** 对象,但是他们提供了与对象相同的功能,参考 Gang of Four 中对象的定义。 ### 封装隐藏了实现细节 -另一个通常与面向对象编程相关的方面是 **封装**(*encapsulation*)的思想:对象的实现细节不能被使用对象的代码获取到。唯一与对象交互的方式是通过对象提供的公有 API;使用对象的代码无法深入到对象内部并直接改变数据或者行为。封装使得改变和重构对象的内部时无需改变使用对象的代码。 +另一个通常与面向对象编程相关的方面是 **封装**(*encapsulation*)的思想:对象的实现细节不能被使用对象的代码获取到。所以唯一与对象交互的方式是通过对象提供的公有 API;使用对象的代码无法深入到对象内部并直接改变数据或者行为。封装使得改变和重构对象的内部时无需改变使用对象的代码。 就像我们在第七章讨论的那样:可以使用 `pub` 关键字来决定模块、类型、函数和方法是公有的,而默认情况下其他一切都是私有的。比如,我们可以定义一个包含一个 `i32` 类型 vector 的结构体 `AveragedCollection `。结构体也可以有一个字段,该字段保存了 vector 中所有值的平均值。这样,希望知道结构体中的 vector 的平均值的人可以随时获取它,而无需自己计算。换句话说,`AveragedCollection` 会为我们缓存平均值结果。示例 17-1 有 `AveragedCollection` 结构体的定义: @@ -92,21 +88,14 @@ impl AveragedCollection { 如果一个语言必须有继承才能被称为面向对象语言的话,那么 Rust 就不是面向对象的。无法定义一个结构体继承父结构体的成员和方法。然而,如果你过去常常在你的编程工具箱使用继承,根据你最初考虑继承的原因,Rust 也提供了其他的解决方案。 -选择继承有两个主要的原因。第一个是为了重用代码:一旦为一个类型实现了特定行为,继承可以对一个不同的类型重用这个实现。相反 Rust 代码可以使用默认 trait 方法实现来进行共享,在示例 10-15 中我们见过在 `Summarizable` trait 上增加的 `summary` 方法的默认实现。任何实现了 `Summarizable` trait 的类型都可以使用 `summary` 方法而无须进一步实现。这类似于父类有一个方法的实现,而通过继承子类也拥有这个方法的实现。当实现 `Summarizable` trait 时也可以选择覆盖 `summary` 的默认实现,这类似于子类覆盖从父类继承的方法实现。 +选择继承有两个主要的原因。第一个是为了重用代码:一旦为一个类型实现了特定行为,继承可以对一个不同的类型重用这个实现。相反 Rust 代码可以使用默认 trait 方法实现来进行共享,在示例 10-14 中我们见过在 `Summary` trait 上增加的 `summarize` 方法的默认实现。任何实现了 `Summary` trait 的类型都可以使用 `summarize` 方法而无须进一步实现。这类似于父类有一个方法的实现,而通过继承子类也拥有这个方法的实现。当实现 `Summary` trait 时也可以选择覆盖 `summarize` 的默认实现,这类似于子类覆盖从父类继承的方法实现。 第二个使用继承的原因与类型系统有关:表现为子类型可以用于父类型被使用的地方。这也被称为 **多态**(*polymorphism*),这意味着如果多种对象共享特定的属性,则可以相互替代使用。 - - - - - > 多态(Polymorphism) > -> 很多人将多态描述为继承的同义词。不过它是一个有关可以用于多种类型的代码的更广泛的概念。对于继承来说,这些类型通常是子类。Rust 则通过泛型来使得对多个不同类型的抽象成为可能,并通过 trait bounds 加强对这些类型所必须提供的内容的限制。这有时被称为 *bounded parametric polymorphism*。 - - +> 很多人将多态描述为继承的同义词。不过它是一个有关可以用于多种类型的代码的更广泛的概念。对于继承来说,这些类型通常是子类。 +> Rust 则通过泛型来使得对多个不同类型的抽象成为可能,并通过 trait bounds 加强对这些类型所必须提供的内容的限制。这有时被称为 *bounded parametric polymorphism*。 近来继承作为一种语言设计的解决方案在很多语言中失宠了,因为其时常带有共享多于所需的代码的风险。子类不应总是共享其父类的多有特征,但是继承却始终如此。如此会使程序设计更为不灵活,并引入无意义的子类方法调用,或由于方法实际并不适用于子类而造成错误的可能性。某些语言还只允许子类继承一个父类,进一步限制了程序设计的灵活性。 diff --git a/src/ch17-02-trait-objects.md b/src/ch17-02-trait-objects.md index 8345ba8..4184449 100644 --- a/src/ch17-02-trait-objects.md +++ b/src/ch17-02-trait-objects.md @@ -1,24 +1,20 @@ ## 为使用不同类型的值而设计的 trait 对象 -> [ch17-02-trait-objects.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch17-02-trait-objects.md) +> [ch17-02-trait-objects.md](https://github.com/rust-lang/book/blob/master/src/ch17-02-trait-objects.md) >
-> commit ccdd9ca7aacea4cefeb6a96e7ffb9ea91a923abd +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 在第八章中,我们谈到了 vector 只能存储同种类型元素的局限。示例 8-10 中提供了一个定义 `SpreadsheetCell` 枚举来储存整型,浮点型和文本成员的替代方案。这意味着可以在每个单元中储存不同类型的数据,并仍能拥有一个代表一排单元的 vector。这在当编译代码时就知道希望可以交替使用的类型为固定集合的情况下是完全可行的。 -然而有时,我们希望库用户在特定情况下能够扩展有效的类型集合。为了展示如何实现这一点,这里将创建一个图形用户接口(Graphical User Interface, GUI)工具的例子,其它通过遍历列表并调用每一个项目的 `draw` 方法来将其绘制到屏幕上;此乃一个 GUI 工具的常见技术。我们将要创建一个叫做 `rust_gui` 的库 crate,它含一个 GUI 库的结构。这个 GUI 库包含一些可供开发者使用的类型,比如 `Button` 或 `TextField`。在此之上,`rust_gui` 的用户希望创建自定义的可以绘制于屏幕上的类型:比如,一个程序员可能会增加 `Image`,另一个可能会增加 `SelectBox`。 +然而有时,我们希望库用户在特定情况下能够扩展有效的类型集合。为了展示如何实现这一点,这里将创建一个图形用户接口(Graphical User Interface, GUI)工具的例子,其它通过遍历列表并调用每一个项目的 `draw` 方法来将其绘制到屏幕上 —— 此乃一个 GUI 工具的常见技术。我们将要创建一个叫做 `gui` 的库 crate,它含一个 GUI 库的结构。这个 GUI 库包含一些可供开发者使用的类型,比如 `Button` 或 `TextField`。在此之上,`gui` 的用户希望创建自定义的可以绘制于屏幕上的类型:比如,一个程序员可能会增加 `Image`,另一个可能会增加 `SelectBox`。 -这个例子中并不会实现一个功能完善的 GUI 库,不过会展示其中各个部分是如何结合在一起的。编写库的时候,我们不可能知晓并定义所有其他程序员希望创建的类型。我们所知晓的是 `rust_gui` 需要记录一系列不同类型的值,并需要能够对其中每一个值调用 `draw` 方法。这里无需知道调用 `draw` 方法时具体会发生什么,只需提供可供这些值调用的方法即可。 +这个例子中并不会实现一个功能完善的 GUI 库,不过会展示其中各个部分是如何结合在一起的。编写库的时候,我们不可能知晓并定义所有其他程序员希望创建的类型。我们所知晓的是 `gui` 需要记录一系列不同类型的值,并需要能够对其中每一个值调用 `draw` 方法。这里无需知道调用 `draw` 方法时具体会发生什么,只需提供可供这些值调用的方法即可。 在拥有继承的语言中,可以定义一个名为 `Component` 的类,该类上有一个 `draw` 方法。其他的类比如 `Button`、`Image` 和 `SelectBox` 会从 `Component` 派生并因此继承 `draw` 方法。它们各自都可以覆盖 `draw` 方法来定义自己的行为,但是框架会把所有这些类型当作是 `Component` 的实例,并在其上调用 `draw`。不过 Rust 并没有继承,我们得另寻出路。 ### 定义通用行为的 trait -为了实现 `rust_gui` 所期望拥有的行为,定义一个 `Draw` trait,其包含名为 `draw` 的方法。接着可以定义一个存放 **trait 对象**(*trait object*) 的 vector。trait 对象指向一个实现了我们指定 trait 的类型实例。我们通过指定某些指针,比如 `&` 引用或 `Box` 智能指针,接着指定相关的 trait(第十九章动态大小类型部分会介绍 trait 对象必须使用指针的原因)。我们可以使用 trait 对象代替泛型或具体类型。任何使用 trait 对象的位置,Rust 的类型系统会在编译时确保任何在此上下文中使用的值会实现其 trait 对象的 trait。如此便无需在编译时就知晓所有可能的类型。 - - - +为了实现 `gui` 所期望拥有的行为,定义一个 `Draw` trait,其包含名为 `draw` 的方法。接着可以定义一个存放 **trait 对象**(*trait object*) 的 vector。trait 对象指向一个实现了我们指定 trait 的类型实例。我们通过指定某些指针,比如 `&` 引用或 `Box` 智能指针,接着指定相关的 trait(第十九章 “动态大小类型” 部分会介绍 trait 对象必须使用指针的原因)。我们可以使用 trait 对象代替泛型或具体类型。任何使用 trait 对象的位置,Rust 的类型系统会在编译时确保任何在此上下文中使用的值会实现其 trait 对象的 trait。如此便无需在编译时就知晓所有可能的类型。 之前提到过,Rust 刻意不将结构体与枚举称为 “对象”,以便与其他语言中的对象相区别。在结构体或枚举中,结构体字段中的数据和 `impl` 块中的行为是分开的,不同于其他语言中将数据和行为组合进一个称为对象的概念中。trait 对象将数据和行为两者相结合,从这种意义上说 **则** 其更类似其他语言中的对象。不过 trait 对象不同于传统的对象,因为不能向 trait 对象增加数据。trait 对象并不像其他语言中的对象那么通用:其(trait 对象)具体的作用是允许对通用行为的抽象。 @@ -34,13 +30,7 @@ pub trait Draw { 示例 17-3:`Draw` trait 的定义 -因为第十章已经讨论过如何定义 trait,这看起来应该比较眼熟。接下来就是新内容了:实例 17-4 定义了一个存放了名叫 `components` 的 vector 的结构体 `Screen`。这个 vector 的类型是 `Box`,此为一个 trait 对象:它是 `Box` 中任何实现了 `Draw` trait 的类型的替身。 - - - +因为第十章已经讨论过如何定义 trait,其语法看起来应该比较眼熟。接下来就是新内容了:实例 17-4 定义了一个存放了名叫 `components` 的 vector 的结构体 `Screen`。这个 vector 的类型是 `Box`,此为一个 trait 对象:它是 `Box` 中任何实现了 `Draw` trait 的类型的替身。 文件名: src/lib.rs @@ -50,7 +40,7 @@ this section where we talk about needing a `&` or a `Box` to be a trait object. # } # pub struct Screen { - pub components: Vec>, + pub components: Vec>, } ``` @@ -66,7 +56,7 @@ pub struct Screen { # } # # pub struct Screen { -# pub components: Vec>, +# pub components: Vec>, # } # impl Screen { @@ -128,7 +118,7 @@ pub struct Button { impl Draw for Button { fn draw(&self) { - // Code to actually draw a button + // 实际绘制按钮的代码 } } ``` @@ -142,8 +132,7 @@ impl Draw for Button { 文件名: src/main.rs ```rust,ignore -extern crate rust_gui; -use rust_gui::Draw; +use gui::Draw; struct SelectBox { width: u32, @@ -153,19 +142,19 @@ struct SelectBox { impl Draw for SelectBox { fn draw(&self) { - // Code to actually draw a select box + // code to actually draw a select box } } ``` -示例 17-8: 另一个使用 `rust_gui` 的 crate 中,在 `SelectBox` 结构体上实现 `Draw` trait +示例 17-8: 另一个使用 `gui` 的 crate 中,在 `SelectBox` 结构体上实现 `Draw` trait 库使用者现在可以在他们的 `main` 函数中创建一个 `Screen` 实例。至此可以通过将 `SelectBox` 和 `Button` 放入 `Box` 转变为 trait 对象来增加组件。接着可以调用 `Screen` 的 `run` 方法,它会调用每个组件的 `draw` 方法。示例 17-9 展示了这个实现: 文件名: src/main.rs ```rust,ignore -use rust_gui::{Screen, Button}; +use gui::{Screen, Button}; fn main() { let screen = Screen { @@ -195,18 +184,7 @@ fn main() { 当编写库的时候,我们不知道何人会在何时增加 `SelectBox` 类型,不过 `Screen` 的实现能够操作并绘制这个新类型,因为 `SelectBox` 实现了 `Draw` trait,这意味着它实现了 `draw` 方法。 -这个概念 ———— 只关心值所反映的信息而不是其具体类型 ———— 类似于动态类型语言中称为 **鸭子类型**(*duck typing*)的概念:如果它走起来像一只鸭子,叫起来像一只鸭子,那么它就是一只鸭子!在示例 17-5 中 `Screen` 上的 `run` 实现中,`run` 并不需要知道各个组件的具体类型是什么。它并不检查组件是 `Button` 或者 `SelectBox` 的实例。通过指定 `Box` 作为 `components` vector 中值的类型,我们就定义了 `Screen` 需要可以在其上调用 `draw` 方法的值。 - - - +这个概念 —— 只关心值所反映的信息而不是其具体类型 —— 类似于动态类型语言中称为 **鸭子类型**(*duck typing*)的概念:如果它走起来像一只鸭子,叫起来像一只鸭子,那么它就是一只鸭子!在示例 17-5 中 `Screen` 上的 `run` 实现中,`run` 并不需要知道各个组件的具体类型是什么。它并不检查组件是 `Button` 或者 `SelectBox` 的实例。通过指定 `Box` 作为 `components` vector 中值的类型,我们就定义了 `Screen` 需要可以在其上调用 `draw` 方法的值。 使用 trait 对象和 Rust 类型系统来进行类似鸭子类型操作的优势是无需在运行时检查一个值是否实现了特定方法或者担心在调用时因为值没有实现方法而产生错误。如果值没有实现 trait 对象所需的 trait 则 Rust 不会编译这些代码。 @@ -214,9 +192,8 @@ at all? Is there something we should clarify in the text? /Carol --> 文件名: src/main.rs -```rust,ignore -extern crate rust_gui; -use rust_gui::Screen; +```rust,ignore,does_not_compile +use gui::Screen; fn main() { let screen = Screen { @@ -234,47 +211,26 @@ fn main() { 我们会遇到这个错误,因为 `String` 没有实现 `rust_gui::Draw` trait: ```text -error[E0277]: the trait bound `std::string::String: rust_gui::Draw` is not satisfied - --> +error[E0277]: the trait bound `std::string::String: gui::Draw` is not satisfied + --> src/main.rs:7:13 | - 4 | Box::new(String::from("Hi")), - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `rust_gui::Draw` is not + 7 | Box::new(String::from("Hi")), + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait gui::Draw is not implemented for `std::string::String` | - = note: required for the cast to the object type `rust_gui::Draw` + = note: required for the cast to the object type `gui::Draw` ``` 这告诉了我们,要么是我们传递了并不希望传递给 `Screen` 的类型并应该提供其他类型,要么应该在 `String` 上实现 `Draw` 以便 `Screen` 可以调用其上的 `draw`。 ### trait 对象执行动态分发 -回忆一下第十章讨论过的,当对泛型使用 trait bound 时编译器所进行单态化处理:编译器为每一个被泛型类型参数代替的具体类型生成了非泛型的函数和方法实现。单态化所产生的代码进行 **静态分发**(*static dispatch*)。静态分发发生于编译器在编译时就知晓调用了什么方法的时候。这与 **动态分发** (*dynamic dispatch*)相对,这时编译器在编译时无法知晓调用了什么方法。在这种情况下,编译器会生成在运行时确定调用了什么方法的代码。 +回忆一下第十章讨论过的,当对泛型使用 trait bound 时编译器所进行单态化处理:编译器为每一个被泛型类型参数代替的具体类型生成了非泛型的函数和方法实现。单态化所产生的代码进行 **静态分发**(*static dispatch*)。静态分发发生于编译器在编译时就知晓调用了什么方法的时候。这与 **动态分发** (*dynamic dispatch*)相对,这时编译器在编译时无法知晓调用了什么方法。在动态分发的情况下,编译器会生成在运行时确定调用了什么方法的代码。 - - - -当使用 trait 对象时,Rust 必须使用动态分发。编译器无法知晓所有可能用于 trait 对象代码的类型,所以它也不知道应该调用哪个类型的哪个方法实现。为此,Rust 在运行时使用 trait 对象中的指针来知晓需要调用哪个方法。动态分发也阻止编译器有选择的内联方法代码,这会相应的禁用一些优化。尽管在编写和支持代码的过程中确实获得了额外的灵活性,但仍然需要权衡取舍。 +当使用 trait 对象时,Rust 必须使用动态分发。编译器无法知晓所有可能用于 trait 对象代码的类型,所以它也不知道应该调用哪个类型的哪个方法实现。为此,Rust 在运行时使用 trait 对象中的指针来知晓需要调用哪个方法。动态分发也阻止编译器有选择的内联方法代码,这会相应的禁用一些优化。尽管在编写示例 17-5和可以支持示例 17-9 中的代码的过程中确实获得了额外的灵活性,但仍然需要权衡取舍。 ### Trait 对象要求对象安全 - - - - 只有 **对象安全**(*object safe*)的 trait 才可以组成 trait 对象。围绕所有使得 trait 对象安全的属性存在一些复杂的规则,不过在实践中,只涉及到两条规则。如果一个 trait 中所有的方法有如下属性时,则该 trait 是对象安全的: - 返回值类型不为 `Self` @@ -292,12 +248,11 @@ pub trait Clone { `String` 实现了 `Clone` trait,当在 `String` 实例上调用 `clone` 方法时会得到一个 `String` 实例。类似的,当调用 `Vec` 实例的 `clone` 方法会得到一个 `Vec` 实例。`clone` 的签名需要知道什么类型会代替 `Self`,因为这是它的返回值。 - 如果尝试做一些违反有关 trait 对象的对象安全规则的事情,编译器会提示你。例如,如果尝试实现示例 17-4 中的 `Screen` 结构体来存放实现了 `Clone` trait 而不是 `Draw` trait 的类型,像这样: -```rust,ignore +```rust,ignore,does_not_compile pub struct Screen { - pub components: Vec>, + pub components: Vec>, } ``` @@ -305,11 +260,11 @@ pub struct Screen { ```text error[E0038]: the trait `std::clone::Clone` cannot be made into an object - --> + --> src/lib.rs:2:5 | -2 | pub components: Vec>, +2 | pub components: Vec>, | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::clone::Clone` cannot be - made into an object +made into an object | = note: the trait cannot require that `Self : Sized` ``` diff --git a/src/ch17-03-oo-design-patterns.md b/src/ch17-03-oo-design-patterns.md index be36961..c2ae9a9 100644 --- a/src/ch17-03-oo-design-patterns.md +++ b/src/ch17-03-oo-design-patterns.md @@ -1,19 +1,13 @@ ## 面向对象设计模式的实现 -> [ch17-03-oo-design-patterns.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch17-03-oo-design-patterns.md) +> [ch17-03-oo-design-patterns.md](https://github.com/rust-lang/book/blob/master/src/ch17-03-oo-design-patterns.md) >
-> commit b18f90970ab7223ee8af18ef466a5ba6ff8482ef +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -**状态模式**(*state pattern*)是一个面向对象设计模式。该模式的关键在于一个值有某些内部状态,体现为一系列的 **状态对象**,同时值的行为随着其内部状态而改变。状态对象共享功能 —— 当然,在 Rust 中使用结构体和 trait 而不是对象和继承。每一个状态对象代表负责其自身的行为和当需要改变为另一个状态时的规则的状态。持有任何一个这种状态对象的值对于不同状态的行为以及何时状态转移毫不知情。 - - - +**状态模式**(*state pattern*)是一个面向对象设计模式。该模式的关键在于一个值有某些内部状态,体现为一系列的 **状态对象**,同时值的行为随着其内部状态而改变。状态对象共享功能:当然,在 Rust 中使用结构体和 trait 而不是对象和继承。每一个状态对象代表负责其自身的行为和当需要改变为另一个状态时的规则的状态。持有任何一个这种状态对象的值对于不同状态的行为以及何时状态转移毫不知情。 使用状态模式意味着当程序的业务需求改变时,无需改变值持有状态或者使用值的代码。我们只需更新某个状态对象中的代码来改变其规则,或者是增加更多的状态对象。让我们看看一个有关状态模式和如何在 Rust 中使用它的例子。 - -让我们看看一个状态设计模式的例子以及如何在 Rust 中使用他们。**状态模式**(*state pattern*)是指一个值有某些内部状态,而它的行为随着其内部状态而改变。内部状态由一系列继承了共享功能的对象表现(我们使用结构体和 trait 因为 Rust 没有对象和继承)。每一个状态对象负责它自身的行为和当需要改变为另一个状态时的规则。持有任何一个这种状态对象的值对于不同状态的行为以及何时状态转移毫不知情。当将来需求改变时,无需改变值持有状态或者使用值的代码。我们只需更新某个状态对象中的代码来改变它的规则,或者是增加更多的状态对象。 - 为了探索这个概念,我们将实现一个增量式的发布博文的工作流。这个博客的最终功能看起来像这样: 1. 博文从空白的草案开始。 @@ -23,12 +17,11 @@ 任何其他对博文的修改尝试都是没有作用的。例如,如果尝试在请求审核之前通过一个草案博文,博文应该保持未发布的状态。 -示例 17-11 展示这个工作流的代码形式。这是一个我们将要在一个叫做 `blog` 的库 crate 中实现的 API 的示例: +示例 17-11 展示这个工作流的代码形式:这是一个我们将要在一个叫做 `blog` 的库 crate 中实现的 API 的示例。这段代码还不能编译,因为还未实现 `blog`。 文件名: src/main.rs ```rust,ignore -extern crate blog; use blog::Post; fn main() { @@ -51,26 +44,17 @@ fn main() { 接下来,我们希望能够请求审核博文,而在等待审核的阶段 `content` 应该仍然返回空字符串。最后当博文审核通过,它应该被发表,这意味着当调用 `content` 时博文的文本将被返回。 - - - - 注意我们与 crate 交互的唯一的类型是 `Post`。这个类型会使用状态模式并会存放处于三种博文所可能的状态之一的值 —— 草案,等待审核和发布。状态上的改变由 `Post` 类型内部进行管理。状态依库用户对 `Post` 实例调用的方法而改变,但是不能直接管理状态变化。这也意味着用户不会在状态上犯错,比如在过审前发布博文。 ### 定义 `Post` 并新建一个草案状态的实例 让我们开始实现这个库吧!我们知道需要一个公有 `Post` 结构体来存放一些文本,所以让我们从结构体的定义和一个创建 `Post` 实例的公有关联函数 `new` 开始,如示例 17-12 所示。还需定义一个私有 trait `State`。`Post` 将在私有字段 `state` 中存放一个 `Option` 类型的 trait 对象 `Box`。稍后将会看到为何 `Option` 是必须的。 -`State` trait 定义了所有不同状态的博文所共享的行为,同时 `Draft`、`PendingReview` 和 `Published` 状态都会实现 `State` 状态。现在这个 trait 并没有任何方法,同时开始将只定义 `Draft` 状态因为这是我们希望博文的初始状态: - 文件名: src/lib.rs ```rust pub struct Post { - state: Option>, + state: Option>, content: String, } @@ -90,13 +74,16 @@ struct Draft {} impl State for Draft {} ``` -示例 17-12: `Post` 结构体的定义和新建 `Post` 实例的 `new` 函数,`State` trait 和结构体 `Draft` -当创建新的 `Post` 时,我们将其 `state` 字段设置为一个存放了 `Box` 的 `Some` 值。这个 `Box` 指向一个 `Draft` 结构体新实例。这确保了无论何时新建一个 `Post` 实例,它都会从草案开始。因为 `Post` 的 `state` 字段是私有的,也就无法创建任何其他状态的 `Post` 了!。 +示例 17-12: `Post` 结构体的定义和新建 `Post` 实例的 `new` 函数,`State` trait 和结构体 `Draft` + +`State` trait 定义了所有不同状态的博文所共享的行为,同时 `Draft`、`PendingReview` 和 `Published` 状态都会实现 `State` 状态。现在这个 trait 并没有任何方法,同时开始将只定义 `Draft` 状态因为这是我们希望博文的初始状态。 + +当创建新的 `Post` 时,我们将其 `state` 字段设置为一个存放了 `Box` 的 `Some` 值。这个 `Box` 指向一个 `Draft` 结构体新实例。这确保了无论何时新建一个 `Post` 实例,它都会从草案开始。因为 `Post` 的 `state` 字段是私有的,也就无法创建任何其他状态的 `Post` 了!。`Post::new` 函数中将 `content` 设置为新建的空 `String`。 ### 存放博文内容的文本 -在 `Post::new` 函数中,我们设置 `content` 字段为新的空 `String`。在示例 17-11 中,展示了我们希望能够调用一个叫做 `add_text` 的方法并向其传递一个 `&str` 来将文本增加到博文的内容中。选择实现为一个方法而不是将 `content` 字段暴露为 `pub` 。这意味着之后可以实现一个方法来控制 `content` 字段如何被读取。`add_text` 方法是非常直观的,让我们在示例 17-13 的 `impl Post` 块中增加一个实现: +在示例 17-11 中,展示了我们希望能够调用一个叫做 `add_text` 的方法并向其传递一个 `&str` 来将文本增加到博文的内容中。选择实现为一个方法而不是将 `content` 字段暴露为 `pub` 。这意味着之后可以实现一个方法来控制 `content` 字段如何被读取。`add_text` 方法是非常直观的,让我们在示例 17-13 的 `impl Post` 块中增加一个实现: 文件名: src/lib.rs @@ -117,7 +104,7 @@ impl Post { `add_text` 获取一个 `self` 的可变引用,因为需要改变调用 `add_text` 的 `Post` 实例。接着调用 `content` 中的 `String` 的 `push_str` 并传递 `text` 参数来保存到 `content` 中。这不是状态模式的一部分,因为它的行为并不依赖博文所处的状态。`add_text` 方法完全不与 `state` 状态交互,不过这是我们希望支持的行为的一部分。 -### 博文草案的内容是空的 +### 确保博文草案的内容是空的 即使调用 `add_text` 并向博文增加一些内容之后,我们仍然希望 `content` 方法返回一个空字符串 slice,因为博文仍然处于草案状态,如示例 17-11 的第 8 行所示。现在让我们使用能满足要求的最简单的方式来实现 `content` 方法:总是返回一个空字符串 slice。当实现了将博文状态改为发布的能力之后将改变这一做法。但是目前博文只能是草案状态,这意味着其内容应该总是空的。示例 17-14 展示了这个占位符实现: @@ -142,19 +129,13 @@ impl Post { ### 请求审核博文来改变其状态 -接下来需要增加请求审核博文的功能,这应当将其状态由 `Draft` 改为 `PendingReview`。我们希望为 `Post` 增加一个获取 `self` 可变引用的公有方法 `request_review`。接着将 `Post` 当前状态内部的 `request_review` 方法而这第二个 `request_review` 方法会消费当前的状态并返回一个新状态。示例 17-15 展示了这个代码: - - - +接下来需要增加请求审核博文的功能,这应当将其状态由 `Draft` 改为 `PendingReview`。示例 17-15 展示了这个代码: 文件名: src/lib.rs ```rust # pub struct Post { -# state: Option>, +# state: Option>, # content: String, # } # @@ -168,13 +149,13 @@ impl Post { } trait State { - fn request_review(self: Box) -> Box; + fn request_review(self: Box) -> Box; } struct Draft {} impl State for Draft { - fn request_review(self: Box) -> Box { + fn request_review(self: Box) -> Box { Box::new(PendingReview {}) } } @@ -182,7 +163,7 @@ impl State for Draft { struct PendingReview {} impl State for PendingReview { - fn request_review(self: Box) -> Box { + fn request_review(self: Box) -> Box { self } } @@ -190,11 +171,9 @@ impl State for PendingReview { 示例 17-15: 实现 `Post` 和 `State` trait 的 `request_review` 方法 -这里给 `State` trait 增加了 `request_review` 方法;所有实现了这个 trait 的类型现在都需要实现 `request_review` 方法。注意不同于使用 `self`、 `&self` 或者 `&mut self` 作为方法的第一个参数,这里使用了 `self: Box`。这个语法意味着这个方法调用只对这个类型的 `Box` 有效。这个语法获取了 `Box` 的所有权,使老状态无效化以便 `Post` 的状态值可以将自身转换为新状态。 +这里为 `Post` 增加一个获取 `self` 可变引用的公有方法 `request_review`。接着将 `Post` 当前状态内部的 `request_review` 方法而这第二个 `request_review` 方法会消费当前的状态并返回一个新状态。 - - +这里给 `State` trait 增加了 `request_review` 方法;所有实现了这个 trait 的类型现在都需要实现 `request_review` 方法。注意不同于使用 `self`、 `&self` 或者 `&mut self` 作为方法的第一个参数,这里使用了 `self: Box`。这个语法意味着这个方法调用只对这个类型的 `Box` 有效。这个语法获取了 `Box` 的所有权,使老状态无效化以便 `Post` 的状态值可以将自身转换为新状态。 为了消费老状态,`request_review` 方法需要获取状态值的所有权。这也就是 `Post` 的 `state` 字段中 `Option` 的来历:调用 `take` 方法将 `state` 字段中的 `Some` 值取出并留下一个 `None`,因为 Rust 不允许在结构体中存在空的字段。这使得我们将 `state` 值移动出 `Post` 而不是借用它。接着将博文的 `state` 值设置为这个操作的结果。 @@ -214,7 +193,7 @@ which changes the state of Post-- I've tried to clarify. /Carol --> ```rust # pub struct Post { -# state: Option>, +# state: Option>, # content: String, # } # @@ -228,19 +207,19 @@ impl Post { } trait State { - fn request_review(self: Box) -> Box; - fn approve(self: Box) -> Box; + fn request_review(self: Box) -> Box; + fn approve(self: Box) -> Box; } struct Draft {} impl State for Draft { -# fn request_review(self: Box) -> Box { +# fn request_review(self: Box) -> Box { # Box::new(PendingReview {}) # } # // --snip-- - fn approve(self: Box) -> Box { + fn approve(self: Box) -> Box { self } } @@ -248,12 +227,12 @@ impl State for Draft { struct PendingReview {} impl State for PendingReview { -# fn request_review(self: Box) -> Box { +# fn request_review(self: Box) -> Box { # self # } # // --snip-- - fn approve(self: Box) -> Box { + fn approve(self: Box) -> Box { Box::new(Published {}) } } @@ -261,11 +240,11 @@ impl State for PendingReview { struct Published {} impl State for Published { - fn request_review(self: Box) -> Box { + fn request_review(self: Box) -> Box { self } - fn approve(self: Box) -> Box { + fn approve(self: Box) -> Box { self } } @@ -286,7 +265,7 @@ impl State for Published { # fn content<'a>(&self, post: &'a Post) -> &'a str; # } # pub struct Post { -# state: Option>, +# state: Option>, # content: String, # } # @@ -305,11 +284,9 @@ impl Post { 这里调用 `Option` 的 `as_ref` 方法是因为需要 `Option` 中值的引用而不是获取其所有权。因为 `state` 是一个 `Option>`,调用 `as_ref` 会返回一个 `Option<&Box>`。如果不调用 `as_ref`,怎会得到一个错误,因为不能将 `state` 移动出借用的 `&self` 函数参数。 -接着调用 `unwrap` 方法,这里我们知道它永远也不会 panic,因为 `Post` 的所有方法都确保在他们返回时 `state` 会有一个 `Some` 值。这就是一个第十二章讨论过的我们知道 `None` 是不可能的而编译器却不能理解的情况。 - -接着我们就有了一个 `&Box`,当调用其 `content` 时,解引用强制多态会作用于 `&` 和 `Box` 这样最终会调用实现了 `State` trait 的类型的 `content` 方法。 +接着调用 `unwrap` 方法,这里我们知道它永远也不会 panic,因为 `Post` 的所有方法都确保在他们返回时 `state` 会有一个 `Some` 值。这就是一个第十二章 “当我们比编译器知道更多的情况” 部分讨论过的我们知道 `None` 是不可能的而编译器却不能理解的情况。 -这意味着需要为 `State` trait 定义增加 `content`,这也是放置根据所处状态返回什么内容的逻辑的地方,如示例 17-18 所示: +接着我们就有了一个 `&Box`,当调用其 `content` 时,解引用强制多态会作用于 `&` 和 `Box` 这样最终会调用实现了 `State` trait 的类型的 `content` 方法。这意味着需要为 `State` trait 定义增加 `content`,这也是放置根据所处状态返回什么内容的逻辑的地方,如示例 17-18 所示: 文件名: src/lib.rs @@ -339,15 +316,9 @@ impl State for Published { 这里增加了一个 `content` 方法的默认实现来返回一个空字符串 slice。这意味着无需为 `Draft` 和 `PendingReview` 结构体实现 `content` 了。`Published` 结构体会覆盖 `content` 方法并会返回 `post.content` 的值。 - 注意这个方法需要生命周期注解,如第十章所讨论的。这里获取 `post` 的引用作为参数,并返回 `post` 一部分的引用,所以返回的引用的生命周期与 `post` 参数相关。 - - - -现在示例完成了 ———— 现在示例 17-11 中所有的代码都能工作!我们通过发布博文工作流的规则实现了状态模式。围绕这些规则的逻辑都存在于状态对象中而不是分散在 `Post` 之中。 +现在示例完成了 —— 现在示例 17-11 中所有的代码都能工作!我们通过发布博文工作流的规则实现了状态模式。围绕这些规则的逻辑都存在于状态对象中而不是分散在 `Post` 之中。 ### 状态模式的权衡取舍 @@ -367,7 +338,7 @@ all of 17-11 now work? --> 另一个缺点是我们会发现一些重复的逻辑。为了消除他们,可以尝试为 `State` trait 中返回 `self` 的 `request_review` 和 `approve` 方法增加默认实现,不过这会违反对象安全性,因为 trait 不知道 `self` 具体是什么。我们希望能够将 `State` 作为一个 trait 对象,所以需要其方法是对象安全的。 -另一个重复是 `Post` 中 `request_review` 和 `approve` 这两个类似的实现。他们都委托调用了 `state` 字段中 `Option` 值的同一方法,并在结果中为 `state` 字段设置了新值。如果 `Post` 中的很多方法都遵循这个模式,我们可能会考虑定义一个宏来消除重复(查看附录 D 以了解宏)。 +另一个重复是 `Post` 中 `request_review` 和 `approve` 这两个类似的实现。他们都委托调用了 `state` 字段中 `Option` 值的同一方法,并在结果中为 `state` 字段设置了新值。如果 `Post` 中的很多方法都遵循这个模式,我们可能会考虑定义一个宏来消除重复(查看第十九章的 “宏” 部分)。 完全按照面向对象语言的定义实现这个模式并没有没有尽可能的利用 Rust 的优势。让我们看看一些代码中可以做出的修改,来将无效的状态和状态转移变为编译时错误。 @@ -380,6 +351,8 @@ all of 17-11 now work? --> 文件名: src/main.rs ```rust,ignore +# use blog::Post; + fn main() { let mut post = Post::new(); @@ -409,7 +382,7 @@ impl Post { } pub fn content(&self) -> &str { - &self.content + &self.content } } @@ -475,7 +448,6 @@ impl PendingReviewPost { 文件名: src/main.rs ```rust,ignore -extern crate blog; use blog::Post; fn main() { @@ -495,7 +467,7 @@ fn main() { 不得不修改 `main` 来重新赋值 `post` 使得这个实现不再完全遵守面向对象的状态模式:状态间的转换不再完全封装在 `Post` 实现中。然而,得益于类型系统和编译时类型检查我们得到了不可能拥有无效状态的属性!这确保了特定的 bug,比如显示未发布博文的内容,将在部署到生产环境之前被发现。 -尝试在这一部分开始所建议的增加额外需求的任务来体会使用这个版本的代码是何感觉。 +尝试为示例 17-20 之后的 `blog` crate 实现这一部分开始所建议的增加额外需求的任务来体会使用这个版本的代码是何感觉。注意在这个设计中一些需求可能已经完成了。 即便 Rust 能够实现面向对象设计模式,也有其他像将状态编码进类型这样的模式存在。这些模式有着不同的权衡取舍。虽然你可能非常熟悉面向对象模式,重新思考这些问题来利用 Rust 提供的像在编译时避免一些 bug 这样有益功能。在 Rust 中面向对象模式并不总是最好的解决方案,因为 Rust 拥有像所有权这样的面向对象语言所没有的功能。 From f97d939df749dda988ecc3302aaa0afa33b8895e Mon Sep 17 00:00:00 2001 From: Lam <15622383059@163.com> Date: Sat, 8 Dec 2018 15:29:47 +0800 Subject: [PATCH 31/49] improve translation & fix typo --- src/ch10-02-traits.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ch10-02-traits.md b/src/ch10-02-traits.md index e8a2121..bd51a3e 100644 --- a/src/ch10-02-traits.md +++ b/src/ch10-02-traits.md @@ -91,9 +91,9 @@ println!("1 new tweet: {}", tweet.summarize()); 注意因为示例 10-13 中我们在相同的 *lib.rs* 里定义了 `Summary` trait 和 `NewsArticle` 与 `Tweet` 类型,所以他们是位于同一作用域的。如果这个 *lib.rs* 是对应 `aggregator` crate 的,而别人想要利用我们 crate 的功能为其自己的库作用域中的结构体实现 `Summary` trait。首先他们需要将 trait 引入作用域。这可以通过指定 `use aggregator::Summary;` 实现,这样就可以为其类型实现 `Summary` trait 了。`Summary` 还必须是公有 trait 使得其他 crate 可以实现它,这也是为什么实例 10-12 中将 `pub` 置于 `trait` 之前。 -一个实现 trait 时需要注意的限制是只有要么 tait 或者类型是位于 crate 作用域本地时才能为其实现该 trait。例如,可以为像 `aggregator` crate 的 `Tweet` 这样的自定义类型实现如标准库中的 `Display` 这样 trait,因为 `Tweet` 类型位于 `aggregator` crate 本地。也可以在 `aggregator` crate 中为 `Vec` 实现 `Summary`,因为 `Summary` trait 位于 `aggregator` crate 本地。 +实现 trait 时需要注意的是,只有当 trait 或者要实现 trait 的类型位于 crate 的本地作用域时,才能为该类型实现 trait。例如,可以为多媒体聚合库 crate 的自定义类型 `Tweet` 实现如标准库中的 `Display` trait,这是因为 `Tweet` 类型位于多媒体聚合库 crate 本地的作用域中。类似地,也可以在多媒体聚合库 crate 中为 `Vec` 实现 `Summary`,这是因为 `Summary` trait 位于多媒体聚合库 crate 本地作用域中。 -但是不能在外部类型上实现外部 trait。例如,不能在 `aggregator` crate 中为 `Vec` 实现 `Display` trait。因为 `Display` 和 `Vec` 都定义于标准库并不位于 `aggregator` crate 本地。这个限制是被称为 **相干性**(*coherence*) 的程序属性的一部分,或者更具体的说是 **孤儿规则**(*orphan rule*),其得名于不存在父类型。这条规则确保了其他人编写的代码不会破坏你代码,反之亦然。没有这条规则的话,两个 crate 可以分别对相同类型实现相同的 trait,而Rust 将无从得知应该使用哪一个实现。 +但是不能为外部类型实现外部 trait。例如,不能在多媒体聚合库 crate 中为 `Vec` 实现 `Display` trait。这是因为 `Display` 和 `Vec` 都定义于标准库中,它们并不位于多媒体聚合库的 crate 本地作用域中。这个限制是被称为 **相干性**(*coherence*) 的程序属性的一部分,或者更具体的说是 **孤儿规则**(*orphan rule*),其得名于不存在父类型。这条规则确保了其他人编写的代码不会破坏你代码,反之亦然。没有这条规则的话,两个 crate 可以分别对相同类型实现相同的 trait,而 Rust 将无从得知应该使用哪一个实现。 ### 默认实现 @@ -291,7 +291,7 @@ fn returns_summarizable(switch: bool) -> impl Summary { ### 使用 trait bounds 来修复 `largest` 函数 -现在你知道了如何使用泛型参数 trait bound 来指定所需的行为。让我们回到实例 10-5 修复使用泛型类型参数的 `largest` 函数定义!最后尝试代时出现的错误是: +现在你知道了如何使用泛型参数 trait bound 来指定所需的行为。让我们回到实例 10-5 修复使用泛型类型参数的 `largest` 函数定义!回顾一下,最后尝试编译代码时出现的错误是: ```text error[E0369]: binary operation `>` cannot be applied to type `T` @@ -369,9 +369,9 @@ fn main() { 另一种 `largest` 的实现方式是返回在 slice 中 `T` 值的引用。如果我们将函数返回值从 `T` 改为 `&T` 并改变函数体使其能够返回一个引用,我们将不需要任何 `Clone` 或 `Copy` 的 trait bounds 而且也不会有任何的堆分配。尝试自己实现这种替代解决方式吧! -### 使用 trait bound 有条件的实现方法 +### 使用 trait bound 有条件地实现方法 -通过使用带有 trait bound 的泛型参数的 `impl` 块,可以有条件的只为实现了特定 trait 的类型实现方法。例如,示例 10-16 中的类型 `Pair` 总是实现了 `new` 方法,不过只有 `Pair` 内部的 `T` 类型实现了 `PartialOrd` trait 来允许比较 **和** `Display` trait 来启用打印,才会实现 `cmp_display` 方法: +通过使用带有 trait bound 的泛型参数的 `impl` 块,可以有条件地只为那些实现了特定 trait 的类型实现方法。例如,示例 10-16 中的类型 `Pair` 总是实现了 `new` 方法,不过只有那些为 `T` 类型实现了 `PartialOrd` trait (来允许比较) **和** `Display` trait (来启用打印)的 `Pair` 才会实现 `cmp_display` 方法: ```rust use std::fmt::Display; @@ -403,7 +403,7 @@ impl Pair { 示例 10-17:根据 trait bound 在泛型上有条件的实现方法 -也可以对任何实现了特定 trait 的类型有条件的实现 trait。对任何满足特定 trait bound 的类型实现 trait 被称为 *blanket implementations*,他们被广泛的用于 Rust 标准库中。例如,标准库为任何实现了 `Display` trait 的类型实现了 `ToString` trait。这个 `impl` 块看起来像这样: +也可以对任何实现了特定 trait 的类型有条件地实现 trait。对任何满足特定 trait bound 的类型实现 trait 被称为 *blanket implementations*,他们被广泛的用于 Rust 标准库中。例如,标准库为任何实现了 `Display` trait 的类型实现了 `ToString` trait。这个 `impl` 块看起来像这样: ```rust,ignore impl ToString for T { From d20d5ca289734f128b2b94e588ff4c41bfaa30f0 Mon Sep 17 00:00:00 2001 From: KaiserY Date: Sat, 8 Dec 2018 21:07:05 +0800 Subject: [PATCH 32/49] check to ch18-03 --- src/ch18-00-patterns.md | 12 +- src/ch18-01-all-the-places-for-patterns.md | 56 +--- src/ch18-02-refutability.md | 18 +- src/ch18-03-pattern-syntax.md | 341 ++++++++------------- 4 files changed, 144 insertions(+), 283 deletions(-) diff --git a/src/ch18-00-patterns.md b/src/ch18-00-patterns.md index ae3d961..0b0432f 100644 --- a/src/ch18-00-patterns.md +++ b/src/ch18-00-patterns.md @@ -1,12 +1,12 @@ # 模式用来匹配值的结构 -> [ch18-00-patterns.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch18-00-patterns.md) +> [ch18-00-patterns.md](https://github.com/rust-lang/book/blob/master/src/ch18-00-patterns.md) >
-> commit 928790637fb32026643c855915b4b2fd9d5abff3 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 模式是 Rust 中特殊的语法,它用来匹配类型中的结构,无论类型是简单还是复杂。结合使用模式和 `match` 表达式以及其他结构可以提供更多对程序控制流的支配权。模式由如下一些内容组合而成: -- 字面量 +- 字面值 - 解构的数组、枚举、结构体或者元组 - 变量 - 通配符 @@ -14,12 +14,6 @@ 这些部分描述了我们要处理的数据的形状,接着可以用其匹配值来决定程序是否拥有正确的数据来运行特定部分的代码。 - - - 我们通过将一些值与模式相比较来使用它。如果模式匹配这些值,我们对值部分进行相应处理。回忆一下第六章讨论 `match` 表达式时像硬币分类器那样使用模式。如果数据符合这个形状,就可以使用这些命名的片段。如果不符合,与该模式相关的代码则不会运行。 本章是所有模式相关内容的参考。我们将涉及到使用模式的有效位置,*refutable* 与 *irrefutable* 模式的区别,和你可能会见到的不同类型的模式语法。在最后,你将会看到如何使用模式创建强大而简洁的代码。 diff --git a/src/ch18-01-all-the-places-for-patterns.md b/src/ch18-01-all-the-places-for-patterns.md index 3d830d2..1cf8856 100644 --- a/src/ch18-01-all-the-places-for-patterns.md +++ b/src/ch18-01-all-the-places-for-patterns.md @@ -1,8 +1,8 @@ ## 所有可能会用到模式的位置 -> [ch18-01-all-the-places-for-patterns.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch18-01-all-the-places-for-patterns.md) +> [ch18-01-all-the-places-for-patterns.md](https://github.com/rust-lang/book/blob/master/src/ch18-01-all-the-places-for-patterns.md) >
-> commit b1de391964190a0cec101ecfc86e05c9351af565 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 模式出现在 Rust 的很多地方。你已经在不经意间使用了很多模式!本部分是一个所有有效模式位置的参考。 @@ -18,26 +18,18 @@ match VALUE { } ``` -`match` 表达式必须是 **穷尽**(*exhaustive*)的,意为 `match` 表达式所有可能的值都必须被考虑到。一个确保覆盖每个可能值的方法是在最后一个分支使用捕获所有的模式 —— 比如,一个匹配任何值的名称永远也不会失败,因此可以覆盖所有匹配剩下的情况。 +`match` 表达式必须是 **穷尽**(*exhaustive*)的,意为 `match` 表达式所有可能的值都必须被考虑到。一个确保覆盖每个可能值的方法是在最后一个分支使用捕获所有的模式:比如,一个匹配任何值的名称永远也不会失败,因此可以覆盖所有匹配剩下的情况。 -有一个特定的模式 `_` 可以匹配所有情况,不过它从不绑定任何变量。这在例如希望忽略任何未指定值的情况很有用。本章之后会详细讲解。 +有一个特定的模式 `_` 可以匹配所有情况,不过它从不绑定任何变量。这在例如希望忽略任何未指定值的情况很有用。本章之后的 “在模式中忽略值” 部分会详细介绍 `_` 模式的更多细节。 ### `if let` 条件表达式 第六章讨论过了 `if let` 表达式,以及它是如何主要用于编写等同于只关心一个情况的 `match` 语句简写的。`if let` 可以对应一个可选的带有代码的 `else` 在 `if let` 中的模式不匹配时运行。 - - - -示例 18-1 展示了也可以组合并匹配 `if let`、`else if` 和 `else if let` 表达式。这相比 `match` 表达式一次只能将一个值与模式比较提供了更多灵活性;一系列 `if let`/`else if`/`else if let` 分支并不要求其条件相互关联。 +示例 18-1 展示了也可以组合并匹配 `if let`、`else if` 和 `else if let` 表达式。这相比 `match` 表达式一次只能将一个值与模式比较提供了更多灵活性;一系列 `if let`、`else if`、`else if let` 分支并不要求其条件相互关联。 示例 18-1 中的代码展示了一系列针对不同条件的检查来决定背景颜色应该是什么。为了达到这个例子的目的,我们创建了硬编码值的变量,在真实程序中则可能由询问用户获得。 -如果用户指定了中意的颜色,将使用其作为背景颜色。如果今天是星期二,背景颜色将是绿色。如果用户指定了他们的年龄字符串并能够成功将其解析为数字的话,我们将根据这个数字使用紫色或者橙色。最后,如果没有一个条件符合,背景颜色将是蓝色: - 文件名: src/main.rs ```rust @@ -64,16 +56,14 @@ fn main() { 示例 18-1: 结合 `if let`、`else if`、`else if let` 以及 `else` +如果用户指定了中意的颜色,将使用其作为背景颜色。如果今天是星期二,背景颜色将是绿色。如果用户指定了他们的年龄字符串并能够成功将其解析为数字的话,我们将根据这个数字使用紫色或者橙色。最后,如果没有一个条件符合,背景颜色将是蓝色: + 这个条件结构允许我们支持复杂的需求。使用这里硬编码的值,例子会打印出 `Using purple as the background color`。 注意 `if let` 也可以像 `match` 分支那样引入覆盖变量:`if let Ok(age) = age` 引入了一个新的覆盖变量 `age`,它包含 `Ok` 成员中的值。这意味着 `if age > 30` 条件需要位于这个代码块内部;不能将两个条件组合为 `if let Ok(age) = age && age > 30`,因为我们希望与 30 进行比较的被覆盖的 `age` 直到大括号开始的新作用域才是有效的。 `if let` 表达式的缺点在于其穷尽性没有为编译器所检查,而 `match` 表达式则检查了。如果去掉最后的 `else` 块而遗漏处理一些情况,编译器也不会警告这类可能的逻辑错误。 - - - ### `while let` 条件循环 一个与 `if let` 结构类似的是 `while let` 条件循环,它允许只要模式匹配就一直进行 `while` 循环。示例 18-2 展示了一个使用 `while let` 的例子,它使用 vector 作为栈并以先进后出的方式打印出 vector 中的值: @@ -92,27 +82,14 @@ while let Some(top) = stack.pop() { 列表 18-2: 使用 `while let` 循环只要 `stack.pop()` 返回 `Some` 就打印出其值 - - 这个例子会打印出 3、2 接着是 1。`pop` 方法取出 vector 的最后一个元素并返回 `Some(value)`。如果 vector 是空的,它返回 `None`。`while` 循环只要 `pop` 返回 `Some` 就会一直运行其块中的代码。一旦其返回 `None`,`while` 循环停止。我们可以使用 `while let` 来弹出栈中的每一个元素。 ### `for` 循环 如同第三章所讲的,`for` 循环是 Rust 中最常见的循环结构,不过还没有讲到的是 `for` 可以获取一个模式。在 `for` 循环中,模式是 `for` 关键字直接跟随的值,正如 `for x in y` 中的 `x`。 - - - 示例 18-3 中展示了如何使用 `for` 循环来解构,或拆开一个元组作为 `for` 循环的一部分: - - ```rust let v = vec!['a', 'b', 'c']; @@ -123,7 +100,7 @@ for (index, value) in v.iter().enumerate() { 列表 18-3: 在 `for` 循环中使用模式来解构元组 -这会打印出: +示例 18-3 的代码会打印出: ```text a is at index 0 @@ -131,7 +108,7 @@ b is at index 1 c is at index 2 ``` -这里使用 `enumerate` 方法适配一个迭代器来产生一个值和其在迭代器中的索引,他们位于一个元组中。第一个 `enumerate` 调用会产生元组 `(0, 'a')`。当这个值匹配模式 `(index, value)`,`index` 将会是 0 而 `value` 将会是 'a',并打印出第一行输出。 +这里使用 `enumerate` 方法适配一个迭代器来产生一个值和其在迭代器中的索引,他们位于一个元组中。第一个 `enumerate` 调用会产生元组 `(0, 'a')`。当这个值匹配模式 `(index, value)`,`index` 将会是 0 而 `value` 将会是 `'a'`,并打印出第一行输出。 ### `let` 语句 @@ -159,14 +136,9 @@ let (x, y, z) = (1, 2, 3); 这里将一个元组与模式匹配。Rust 会比较值 `(1, 2, 3)` 与模式 `(x, y, z)` 并发现此值匹配这个模式。在这个例子中,将会把 `1` 绑定到 `x`,`2` 绑定到 `y` 并将 `3` 绑定到 `z`。你可以将这个元组模式看作是将三个独立的变量模式结合在一起。 - - - 如果模式中元素的数量不匹配元组中元素的数量,则整个类型不匹配,并会得到一个编译时错误。例如,示例 18-5 展示了尝试用两个变量解构三个元素的元组,这是不行的: -```rust,ignore +```rust,ignore,does_not_compile let (x, y) = (1, 2, 3); ``` @@ -193,7 +165,7 @@ error[E0308]: mismatched types ```rust fn foo(x: i32) { - // code goes here + // 代码 } ``` @@ -216,8 +188,8 @@ fn main() { 列表 18-7: 一个在参数中解构元组的函数 -这会打印出 `Current location: (3, 5)`。值 `&(3, 5)` 会匹配模式 `&(x, y)`,如此 `x` 得到了值 3,而 `y`得到了值 5。 +这会打印出 `Current location: (3, 5)`。值 `&(3, 5)` 会匹配模式 `&(x, y)`,如此 `x` 得到了值 `3`,而 `y`得到了值 `5`。 -因为如第十三章所讲闭包类似于函数,也可以在闭包参数中使用模式。 +因为如第十三章所讲闭包类似于函数,也可以在闭包参数列表中使用模式。 -现在我们见过了很多使用模式的方式了,不过模式在每个使用它的地方并不以相同的方式工作;在一些地方,模式必须是 *irrefutable* 的,意味着他们必须匹配所提供的任何值。在另一些情况,他们则可以是 refutable 的。接下来让我们讨论这个。 +现在我们见过了很多使用模式的方式了,不过模式在每个使用它的地方并不以相同的方式工作;在一些地方,模式必须是 *irrefutable* 的,意味着他们必须匹配所提供的任何值。在另一些情况,他们则可以是 refutable 的。接下来让我们讨论这两个概念。 diff --git a/src/ch18-02-refutability.md b/src/ch18-02-refutability.md index 8ab6f40..25330f2 100644 --- a/src/ch18-02-refutability.md +++ b/src/ch18-02-refutability.md @@ -1,18 +1,18 @@ ## Refutability(可反驳性): 模式是否会匹配失效 -> [ch18-02-refutability.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch18-02-refutability.md) +> [ch18-02-refutability.md](https://github.com/rust-lang/book/blob/master/src/ch18-02-refutability.md) >
-> commit 267f442fa1c637eab07b4eebb64a6dcd2c943a36 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 模式有两种形式:refutable(可反驳的)和 irrefutable(不可反驳的)。能匹配任何传递的可能值的模式被称为是 **不可反驳的**(*irrefutable*)。一个例子就是 `let x = 5;` 语句中的 `x`,因为 `x` 可以匹配任何值所以不可能会失败。对某些可能的值进行匹配会失败的模式被称为是 **可反驳的**(*refutable*)。一个这样的例子便是 `if let Some(x) = a_value` 表达式中的 `Some(x)`;如果变量 `a_value` 中的值是 `None` 而不是 `Some`,那么 `Some(x)` 模式不能匹配。 -`let` 语句、 函数参数和 `for` 循环只能接受不可反驳的模式,因为通过不匹配的值程序无法进行有意义的工作。`if let` 和 `while let` 表达式被限制为只能接受可反驳的模式,因为根据定义他们意在处理可能的失败 ———— 条件表达式的功能就是根据成功或失败执行不同的操作。 +`let` 语句、 函数参数和 `for` 循环只能接受不可反驳的模式,因为通过不匹配的值程序无法进行有意义的工作。`if let` 和 `while let` 表达式被限制为只能接受可反驳的模式,因为根据定义他们意在处理可能的失败:条件表达式的功能就是根据成功或失败执行不同的操作。 通常无需担心可反驳和不可反驳模式的区别,不过确实需要熟悉可反驳性的概念,这样当在错误信息中看到时就知道如何应对。遇到这些情况,根据代码行为的意图,需要修改模式或者使用模式的结构。 让我们看看一个尝试在 Rust 要求不可反驳模式的地方使用可反驳模式以及相反情况的例子。在示例 18-8 中,有一个 `let` 语句,不过模式被指定为可反驳模式 `Some(x)`。如你所见,这会出现错误: -```rust,ignore +```rust,ignore,does_not_compile let Some(x) = some_option_value; ``` @@ -22,7 +22,7 @@ let Some(x) = some_option_value; ```text error[E0005]: refutable pattern in local binding: `None` not covered - --> :3:5 + --> | 3 | let Some(x) = some_option_value; | ^^^^^^^ pattern `None` not covered @@ -41,15 +41,9 @@ if let Some(x) = some_option_value { 示例 18-9: 使用 `if let` 和一个带有可反驳模式的代码块来代替 `let` - - - 我们给了代码一个得以继续的出路!这段代码可以完美运行,当让如此意味着我们不能再使用不可反驳模式并免于收到错误。如果为 `if let` 提供了一个总是会匹配的模式,比如示例 18-10 中的 `x`,则会出错: -```rust,ignore +```rust,ignore,does_not_compile if let x = 5 { println!("{}", x); }; diff --git a/src/ch18-03-pattern-syntax.md b/src/ch18-03-pattern-syntax.md index 8d71848..0a9de37 100644 --- a/src/ch18-03-pattern-syntax.md +++ b/src/ch18-03-pattern-syntax.md @@ -1,16 +1,10 @@ ## 所有的模式语法 -> [ch18-03-pattern-syntax.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch18-03-pattern-syntax.md) +> [ch18-03-pattern-syntax.md](https://github.com/rust-lang/book/blob/master/src/ch18-03-pattern-syntax.md) >
-> commit 3f91c488ad4261dee6a61db4f60c197074151aac +> commit bc6d44e5d2cc2ec291c3c93ee5a25b4a634a4403 -通过本书我们已领略过许多不同类型模式的例子. 本节会统一列出所有在模式中有效的语法并且会阐述你为什么可能会希望使用其中的每一个。 - - +通过本书我们已领略过许多不同类型模式的例子。本节会统一列出所有在模式中有效的语法并且会阐述你为什么可能会希望使用其中的每一个。 ### 匹配字面值 @@ -27,17 +21,13 @@ match x { } ``` -这段代码会打印 `one` 因为 `x` 的值是 1。 +这段代码会打印 `one` 因为 `x` 的值是 1。这个语法用于代码得到某个具体值时进行操作。 ### 匹配命名变量 - - - -命名变量是匹配任何值的不可反驳模式,这在之前已经使用过数次。然而当其用于 `match` 表达式时情况会有些复杂。因为 `match` 会开始一个新作用域,`match` 表达式中作为模式的一部分声明的变量会覆盖 `match` 结构之外的同名变量 ———— 与所有变量一样。在示例 18-11 中,声明了一个值为 `Some(5)` 的变量 `x` 和一个值为 `10` 的变量 `y`。接着在值 `x` 上创建了一个 `match` 表达式。观察匹配分支中的模式和结尾的 `println!`,并尝试在运行代码之前计算出会打印什么,或者继续阅读: +命名变量是匹配任何值的不可反驳模式,这在之前已经使用过数次。然而当其用于 `match` 表达式时情况会有些复杂。因为 `match` 会开始一个新作用域,`match` 表达式中作为模式的一部分声明的变量会覆盖 `match` 结构之外的同名变量,与所有变量一样。在示例 18-11 中,声明了一个值为 `Some(5)` 的变量 `x` 和一个值为 `10` 的变量 `y`。接着在值 `x` 上创建了一个 `match` 表达式。观察匹配分支中的模式和结尾的 `println!`,并尝试在运行代码之前计算出会打印什么,或者继续阅读: -Filename: src/main.rs +文件名: src/main.rs ```rust fn main() { @@ -60,26 +50,16 @@ fn main() { 第二个匹配分支中的模式引入了一个新变量 `y`,它会匹配任何 `Some` 中的值。因为我们在 `match` 表达式的新作用域中,这是一个新变量,而不是开头声明为值 10 的那个 `y`。这个新的 `y` 绑定会匹配任何 `Some` 中的值,在这里是 `x` 中的值。因此这个 `y` 绑定了 `x` 中 `Some` 内部的值。这个值是 5,所以这个分支的表达式将会执行并打印出 `Matched, y = 5`。 - - - 如果 `x` 的值是 `None` 而不是 `Some(5)`,头两个分支的模式不会匹配,所以会匹配下划线。这个分支的模式中没有引入变量 `x`,所以此时表达式中的 `x` 会是外部没有被覆盖的 `x`。在这个假想的例子中,`match` 将会打印 `Default case, x = None`。 一旦 `match` 表达式执行完毕,其作用域也就结束了,同理内部 `y` 的作用域也结束了。最后的 `println!` 会打印 `at the end: x = Some(5), y = 10`。 -为了创建能够比较外部 `x` 和 `y` 的值,而不引入覆盖变量的 `match` 表达式,我们需要相应的使用带有条件的匹配守卫(match guard)。本部分的后面会讨论匹配守卫。 +为了创建能够比较外部 `x` 和 `y` 的值,而不引入覆盖变量的 `match` 表达式,我们需要相应的使用带有条件的匹配守卫(match guard)。“匹配守卫提供的额外条件” 会讨论匹配守卫。 ### 多个模式 在 `match` 表达式中,可以使用 `|` 语法匹配多个模式,它代表 **或**(*or*)的意思。例如,如下代码将 `x` 的值与匹配分支向比较,第一个分支有 **或** 选项,意味着如果 `x` 的值匹配此分支的任一个值,它就会运行: - - - ```rust let x = 1; @@ -92,23 +72,10 @@ match x { 上面的代码会打印 `one or two`。 - - - ### 通过 `...` 匹配值的范围 `...` 语法允许你匹配一个闭区间范围内的值。在如下代码中,当模式匹配任何在此范围内的值时,该分支会执行: - - - ```rust let x = 5; @@ -122,9 +89,6 @@ match x { 范围只允许用于数字或 `char` 值,因为编译器会在编译时检查范围不为空。`char` 和 数字值是 Rust 唯一知道范围是否为空的类型。 - - - 如下是一个使用 `char` 类型值范围的例子: ```rust @@ -141,10 +105,6 @@ Rust 知道 `c` 位于第一个模式的范围内,并会打印出 `early ASCII ### 解构并分解值 - - - 也可以使用模式来解构结构体、枚举、元组和引用,以便使用这些值的不同部分。让我们来分别看一看。 #### 解构结构体 @@ -170,17 +130,9 @@ fn main() { 示例 18-12: 解构一个结构体的字段为单独的变量 - - +这段代码创建了变量 `a` 和 `b` 来匹配变量 `p` 中的 `x` 和 `y` 字段。这个例子展示了模式中的变量名不必与结构体中的字段名一致。不过通常希望变量名与字段名一致以便于理解变量来自于哪些字段。 -这段代码创建了变量 `a` 和 `b` 来匹配变量 `p` 中的 `x` 和 `y` 字段。 - -这个例子展示了模式中的变量名不必与结构体中的字段名一致,不过通常希望变量名与字段名一致以便于理解变量来自于哪些字段。因为变量名匹配字段名是常见的,同时因为 `let Point { x: x, y: y } = p;` 包含了很多重复,所以对于匹配结构体字段的模式存在简写:只需列出结构体字段的名称,则模式创建的变量会有相同的名称。示例 18-13 展示了与示例 18-12 有着相同行为的代码,不过 `let` 模式创建的变量为 `x` 和 `y` 而不是 `a` 和 `b`: +因为变量名匹配字段名是常见的,同时因为 `let Point { x: x, y: y } = p;` 包含了很多重复,所以对于匹配结构体字段的模式存在简写:只需列出结构体字段的名称,则模式创建的变量会有相同的名称。示例 18-13 展示了与示例 18-12 有着相同行为的代码,不过 `let` 模式创建的变量为 `x` 和 `y` 而不是 `a` 和 `b`: 文件名: src/main.rs @@ -205,11 +157,7 @@ fn main() { 也可以在部分结构体模式中使用字面值进行结构,而不是为所有的字段创建变量。这允许我们测试一些字段为特定值的同时创建其他字段的变量。 -示例 18-14 展示了一个 `match` 语句将 `Point` 值分成了三种情况:直接位于 `x` 轴上(此时 `y = 0` 为真)、位于 `y` 轴上(`x = 0`)或其他的点: - - - +示例 18-14 展示了一个 `match` 语句将 `Point` 值分成了三种情况:直接位于 `x` 轴上(此时 `y = 0` 为真)、位于 `y` 轴上(`x = 0`)或不在任何轴上的点。 文件名: src/main.rs @@ -232,7 +180,9 @@ fn main() { 示例 18-14: 解构和匹配模式中的字面值 -第一个分支通过指定字段 `y` 匹配字面值 `0` 来匹配任何位于 `x` 轴上的点。此模式仍然创建了变量 `x` 以便在分支的代码中使用。类似的,第二个分支通过指定字段 `x` 匹配字面值 `0` 来匹配任何位于 `y` 轴上的点,并为字段 `y` 创建了变量 `y`。第三个分支没有指定任何字面值,所以其会匹配任何其他的 `Point` 并为 `x` 和 `y` 两个字段创建变量。 +第一个分支通过指定字段 `y` 匹配字面值 `0` 来匹配任何位于 `x` 轴上的点。此模式仍然创建了变量 `x` 以便在分支的代码中使用。 + +类似的,第二个分支通过指定字段 `x` 匹配字面值 `0` 来匹配任何位于 `y` 轴上的点,并为字段 `y` 创建了变量 `y`。第三个分支没有指定任何字面值,所以其会匹配任何其他的 `Point` 并为 `x` 和 `y` 两个字段创建变量。 在这个例子中,值 `p` 因为其 `x` 包含 0 而匹配第二个分支,因此会打印出 `On the y axis at 7`。 @@ -287,17 +237,53 @@ fn main() { 对于像 `Message::Write` 这样的包含一个元素,以及像 `Message::ChangeColor` 这样包含两个元素的类元组枚举成员,其模式则类似于用于解构元组的模式。模式中变量的数量必须与成员中元素的数量一致。 -#### 解构引用 +#### 解构嵌套的结构体 & 枚举 + +目前为止,所有的例子都只匹配了深度为一级的结构体。当然也可以匹配嵌套的结构体! + +我们可以重构上面的例子来同时支持 RGB 和 HSV 色彩模式: + +```rust +enum Color { + Rgb(i32, i32, i32), + Hsv(i32, i32, i32) +} + +enum Message { + Quit, + Move { x: i32, y: i32 }, + Write(String), + ChangeColor(Color), +} + +fn main() { + let msg = Message::ChangeColor(Color::Hsv(0, 160, 255)); -当模式所匹配的值中包含引用时,需要解构引用之中的值,这可以通过在模式中指定 `&` 做到。这让我们得到一个包含引用所指向数据的变量,而不是包含引用的变量。 + match msg { + Message::ChangeColor(Color::Rgb(r, g, b)) => { + println!( + "Change the color to red {}, green {}, and blue {}", + r, + g, + b + ) + }, + Message::ChangeColor(Color::Hsv(h, s, v)) => { + println!( + "Change the color to hue {}, saturation {}, and value {}", + h, + s, + v + ) + } + _ => () + } +} +``` - - +#### 解构引用 -这在迭代器遍历引用,不过我们需要使用闭包中的值而不是其引用时非常有用 +当模式所匹配的值中包含引用时,需要解构引用之中的值,这可以通过在模式中指定 `&` 做到。这让我们得到一个包含引用所指向数据的变量,而不是包含引用的变量。这个技术在通过迭代器遍历引用时,我们需要使用闭包中的值而不是其引用时非常有用。 示例 18-16 中的例子遍历一个 vector 中的 `Point` 实例的引用,并同时解构引用和其中的结构体以方便对 `x` 和 `y` 值进行计算: @@ -321,9 +307,6 @@ let sum_of_squares: i32 = points 示例 18-16: 将结构体的引用解构到其字段值中 - - - 这段代码的结果是变量 `sum_of_squares` 的值为 135,这个结果是将 `points` vector 中每一个 `Point` 的 `x` 和 `y` 的平方相加后求和得到的数字。 如果没有在 `&Point { x, y }` 中包含 `&` 则会得到一个类型不匹配错误,因为这样 `iter` 会遍历 vector 中项的引用而不是值本身。这个错误看起来像这样: @@ -356,10 +339,6 @@ let ((feet, inches), Point {x, y}) = ((3, 10), Point { x: 3, y: -10 }); 这将复杂的类型分解成部分组件以便可以单独使用我们感兴趣的值。 - - - 通过模式解构是一个方便利用部分值片段的手段,比如结构体中每个单独字段的值。 ### 忽略模式中的值 @@ -384,21 +363,12 @@ fn main() { 示例 18-17: 在函数签名中使用 `_` - - - -这段代码会完全忽略作为第一个参数传递的值,3,并会打印出 `This code only uses the y parameter: 4`。大部分情况当你不再需要特定函数参数时,最好修改签名不再包含无用的参数。 +这段代码会完全忽略作为第一个参数传递的值 `3`,并会打印出 `This code only uses the y parameter: 4`。 -在一些情况下忽略函数参数会变得特别有用,比如实现 trait 时,当你需要特定类型签名但是函数实现并不需要某个参数时。此时编译器就不会警告说存在未使用的函数参数,就跟使用命名参数一样。 +大部分情况当你不再需要特定函数参数时,最好修改签名不再包含无用的参数。在一些情况下忽略函数参数会变得特别有用,比如实现 trait 时,当你需要特定类型签名但是函数实现并不需要某个参数时。此时编译器就不会警告说存在未使用的函数参数,就跟使用命名参数一样。 #### 使用嵌套的 `_` 忽略部分值 - - - 当只需要测试部分值但在期望运行的代码部分中没有使用它们时,也可以在另一个模式内部使用 `_` 来只忽略部分值。示例 18-18 展示了负责从设置中获取一个值的代码。业务需求是用户不允许覆盖某个设置中已经存在的自定义配置,但是可以重设设置和在目前未设置时提供新的设置。 ```rust @@ -423,14 +393,8 @@ println!("setting is {:?}", setting_value); 对于所有其他情况(`setting_value` 或 `new_setting_value` 任一为 `None`),这由第二个分支的 `_` 模式体现,这时确实希望允许 `new_setting_value` 变为 `setting_value`。 - - - 也可以在一个模式中的多处使用下划线来忽略特定值,如示例 18-19 所示,这里忽略了一个五元元组中的第二和第四个值: -我们也可以在一个模式中多处使用下划线, 在例18-17中我们将忽略掉一个五元元组中的第二和第四个值: - ```rust let numbers = (2, 4, 8, 16, 32); @@ -462,9 +426,9 @@ fn main() { 这里得到了警告说未使用变量 `y`,不过没有警告说未使用下划线开头的变量。 -注意, 只使用`_`和使用以下划线开头的名称有些微妙的不同:比如 `_x` 仍会将值绑定到变量,而 `_` 则完全不会绑定。为了展示这个区别的意义,示例 18-21 会产生一个错误。 +注意, 只使用 `_` 和使用以下划线开头的名称有些微妙的不同:比如 `_x` 仍会将值绑定到变量,而 `_` 则完全不会绑定。为了展示这个区别的意义,示例 18-21 会产生一个错误。 -```rust,ignore +```rust,ignore,does_not_compile let s = Some(String::from("Hello!")); if let Some(_s) = s { @@ -530,7 +494,7 @@ fn main() { } ``` -示例 18-24: 用 `..` 匹配元组中的第一个和最后一个值并忽略掉所有其它值 +示例 18-24: 只匹配元组中的第一个和最后一个值并忽略掉所有其它值 这里用 `first` 和 `last` 来匹配第一个和最后一个值。`..` 将匹配并忽略中间的所有值。 @@ -538,7 +502,7 @@ fn main() { 文件名: src/main.rs -```rust,ignore +```rust,ignore,does_not_compile fn main() { let numbers = (2, 4, 8, 16, 32); @@ -562,84 +526,13 @@ error: `..` can only be used once per tuple or tuple struct pattern | ^^ ``` -Rust 不可能决定在元组中匹配 `second` 值之前应该忽略多少个值,以及在之后忽略多少个值。这段代码可能表明我们意在忽略 2,绑定 `second` 为 4,接着忽略 8、16 和 32;抑或是意在忽略 2 和 4,绑定 `second` 为 8,接着忽略 16 和 32,以此类推。变量名 `second` 对于 Rust 来说并没有任何特殊意义,所以会得到编译错误,因为在这两个地方使用 `..` 是有歧义的。 - -### 使用 `ref` 和 `ref mut` 在模式中创建引用 - -这里我们将看到使用 `ref` 来创建引用,这样值的所有权就不会移动到模式的变量中。通常当匹配模式时,模式所引入的变量将绑定一个值。Rust 的所有权规则意味着这个值将被移动到 `match` 中,或者任何使用此模式的位置。示例 18-26 展示了一个带有变量的模式的例子,并接着在 `match` 之后使用这整个值。这会编译失败,因为值 `robot_name` 的一部分在第一个 `match` 分支时被移动到了模式的变量 `name` 中: - - - - -```rust,ignore -let robot_name = Some(String::from("Bors")); - -match robot_name { - Some(name) => println!("Found a name: {}", name), - None => (), -} - -println!("robot_name is: {:?}", robot_name); -``` - -示例 18-26: 在匹配分支的模式中创建获取值所有权的变量 - -这个例子会编译失败,因为当 `name` 绑定 `robot_name` 的 `Some` 中的值时,其被移动到了 `match` 中。因为 `robot_name` 的部分所有权被移动到了 `name` 中,就不再能够在 `match` 之后的 `println!` 中使用 `robot_name`,因为 `robot_name` 不再有所有权。 - - - - - - - -为了修复这段代码,需要让 `Some(name)` 模式借用部分 `robot_name` 而不是获取其所有权。在模式之外,我们见过了使用 `&` 创建引用来借用值,所以可能会想到的解决方案是将 `Some(name)` 改为 `Some(&name)`。 - -然而,在 “解构并分解值” 部分我们见过了模式中的 `&` 并不能 **创建** 引用,它会 **匹配** 值中已经存在的引用。因为 `&` 在模式中已经有其他意义,不能够使用 `&` 在模式中创建引用。 - -相对的,为了在模式中创建引用,可以在新变量前使用 `ref` 关键字,如示例 18-27 所示: - -```rust -let robot_name = Some(String::from("Bors")); - -match robot_name { - Some(ref name) => println!("Found a name: {}", name), - None => (), -} - -println!("robot_name is: {:?}", robot_name); -``` - -示例 18-27: 创建一个引用以便模式变量不会获取其所有权 - -这个例子可以编译,因为 `robot_name` 中 `Some` 成员的值没有被移动到 `match` 中;`match` 值获取了 `robot_name` 中数据的引用而没有移动它。 - -为了能够修改模式中匹配的值需要创建可变引用,使用 `ref mut` 替代 `&mut`,类似于上面用 `ref` 替代 `&`:模式中的 `&mut` 用于匹配已经存在的可变引用,而不是新建一个。示例 18-28 展示了一个创建可变引用模式的例子: - -```rust -let mut robot_name = Some(String::from("Bors")); - -match robot_name { - Some(ref mut name) => *name = String::from("Another name"), - None => (), -} - -println!("robot_name is: {:?}", robot_name); -``` - -示例 18-28: 在模式中使用 `ref mut` 来创建一个值的可变引用 - -上例可以编译并打印出 `robot_name is: Some("Another name")`。因为 `name` 是一个可变引用,我们需要在匹配分支代码中使用 `*` 运算符解引用以便能够修改它。 +Rust 不可能决定在元组中匹配 `second` 值之前应该忽略多少个值,以及在之后忽略多少个值。这段代码可能表明我们意在忽略 `2`,绑定 `second` 为 `4`,接着忽略 `8`、`16` 和 `32`;抑或是意在忽略 `2` 和 `4`,绑定 `second` 为 `8`,接着忽略 `16` 和 `32`,以此类推。变量名 `second` 对于 Rust 来说并没有任何特殊意义,所以会得到编译错误,因为在这两个地方使用 `..` 是有歧义的。 ### 匹配守卫提供的额外条件 - - **匹配守卫**(*match guard*)是一个指定与 `match` 分支模式之后的额外 `if` 条件,它也必须被满足才能选择此分支。匹配守卫用于表达比单独的模式所能允许的更为复杂的情况。 -这个条件可以使用模式中创建的变量。示例 18-29 展示了一个 `match`,其中第一个分支有模式 `Some(x)` 还有匹配守卫 `if x < 5`: +这个条件可以使用模式中创建的变量。示例 18-26 展示了一个 `match`,其中第一个分支有模式 `Some(x)` 还有匹配守卫 `if x < 5`: ```rust let num = Some(4); @@ -651,25 +544,15 @@ match num { } ``` -示例 18-29: 在模式中加入匹配守卫 +示例 18-26: 在模式中加入匹配守卫 -例18-27: 往一个模式中加入匹配守卫 - -上例会打印出 `less than five: 4`。当 `num` 与模式中第一个分支比较时,因为 `Some(4)` 匹配 `Some(x)` 所以可以匹配。接着匹配守卫检查 `x` 值是否小于 5,因为 4 小于 5,所以第一个分支被选择。 +上例会打印出 `less than five: 4`。当 `num` 与模式中第一个分支比较时,因为 `Some(4)` 匹配 `Some(x)` 所以可以匹配。接着匹配守卫检查 `x` 值是否小于 `5`,因为 `4` 小于 `5`,所以第一个分支被选择。 相反如果 `num` 为 `Some(10)`,因为 10 不小于 5 所以第一个分支的匹配守卫为假。接着 Rust 会前往第二个分支,这会匹配因为它没有匹配守卫所以会匹配任何 `Some` 成员。 无法在模式中表达 `if x < 5` 的条件,所以匹配守卫提供了表现此逻辑的能力。 - - - -在示例 18-11 中,我们提到可以使用匹配守卫来解决模式中变量覆盖的问题,那里 `match` 表达式的模式中新建了一个变量而不是使用 `match` 之外的同名变量。新变量意味着不能够测试外部变量的值。实例 18-30 展示了如何使用匹配守卫修复这个问题: - - - +在示例 18-11 中,我们提到可以使用匹配守卫来解决模式中变量覆盖的问题,那里 `match` 表达式的模式中新建了一个变量而不是使用 `match` 之外的同名变量。新变量意味着不能够测试外部变量的值。实例 18-27 展示了如何使用匹配守卫修复这个问题: 文件名: src/main.rs @@ -688,27 +571,13 @@ fn main() { } ``` -示例 18-30: 使用匹配守卫来测试与外部变量的相等性 +示例 18-27: 使用匹配守卫来测试与外部变量的相等性 现在这会打印出 `Default case, x = Some(5)`。现在第二个匹配分支中的模式不会引入一个覆盖外部 `y` 的新变量 `y`,这意味着可以在匹配守卫中使用外部的 `y`。相比指定会覆盖外部 `y` 的模式 `Some(y)`,这里指定为 `Some(n)`。此新建的变量 `n` 并没有覆盖任何值,因为 `match` 外部没有变量 `n`。 在匹配守卫 `if n == y` 中,这并不是一个模式所以没有引入新变量。这个 `y` **正是** 外部的 `y` 而不是新的覆盖变量 `y`,这样就可以通过比较 `n` 和 `y` 来表达寻找一个与外部 `y` 相同的值的概念了。 - - - -也可以在匹配守卫中使用或运算符 `|` 来指定多个模式,同时匹配守卫的条件会作用域所有的模式。示例 18-31 展示了结合匹配守卫与使用了 `|` 的模式的优先级。这个例子中重要的部分是匹配守卫 `if y` 作用于 4、5 **和** 6,即使这看起来好像 `if y` 只作用于 6: - - - +也可以在匹配守卫中使用 **或** 运算符 `|` 来指定多个模式,同时匹配守卫的条件会作用域所有的模式。示例 18-28 展示了结合匹配守卫与使用了 `|` 的模式的优先级。这个例子中重要的部分是匹配守卫 `if y` 作用于 `4`、`5` **和** `6`,即使这看起来好像 `if y` 只作用于 `6`: ```rust let x = 4; @@ -720,17 +589,9 @@ match x { } ``` -示例 18-31: 结合多个模式与匹配守卫 - -这个匹配条件表明此分支值匹配 `x` 值为 4、5 或 6 **同时** `y` 为 `true` 的情况。运行这段代码时会发生的是第一个分支的模式因 `x` 为 4 而匹配,不过匹配守卫 `if y` 为假,所以第一个分支不会被选择。代码移动到第二个分支,这会匹配,此程序会打印出 `no`。 +示例 18-28: 结合多个模式与匹配守卫 - - - -这是因为 `if` 条件作用于整个 `4 | 5 | 6` 模式,而不仅是最后的值 `6`。换句话说,匹配守卫与模式的优先级关系看起来像这样: +这个匹配条件表明此分支值匹配 `x` 值为 `4`、`5` 或 `6` **同时** `y` 为 `true` 的情况。运行这段代码时会发生的是第一个分支的模式因 `x` 为 `4` 而匹配,不过匹配守卫 `if y` 为假,所以第一个分支不会被选择。代码移动到第二个分支,这会匹配,此程序会打印出 `no`。这是因为 `if` 条件作用于整个 `4 | 5 | 6` 模式,而不仅是最后的值 `6`。换句话说,匹配守卫与模式的优先级关系看起来像这样: ```text (4 | 5 | 6) if y => ... @@ -746,14 +607,7 @@ Does this make sense now? /Carol --> ### `@` 绑定 - - - -at 运算符 `@` 允许我们在创建一个存放值的变量的同时测试其值是否匹配模式。示例 18-32 展示了一个例子,这里我们希望测试 `Message::Hello` 的 `id` 字段是否位于 `3...7` 范围内,同时也希望能其值绑定到 `id_variable` 变量中以便此分支相关联的代码可以使用它。可以将 `id_variable` 命名为 `id`,与字段同名,不过出于示例的目的这里选择了不同的名称: +*at* 运算符(`@`)允许我们在创建一个存放值的变量的同时测试其值是否匹配模式。示例 18-29 展示了一个例子,这里我们希望测试 `Message::Hello` 的 `id` 字段是否位于 `3...7` 范围内,同时也希望能其值绑定到 `id_variable` 变量中以便此分支相关联的代码可以使用它。可以将 `id_variable` 命名为 `id`,与字段同名,不过出于示例的目的这里选择了不同的名称: ```rust enum Message { @@ -775,7 +629,7 @@ match msg { } ``` -示例 18-32: 使用 `@` 在模式中绑定值的同时测试它 +示例 18-29: 使用 `@` 在模式中绑定值的同时测试它 上例会打印出 `Found an id in range: 5`。通过在 `3...7` 之前指定 `id_variable @`,我们捕获了任何匹配此范围的值并同时测试其值匹配这个范围模式。 @@ -785,6 +639,53 @@ match msg { 使用 `@` 可以在一个模式中同时测试和保存变量值。 +### 遗留模式: `ref` 和 `ref mut` + +在老版本的 Rust 中,`match` 会假设你希望移动匹配到的值。不过有时并不希望如此。例如: + +```rust +let robot_name = &Some(String::from("Bors")); + +match robot_name { + Some(name) => println!("Found a name: {}", name), + None => (), +} + +println!("robot_name is: {:?}", robot_name); +``` + +这里 `robot_name` 是一个 `&Option`。Rust 会抱怨 `Some(name)` 不匹配 `&Option`,所以不得不这么写: + +```rust,ignore +let robot_name = &Some(String::from("Bors")); + +match robot_name { + &Some(name) => println!("Found a name: {}", name), + None => (), +} + +println!("robot_name is: {:?}", robot_name); +``` + +接着 Rust 会说 `name` 尝试将 `String` 从 option 中移出,不过因为这是一个引用的 option,所以是借用的,因此不能被移动。这就是 `ref` 出场的地方: + +```rust +let robot_name = &Some(String::from("Bors")); + +match robot_name { + &Some(ref name) => println!("Found a name: {}", name), + None => (), +} + +println!("robot_name is: {:?}", robot_name); +``` + +`ref` 关键字就像模式中 `&` 的对立面;它表明 “请将 `ref` 绑定到一个 `&String` 上,不要尝试移动”。换句话说,`&Some` 中的 `&` 匹配的是一个引用,而 `ref` **创建** 了一个引用。`ref mut` 类似 `ref` 不过对应的是可变引用。 + +无论如何,今天的 Rust 不再这样工作。如果尝试 `match` 某些借用的值,那么所有创建的绑定也都会尝试借用。这也意味着之前的代码也能正常工作。 + +因为 Rust 是后向兼容的(backwards compatible),所以不会移除 `ref` 和 `ref mut`,同时它们在一些不明确的场景还有用,比如希望可变地借用结构体的部分值而可变地借用另一部分的情况。你可能会在老的 Rust 代码中看到它们,所以请记住它们仍有价值。 + ## 总结 模式是 Rust 中一个很有用的功能,它帮助我们区分不同类型的数据。当用于 `match` 语句时,Rust 确保模式会包含每一个可能的值,否则程序将不能编译。`let` 语句和函数参数的模式使得这些结构更强大,可以在将值解构为更小部分的同时为变量赋值。可以创建简单或复杂的模式来满足我们的要求。 From 50d8b516897231c195d2c39cbf3bf1b7d0f055c6 Mon Sep 17 00:00:00 2001 From: KaiserY Date: Sun, 9 Dec 2018 01:21:01 +0800 Subject: [PATCH 33/49] check to ch19-03 --- src/ch19-00-advanced-features.md | 11 +-- src/ch19-01-unsafe-rust.md | 130 ++++++++++++------------------ src/ch19-02-advanced-lifetimes.md | 101 ++++++++++++----------- src/ch19-03-advanced-traits.md | 72 +++++------------ 4 files changed, 130 insertions(+), 184 deletions(-) diff --git a/src/ch19-00-advanced-features.md b/src/ch19-00-advanced-features.md index d74ffae..d01f24d 100644 --- a/src/ch19-00-advanced-features.md +++ b/src/ch19-00-advanced-features.md @@ -1,17 +1,18 @@ # 高级特征 -> [ch19-00-advanced-features.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch19-00-advanced-features.md) +> [ch19-00-advanced-features.md](https://github.com/rust-lang/book/blob/master/src/ch19-00-advanced-features.md) >
-> commit 9f03d42e2f47871fe813496b9324548ef4457862 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -我们已经走得很远了!现在我们已经学习了 99% 的编写 Rust 时需要了解的内容。在第二十章开始另一个新项目之前,让我们聊聊你可能会遇到的最后 1% 的内容。当你不经意间遇到未知的内容时请随意将本章作为参考;这里将要学习的特征在某些非常特定的情况下很有用处。我们并不希望忽略这些特性,但是你会发现很少会碰到它们。 +现在我们已经学习了 Rust 编程语言中最常用的部分。在第二十章开始另一个新项目之前,让我们聊聊一些总有一天你会遇上的部分内容。你可以将本章作为不经意间遇到未知的内容时的参考。本章将要学习的功能在一些非常特定的场景下很有用处。虽然很少会碰到它们,我们希望确保你了解 Rust 提供的所有功能。 本章将涉及如下内容: -* 不安全 Rust:用于当需要舍弃 Rust 的某些保证并由你自己负责维持这些保证 +* 不安全 Rust:用于当需要舍弃 Rust 的某些保证并负责手动维持这些保证 * 高级生命周期:用于复杂生命周期情况的语法 * 高级 trait:与 trait 相关的关联类型,默认类型参数,完全限定语法(fully qualified syntax),超(父)trait(supertraits)和 newtype 模式 -* 高级类型:关于 newtype 模式的更多内容,类型别名,“never” 类型和动态大小类型 +* 高级类型:关于 newtype 模式的更多内容,类型别名,never 类型和动态大小类型 * 高级函数和闭包:函数指针和返回闭包 +* 宏:定义在编译时定义更多更多代码的方式 对所有人而言,这都是一个介绍 Rust 迷人特性的宝典!让我们翻开它吧! \ No newline at end of file diff --git a/src/ch19-01-unsafe-rust.md b/src/ch19-01-unsafe-rust.md index b4f61f8..a288c03 100644 --- a/src/ch19-01-unsafe-rust.md +++ b/src/ch19-01-unsafe-rust.md @@ -1,10 +1,10 @@ ## 不安全 Rust -> [ch19-01-unsafe-rust.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch19-01-unsafe-rust.md) +> [ch19-01-unsafe-rust.md](https://github.com/rust-lang/book/blob/master/src/ch19-01-unsafe-rust.md) >
-> commit c2b43bd978a9176ac9aba22595e33d2335b2d04b +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -目前为止讨论过的代码都有 Rust 在编译时会强制执行的内存安全保证。然而,Rust 还隐藏有第二种语言,它不会强制执行这类内存安全保证:不安全 Rust。它与常规 Rust 代码无异,但是会提供额外的超级力量。 +目前为止讨论过的代码都有 Rust 在编译时会强制执行的内存安全保证。然而,Rust 还隐藏有第二种语言,它不会强制执行这类内存安全保证:这被称为 **不安全 Rust**(*unsafe Rust*)。它与常规 Rust 代码无异,但是会提供额外的超级力量。 不安全 Rust 之所以存在,是因为静态分析本质上是保守的。当编译器尝试确定一段代码是否支持某个保证时,拒绝一些有效的程序比接受无效程序要好一些。这必然意味着有时代码可能是合法的,但是 Rust 不这么认为!在这种情况下,可以使用不安全代码告诉编译器,“相信我,我知道我在干什么。”这么做的缺点就是你只能靠自己了:如果不安全代码出错了,比如解引用空指针,可能会导致不安全的内存使用。 @@ -12,14 +12,14 @@ ### 不安全的超级力量 -可以通过 `unsafe` 关键字来切换到不安全 Rust,接着可以开启一个新的存放不安全代码的块。这里有四类可以在不安全 Rust 中进行而不能用于安全 Rust 的操作。称之为 “不安全的超级力量。”这些超级力量是: +可以通过 `unsafe` 关键字来切换到不安全 Rust,接着可以开启一个新的存放不安全代码的块。这里有四类可以在不安全 Rust 中进行而不能用于安全 Rust 的操作,它们称之为 “不安全的超级力量。” 这些超级力量是: -1. 解引用裸指针 -2. 调用不安全的函数或方法 -3. 访问或修改可变静态变量 -4. 实现不安全 trait +* 解引用裸指针 +* 调用不安全的函数或方法 +* 访问或修改可变静态变量 +* 实现不安全 trait -有一点很重要,`unsafe` 并不会关闭借用检查器或禁用任何其他 Rust 安全检查:如果在不安全代码中使用引用,其仍会被检查。`unsafe` 关键字只是提供了那四个不会被编译器检查内存安全的功能。你仍然能在不安全块中获得某种程度的安全! +有一点很重要,`unsafe` 并不会关闭借用检查器或禁用任何其他 Rust 安全检查:如果在不安全代码中使用引用,其仍会被检查。`unsafe` 关键字只是提供了那四个不会被编译器检查内存安全的功能。你仍然能在不安全块中获得某种程度的安全。 再者,`unsafe` 不意味着块中的代码就一定是危险的或者必然导致内存安全问题:其意图在于作为程序员你将会确保 `unsafe` 块中的代码以有效的方式访问内存。 @@ -31,24 +31,17 @@ ### 解引用裸指针 -回到第四章的 “悬垂引用” 部分,那里提到了编译器会确保引用总是有效的。不安全 Rust 有两个被称为 **裸指针**(*raw pointers*)的类似于引用的新类型。和引用一样,裸指针是可变或不可变的,分别写作 `*const T` 和 `*mut T`。这里的星号不是解引用运算符;它是类型名称的一部分。在裸指针的上下文中,“裸指针” 意味着指针解引用之后不能直接赋值。 +回到第四章的 “悬垂引用” 部分,那里提到了编译器会确保引用总是有效的。不安全 Rust 有两个被称为 **裸指针**(*raw pointers*)的类似于引用的新类型。和引用一样,裸指针是可变或不可变的,分别写作 `*const T` 和 `*mut T`。这里的星号不是解引用运算符;它是类型名称的一部分。在裸指针的上下文中,**不可变** 意味着指针解引用之后不能直接赋值。 与引用和智能指针的区别在于,记住裸指针 -- 允许忽略借用规则,可以同时拥有不可变和可变的指针,或多个指向相同位置的可变指针 -- 不保证指向有效的内存 -- 允许为空 -- 不能实现任何自动清理功能 +* 允许忽略借用规则,可以同时拥有不可变和可变的指针,或多个指向相同位置的可变指针 +* 不保证指向有效的内存 +* 允许为空 +* 不能实现任何自动清理功能 通过去掉 Rust 强加的保证,你可以放弃安全保证以换取性能或使用另一个语言或硬件接口的能力,此时 Rust 的保证并不适用。 - - - 示例 19-1 展示了如何从引用同时创建不可变和可变裸指针。 ```rust @@ -60,16 +53,11 @@ let r2 = &mut num as *mut i32; 示例 19-1: 通过引用创建裸指针 - - - -注意这里没有引入 `unsafe` 关键字 ———— 可以在安全代码中 **创建** 裸指针,只是不能在不安全块之外 **解引用** 裸指针,稍后便会看到。 +注意这里没有引入 `unsafe` 关键字。可以在安全代码中 **创建** 裸指针,只是不能在不安全块之外 **解引用** 裸指针,稍后便会看到。 这里使用 `as` 将不可变和可变引用强转为对应的裸指针类型。因为直接从保证安全的引用来创建他们,可以知道这些特定的裸指针是有效,但是不能对任何裸指针做出如此假设。 -接下来会创建一个不能确定其有效性的裸指针,示例 19-2 展示了如何创建一个指向任意内存地址的裸指针。尝试使用任意内存是未定义行为:此地址可能有数据也可能没有,编译器可能会优化掉这个内存访问,或者程序可能会出现段错误(segfault)。通常没有好的理由编写这样的代码,不过却是可行的: +接下来会创建一个不能确定其有效性的裸指针,示例 19-2 展示了如何创建一个指向任意内存地址的裸指针。尝试使用任意内存是未定义行为:此地址可能有数据也可能没有,编译器可能会优化掉这个内存访问,或者程序可能会出现段错误(segmentation fault)。通常没有好的理由编写这样的代码,不过却是可行的: ```rust let address = 0x012345usize; @@ -78,9 +66,9 @@ let r = address as *const i32; 示例 19-2: 创建指向任意内存地址的裸指针 -记得我们说过可以在安全代码中创建裸指针,不过不能 **解引用** 裸指针和读取其指向的数据。现在我们要做的就是对裸指针使用解引用运算符 `*`,只要求一个 `unsafe` 块,如示例 19-3 所示: +记得我们说过可以在安全代码中创建裸指针,不过不能 **解引用** 裸指针和读取其指向的数据。现在我们要做的就是对裸指针使用解引用运算符 `*`,这需要一个 `unsafe` 块,如示例 19-3 所示: -```rust +```rust,unsafe let mut num = 5; let r1 = &num as *const i32; @@ -98,19 +86,15 @@ unsafe { 还需注意示例 19-1 和 19-3 中创建了同时指向相同内存位置 `num` 的裸指针 `*const i32` 和 `*mut i32`。相反如果尝试创建 `num` 的不可变和可变引用,这将无法编译因为 Rust 的所有权规则不允许拥有可变引用的同时拥有不可变引用。通过裸指针,就能够同时创建同一地址的可变指针和不可变指针,若通过可变指针修改数据,则可能潜在造成数据竞争。请多加小心! -既然存在这么多的危险,为何还要使用裸指针呢?一个主要的应用场景便是调用 C 代码接口,这在下一部分不安全函数中会讲到。另一个场景是构建借用检查器无法理解的安全抽象。让我们先介绍不安全函数,接着看一看使用不安全代码的安全抽象的例子。 +既然存在这么多的危险,为何还要使用裸指针呢?一个主要的应用场景便是调用 C 代码接口,这在下一部分 “调用不安全函数或方法” 中会讲到。另一个场景是构建借用检查器无法理解的安全抽象。让我们先介绍不安全函数,接着看一看使用不安全代码的安全抽象的例子。 ### 调用不安全函数或方法 第二类要求使用不安全块的操作是调用不安全函数。不安全函数和方法与常规函数方法十分类似,除了其开头有一个额外的 `unsafe`。`unsafe` 表明我们作为程序需要满足其要求,因为 Rust 不会保证满足这些要求。通过在 `unsafe` 块中调用不安全函数,我们表明已经阅读过此函数的文档并对其是否满足函数自身的契约负责。 - - - 如下是一个没有做任何操作的不安全函数 `dangerous` 的例子: -```rust +```rust,unsafe unsafe fn dangerous() {} unsafe { @@ -128,7 +112,7 @@ error[E0133]: call to unsafe function requires unsafe function or block | ^^^^^^^^^^^ call to unsafe function ``` -通过将 `dangerous` 调用插入 `unsafe` 块中,我们就向 Rust 保证了我们已经阅读过函数的文档,理解如何正确,并验证过所有内容的正确性。 +通过将 `dangerous` 调用插入 `unsafe` 块中,我们就向 Rust 保证了我们已经阅读过函数的文档,理解如何正确使用,并验证过其满足函数的契约。 不安全函数体也是有效的 `unsafe` 块,所以在不安全函数中进行另一个不安全操作时无需新增额外的 `unsafe` 块。 @@ -149,11 +133,9 @@ assert_eq!(b, &mut [4, 5, 6]); 示例 19-4: 使用安全的 `split_at_mut` 函数 -这个函数无法只通过安全 Rust 实现。一个尝试可能看起来像示例 19-5,它不能编译。处于简单考虑,我们将 `split_at_mut` 实现为函数而不是方法,并只处理 `i32` 值而非泛型 `T` 的 slice。 - -用安全的Rust代码是不能实现这个函数的. 如果要试一下用安全的Rust来实现它可以参考例19-5. 简单起见, 我们把`split_at_mut`实现成一个函数而不是一个方法, 这个函数只处理`i32`类型的切片而不是泛型类型`T`的切片: +这个函数无法只通过安全 Rust 实现。一个尝试可能看起来像示例 19-5,它不能编译。出于简单考虑,我们将 `split_at_mut` 实现为函数而不是方法,并只处理 `i32` 值而非泛型 `T` 的 slice。 -```rust,ignore +```rust,ignore,does_not_compile fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) { let len = slice.len(); @@ -166,11 +148,11 @@ fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) { 示例 19-5: 尝试只使用安全 Rust 来实现 `split_at_mut` -此函数有限获取 slice 的长度,然后通过检查参数是否小于或等于这个长度来断言参数所给定的索引位于 slice 当中。该断言意味着如果传入的索引比要分割的 slice 的索引更大,此函数在尝试使用这个索引前 panic。 +此函数首先获取 slice 的长度,然后通过检查参数是否小于或等于这个长度来断言参数所给定的索引位于 slice 当中。该断言意味着如果传入的索引比要分割的 slice 的索引更大,此函数在尝试使用这个索引前 panic。 次后我们在一个元组中返回两个可变的 slice:一个从原始 slice 的开头直到 `mid` 索引,另一个从 `mid` 直到原 slice 的结尾。 -如果尝试编译此代码,会得到一个错误: +如果尝试编译示例 19-5 的代码,会得到一个错误: ```text error[E0499]: cannot borrow `*slice` as mutable more than once at a time @@ -184,11 +166,11 @@ error[E0499]: cannot borrow `*slice` as mutable more than once at a time | - first borrow ends here ``` -Rust 的借用检查器不能理解我们要借用这个 slice 的两个不同部分:它只知道我们借用了同一个 slice 两次。本质上借用 slice 的不同部分是可以的,因为这样两个 slice 不会重叠,不过 Rust 还没有智能到理解这些。当我们知道某些事是可以的而 Rust 不知道的时候,就是触及不安全代码的时候了 +Rust 的借用检查器不能理解我们要借用这个 slice 的两个不同部分:它只知道我们借用了同一个 slice 两次。本质上借用 slice 的不同部分是可以的,因为结果两个 slice 不会重叠,不过 Rust 还没有智能到能够理解这些。当我们知道某些事是可以的而 Rust 不知道的时候,就是触及不安全代码的时候了 示例 19-6 展示了如何使用 `unsafe` 块,裸指针和一些不安全函数调用来实现 `split_at_mut`: -```rust +```rust,unsafe use std::slice; fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) { @@ -206,7 +188,7 @@ fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) { 示例 19-6: 在 `split_at_mut` 函数的实现中使用不安全代码 -回忆第四章的 “Slice” 部分,slice 是一个指向一些数据的指针,并带有该 slice 的长度。可以使用 `len` 方法获取 slice 的长度,使用 `as_mut_ptr` 方法访问 slice 的裸指针。在这个例子中,因为有一个 `i32` 值的可变 slice,`as_mut_ptr` 返回一个 `*mut i32` 类型的裸指针,储存在 `ptr` 变量中。 +回忆第四章的 “Slice 类型” 部分,slice 是一个指向一些数据的指针,并带有该 slice 的长度。可以使用 `len` 方法获取 slice 的长度,使用 `as_mut_ptr` 方法访问 slice 的裸指针。在这个例子中,因为有一个 `i32` 值的可变 slice,`as_mut_ptr` 返回一个 `*mut i32` 类型的裸指针,储存在 `ptr` 变量中。 我们保持索引 `mid` 位于 slice 中的断言。接着是不安全代码:`slice::from_raw_parts_mut` 函数获取一个裸指针和一个长度来创建一个 slice。这里使用此函数从 `ptr` 中创建了一个有 `mid` 个项的 slice。之后在 `ptr` 上调用 `offset` 方法并使用 `mid` 作为参数来获取一个从 `mid` 开始的裸指针,使用这个裸指针并以 `mid` 之后项的数量为长度创建一个 slice。 @@ -216,33 +198,30 @@ fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) { 与此相对,示例 19-7 中的 `slice::from_raw_parts_mut` 在使用 slice 时很有可能会崩溃。这段代码获取任意内存地址并创建了一个长为一万的 slice: -```rust +```rust,unsafe use std::slice; -let address = 0x012345usize; +let address = 0x01234usize; let r = address as *mut i32; -let slice = unsafe { +let slice : &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) }; ``` 示例 19-7: 通过任意内存地址创建 slice -我们并不拥有这个任意地址的内存,也不能保证这段代码创建的 slice 包含有效的 `i32` 值。试图使用臆测为有效的 `slice` 会导致未定义的行为。 +我们并不拥有这个任意地址的内存,也不能保证这段代码创建的 slice 包含有效的 `i32` 值。试图使用臆测为有效的 `slice` 会导致未定义的行为。如果我们没有注意将 `address` 向 4(字节)对齐(`i32` 的对齐方式),那么甚至调用 `slice::from_raw_parts_mut` 已经是为定义行为了 —— slice 必须总是对齐的,即使它没有被使用(哪怕甚至为空)。 #### 使用 `extern` 函数调用外部代码 有时你的 Rust 代码可能需要与其他语言编写的代码交互。为此 Rust 有一个关键字,`extern`,有助于创建和使用 **外部函数接口**(*Foreign Function Interface*, FFI)。外部函数接口是一个编程语言用以定义函数的方式,其允许不同(外部)编程语言调用这些函数。 - - - 示例 19-8 展示了如何集成 C 标准库中的 `abs` 函数。`extern` 块中声明的函数在 Rust 代码中总是不安全的。因为其他语言不会强制执行 Rust 的规则且 Rust 无法检查它们,所以确保其安全是程序员的责任: 文件名: src/main.rs -```rust +```rust,unsafe extern "C" { fn abs(input: i32) -> i32; } @@ -258,29 +237,20 @@ fn main() { 在 `extern "C"` 块中,列出了我们希望能够调用的另一个语言中的外部函数的签名和名称。`"C"` 部分定义了外部函数所使用的 **应用程序接口**(*application binary interface*,ABI) —— ABI 定义了如何在汇编语言层面调用此函数。`"C"` ABI 是最常见的,并遵循 C 编程语言的 ABI。 - - -##### 通过其它语言调用 Rust 函数 - -也可以使用 `extern` 来创建一个允许其他语言调用 Rust 函数的接口。不同于 `extern` 块,就在 `fn` 关键字之前增加 `extern` 关键字并指定所用到的 ABI。还需增加 `#[no_mangle]` 注解来告诉 Rust 编译器不要 mangle 此函数的名称。mangle 发生于当编译器将我们指定的函数名修改为不同的名称时,这会增加用于其他编译过程的额外信息,不过会使其名称更难以阅读。每一个编程语言的编译器都会以稍微不同的方式 mangle 函数名,所以为了使 Rust 函数能在其他语言中指定,必须禁用 Rust 编译器的 name mangling。 - - - - -在如下的例子中,一旦其编译为动态库并从 C 语言中链接,`call_from_c` 函数就能够在 C 代码中访问: - -```rust -#[no_mangle] -pub extern "C" fn call_from_c() { - println!("Just called a Rust function from C!"); -} -``` - -`extern` 的使用无需 `unsafe`。 - - +> #### 从其它语言调用 Rust 函数 +> +> 也可以使用 `extern` 来创建一个允许其他语言调用 Rust 函数的接口。不同于 `extern` 块,就在 `fn` 关键字之前增加 `extern` 关键字并指定所用到的 ABI。还需增加 `#[no_mangle]` 注解来告诉 Rust 编译器不要 mangle 此函数的名称。*Mangling* 发生于当编译器将我们指定的函数名修改为不同的名称时,这会增加用于其他编译过程的额外信息,不过会使其名称更难以阅读。每一个编程语言的编译器都会以稍微不同的方式 mangle 函数名,所以为了使 Rust 函数能在其他语言中指定,必须禁用 Rust 编译器的 name mangling。 +> +> 在如下的例子中,一旦其编译为动态库并从 C 语言中链接,`call_from_c` 函数就能够在 C 代码中访问: +> +> ```rust +> #[no_mangle] +> pub extern "C" fn call_from_c() { +> println!("Just called a Rust function from C!"); +> } +> ``` +> +> `extern` 的使用无需 `unsafe`。 ### 访问或修改可变静态变量 @@ -308,7 +278,7 @@ fn main() { 文件名: src/main.rs -```rust +```rust,unsafe static mut COUNTER: u32 = 0; fn add_to_count(inc: u32) { @@ -336,7 +306,7 @@ fn main() { 最后一个只能用在 `unsafe` 中的操作是实现不安全 trait。当至少有一个方法中包含编译器不能验证的不变量时 trait 是不安全的。可以在 `trait` 之前增加 `unsafe` 关键字将 trait 声明为 `unsafe`,同时 trait 的实现也必须标记为 `unsafe`,如示例 19-11 所示: -```rust +```rust,unsafe unsafe trait Foo { // methods go here } @@ -354,4 +324,4 @@ unsafe impl Foo for i32 { ### 何时使用不安全代码 -使用 `unsafe` 来进行这四个操作之一是没有问题的,甚至是不需要深思熟虑的,不过使得 `unsafe` 代码正确也实属不易因为编译器不能帮助保证内存安全。当有理由使用 `unsafe` 代码时,是可以这么做的,通过使用显式的 `unsafe` 标注使得在出现错误时易于追踪问题的源头。 +使用 `unsafe` 来进行这四个操作(超级力量)之一是没有问题的,甚至是不需要深思熟虑的,不过使得 `unsafe` 代码正确也实属不易因为编译器不能帮助保证内存安全。当有理由使用 `unsafe` 代码时,是可以这么做的,通过使用显式的 `unsafe` 标注使得在出现错误时易于追踪问题的源头。 diff --git a/src/ch19-02-advanced-lifetimes.md b/src/ch19-02-advanced-lifetimes.md index 1eb10da..24ee170 100644 --- a/src/ch19-02-advanced-lifetimes.md +++ b/src/ch19-02-advanced-lifetimes.md @@ -1,26 +1,23 @@ ## 高级生命周期 -> [ch19-02-advanced-lifetimes.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch19-02-advanced-lifetimes.md) +> [ch19-02-advanced-lifetimes.md](https://github.com/rust-lang/book/blob/master/src/ch19-02-advanced-lifetimes.md) >
-> commit f7f5e4835c1c4f8ddb502a1dd09a1584ed6f4b6f +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 回顾第十章 “生命周期与引用有效性” 部分,我们学习了怎样使用生命周期参数注解引用来帮助 Rust 理解不同引用的生命周期如何相互联系。我们理解了每一个引用都有生命周期,不过大部分情况 Rust 允许我们省略生命周期。这里我们会看到三个还未涉及到的生命周期高级特征: * 生命周期子类型(lifetime subtyping),一个确保某个生命周期长于另一个生命周期的方式 * 生命周期 bound(lifetime bounds),用于指定泛型引用的生命周期 * trait 对象生命周期(trait object lifetimes),以及他们是如何推断的,以及何时需要指定 - - - +* 匿名生命周期:使(生命周期)省略更为明显 ### 生命周期子类型确保某个生命周期长于另一个生命周期 -生命周期子类型是一个指定某个生命周期应该长于另一个生命周期的方式。为了探索生命周期子类型,想象一下我们想要编写一个解析器。为此会有一个储存了需要解析的字符串的引用的结构体 `Context`。解析器将会解析字符串并返回成功或失败。其实现看起来像示例 19-12 中的代码,除了缺少了必须的生命周期注解,所以这还不能编译: +生命周期子类型是一个指定某个生命周期应该长于另一个生命周期的方式。为了探索生命周期子类型,想象一下我们想要编写一个解析器。为此会有一个储存了需要解析的字符串的引用的结构体 `Context`。解析器将会解析字符串并返回成功或失败。解析器需要借用 `Context` 来进行解析。其实现看起来像示例 19-12 中的代码,除了缺少了必须的生命周期注解,所以这还不能编译: 文件名: src/lib.rs -```rust,ignore +```rust,ignore,does_not_compile struct Context(&str); struct Parser { @@ -38,23 +35,11 @@ impl Parser { 编译代码会导致一个表明 Rust 期望 `Context` 中字符串 slice 和 `Parser` 中 `Context` 的引用的生命周期的错误。 - - - 为了简单起见,`parse` 方法返回 `Result<(), &str>`。也就是说,成功时不做任何操作,失败时则返回字符串 slice 没有正确解析的部分。真实的实现将会包含比这更多的错误信息,并将会在解析成功时返回实际结果,不过我们将去掉这些部分的实现,因为他们与这个例子的生命周期部分并不相关。 为了保持代码简单,我们不准备实际编写任何解析逻辑。解析逻辑的某处非常有可能通过返回引用输入中无效部分的错误来处理无效输入,而考虑到生命周期,这个引用是使得这个例子有趣的地方。所以我们将假设解析器的逻辑为输入的第一个字节之后是无效的。注意如果第一个字节并不位于一个有效的字符范围内(比如 Unicode)代码将会 panic;这里又一次简化了例子以专注于涉及到的生命周期。 - - - -为了使代码能够编译,我们需要放入 `Context` 中字符串 slice 和 `Parser` 中 `Context` 引用的生命周期参数。最直接的方法是在每处都使用相同的生命周期,如示例 19-13 所示: - -那么我们如何为 `Context` 中的字符串 slice 和 `Parser` 中 `Context` 的引用放入生命周期参数呢?最直接的方法是在每处都使用相同的生命周期,如列表 19-13 所示: +为了使代码能够编译,我们需要放入 `Context` 中字符串 slice 和 `Parser` 中 `Context` 引用的生命周期参数。最直接的方法是在每处都使用相同的生命周期,如示例 19-13 所示。回忆第十章 “结构体定义中的生命周期注解” 部分,`struct Context<'a>`、`struct Parser<'a>` 和 `impl<'a>` 每一个都声明了一个新的生命周期参数。虽然这些名字碰巧都一样,例子中声明的三个生命周期参数并不相关。 文件名: src/lib.rs @@ -72,21 +57,15 @@ impl<'a> Parser<'a> { } ``` -示例 19-13: 将所有 `Context` 和 `Parser` 中的引用标注为相同的生命周期参数 +示例 19-13: 将所有 `Context` 和 `Parser` 中的引用标注生命周期参数 这次可以编译了,并告诉了 Rust `Parser` 存放了一个 `Context` 的引用,拥有生命周期 `'a`,且 `Context` 存放了一个字符串 slice,它也与 `Parser` 中 `Context` 的引用存在的一样久。Rust 编译器的错误信息表明这些引用需要生命周期参数,现在我们增加了这些生命周期参数。 - - - - - 接下来,在示例 19-14 中,让我们编写一个获取 `Context` 的实例,使用 `Parser` 来解析其内容,并返回 `parse` 的返回值的函数。这还不能运行: 文件名: src/lib.rs -```rust,ignore +```rust,ignore,does_not_compile fn parse_context(context: Context) -> Result<(), &str> { Parser { context: &context }.parse() } @@ -130,14 +109,9 @@ note: borrowed value must be valid for the anonymous lifetime #1 defined on the | |_^ ``` -这些错误表明我们创建的两个 `Parser` 实例和 `context` 参数从 `Parser` 被创建开始一直存活到 `parse_context` 函数结束,不过他们都需要在整个函数的生命周期中都有效。 - -换句话说,`Parser` 和 `context` 需要比整个函数 **长寿**(*outlive*)并在函数开始之前和结束之后都有效以确保代码中的所有引用始终是有效的。虽然我们创建的两个 `Parser` 和 `context` 参数在函数的结尾就离开了作用域(因为 `parse_context` 获取了 `context` 的所有权)。 +这些错误表明我们创建的两个 `Parser` 实例和 `context` 参数从 `Parser` 被创建开始一直存活到 `parse_context` 函数结束。不过他们都需要在整个函数的生命周期中都有效。 - - +换句话说,`Parser` 和 `context` 需要比整个函数 **长寿**(*outlive*)并在函数开始之前和结束之后都有效以确保代码中的所有引用始终是有效的。虽然我们创建的两个 `Parser` 和 `context` 参数在函数的结尾就离开了作用域,因为 `parse_context` 获取了 `context` 的所有权。 为了理解为什么会得到这些错误,让我们再次看看示例 19-13 中的定义,特别是 `parse` 方法的签名中的引用: @@ -145,9 +119,6 @@ previous paragraph. Is there something that's unclear? /Carol --> fn parse(&self) -> Result<(), &str> { ``` - - - 还记得(生命周期)省略规则吗?如果标注了引用生命周期而不加以省略,签名看起来应该是这样: ```rust,ignore @@ -168,7 +139,7 @@ Rust 认为我们尝试返回一个在函数结尾离开作用域的值,因为 文件名: src/lib.rs -```rust,ignore +```rust,ignore,does_not_compile struct Context<'s>(&'s str); struct Parser<'c, 's> { @@ -239,18 +210,15 @@ struct Parser<'c, 's: 'c> { 在第十章 “trait bound” 部分,我们讨论了如何在泛型类型上使用 trait bound。也可以像泛型那样为生命周期参数增加限制,这被称为 **生命周期 bound**(*lifetime bounds*)。生命周期 bound 帮助 Rust 验证泛型的引用不会存在的比其引用的数据更久。 - - - 例如,考虑一下一个封装了引用的类型。回忆一下第十五章 “`RefCell` 和内部可变性模式” 部分的 `RefCell` 类型:其 `borrow` 和 `borrow_mut` 方法分别返回 `Ref` 和 `RefMut` 类型。这些类型是引用的封装,他们在运行时记录检查借用规则。`Ref` 结构体的定义如示例 19-16 所示,目前还不带有生命周期 bound: 文件名: src/lib.rs -```rust,ignore +```rust,ignore,does_not_compile struct Ref<'a, T>(&'a T); ``` -示例 19-16: 定义结构体来封装泛型的引用;开始时没有生命周期约束 +示例 19-16: 定义结构体来封装泛型的引用,没有生命周期 bound 若不显式限制生命周期 `'a` 为与泛型参数 `T` 有关,会得到一个错误因为 Rust 不知道泛型 `T` 会存活多久: @@ -280,8 +248,6 @@ consider adding an explicit lifetime bound `T: 'a` so that the reference type 示例 19-17 展示了如何按照这个建议,在声明泛型 `T` 时指定生命周期 bound。。 -列表 19-17 展示了按照这个建议,在声明泛型 `T` 时指定生命周期约束。 - ```rust struct Ref<'a, T: 'a>(&'a T); ``` @@ -318,7 +284,7 @@ impl<'a> Red for Ball<'a> { } fn main() { let num = 5; - let obj = Box::new(Ball { diameter: &num }) as Box; + let obj = Box::new(Ball { diameter: &num }) as Box; } ``` @@ -333,4 +299,43 @@ fn main() { 当必须明确指定时,可以为像 `Box` 这样的 trait 对象增加生命周期 bound,根据需要使用语法 `Box` 或 `Box`。正如其他的 bound,这意味着任何 `Red` trait 的实现如果在内部包含有引用, 这些引用就必须拥有与 trait 对象 bound 中所指定的相同的生命周期。 +### 匿名生命周期 + +比方说有一个封装了一个字符串 slice 的结构体,如下: + +```rust +struct StrWrap<'a>(&'a str); +``` + +可以像这样编写一个返回它们的函数: + +```rust +# struct StrWrap<'a>(&'a str); +fn foo<'a>(string: &'a str) -> StrWrap<'a> { + StrWrap(string) +} +``` + +不过这里有很多的 `'a`!为了消除这些噪音,可以使用匿名生命周期,`'_`,如下: + +```rust +# struct StrWrap<'a>(&'a str); +fn foo(string: &str) -> StrWrap<'_> { + StrWrap(string) +} +``` + +`'_` 表明 “在此处使用省略的生命周期。” 这意味着我们仍然知道 `StrWrap` 包含一个引用,不过无需所有的生命周期注解来知道。 + +其也能用于 `impl`;例如: + +```rust,ignore +// 冗余 +impl<'a> fmt::Debug for StrWrap<'a> { + +// 省略 +impl fmt::Debug for StrWrap<'_> { + +``` + 接下来,让我们看看一些其他处理 trait 的高级功能吧! diff --git a/src/ch19-03-advanced-traits.md b/src/ch19-03-advanced-traits.md index 8307a36..6221151 100644 --- a/src/ch19-03-advanced-traits.md +++ b/src/ch19-03-advanced-traits.md @@ -1,8 +1,8 @@ ## 高级 trait -> [ch19-03-advanced-traits.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch19-03-advanced-traits.md) +> [ch19-03-advanced-traits.md](https://github.com/rust-lang/book/blob/master/src/ch19-03-advanced-traits.md) >
-> commit 9d5b9a573daf5fa0c98b3a3005badcea4a0a5211 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 第十章 “trait:定义共享的行为” 部分,我们第一次涉及到了 trait,不过就像生命周期一样,我们并没有覆盖一些较为高级的细节。现在我们更加了解 Rust 了,可以深入理解其本质了。 @@ -10,10 +10,6 @@ **关联类型**(*associated types*)是一个将类型占位符与 trait 相关联的方式,这样 trait 的方法签名中就可以使用这些占位符类型。trait 的实现者会针对特定的实现在这个类型的位置指定相应的具体类型。如此可以定义一个使用多种类型的 trait,直到实现此 trait 时都无需知道这些类型具体是什么。 - - - 本章所描述的大部分内容都非常少见。关联类型则比较适中;它们比本书其他的内容要少见,不过比本章中的很多内容要更常见。 一个带有关联类型的 trait 的例子是标准库提供的 `Iterator` trait。它有一个叫做 `Item` 的关联类型来替代遍历的值的类型。第十三章的 “`Iterator` trait 和 `next` 方法” 部分曾提到过 `Iterator` trait 的定义如示例 19-20 所示: @@ -21,17 +17,16 @@ to specify a type prior to use, is that right? --> ```rust pub trait Iterator { type Item; + fn next(&mut self) -> Option; } ``` 示例 19-20: `Iterator` trait 的定义中带有关联类型 `Item` -`Iterator` trait 有一个关联类型 `Item`。`Item` 是一个占位类型,同时 `next` 方法会返回 `Option` 类型的值。这个 trait 的实现者会指定 `Item` 的具体类型,然而不管实现者指定何种类型, `next` 方法都会返回一个包含了此具体类型值的 `Option`。 - -#### 关联类型 vs 泛型 +`Item` 是一个占位类型,同时 `next` 方法定义表明它返回 `Option` 类型的值。这个 trait 的实现者会指定 `Item` 的具体类型,然而不管实现者指定何种类型, `next` 方法都会返回一个包含了此具体类型值的 `Option`。 -这可能看起来像一个类似泛型的概念,因为它允许定义一个函数而不指定其可以处理的类型。那么为什么要使用关联类型呢? +关联类型看起来像一个类似泛型的概念,因为它允许定义一个函数而不指定其可以处理的类型。那么为什么要使用关联类型呢? 让我们通过一个在第十三章中出现的 `Counter` 结构体上实现 `Iterator` trait 的例子来检视其中的区别。在示例 13-21 中,指定了 `Item` 的类型为 `u32`: @@ -53,30 +48,26 @@ pub trait Iterator { } ``` -示例 19-21: 一个使用泛型的 `Iterator` trait 假象定义 +示例 19-21: 一个使用泛型的 `Iterator` trait 假想定义 区别在于当如示例 19-21 那样使用泛型时,则不得不在每一个实现中标注类型。这是因为我们也可以实现为 `Iterator for Counter`,或任何其他类型,这样就可以有多个 `Counter` 的 `Iterator` 的实现。换句话说,当 trait 有泛型参数时,可以多次实现这个 trait,每次需改变泛型参数的具体类型。接着当使用 `Counter` 的 `next` 方法时,必须提供类型注解来表明希望使用 `Iterator` 的哪一个实现。 -通过关联类型,则无需标注类型因为不能多次实现这个 trait。对于示例 19-20,我们只能选择一次 `Item` 会是什么类型,因为只能有一个 `impl Iterator for Counter`。当调用 `Counter` 的 `next` 时不必每次指定我们需要 `u32` 值的迭代器。 +通过关联类型,则无需标注类型因为不能多次实现这个 trait。对于示例 19-20 使用关联类型的定义,我们只能选择一次 `Item` 会是什么类型,因为只能有一个 `impl Iterator for Counter`。当调用 `Counter` 的 `next` 时不必每次指定我们需要 `u32` 值的迭代器。 ### 默认泛型类型参数和运算符重载 当使用泛型类型参数时,可以为泛型指定一个默认的具体类型。如果默认类型就足够的话,这消除了为具体类型实现 trait 的需要。为泛型类型指定默认类型的语法是在声明泛型类型时使用 ``。 -这种情况的一个非常好的例子是用于运算符重载。运算符重载是指在特定情况下自定义运算符(比如 `+`)行为的操作。 +这种情况的一个非常好的例子是用于运算符重载。**运算符重载**(*Operator overloading*)是指在特定情况下自定义运算符(比如 `+`)行为的操作。 - - - -Rust 并不允许创建自定义运算符或重载任意运算符,不过 `std::ops` 中所列出的运算符和相应的 trait **可以** 通过实现运算符相关 trait 来重载。例如,示例 19-22 中展示了如何在 `Point` 结构体上实现 `Add` trait 来重载 `+` 运算符,这样就可以将两个 `Point` 实例相加了: +Rust 并不允许创建自定义运算符或重载任意运算符,不过 `std::ops` 中所列出的运算符和相应的 trait 可以通过实现运算符相关 trait 来重载。例如,示例 19-22 中展示了如何在 `Point` 结构体上实现 `Add` trait 来重载 `+` 运算符,这样就可以将两个 `Point` 实例相加了: 文件名: src/main.rs ```rust use std::ops::Add; -#[derive(Debug,PartialEq)] +#[derive(Debug, PartialEq)] struct Point { x: i32, y: i32, @@ -113,14 +104,11 @@ trait Add { } ``` -这看来应该很熟悉,这是一个带有一个方法和一个关联类型的 trait。比较陌生的部分是尖括号中的 `RHS=Self`:这个语法叫做 **默认类型参数**(*default type parameters*)。`RHS` 是一个泛型类型参数 —— “right hand side” 的缩写 —— 它用于 `add` 方法中的 `rhs` 参数。如果实现 `Add` trait 时不指定 `RHS` 的具体类型,`RHS` 的类型将是默认的 `Self` 类型,也就是在其上实现 `Add` 的类型。 - - - +这看来应该很熟悉,这是一个带有一个方法和一个关联类型的 trait。比较陌生的部分是尖括号中的 `RHS=Self`:这个语法叫做 **默认类型参数**(*default type parameters*)。`RHS` 是一个泛型类型参数(“right hand side” 的缩写),它用于定义 `add` 方法中的 `rhs` 参数。如果实现 `Add` trait 时不指定 `RHS` 的具体类型,`RHS` 的类型将是默认的 `Self` 类型,也就是在其上实现 `Add` 的类型。 当为 `Point` 实现 `Add` 时,使用了默认的 `RHS`,因为我们希望将两个 `Point` 实例相加。让我们看看一个实现 `Add` trait 时希望自定义 `RHS` 类型而不是使用默认类型的例子 -这里有两个存放不同单元值的结构体,`Millimeters` 和 `Meters`。我们希望能够将毫米值与米值相加,并让 `Add` 的实现正确处理转换。可以为 `Millimeters` 实现 `Add` 并以 `Meters` 作为右手边,如示例 19-23 所示: +这里有两个存放不同单元值的结构体,`Millimeters` 和 `Meters`。我们希望能够将毫米值与米值相加,并让 `Add` 的实现正确处理转换。可以为 `Millimeters` 实现 `Add` 并以 `Meters` 作为 `RHS`,如示例 19-23 所示。 文件名: src/lib.rs @@ -145,12 +133,8 @@ impl Add for Millimeters { 默认参数类型主要用于如下两个方面: -1. 扩展类型而不破坏现有代码。 -2. 在大部分用户都不需要的特定情况进行自定义。 - - - +* 扩展类型而不破坏现有代码。 +* 在大部分用户都不需要的特定情况进行自定义。 标准库的 `Add` trait 就是一个第二个目的例子:大部分时候你会将两个相似的类型相加,不过它提供了自定义额外行为的能力。在 `Add` trait 定义中使用默认类型参数意味着大部分时候无需指定额外的参数。换句话说,一小部分实现的样板代码是不必要的,这样使用 trait 就更容易了。 @@ -160,14 +144,8 @@ it could be clearer /Carol--> Rust 既不能避免一个 trait 与另一个 trait 拥有相同名称的方法,也不能阻止为同一类型同时实现这两个 trait。甚至直接在类型上实现开始已经有的同名方法也是可能的! - - - 不过,当调用这些同名方法时,需要告诉 Rust 我们希望使用哪一个。考虑一下示例 19-24 中的代码,这里定义了 trait `Pilot` 和 `Wizard` 都拥有方法 `fly`。接着在一个本身已经实现了名为 `fly` 方法的类型 `Human` 上实现这两个 trait。每一个 `fly` 方法都进行了不同的操作: -甚至也可以直接在类型上实现相同名称的方法!那么为了能使用相同的名称调用每一个方法,需要告诉 Rust 我们希望使用哪个方法。考虑一下列表 19-27 中的代码,trait `Foo` 和 `Bar` 都拥有方法 `f`,并在结构体 `Baz` 上实现了这两个 trait,结构体也有一个叫做 `f` 的方法: - 文件名: src/main.rs ```rust @@ -342,7 +320,7 @@ A baby dog is called a Spot 文件名: src/main.rs -```rust,ignore +```rust,ignore,does_not_compile fn main() { println!("A baby dog is called a {}", Animal::baby_name()); } @@ -410,7 +388,7 @@ A baby dog is called a puppy 有时我们可能会需要某个 trait 使用另一个 trait 的功能。在这种情况下,需要能够依赖相关的 trait 也被实现。这个所需的 trait 是我们实现的 trait 的 **父(超) trait**(*supertrait*)。 -例如我们希望创建一个带有 `outline_print` 方法的 trait `OutlinePrint`,它会打印出带有星号框的值。也就是说,如果 `Point` 实现了 `Display` 并返回 `(x, y)`,调用以 1 作为 `x` 和 3 作为 `y` 的 `Point` 实例的 `outline_print` 会显示如下: +例如我们希望创建一个带有 `outline_print` 方法的 trait `OutlinePrint`,它会打印出带有星号框的值。也就是说,如果 `Point` 实现了 `Display` 并返回 `(x, y)`,调用以 `1` 作为 `x` 和 `3` 作为 `y` 的 `Point` 实例的 `outline_print` 会显示如下: ```text ********** @@ -466,7 +444,7 @@ error[E0277]: the trait bound `Point: std::fmt::Display` is not satisfied | 20 | impl OutlinePrint for Point {} | ^^^^^^^^^^^^ `Point` cannot be formatted with the default formatter; - try using `:?` instead if you are using a format string +try using `:?` instead if you are using a format string | = help: the trait `std::fmt::Display` is not implemented for `Point` ``` @@ -490,15 +468,13 @@ impl fmt::Display for Point { } ``` -那么在 `Point` 上实现 `OutlinePrint` trait 将能成功编译并可以在 `Point` 实例上调用 `outline_print` 来显示位于星号框中的点的值。 +那么在 `Point` 上实现 `OutlinePrint` trait 将能成功编译,并可以在 `Point` 实例上调用 `outline_print` 来显示位于星号框中的点的值。 ### newtype 模式用以在外部类型上实现外部 trait 在第十章的 “为类型实现 trait” 部分,我们提到了孤儿规则(orphan rule),它说明只要 trait 或类型对于当前 crate 是本地的话就可以在此类型上实现该 trait。一个绕开这个限制的方法是使用**newtype 模式**(*newtype pattern*),它涉及到在一个元组结构体(第五章 “用没有命名字段的元组结构体来创建不同的类型” 部分介绍了元组结构体)中创建一个新类型。这个元组结构体带有一个字段作为希望实现 trait 的类型的简单封装。接着这个封装类型对于 crate 是本地的,这样就可以在这个封装上实现 trait。“Newtype” 是一个源自(U.C.0079,逃)Haskell 编程语言的概念。使用这个模式没有运行时性能惩罚,这个封装类型在编译时就被省略了。 -例如,如果想要在 `Vec` 上实现 `Display`,而孤儿规则阻止我们直接这么做,因为 `Display` trait 和 `Vec` 都定义于我们的 crate 之外。可以创建一个包含 `Vec` 实例的 `Wrapper` 结构体,接着可以如列表 19-31 那样在 `Wrapper` 上实现 `Display` 并使用 `Vec` 的值: - -可以创建一个包含 `Vec` 实例的 `Wrapper` 结构体。接着可以如列表 19-30 那样在 `Wrapper` 上实现 `Display` 并使用 `Vec` 的值: +例如,如果想要在 `Vec` 上实现 `Display`,而孤儿规则阻止我们直接这么做,因为 `Display` trait 和 `Vec` 都定义于我们的 crate 之外。可以创建一个包含 `Vec` 实例的 `Wrapper` 结构体,接着可以如列表 19-31 那样在 `Wrapper` 上实现 `Display` 并使用 `Vec` 的值: 文件名: src/main.rs @@ -521,14 +497,8 @@ fn main() { 示例 19-31: 创建 `Wrapper` 类型封装 `Vec` 以便能够实现 `Display` -`Display` 的实现使用 `self.0` 来访问其内部的 `Vec`,因为 `Wrapper` 是元组结构体而 `Vec` 是结构体总位于索引 0 的项。接着就可以使用 `Wrapper` 中 `Display` 的功能了。 - - - +`Display` 的实现使用 `self.0` 来访问其内部的 `Vec`,因为 `Wrapper` 是元组结构体而 `Vec` 是结构体总位于索引 0 的项。接着就可以使用 `Wrapper` 中 `Display` 的功能了。 -此方法的缺点是,因为 `Wrapper` 是一个新类型,它没有定义于其值之上的方法;必须直接在 `Wrapper` 上实现 `Vec` 的所有方法,这样就可以代理到`self.0` 上 —— 这就允许我们完全像 `Vec` 那样对待 `Wrapper`。如果希望新类型拥有其内部类型的每一个方法,为封装类型实现 `Deref` trait(第十五章 “通过 `Deref` trait 将智能指针当作常规引用处理” 部分讨论过)并返回其内部类型是一种解决方案。如果不希望封装类型拥有所有内部类型的方法,比如为了限制封装类型的行为,则必须只自行实现所需的方法。 +此方法的缺点是,因为 `Wrapper` 是一个新类型,它没有定义于其值之上的方法;必须直接在 `Wrapper` 上实现 `Vec` 的所有方法,这样就可以代理到`self.0` 上 —— 这就允许我们完全像 `Vec` 那样对待 `Wrapper`。如果希望新类型拥有其内部类型的每一个方法,为封装类型实现 `Deref` trait(第十五章 “通过 `Deref` trait 将智能指针当作常规引用处理” 部分讨论过)并返回其内部类型是一种解决方案。如果不希望封装类型拥有所有内部类型的方法 —— 比如为了限制封装类型的行为 —— 则必须只自行实现所需的方法。 上面便是 newtype 模式如何与 trait 结合使用的;还有一个不涉及 trait 的实用模式。现在让我们将话题的焦点转移到一些与 Rust 类型系统交互的高级方法上来吧。 From 6cbe425a5e0df763aadc7bbe020481db91a237c8 Mon Sep 17 00:00:00 2001 From: Lam <15622383059@163.com> Date: Sun, 9 Dec 2018 14:07:59 +0800 Subject: [PATCH 34/49] improve translation and fix typo --- .idea/vcs.xml | 6 ++++++ src/ch10-03-lifetime-syntax.md | 16 ++++++++-------- 2 files changed, 14 insertions(+), 8 deletions(-) create mode 100644 .idea/vcs.xml diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/ch10-03-lifetime-syntax.md b/src/ch10-03-lifetime-syntax.md index a3674c1..51a6b1a 100644 --- a/src/ch10-03-lifetime-syntax.md +++ b/src/ch10-03-lifetime-syntax.md @@ -209,7 +209,7 @@ fn main() { 在这个例子中,`string1` 直到外部作用域结束都是有效的,`string2` 则在内部作用域中是有效的,而 `result` 则引用了一些直到内部作用域结束都是有效的值。借用检查器认可这些代码;它能够编译和运行,并打印出 `The longest string is long string is long`。 -接下来,让我们尝试一个 `result` 的引用的生命周期肯定比两个参数的要短的例子。将 `result` 变量的声明从内部作用域中移动出来,但是将 `result` 和 `string2` 变量的赋值语句一同留在内部作用域里。接下来,我们将使用 `result` 的 `println!` 移动到内部作用域之外,就在其结束之后。注意示例 10-24 中的代码不能编译: +接下来,让我们尝试另外一个例子,该例子揭示了 `result` 的引用的生命周期必须是两个参数中较短的那个。以下代码将 `result` 变量的声明移动出内部作用域,但是将 `result` 和 `string2` 变量的赋值语句一同留在内部作用域中。接着,使用了变量 `result` 的 `println!` 也被移动到内部作用域之外。注意示例 10-24 中的代码不能通过编译: 文件名: src/main.rs @@ -225,7 +225,7 @@ fn main() { } ``` -示例 10-24:在 `string2` 离开作用域之后使用 `result` 的尝试不能编译 +示例 10-24:尝试在 `string2` 离开作用域之后使用 `result` 如果尝试编译会出现如下错误: @@ -244,13 +244,13 @@ error[E0597]: `string2` does not live long enough 错误表明为了保证 `println!` 中的 `result` 是有效的,`string2` 需要直到外部作用域结束都是有效的。Rust 知道这些是因为(`longest`)函数的参数和返回值都使用了相同的生命周期参数 `'a`。 -以人类的理解 `string1` 更长,因此 `result` 会包含指向 `string1` 的引用。因为 `string1` 尚未离开作用域,对于 `println!` 来说 `string1` 的引用仍然是有效的。然而,我们通过生命周期参数告诉 Rust 的是 `longest` 函数返回的引用的生命周期应该与传入参数的生命周期中较短那个保持一致。因此,借用检查器不允许示例 10-24 中的代码,因为它可能会存在无效的引用。 +如果从人的角度读上述代码,我们可能会觉得这个代码是正确的。 `string1` 更长,因此 `result` 会包含指向 `string1` 的引用。因为 `string1` 尚未离开作用域,对于 `println!` 来说 `string1` 的引用仍然是有效的。然而,我们通过生命周期参数告诉 Rust 的是: `longest` 函数返回的引用的生命周期应该与传入参数的生命周期中较短那个保持一致。因此,借用检查器不允许示例 10-24 中的代码,因为它可能会存在无效的引用。 请尝试更多采用不同的值和不同生命周期的引用作为 `longest` 函数的参数和返回值的实验。并在开始编译前猜想你的实验能否通过借用检查器,接着编译一下看看你的理解是否正确! ### 深入理解生命周期 -指定生命周期参数的正确方式依赖函数具体的功能。例如,如果将 `longest` 函数的实现修改为总是返回第一个参数而不是最长的字符串 slice,就不需要为参数 `y` 指定一个生命周期。如下代码将能够编译: +指定生命周期参数的正确方式依赖函数实现的具体功能。例如,如果将 `longest` 函数的实现修改为总是返回第一个参数而不是最长的字符串 slice,就不需要为参数 `y` 指定一个生命周期。如下代码将能够编译: 文件名: src/main.rs @@ -297,11 +297,11 @@ function body at 1:1... 出现的问题是 `result` 在 `longest` 函数的结尾将离开作用域并被清理,而我们尝试从函数返回一个 `result` 的引用。无法指定生命周期参数来改变悬垂引用,而且 Rust 也不允许我们创建一个悬垂引用。在这种情况,最好的解决方案是返回一个有所有权的数据类型而不是一个引用,这样函数调用者就需要负责清理这个值了。 -从结果上看,生命周期语法是关于如何联系函数不同参数和返回值的生命周期的。一旦他们形成了某种联系,Rust 就有了足够的信息来允许内存安全的操作并阻止会产生悬垂指针亦或是违反内存安全的行为。 +综上,生命周期语法是用于将函数的多个参数与其返回值的生命周期进行关联的。一旦他们形成了某种关联,Rust 就有了足够的信息来允许内存安全的操作并阻止会产生悬垂指针亦或是违反内存安全的行为。 ### 结构体定义中的生命周期注解 -目前为止,我们只定义过有所有权类型的结构体。也可以定义存放引用的结构体,不过需要为结构体定义中的每一个引用添加生命周期注解。示例 10-25 中有一个存放了一个字符串 slice 的结构体 `ImportantExcerpt`: +目前为止,我们只定义过有所有权类型的结构体。接下来,我们将定义包含引用的结构体,不过这需要为结构体定义中的每一个引用添加生命周期注解。示例 10-25 中有一个存放了一个字符串 slice 的结构体 `ImportantExcerpt`: 文件名: src/main.rs @@ -327,7 +327,7 @@ fn main() { ### 生命周期省略(Lifetime Elision) -在这一部分,我们知道了每一个引用都有一个生命周期,而且需要为使用了引用的函数或结构体指定生命周期。然而,第四章的示例 4-9 中有一个函数,我们在示例 10-26 中再次展示出来,它没有生命周期注解却能成功编译: +现在我们已经知道了每一个引用都有一个生命周期,而且我们需要为那些使用了引用的函数或结构体指定生命周期。然而,第四章的示例 4-9 中有一个函数,如示例 10-26 所示,它没有生命周期注解却能编译成功: 文件名: src/lib.rs @@ -405,7 +405,7 @@ fn longest(x: &str, y: &str) -> &str { fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str { ``` -再来应用第二条规则,它并不适用因为存在多于一个输入生命周期。再来看第三条规则,它同样也不适用因为没有 `self` 参数。然后我们就没有更多规则了,不过还没有计算出返回值的类型的生命周期。这就是为什么在编译示例 10-21 的代码时会出现错误的原因:编译器使用所有已知的生命周期省略规则,不过仍不能计算出签名中所有引用的生命周期。 +再来应用第二条规则,因为函数存在多个输入生命周期,它并不适用于这种情况。再来看第三条规则,它同样也不适用,这是因为没有 `self` 参数。应用了三个规则之后编译器还没有计算出返回值类型的生命周期。这就是为什么在编译示例 10-21 的代码时会出现错误的原因:编译器使用所有已知的生命周期省略规则,仍不能计算出签名中所有引用的生命周期。 因为第三条规则真正能够适用的就只有方法签名,现在就让我们看看那种情况中的生命周期,并看看为什么这条规则意味着我们经常不需要在方法签名中标注生命周期。 From 75927bd8c008617149e14b71720ad78bfc1899c0 Mon Sep 17 00:00:00 2001 From: Lam <15622383059@163.com> Date: Sun, 9 Dec 2018 15:57:56 +0800 Subject: [PATCH 35/49] delete vcs.xml --- .idea/vcs.xml | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 .idea/vcs.xml diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file From b12891445983894801f8200797a377702e577358 Mon Sep 17 00:00:00 2001 From: Lam <15622383059@163.com> Date: Sun, 9 Dec 2018 23:21:34 +0800 Subject: [PATCH 36/49] improve some translation --- src/ch11-02-running-tests.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/ch11-02-running-tests.md b/src/ch11-02-running-tests.md index 9b32b0a..9792a85 100644 --- a/src/ch11-02-running-tests.md +++ b/src/ch11-02-running-tests.md @@ -6,11 +6,11 @@ 就像 `cargo run` 会编译代码并运行生成的二进制文件一样,`cargo test` 在测试模式下编译代码并运行生成的测试二进制文件。可以指定命令行参数来改变 `cargo test` 的默认行为。例如,`cargo test` 生成的二进制文件的默认行为是并行的运行所有测试,并截获测试运行过程中产生的输出,阻止他们被显示出来,使得阅读测试结果相关的内容变得更容易。 -这些选项的一部分可以传递给 `cargo test`,而另一些则需要传递给生成的测试二进制文件。为了分隔两种类型的参数,首先列出传递给 `cargo test` 的参数,接着是分隔符 `--`,再之后是传递给测试二进制文件的参数。运行 `cargo test --help` 会告诉你 `cargo test` 的相关参数,而运行 `cargo test -- --help` 则会告诉你可以在分隔符 `--` 之后使用的相关参数。 +可以将一部分命令行参数传递给 `cargo test`,而将另外一部分传递给生成的测试二进制文件。为了分隔这两种参数,需要先列出传递给 `cargo test` 的参数,接着是分隔符 `--`,再之后是传递给测试二进制文件的参数。运行 `cargo test --help` 会提示 `cargo test` 的有关参数,而运行 `cargo test -- --help` 可以提示在分隔符 `--` 之后使用的有关参数。 ### 并行或连续的运行测试 -当运行多个测试时,他们默认使用线程来并行的运行。这意味着测试会更快的运行完毕,所以你可以更快的得到代码能否工作的反馈。因为测试是在同时运行的,你应该确保测试不能相互依赖,或依赖任何共享的状态,包括依赖共享的环境,比如当前工作目录或者环境变量。 +当运行多个测试时, Rust 默认使用线程来并行运行。这意味着测试会更快地运行完毕,所以你可以更快的得到代码能否工作的反馈。因为测试是在同时运行的,你应该确保测试不能相互依赖,或依赖任何共享的状态,包括依赖共享的环境,比如当前工作目录或者环境变量。 举个例子,每一个测试都运行一些代码,假设这些代码都在硬盘上创建一个 *test-output.txt* 文件并写入一些数据。接着每一个测试都读取文件中的数据并断言这个文件包含特定的值,而这个值在每个测试中都是不同的。因为所有测试都是同时运行的,一个测试可能会在另一个测试读写文件过程中修改了文件。那么第二个测试就会失败,并不是因为代码不正确,而是因为测试并行运行时相互干扰。一个解决方案是使每一个测试读写不同的文件;另一个解决方案是一次运行一个测试。 @@ -107,13 +107,13 @@ failures: test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out ``` -注意测试的输出和测试结果的输出是相互交叉的;这是由于测试是并行运行的,也就是上一部分讲到的。尝试一同使用 `--test-threads=1` 和 `--nocapture` 功能来看看输出是什么样子! +注意测试的输出和测试结果的输出是相互交叉的,这是由于测试是并行运行的(见上一部分)。尝试一同使用 `--test-threads=1` 和 `--nocapture` 功能来看看输出是什么样子! -### 通过名称来运行测试的子集 +### 通过指定名字来运行部分测试 -有时运行整个测试集会耗费很长时间。如果你负责特定位置的代码,你可能会希望只运行这些代码相关的测试。你可以向 `cargo test` 传递希望运行的测试的部分名称作为参数来选择运行哪些测试。 +有时运行整个测试集会耗费很长时间。如果你负责特定位置的代码,你可能会希望只运行与这些代码相关的测试。你可以向 `cargo test` 传递所希望运行的测试名称的参数来选择运行哪些测试。 -为了展示如何运行测试的子集,示例 11-11 为 `add_two` 函数创建了三个测试,我们可以选择具体运行哪一个: +为了展示如何运行部分测试,示例 11-11 为 `add_two` 函数创建了三个测试,我们可以选择具体运行哪一个: 文件名: src/lib.rs @@ -240,4 +240,4 @@ test expensive_test ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out ``` -通过控制运行哪些测试,你可以确保能够快速地运行 `cargo test` 。当某个时刻需要检查 `ignored` 测试的结果,而且你也有时间等待这个结果的话,就可以选择执行 `cargo test -- --ignored`。 +通过控制运行哪些测试,你可以确保能够快速地运行 `cargo test` 。当你需要运行 `ignored` 的测试时,可以执行 `cargo test -- --ignored`。 From 36293d02bc358bf08aaf1c6e9b61b74331c04cf3 Mon Sep 17 00:00:00 2001 From: KaiserY Date: Sun, 9 Dec 2018 23:22:10 +0800 Subject: [PATCH 37/49] check to apendix-03 --- README.md | 12 +- src/PREFACE.md | 2 +- src/SUMMARY.md | 7 +- src/appendix-00.md | 6 +- src/appendix-01-keywords.md | 116 +++--- src/appendix-02-operators.md | 81 ++-- src/appendix-03-derivable-traits.md | 43 +-- src/appendix-04-useful-development-tools.md | 5 + src/appendix-05-editions.md | 5 + src/appendix-06-translation.md | 25 ++ src/appendix-07-nightly-rust.md | 6 +- src/ch19-04-advanced-types.md | 63 +-- ...ch19-05-advanced-functions-and-closures.md | 34 +- src/ch19-06-macros.md | 362 ++++++++++++++++++ src/ch20-00-final-project-a-web-server.md | 12 +- src/ch20-01-single-threaded.md | 136 ++----- src/ch20-02-multithreaded.md | 123 ++---- src/ch20-03-graceful-shutdown-and-cleanup.md | 24 +- 18 files changed, 680 insertions(+), 382 deletions(-) create mode 100644 src/appendix-04-useful-development-tools.md create mode 100644 src/appendix-05-editions.md create mode 100644 src/appendix-06-translation.md create mode 100644 src/ch19-06-macros.md diff --git a/README.md b/README.md index bb6ebe3..876eb58 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# Rust 程序设计语言(第二版) 简体中文版 +# Rust 程序设计语言(第二版 & 2018 edition) 简体中文版 [![Build Status](https://travis-ci.org/KaiserY/trpl-zh-cn.svg?branch=master)](https://travis-ci.org/KaiserY/trpl-zh-cn) ## 状态 -目前正向 2018 edtion 过渡,目前官方仓库状已经不再分版本提供源码了,故本仓库准备与官方保持一致。已更新到第十七章。 +2018 edition 的翻译迁移已基本完成,欢迎阅读学习! PS: @@ -17,15 +17,15 @@ PS: ### 构建 -你可以将本mdbook构建成一系列静态html页面。这里我们采用[vuepress](https://vuepress.vuejs.org/zh/)打包出静态网页。在这之前,你需要安装[Nodejs](https://nodejs.org/zh-cn/)。 +你可以将本 mdbook 构建成一系列静态 html 页面。这里我们采用 [vuepress](https://vuepress.vuejs.org/zh/) 打包出静态网页。在这之前,你需要安装 [Nodejs](https://nodejs.org/zh-cn/)。 -全局安装vuepress +全局安装 vuepress ``` bash npm i -g vuepress ``` -cd到项目目录,然后开始构建。构建好的静态文档会出现在"./src/.vuepress/dist"中 +cd 到项目目录,然后开始构建。构建好的静态文档会出现在 "./src/.vuepress/dist" 中 ```bash vuepress build ./src @@ -33,7 +33,7 @@ vuepress build ./src ### 文档撰写 -vuepress会启动一个本地服务器,并在浏览器对你保存的文档进行实时热更新。 +vuepress 会启动一个本地服务器,并在浏览器对你保存的文档进行实时热更新。 ```bash vuepress dev ./src diff --git a/src/PREFACE.md b/src/PREFACE.md index 24a75b3..5328a4e 100644 --- a/src/PREFACE.md +++ b/src/PREFACE.md @@ -1 +1 @@ -# Rust 程序设计语言(第二版) 简体中文版 +# Rust 程序设计语言(第二版 & 2018 edition)简体中文版 diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 90bd032..0c0e228 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -115,6 +115,7 @@ - [高级 trait](ch19-03-advanced-traits.md) - [高级类型](ch19-04-advanced-types.md) - [高级函数与闭包](ch19-05-advanced-functions-and-closures.md) + - [宏](ch19-06-macros.md) - [最后的项目: 构建多线程 web server](ch20-00-final-project-a-web-server.md) - [单线程 web server](ch20-01-single-threaded.md) @@ -125,7 +126,7 @@ - [A - 关键字](appendix-01-keywords.md) - [B - 运算符与符号](appendix-02-operators.md) - [C - 可派生的 trait](appendix-03-derivable-traits.md) - - [D - 宏](appendix-04-macros.md) - - [E - 本书翻译](appendix-05-translation.md) - - [F - 最新功能](appendix-06-newest-features.md) + - [D - 实用开发工具](appendix-04-useful-development-tools.md) + - [E - 版本](appendix-05-editions.md) + - [F - 本书译本](appendix-06-translation.md) - [G - Rust 是如何开发的与 “Nightly Rust”](appendix-07-nightly-rust.md) diff --git a/src/appendix-00.md b/src/appendix-00.md index 6c9254a..2e8f317 100644 --- a/src/appendix-00.md +++ b/src/appendix-00.md @@ -1,7 +1,7 @@ # 附录 -> [appendix-00.md](https://github.com/rust-lang/book/blob/master/second-edition/src/appendix-00.md) +> [appendix-00.md](https://github.com/rust-lang/book/blob/master/src/appendix-00.md) >
-> commit 4f2dc564851dc04b271a2260c834643dfd86c724 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -附录部分包含一些在你的Rust之旅中可能用到的参考资料。 \ No newline at end of file +附录部分包含一些在你的 Rust 之旅中可能用到的参考资料。 \ No newline at end of file diff --git a/src/appendix-01-keywords.md b/src/appendix-01-keywords.md index af4427b..e2c4972 100644 --- a/src/appendix-01-keywords.md +++ b/src/appendix-01-keywords.md @@ -1,81 +1,109 @@ -## 附录A - 关键字 +## 附录 A: 关键字 -> [appendix-01-keywords.md](https://github.com/rust-lang/book/blob/master/second-edition/src/appendix-01-keywords.md) +> [appendix-01-keywords.md](https://raw.githubusercontent.com/rust-lang/book/master/src/appendix-01-keywords.md) >
-> commit 32215c1d96c9046c0b553a05fa5ec3ede2e125c3 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -下面的列表中是Rust正在使用或者以后会用关键字。因此,这些关键字不能被用作标识符,例如 -函数、变量、参数、结构体、模块、crate、常量、宏、静态值、属性、类型、trait 或生命周期 +下面的列表包含 Rust 中正在使用或者以后会用到的关键字。因此,这些关键字不能被用作标识符(除了 [原始标识符][raw-identifiers]),这包括函数、变量、参数、结构体字段、模块、crate、常量、宏、静态值、属性、类型、trait 或生命周期 的名字。 ### 目前正在使用的关键字 -* `as` - 强制类型转换或者对使用`use`和`extern crate`声明引入的项目重命名 +如下关键字目前有对应其描述的功能。 + +* `as` - 强制类型转换,消除特定包含项的 trait 的歧义,或者对 `use` 和 `extern crate` 语句中的项重命名 * `break` - 立刻退出循环 -* `const` - 定义常量或者 **不变原生指针** (*constant raw pointers*) -* `continue` - 跳出本次循环,进入下一次循环 -* `crate` - 引入一个外部 **crate** 或一个代表 **crate** 的宏变量 -* `else` - 创建 `if` 和 `if let` 控制流的分支 +* `const` - 定义常量或不变裸指针(constant raw pointer) +* `continue` - 继续进入下一次循环迭代 +* `crate` - 链接(link)一个外部 **crate** 或一个代表宏定义的 **crate** 的宏变量 +* `dyn` - 动态分发 trait 对象 +* `else` - 作为 `if` 和 `if let` 控制流结构的 fallback * `enum` - 定义一个枚举 -* `extern` - 引入一个外部 **crate** 、函数或变量 -* `false` - 布尔值 `false` +* `extern` - 链接一个外部 **crate** 、函数或变量 +* `false` - 布尔字面值 `false` * `fn` - 定义一个函数或 **函数指针类型** (*function pointer type*) -* `for` - 遍历一个迭代器或实现一个 **trait**或者指定一个具体的生命周期 +* `for` - 遍历一个迭代器或实现一个 trait 或者指定一个更高级的生命周期 * `if` - 基于条件表达式的结果分支 -* `impl` - 实现一个方法或 **trait** 功能 -* `in` - for循环语法的一部分 +* `impl` - 实现自有或 trait 功能 +* `in` - `for` 循环语法的一部分 * `let` - 绑定一个变量 * `loop` - 无条件循环 * `match` - 模式匹配 * `mod` - 定义一个模块 -* `move` - 使闭包获取所有权 -* `mut` - 表示一个可变绑定 -* `pub` - 在结构体、`impl`块或模块中表示可以被外部使用 -* `ref` - 绑定一个引用 +* `move` - 使闭包获取其所捕获项的所有权 +* `mut` - 表示引用、裸指针或模式绑定的可变性性 +* `pub` - 表示结构体字段、`impl` 块或模块的公有可见性 +* `ref` - 通过引用绑定 * `return` - 从函数中返回 -* `Self` - 实现一个 **trait** 类型的类型别名 +* `Self` - 实现 trait 的类型的类型别名 * `self` - 表示方法本身或当前模块 * `static` - 表示全局变量或在整个程序执行期间保持其生命周期 * `struct` - 定义一个结构体 * `super` - 表示当前模块的父模块 -* `trait` - 定义一个 **trait** -* `true` - 布尔值 `true` -* `type` - 定义一个类型别名或相关联的类型 -* `unsafe` - 表示不安全的代码、函数、**traits** 或者方法实现 +* `trait` - 定义一个 trait +* `true` - 布尔字面值 `true` +* `type` - 定义一个类型别名或关联类型 +* `unsafe` - 表示不安全的代码、函数、trait 或实现 * `use` - 引入外部空间的符号 -* `where` - 表示一个类型约束 [\[For example\]][ch13-01] +* `where` - 表示一个约束类型的从句 * `while` - 基于一个表达式的结果判断是否进行循环 -[ch13-01]: ch13-01-closures.html#使用带有泛型和-fn-trait-的闭包 - - - +### 保留做将来使用的关键字 -### 未使用的保留字 - -这些关键字没有目前任何功能,但是它们是 Rust 未来会使用的保留字。 +如下关键字没有任何功能,不过由 Rust 保留以备将来的应用。 * `abstract` -* `alignof` +* `async` * `become` * `box` * `do` * `final` * `macro` -* `offsetof` * `override` * `priv` -* `proc` -* `pure` -* `sizeof` +* `try` * `typeof` * `unsized` * `virtual` * `yield` + +### 原始标识符 +[raw-identifiers]: #raw-identifiers + +原始标识符(Raw identifiers)允许你使用通常不能使用的关键字,其带有 `r#` 前缀。 + +例如,`match` 是关键字。如果尝试编译这个函数: + +```rust,ignore +fn match(needle: &str, haystack: &str) -> bool { + haystack.contains(needle) +} +``` + +会得到这个错误: + +```text +error: expected identifier, found keyword `match` + --> src/main.rs:4:4 + | +4 | fn match(needle: &str, haystack: &str) -> bool { + | ^^^^^ expected identifier, found keyword +``` + +可以通过原始标识符编写: + +```rust +fn r#match(needle: &str, haystack: &str) -> bool { + haystack.contains(needle) +} + +fn main() { + assert!(r#match("foo", "foobar")); +} +``` + +注意 `r#` 前缀需同时用于函数名和调用。 + +#### 动机 + +出于一些原因这个功能是实用的,不过其主要动机是解决跨版本问题。比如,`try` 在 2015 edition 中不是关键字,而在 2018 edition 则是。所以如果如果用 2015 edition 编写的库中带有 `try` 函数,在 2018 edition 中调用时就需要使用原始标识符。 \ No newline at end of file diff --git a/src/appendix-02-operators.md b/src/appendix-02-operators.md index 0d5d901..9304b45 100644 --- a/src/appendix-02-operators.md +++ b/src/appendix-02-operators.md @@ -1,21 +1,20 @@ -## 附录B - 运算符与符号 -> [appendix-02-operators.md](https://github.com/rust-lang/book/blob/master/second-edition/src/appendix-02-operators.md) ->
-> commit d50521fc08e51892cdf1edf5e35f3847a42f9432 +## 附录 B:运算符与符号 -[commit]: https://github.com/rust-lang/book/commit/d50521fc08e51892cdf1edf5e35f3847a42f9432 +> [appendix-02-operators.md](https://github.com/rust-lang/book/blob/master/src/appendix-02-operators.md) +>
+> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -该附录包含了 Rust 语法的词汇表,包括运算符以及其他的符号,这些符号以其自身或者在路径、泛型、trait bounds、宏、属性、注释、元组以及大括号的上下文中出现。 +该附录包含了 Rust 语法的词汇表,包括运算符以及其他的符号,这些符号单独出现或出现在路径、泛型、trait bounds、宏、属性、注释、元组以及大括号上下文中。 ### 运算符 -表B-1包含了 Rust 中的运算符、运算符如何出现在上下文中的示例、简短解释以及该运算符是否可重载。如果一个运算符是可重载的,则该运算符上用于重载的相关 trait 也会列出。 +表 B-1 包含了 Rust 中的运算符、运算符如何出现在上下文中的示例、简短解释以及该运算符是否可重载。如果一个运算符是可重载的,则该运算符上用于重载的相关 trait 也会列出。 -表 B-1: 运算符 +表 B-1: 运算符 | 运算符 | 示例 | 解释 | 是否可重载 | |----------|---------|-------------|---------------| -| `!` | `ident!(...)`, `ident!{...}`, `ident![...]` | 宏扩展 | | +| `!` | `ident!(...)`, `ident!{...}`, `ident![...]` | 宏展开 | | | `!` | `!expr` | 按位非或逻辑非 | `Not` | | `!=` | `var != expr` | 不等比较 | `PartialEq` | | `%` | `expr % expr` | 算术取模 | `Rem` | @@ -28,7 +27,7 @@ | `*` | `expr * expr` | 算术乘法 | `Mul` | | `*=` | `var *= expr` | 算术乘法与赋值 | `MulAssign` | | `*` | `*expr` | 解引用 | | -| `*` | `*const type`, `*mut type` | 原生指针 | | +| `*` | `*const type`, `*mut type` | 裸指针 | | | `+` | `trait + trait`, `'a + trait` | 复合类型限制 | | | `+` | `expr + expr` | 算术加法 | `Add` | | `+=` | `var += expr` | 算术加法与赋值 | `AddAssign` | @@ -75,32 +74,34 @@ 表 B-2 展示了以其自身出现以及出现在合法其他各个地方的符号。 -表 B-2:独立语法 +表 B-2:独立语法 | 符号 | 解释 | |--------|-------------| | `'ident` | 命名生命周期或循环标签 | | `...u8`, `...i32`, `...f64`, `...usize`, 等 | 指定类型的数值常量 | | `"..."` | 字符串常量 | -| `r"..."`, `r#"..."#`, `r##"..."##`, etc. | 原生字符串常量, 未处理的遗漏字符 | -| `b"..."` | 字节字符串; 构造一个 `[u8]` 类型而非字符串 | -| `br"..."`, `br#"..."#`, `br##"..."##`, 等 | 原生字节字符串常量,原生字节和字节结合的字符串 | -| `'...'` | 字符常量 | -| `b'...'` | ASCII码字节常量 | -| \|...\| expr | 结束 | -| `!` | 对一个离散函数来说最后总是空类型 | -| `_` | “忽略”模式绑定, 也用于整数常量的可读性 | +| `r"..."`, `r#"..."#`, `r##"..."##`, etc. | 原始字符串字面值, 未处理的转义字符 | +| `b"..."` | 字节字符串字面值; 构造一个 `[u8]` 类型而非字符串 | +| `br"..."`, `br#"..."#`, `br##"..."##`, 等 | 原始字节字符串字面值,原始和字节字符串字面值的结合 | +| `'...'` | 字符字面值 | +| `b'...'` | ASCII 码字节字面值 | +| \|...\| expr | 闭包 | +| `!` | 离散函数的总是为空的类型 | +| `_` | “忽略” 模式绑定;也用于增强整型字面值的可读性 | + +表 B-3 展示了出现在从模块结构到项的路径上下文中的符号 -表 B-3: 路径相关语法 +表 B-3:路径相关语法 | 符号 | 解释 | |--------|-------------| | `ident::ident` | 命名空间路径 | -| `::path` | 与crate根相关的路径(如一个明确的绝对路径) | -| `self::path` | 当前模块相关路径(如一个明确相关路径)| -| `super::path` | 父模块相关路径 | -| `type::ident`, `::ident` | 相关常量、函数以及类型 | -| `::...` | 不可以被直接命名的相关项类型(如 `<&T>::...`,`<[T]>::...`, 等) | +| `::path` | 与 crate 根相对的路径(如一个显式绝对路径) | +| `self::path` | 与当前模块相对的路径(如一个显式相对路径)| +| `super::path` | 与父模块相对的路径 | +| `type::ident`, `::ident` | 关联常量、函数以及类型 | +| `::...` | 不可以被直接命名的关联项类型(如 `<&T>::...`,`<[T]>::...`, 等) | | `trait::method(...)` | 通过命名定义的 trait 来消除方法调用的二义性 | | `type::method(...)` | 通过命名定义的类型来消除方法调用的二义性 | | `::method(...)` | 通过命名 trait 和类型来消除方法调用的二义性 | @@ -108,12 +109,12 @@ 表 B-4 展示了出现在泛型类型参数上下文中的符号。 -表 B-4:泛型 +表 B-4:泛型 | 符号 | 解释 | |--------|-------------| | `path<...>` | 为一个类型中的泛型指定具体参数(如 `Vec`) | -| `path::<...>`, `method::<...>` | 为一个泛型、函数或表达式中的方法指定具体参数,通常指 [turbofish][turbofish] (如 `"42".parse::()`)| +| `path::<...>`, `method::<...>` | 为一个泛型、函数或表达式中的方法指定具体参数,通常指 turbofish(如 `"42".parse::()`)| | `fn ident<...> ...` | 泛型函数定义 | | `struct ident<...> ...` | 泛型结构体定义 | | `enum ident<...> ...` | 泛型枚举定义 | @@ -121,11 +122,9 @@ | `for<...> type` | 高级生命周期限制 | | `type` | 泛型,其一个或多个相关类型必须被指定为特定类型(如 `Iterator`)| -[turbofish]: https://matematikaadit.github.io/posts/rust-turbofish.html - 表 B-5 展示了出现在使用 trait bounds 约束泛型参数上下文中的符号。 -表 B-5: Trait Bound 约束 +表 B-5: Trait Bound 约束 | 符号 | 解释 | |--------|-------------| @@ -138,7 +137,7 @@ 表 B-6 展示了在调用或定义宏以及在其上指定属性时的上下文中出现的符号。 -表 B-6: 宏与属性 +表 B-6: 宏与属性 | 符号 | 解释 | |--------|-------------| @@ -150,7 +149,7 @@ 表 B-7 展示了写注释的符号。 -表 B-7: 注释 +表 B-7: 注释 | 符号 | 注释 | |--------|-------------| @@ -163,28 +162,32 @@ 表 B-8 展示了出现在使用元组时上下文中的符号。 +表 B-8: 元组 + | 符号 | 解释 | |--------|-------------| -| `()` | 空元祖(亦称单元), 用于常量或类型中 | +| `()` | 空元组(亦称单元),即是字面值也是类型 | | `(expr)` | 括号表达式 | | `(expr,)` | 单一元素元组表达式 | | `(type,)` | 单一元素元组类型 | | `(expr, ...)` | 元组表达式 | | `(type, ...)` | 元组类型 | -| `expr(expr, ...)` | 函数调用表达式; 也用于初始化元组结构体 `struct` 以及元组枚举 `enum` 变体 | +| `expr(expr, ...)` | 函数调用表达式;也用于初始化元组结构体 `struct` 以及元组枚举 `enum` 变体 | | `ident!(...)`, `ident!{...}`, `ident![...]` | 宏调用 | | `expr.0`, `expr.1`, etc. | 元组索引 | -表 B-9 使用大括号的符号。 +表 B-9 展示了使用大括号的上下文。 + +表 B-9: 大括号 | 符号 | 解释 | |---------|-------------| | `{...}` | 块表达式 | -| `Type {...}` | `struct` | +| `Type {...}` | `struct` 字面值 | -表 B-10 展示了使用方括号的符号。 +表 B-10 展示了使用方括号的上下文。 -表 B-10: 方括号 +表 B-10: 方括号 | 符号 | 解释 | |---------|-------------| @@ -192,4 +195,4 @@ | `[expr; len]` | 复制了 `len`个 `expr`的数组 | | `[type; len]` | 包含 `len`个 `type` 类型的数组| | `expr[expr]` | 集合索引。 重载(`Index`, `IndexMut`) | -| `expr[..]`, `expr[a..]`, `expr[..b]`, `expr[a..b]` | 集合索引,使用 `Range`,`RangeFrom`,`RangeTo` 或 `RangeFull` 作为索引来代替集合切片 | +| `expr[..]`, `expr[a..]`, `expr[..b]`, `expr[a..b]` | 集合索引,使用 `Range`,`RangeFrom`,`RangeTo` 或 `RangeFull` 作为索引来代替集合 slice | diff --git a/src/appendix-03-derivable-traits.md b/src/appendix-03-derivable-traits.md index 2a86f8f..6411766 100644 --- a/src/appendix-03-derivable-traits.md +++ b/src/appendix-03-derivable-traits.md @@ -1,15 +1,12 @@ -## 附录C - 可派生的 trait +## 附录 C:可派生的 trait -> [appendix-03-derivable-traits.md][appendix-03] +> [appendix-03-derivable-traits.md](https://github.com/rust-lang/book/blob/master/src/appendix-03-derivable-traits.md) >
-> commit 32215c1d96c9046c0b553a05fa5ec3ede2e125c3 +> commit a86c1d315789b3ca13b20d50ad5005c62bdd9e37 -[appendix-03]: https://github.com/rust-lang/book/blob/master/second-edition/src/appendix-03-derivable-traits.md -[commit]: https://github.com/rust-lang/book/commit/32215c1d96c9046c0b553a05fa5ec3ede2e125c3 +在本书的各个部分中,我们讨论了可应用于结构体和枚举定义的 `derive` 属性。`derive` 属性会在使用 `derive` 语法标记的类型上生成对应 trait 的默认实现的代码。 -在本书的各个部分中,我们讨论了可应用于结构体和枚举的 `derive` 属性。`derive` 属性生成的代码在使用 `derive` 语法注释的类型之上实现了带有默认实现的 trait 。 - -在该附录中,我们提供标准库中所有可以使用 `derive` 的 trait 的参考。每部分都包含: +在本附录中提供了标准库中所有所有可以使用 `derive` 的 trait 的参考。这些部分涉及到: * 该 trait 将会派生什么样的操作符和方法 * 由 `derive` 提供什么样的 trait 实现 @@ -17,31 +14,31 @@ * 是否允许实现该 trait 的条件 * 需要 trait 操作的例子 -与 `derive` 属性提供的行为相比如果你需要与之不同的行为,请查阅标准库文档以获取每个 trait 的详情,来手动实现它们。 +如果你希望不同于 `derive` 属性所提供的行为,请查阅 [标准库文档](https://doc.rust-lang.org/std/index.html) 中每个 trait 的细节以了解如何手动实现它们。 -在类型上无法使用 `derive` 实现标准库的其余 trait。这些 trait 没有合理的默认行为, 因此,你可以以一种尝试完成的合理方式实现它们。 +标准库中定义的其它 trait 不能通过 `derive` 在类型上实现。这些 trait 不存在有意义的默认行为,所以由你负责以合理的方式实现它们。 -一个无法被派生的 trait 的例子是为最终用户处理格式化的 `Display` 。你应该时常考虑使用合适的方法来为最终用户显示一个类型。最终用户应该看到类型的什么部分?他们会找出相关部分吗?对他们来说,最相互关联的数据格式是什么样的?Rust 编译器没有这样的洞察力,因此,无法为你提供合适的默认行为。 +一个无法被派生的 trait 的例子是为终端用户处理格式化的 `Display` 。你应该时常考虑使用合适的方法来为终端用户显示一个类型。终端用户应该看到类型的什么部分?他们会找出相关部分吗?对他们来说最相关的数据格式是什么样的?Rust 编译器没有这样的洞察力,因此无法为你提供合适的默认行为。 -本附录所提供的可派生 trait 列表并不全面:库可以为它们自己的 trait 实现 `derive` , 让可以使用 `derive` 的 trait 列表真诚的开放。实现 `derive` 涉及使用程序化宏,这在附录D中有介绍。 +本附录所提供的可派生 trait 列表并不全面:库可以为其自己的 trait 实现 `derive`,可以使用 `derive` 的 trait 列表事实上是无限的。实现 `derive` 涉及到过程宏的应用,这在第十九章的 “宏” 有介绍。 -## 编程人员输出的 `Debug` +### 用于程序员输出的 `Debug` -`Debug` trait 在格式化字符串中使调试格式化,你可以在 `{}` 占位符里面加上 `:?` 显示它。 +`Debug` trait 用于开启格式化字符串中的调试格式,其通过在 `{}` 占位符中增加 `:?` 表明。 -`Debug` trait 允许你以调试目的来打印一个类型的实例,因此,使用类型的你以及其他的编程人员可以让程序在执行时在指定点上显示一个实例。 +`Debug` trait 允许以调试目的来打印一个类型的实例,所以使用该类型的程序员可以在程序执行的特定时间点观察其实例。 -例如,在使用 `assert_eq!` 宏时,Debug` trait 是必须的。如果等式断言失败,这个宏就把给定实例的值作为参数打印出来,因此,编程人员可以看到两个实例为什么不相等。 +例如,在使用 `assert_eq!` 宏时,`Debug` trait 是必须的。如果等式断言失败,这个宏就把给定实例的值作为参数打印出来,如此程序员可以看到两个实例为什么不相等。 ## 等值比较的 `PartialEq` 和 `Eq` -`PartialEq` trait 可以比较一个类型的实例以检查是否相等,并且可以使用 `==` 和 `!=` 操作符。 +`PartialEq` trait 可以比较一个类型的实例以检查是否相等,并开启了 `==` 和 `!=` 运算符的功能。 -派生的 `PartialEq` 实现了 `eq` 方法。当 `PartialEq` 在结构体上派生时,只有*所有*的字段都相等时两个实例才相等,同时只要有字段不相等则两个实例就不相等。当在枚举上派生时,每一个变体(variant)都和它自身相等,且和其他变体都不相等。 +派生的 `PartialEq` 实现了 `eq` 方法。当 `PartialEq` 在结构体上派生时,只有*所有* 的字段都相等时两个实例才相等,同时只要有任何字段不相等则两个实例就不相等。当在枚举上派生时,每一个成员都和其自身相等,且和其他成员都不相等。 例如,当使用 `assert_eq!` 宏时,需要比较比较一个类型的两个实例是否相等,则 `PartialEq` trait 是必须的。 -`Eq` trait 没有方法。其目的是为每一个注解类型的值作标志,其值等于其自身。 `Eq` trait 只能应用于那些实现了 `PartialEq` 的类型,但并非所有实现了 `PartialEq` 的类型可以实现 `Eq`。浮点类型就是一个例子:浮点数状态的实现,两个非数字(`NaN`,not-a-number)值是互不相等的。 +`Eq` trait 没有方法。其作用是表明每一个被标记类型的值等于其自身。`Eq` trait 只能应用于那些实现了 `PartialEq` 的类型,但并非所有实现了 `PartialEq` 的类型都可以实现 `Eq`。浮点类型就是一个例子:浮点数的实现表明两个非数字(`NaN`,not-a-number)值是互不相等的。 例如,对于一个 `HashMap` 中的 key 来说, `Eq` 是必须的,这样 `HashMap` 就可以知道两个 key 是否一样了。 @@ -49,19 +46,19 @@ `PartialOrd` trait 可以基于排序的目的而比较一个类型的实例。实现了 `PartialOrd` 的类型可以使用 `<`、 `>`、`<=` 和 `>=` 操作符。但只能在同时实现了 `PartialEq` 的类型上使用 `PartialOrd`。 -派生 `PartialOrd` 实现了 `partial_cmp` 方法,其返回一个 `Option` ,但当给定值无法产生顺序时将返回 `None`。尽管大多数类型的值都可以比较,但一个无法产生顺序的例子是:浮点类型的非数字值(`NaN`,not-a-number)。当在浮点数上调用 `partial_cmp` 时,`NaN` 的浮点数将返回 `None`。 +派生 `PartialOrd` 实现了 `partial_cmp` 方法,其返回一个 `Option` ,但当给定值无法产生顺序时将返回 `None`。尽管大多数类型的值都可以比较,但一个无法产生顺序的例子是:浮点类型的非数字值。当在浮点数上调用 `partial_cmp` 时,`NaN` 的浮点数将返回 `None`。 当在结构体上派生时,`PartialOrd` 以在结构体定义中字段出现的顺序比较每个字段的值来比较两个实例。当在枚举上派生时,认为在枚举定义中声明较早的枚举变体小于其后的变体。 例如,对于来自于 `rand` carte 中的 `gen_range` 方法来说,当在一个大值和小值指定的范围内生成一个随机值时,`PartialOrd` trait 是必须的。 -`Ord` trait 也让你明白在一个带注解类型上的任意两个值存在有效顺序。`Ord` trait 实现了 `cmp` 方法,它返回一个 `Ordering` 而不是 `Option`,因为总存在一个合法的顺序。只可以在实现了 `PartialOrd` 和 `Eq`( `Eq` 依赖 `PartialEq` )的类型上使用 `Ord` trait 。当在结构体或枚举上派生时, `cmp` 和以 `PartialOrd` 派生实现的 `partial_cmp` 表现一致。 +`Ord` trait 也让你明白在一个带注解类型上的任意两个值存在有效顺序。`Ord` trait 实现了 `cmp` 方法,它返回一个 `Ordering` 而不是 `Option`,因为总存在一个合法的顺序。只可以在实现了 `PartialOrd` 和 `Eq`(`Eq` 依赖 `PartialEq`)的类型上使用 `Ord` trait 。当在结构体或枚举上派生时, `cmp` 和以 `PartialOrd` 派生实现的 `partial_cmp` 表现一致。 -例如,当在 `BTreeSet` (一种基于有序值存储数据的数据结构)上存值时,`Ord` 是必须的。 +例如,当在 `BTreeSet`(一种基于有序值存储数据的数据结构)上存值时,`Ord` 是必须的。 ## 复制值的 `Clone` 和 `Copy` -`Clone` trait 可以明确地创建一个值的深拷贝( deep copy ),复制过程可能包含任意代码的执行以及堆上数据的复制。查阅第四章“变量和数据的交互方式:移动”以获取有关 `Clone` 的更多信息。 +`Clone` trait 可以明确地创建一个值的深拷贝(deep copy),复制过程可能包含任意代码的执行以及堆上数据的复制。查阅第四章 “变量和数据的交互方式:移动” 以获取有关 `Clone` 的更多信息。 派生 `Clone` 实现了 `clone` 方法,其为整个的类型实现时,在类型的每一部分上调用了 `clone` 方法。这意味着类型中所有字段或值也必须实现 `Clone` 来派生 `Clone` 。 diff --git a/src/appendix-04-useful-development-tools.md b/src/appendix-04-useful-development-tools.md new file mode 100644 index 0000000..9c0d95f --- /dev/null +++ b/src/appendix-04-useful-development-tools.md @@ -0,0 +1,5 @@ +## 附录 E:实用开发工具 + +> [appendix-04-useful-development-tools.md](https://github.com/rust-lang/book/blob/master/src/appendix-04-useful-development-tools.md) +>
+> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f \ No newline at end of file diff --git a/src/appendix-05-editions.md b/src/appendix-05-editions.md new file mode 100644 index 0000000..51bb588 --- /dev/null +++ b/src/appendix-05-editions.md @@ -0,0 +1,5 @@ +## 附录 E:版本 + +> [appendix-05-editions.md](https://github.com/rust-lang/book/blob/master/src/appendix-05-editions.md) +>
+> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f \ No newline at end of file diff --git a/src/appendix-06-translation.md b/src/appendix-06-translation.md new file mode 100644 index 0000000..7e00418 --- /dev/null +++ b/src/appendix-06-translation.md @@ -0,0 +1,25 @@ +## 附录 F:本书译本 + +> [appendix-06-translation.md](https://github.com/rust-lang/book/blob/master/src/appendix-06-translation.md) +>
+> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f + +一些非英语语言的资源。多数仍在翻译中;查阅 [翻译标签][label] 来帮助我们或使我们知道新的翻译! + +[label]: https://github.com/rust-lang/book/issues?q=is%3Aopen+is%3Aissue+label%3ATranslations + +- [Português](https://github.com/rust-br/rust-book-pt-br) (BR) +- [Português](https://github.com/nunojesus/rust-book-pt-pt) (PT) +- [Tiếng việt](https://github.com/hngnaig/rust-lang-book/tree/vi-VN) +- [简体中文](http://www.broadview.com.cn/article/144), [另一个版本(译者注:已基本翻译完毕)](https://github.com/KaiserY/trpl-zh-cn) +- [Українська](https://github.com/pavloslav/rust-book-uk-ua) +- [Español](https://github.com/thecodix/book) +- [Italiano](https://github.com/AgeOfWar/rust-book-it) +- [Русский](https://github.com/iDeBugger/rust-book-ru) +- [한국어](https://github.com/rinthel/rust-lang-book-ko) +- [日本語](https://github.com/hazama-yuinyan/book) +- [Français](https://github.com/quadrifoglio/rust-book-fr) +- [Polski](https://github.com/paytchoo/book-pl) +- [עברית](https://github.com/idanmel/rust-book-heb) +- [Cebuano](https://github.com/agentzero1/book) +- [Tagalog](https://github.com/josephace135/book) \ No newline at end of file diff --git a/src/appendix-07-nightly-rust.md b/src/appendix-07-nightly-rust.md index 39b5963..fc48b22 100644 --- a/src/appendix-07-nightly-rust.md +++ b/src/appendix-07-nightly-rust.md @@ -1 +1,5 @@ -## G - Rust 是如何开发的与 “Nightly Rust” +## 附录 G:Rust 是如何开发的与 “Nightly Rust” + +> [appendix-07-nightly-rust.md](https://github.com/rust-lang/book/blob/master/src/appendix-07-nightly-rust.md) +>
+> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f \ No newline at end of file diff --git a/src/ch19-04-advanced-types.md b/src/ch19-04-advanced-types.md index 756befa..7a7c3f1 100644 --- a/src/ch19-04-advanced-types.md +++ b/src/ch19-04-advanced-types.md @@ -1,20 +1,18 @@ ## 高级类型 -> [ch19-04-advanced-types.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch19-04-advanced-types.md) +> [ch19-04-advanced-types.md](https://github.com/rust-lang/book/blob/master/src/ch19-04-advanced-types.md) >
-> commit 9d5b9a573daf5fa0c98b3a3005badcea4a0a5211 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f Rust 的类型系统有一些我们曾经提到但没有讨论过的功能。首先我们从一个关于为什么 newtype 与类型一样有用的更宽泛的讨论开始。接着会转向类型别名(type aliases),一个类似于 newtype 但有着稍微不同的语义的功能。我们还会讨论 `!` 类型和动态大小类型。 -### 为了类型安全和抽象而使用 newtype 模式 +> 这一部分假设你已经阅读了之前的 “newtype 模式用于在外部类型上实现外部 trait” 部分。 -> 这一部分假设你已经阅读了 “高级 trait” 部分的 newtype 模式相关内容。 +### 为了类型安全和抽象而使用 newtype 模式 newtype 模式可以用于一些其他我们还未讨论的功能,包括静态的确保某值不被混淆,和用来表示一个值的单元。实际上示例 19-23 中已经有一个这样的例子:`Millimeters` 和 `Meters` 结构体都在 newtype 中封装了 `u32` 值。如果编写了一个有 `Millimeters` 类型参数的函数,不小心使用 `Meters` 或普通的 `u32` 值来调用该函数的程序是不能编译的。 -另一个 newtype 模式的应用在于抽象掉一些类型的实现细节:例如,封装类型可以暴露出与直接使用其内部私有类型时所不同的 API,以便限制其功能。 - - +另一个 newtype 模式的应用在于抽象掉一些类型的实现细节:例如,封装类型可以暴露出与直接使用其内部私有类型时所不同的公有 API,以便限制其功能。 newtype 也可以隐藏其内部的泛型类型。例如,可以提供一个封装了 `HashMap` 的 `People` 类型,用来储存人名以及相应的 ID。使用 `People` 的代码只需与提供的公有 API 交互即可,比如向 `People` 集合增加名字字符串的方法,这样这些代码就无需知道在内部我们将一个 `i32` ID 赋予了这个名字了。newtype 模式是一种实现第十七章 “封装隐藏了实现细节” 部分所讨论的隐藏实现细节的封装的轻量级方法。 @@ -42,19 +40,19 @@ println!("x + y = {}", x + y); 类型别名的主要用途是减少重复。例如,可能会有这样很长的类型: ```rust,ignore -Box +Box ``` 在函数签名或类型注解中每次都书写这个类型将是枯燥且易于出错的。想象一下如示例 19-32 这样全是如此代码的项目: ```rust -let f: Box = Box::new(|| println!("hi")); +let f: Box = Box::new(|| println!("hi")); -fn takes_long_type(f: Box) { +fn takes_long_type(f: Box) { // --snip-- } -fn returns_long_type() -> Box { +fn returns_long_type() -> Box { // --snip-- # Box::new(|| ()) } @@ -65,7 +63,7 @@ fn returns_long_type() -> Box { 类型别名通过减少项目中重复代码的数量来使其更加易于控制。这里我们为这个冗长的类型引入了一个叫做 `Thunk` 的别名,这样就可以如示例 19-33 所示将所有使用这个类型的地方替换为更短的 `Thunk`: ```rust -type Thunk = Box; +type Thunk = Box; let f: Thunk = Box::new(|| println!("hi")); @@ -104,7 +102,7 @@ pub trait Write { type Result = Result; ``` -因为这位于 `std::io` 中,可用的完全限定的别名是 `std::io::Result`;也就是说,`Result` 中 `E` 放入了 `std::io::Error`。`Write` trait 中的函数最终看起来像这样: +因为这位于 `std::io` 中,可用的完全限定的别名是 `std::io::Result` —— 也就是说,`Result` 中 `E` 放入了 `std::io::Error`。`Write` trait 中的函数最终看起来像这样: ```rust,ignore pub trait Write { @@ -118,7 +116,7 @@ pub trait Write { 类型别名在两个方面有帮助:易于编写 **并** 在整个 `std::io` 中提供了一致的接口。因为这是一个别名,它只是另一个 `Result`,这意味着可以在其上使用 `Result` 的任何方法,以及像 `?` 这样的特殊语法。 -### 从不返回的 `!`,never type +### 从不返回的 never type Rust 有一个叫做 `!` 的特殊类型。在类型理论术语中,它被称为 *empty type*,因为它没有值。我们更倾向于称之为 *never type*。这个名字描述了它的作用:在函数从不返回的时候充当返回值。例如: @@ -128,10 +126,9 @@ fn bar() -> ! { } ``` -这读 “函数 `bar` 从不返回”,而从不返回的函数被称为 **发散函数**(*diverging functions*)。不能创建 `!` 类型的值,所以 `bar` 也不可能返回。 - +这读 “函数 `bar` 从不返回”,而从不返回的函数被称为 **发散函数**(*diverging functions*)。不能创建 `!` 类型的值,所以 `bar` 也不可能返回值。 -不过一个不能创建值的类型有什么用呢?如果你回想一下第二章,曾经有一些看起来像这样的代码,如示例 19-34 所重现的: +不过一个不能创建值的类型有什么用呢?如果你回想一下示例 2-5 中的代码,曾经有一些看起来像这样的代码,如示例 19-34 所重现的: ```rust # let guess = "3"; @@ -148,27 +145,19 @@ let guess: u32 = match guess.trim().parse() { 当时我们忽略了代码中的一些细节。在第六章 “`match` 控制流运算符” 部分,我们学习了 `match` 的分支必须返回相同的类型。如下代码不能工作: -```rust,ignore -let guess = match guess.trim().parse() { +```rust,ignore,does_not_compile +let guess = match guess.trim().parse() { Ok(_) => 5, Err(_) => "hello", } ``` -这里的 `guess` 必须既是整型也是字符串,而 Rust 要求 `guess` 只能是一个类型。那么 `continue` 返回了什么呢?为什么示例 19-34 中会允许一个分支返回 `u32` 而另一个分支却以 `continue` 结束呢? +这里的 `guess` 必须既是整型 **也是** 字符串,而 Rust 要求 `guess` 只能是一个类型。那么 `continue` 返回了什么呢?为什么示例 19-34 中会允许一个分支返回 `u32` 而另一个分支却以 `continue` 结束呢? 正如你可能猜到的,`continue` 的值是 `!`。也就是说,当 Rust 要计算 `guess` 的类型时,它查看这两个分支。前者是 `u32` 值,而后者是 `!` 值。因为 `!` 并没有一个值,Rust 决定 `guess` 的类型是 `u32`。 描述 `!` 的行为的正式方式是 never type 可以强转为任何其他类型。允许 `match` 的分支以 `continue` 结束是因为 `continue` 并不真正返回一个值;相反它把控制权交回上层循环,所以在 `Err` 的情况,事实上并未对 `guess` 赋值。 - - - never type 的另一个用途是 `panic!`。还记得 `Option` 上的 `unwrap` 函数吗?它产生一个值或 panic。这里是它的定义: ```rust,ignore @@ -182,7 +171,7 @@ impl Option { } ``` -这里与示例 19-34 中的 `match` 发生了相同的情况:我们知道 `val` 是 `T` 类型,`panic!` 是 `!` 类型,所以整个 `match` 表达式的结果是 `T` 类型。这能工作是因为 `panic!` 并不产生一个值;它会终止程序。对于 `None` 的情况,`unwrap` 并不返回一个值,所以这些代码是有效。 +这里与示例 19-34 中的 `match` 发生了相同的情况:Rust 知道 `val` 是 `T` 类型,`panic!` 是 `!` 类型,所以整个 `match` 表达式的结果是 `T` 类型。这能工作是因为 `panic!` 并不产生一个值;它会终止程序。对于 `None` 的情况,`unwrap` 并不返回一个值,所以这些代码是有效。 最后一个有着 `!` 类型的表达式是 `loop`: @@ -202,16 +191,11 @@ loop { 让我们深入研究一个贯穿本书都在使用的动态大小类型的细节:`str`。没错,不是 `&str`,而是 `str` 本身。`str` 是一个 DST;直到运行时我们都不知道字符串有多长。因为直到运行时都不能知道大其小,也就意味着不能创建 `str` 类型的变量,也不能获取 `str` 类型的参数。考虑一下这些代码,他们不能工作: -```rust,ignore +```rust,ignore,does_not_compile let s1: str = "Hello there!"; let s2: str = "How's it going?"; ``` - - - Rust 需要知道应该为特定类型的值分配多少内存,同时所有同一类型的值必须使用相同数量的内存。如果允许编写这样的代码,也就意味着这两个 `str` 需要占用完全相同大小的空间,不过它们有着不同的长度。这也就是为什么不可能创建一个存放动态大小类型的变量的原因。 那么该怎么办呢?你已经知道了这种问题的答案:`s1` 和 `s2` 的类型是 `&str` 而不是 `str`。如果你回想第四章 “字符串 slice” 部分,slice 数据结储存了开始位置和 slice 的长度。 @@ -220,15 +204,6 @@ Rust 需要知道应该为特定类型的值分配多少内存,同时所有同 可以将 `str` 与所有类型的指针结合:比如 `Box` 或 `Rc`。事实上,之前我们已经见过了,不过是另一个动态大小类型:trait。每一个 trait 都是一个可以通过 trait 名称来引用的动态大小类型。在第十七章 “为使用不同类型的值而设计的 trait 对象” 部分,我们提到了为了将 trait 用于 trait 对象,必须将他们放入指针之后,比如 `&Trait` 或 `Box`(`Rc` 也可以)。trait 之所以是动态大小类型的是因为只有这样才能使用它。 -#### `Sized` trait - - - - - - 为了处理 DST,Rust 有一个特定的 trait 来决定一个类型的大小是否在编译时可知:这就是 `Sized` trait。这个 trait 自动为编译器在编译时就知道大小的类型实现。另外,Rust 隐式的为每一个泛型函数增加了 `Sized` bound。也就是说,对于如下泛型函数定义: ```rust,ignore diff --git a/src/ch19-05-advanced-functions-and-closures.md b/src/ch19-05-advanced-functions-and-closures.md index 74c4fdc..3929721 100644 --- a/src/ch19-05-advanced-functions-and-closures.md +++ b/src/ch19-05-advanced-functions-and-closures.md @@ -1,14 +1,14 @@ ## 高级函数与闭包 -> [ch19-05-advanced-functions-and-closures.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch19-05-advanced-functions-and-closures.md) +> [ch19-05-advanced-functions-and-closures.md](https://github.com/rust-lang/book/blob/master/src/ch19-05-advanced-functions-and-closures.md) >
-> commit 509cb42ece610bdac8eaad26d57fb604dc078623 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 最后我们将探索一些有关函数和闭包的高级功能:函数指针以及返回值闭包。 ### 函数指针 -我们讨论过了如何向函数传递闭包;也可以向函数传递常规函数!这在我们希望传递已经定义的函数而不是重新定义闭包作为参数是很有用。通过函数指针允许我们使用函数作为另一个函数的参数。函数的类型是 `fn` (使用小写的 “f” )以免与 `Fn` 闭包 trait 相混淆。`fn` 被称为**函数指针**(*function pointer*)。指定参数为函数指针的语法类似于闭包,如示例 19-34 所示: +我们讨论过了如何向函数传递闭包;也可以向函数传递常规函数!这在我们希望传递已经定义的函数而不是重新定义闭包作为参数是很有用。通过函数指针允许我们使用函数作为另一个函数的参数。函数的类型是 `fn` (使用小写的 “f” )以免与 `Fn` 闭包 trait 相混淆。`fn` 被称为 **函数指针**(*function pointer*)。指定参数为函数指针的语法类似于闭包,如示例 19-35 所示: 文件名: src/main.rs @@ -28,7 +28,7 @@ fn main() { } ``` -示例 19-35: 使用 `fn` 类型接受函数指针作为参数 +示例 19-34: 使用 `fn` 类型接受函数指针作为参数 这会打印出 `The answer is: 12`。`do_twice` 中的 `f` 被指定为一个接受一个 `i32` 参数并返回 `i32` 的 `fn`。接着就可以在 `do_twice` 函数体中调用 `f`。在 `main` 中,可以将函数名 `add_one` 作为第一个参数传递给 `do_twice`。 @@ -36,7 +36,7 @@ fn main() { 函数指针实现了所有三个闭包 trait(`Fn`、`FnMut` 和 `FnOnce`),所以总是可以在调用期望闭包的函数时传递函数指针作为参数。倾向于编写使用泛型和闭包 trait 的函数,这样它就能接受函数或闭包作为参数。 -一个只期望接受 `fn` 而不接受闭包的情况的例子是与不存在闭包的外部代码交互时:C 语言的函数可以接受函数作为参数,但没有闭包。 +一个只期望接受 `fn` 而不接受闭包的情况的例子是与不存在闭包的外部代码交互时:C 语言的函数可以接受函数作为参数,但 C 语言没有闭包。 作为一个既可以使用内联定义的闭包又可以使用命名函数的例子,让我们看看一个 `map` 的应用。使用 `map` 函数将一个数字 vector 转换为一个字符串 vector,就可以使用闭包,比如这样: @@ -60,6 +60,20 @@ let list_of_strings: Vec = list_of_numbers 注意这里必须使用 “高级 trait” 部分讲到的完全限定语法,因为存在多个叫做 `to_string` 的函数;这里使用了定义于 `ToString` trait 的 `to_string` 函数,标准库为所有实现了 `Display` 的类型实现了这个 trait。 +另一个实用的模式暴露了元组结构体和元组结构体枚举成员的实现细节。这些项使用 `()` 作为初始化语法,这看起来就像函数调用,同时它们确实被实现为返回由参数构造的实例的函数。它们也被称为实现了闭包 trait 的函数指针,并可以采用类似如下的方式调用: + +```rust +enum Status { + Value(u32), + Stop, +} + +let list_of_statuses: Vec = + (0u32..20) + .map(Status::Value) + .collect(); +``` + 一些人倾向于函数风格,一些人喜欢闭包。这两种形式最终都会产生同样的代码,所以请使用对你来说更明白的形式吧。 ### 返回闭包 @@ -68,7 +82,7 @@ let list_of_strings: Vec = list_of_numbers 这段代码尝试直接返回闭包,它并不能编译: -```rust,ignore +```rust,ignore,does_not_compile fn returns_closure() -> Fn(i32) -> i32 { |x| x + 1 } @@ -93,15 +107,11 @@ std::marker::Sized` is not satisfied 错误又一次指向了 `Sized` trait!Rust 并不知道需要多少空间来储存闭包。不过我们在上一部分见过这种情况的解决办法:可以使用 trait 对象: ```rust -fn returns_closure() -> Box i32> { +fn returns_closure() -> Box i32> { Box::new(|x| x + 1) } ``` 这段代码正好可以编译。关于 trait 对象的更多内容,请回顾第十七章的 “为使用不同类型的值而设计的 trait 对象” 部分。 -## 总结 - -好的!现在我们学习了 Rust 并不常用但在特定情况下你可能用得着的功能。我们介绍了很多复杂的主题,这样若你在错误信息提示或阅读他人代码时遇到他们,至少可以说之前已经见过这些概念和语法了。你可以使用本章作为一个解决方案的参考。 - -接下来,我们将再开始一个项目,将本书所学的所有内容付与实践! \ No newline at end of file +接下来让我们学习宏! diff --git a/src/ch19-06-macros.md b/src/ch19-06-macros.md new file mode 100644 index 0000000..2ad91ce --- /dev/null +++ b/src/ch19-06-macros.md @@ -0,0 +1,362 @@ +## 宏 + +> [ch19-06-macros.md](https://github.com/rust-lang/book/blob/master/src/ch19-06-macros.md) +>
+> commit 6babe749f8d97131b97c3cb9e7ca76e6115b90c1 + +我们已经在本书中使用过像 `println!` 这样的宏了,不过还没完全探索什么是宏以及它是如何工作的。**宏**(*Macro*)指的是 Rust 中一系列的功能: + +* **声明**(*Declarative*)宏,使用 `macro_rules!` +* **过程**(*Procedural*),其有三种类型: + * 自定义 `#[derive]` 宏 + * 类属性(Attribute)宏 + * 类函数宏 + +我们会依次讨论每一种宏,不过首要的是,为什么已经有了函数还需要宏呢? + +### 宏和函数的区别 + +从根本上来说,宏是一种为写其他代码而写代码的方式,即所谓的 **元编程**(*metaprogramming*)。在附录 C 中会探讨 `derive` 属性,其生成各种 trait 的实现。我们也在本书中使用过 `println!` 宏和 `vec!` 宏。所有的这些宏 **展开** 来以生成比你手写更多的代码。 + +元编程对于减少大量编写和维护的代码是非常有用的,它也扮演了函数的角色。但宏有一些函数所没有的附加能力。 + +一个函数标签必须声明函数参数个数和类型。相比之下,宏只接受一个可变参数:用一个参数调用 `println!("hello")` 或用两个参数调用 `println!("hello {}", name)` 。而且,宏可以在编译器翻译代码前展开,例如,宏可以在一个给定类型上实现 trait 。因为函数是在运行时被调用,同时 trait 需要在运行时实现,所以函数无法像宏这样。 + +实现一个宏而不是函数的消极面是宏定义要比函数定义更复杂,因为你正在编写生成 Rust 代码的 Rust 代码。由于这样的间接性,宏定义通常要比函数定义更难阅读、理解以及维护。 + +宏和函数的最后一个重要的区别是:在调用宏 **之前** 必须定义并将其引入作用域,而函数则可以在任何地方定义和调用。 + +### 使用 `macro_rules!` 的声明宏用于通用元编程 + +Rust 最常用的宏形式是 **声明宏**(*declarative macros*)。它们有时也被称为 “macros by example”、“`macro_rules!` 宏” 或者就是 “macros”。其核心概念是,声明宏允许我们编写一些类似 Rust `match` 表达式的代码。正如在第六章讨论的那样,`match` 表达式是控制结构,其接收一个表达式,与表达式的结果进行模式匹配,然后根据模式匹配执行相关代码。宏也将一个值和包含相关代码的模式进行比较;此种情况下,该值是传递给宏的 Rust 源代码字面值,模式用于和传递给宏的源代码进行比较,同时每个模式的相关代码则用于替换传递给宏的代码。所有这一切都发生于编译时。 + +可以使用 `macro_rules!` 来定义宏。让我们通过查看 `vec!` 宏定义来探索如何使用 `macro_rules!` 结构。第八章讲述了如何使用 `vec!` 宏来生成一个给定值的 vector。例如,下面的宏用三个整数创建一个 vector `: + +```rust +let v: Vec = vec![1, 2, 3]; +``` + +也可以使用 `vec!` 宏来构造两个整数的 vector 或五个字符串 slice 的 vector 。但却无法使用函数做相同的事情,因为我们无法预先知道参数值的数量和类型。 + +在示例 19-36 中展示了一个 `vec!` 稍微简化的定义。 + +文件名: src/lib.rs + +```rust +#[macro_export] +macro_rules! vec { + ( $( $x:expr ),* ) => { + { + let mut temp_vec = Vec::new(); + $( + temp_vec.push($x); + )* + temp_vec + } + }; +} +``` + +示例 19-36: 一个 `vec!` 宏定义的简化版本 + +> 注意:标准库中实际定义的 `vec!` 包括预分配适当量的内存的代码。这部分为代码优化,为了让示例简化,此处并没有包含在内。 + + +无论何时导入定义了宏的包,`#[macro_export]` 注解说明宏应该是可用的。 如果没有该注解,这个宏不能被引入作用域。 + +接着使用 `macro_rules!` 和宏名称开始宏定义,且所定义的宏并 *不带* 感叹号。名字后跟大括号表示宏定义体,在该例中宏名称是 `vec` 。 + +`vec!` 宏的结构和 `match` 表达式的结构类似。此处有一个单边模式 `( $( $x:expr ),* )` ,后跟 `=>` 以及和模式相关的代码块。如果模式匹配,该相关代码块将被执行。假设这只是这个宏中的模式,且只有一个有效匹配,其他任何匹配都是错误的。更复杂的宏会有多个单边模式。 + +宏定义中有效模式语法和在第十八章提及的模式语法是不同的,因为宏模式所匹配的是 Rust 代码结构而不是值。回过头来检查下示例 D-1 中模式片段什么意思。对于全部的宏模式语法,请查阅[参考]。 + +[参考]: https://doc.rust-lang.org/reference/macros.html + +首先,一对括号包含了全部模式。接下来是后跟一对括号的美元符号( `$` ),其通过替代代码捕获了符合括号内模式的值。`$()` 内则是 `$x:expr` ,其匹配 Rust 的任意表达式或给定 `$x` 名字的表达式。 + +`$()` 之后的逗号说明一个逗号分隔符可以有选择的出现代码之后,这段代码与在 `$()` 中所捕获的代码相匹配。紧随逗号之后的 `*` 说明该模式匹配零个或多个 `*` 之前的任何模式。 + +当以 `vec![1, 2, 3];` 调用宏时,`$x` 模式与三个表达式 `1`、`2` 和 `3` 进行了三次匹配。 + +现在让我们来看看这个出现在与此单边模式相关的代码块中的模式:在 `$()*` 部分中所生成的 `temp_vec.push()` 为在匹配到模式中的 `$()` 每一部分而生成。`$x` 由每个与之相匹配的表达式所替换。当以 `vec![1, 2, 3];` 调用该宏时,替换该宏调用所生成的代码会是下面这样: + +```rust,ignore +let mut temp_vec = Vec::new(); +temp_vec.push(1); +temp_vec.push(2); +temp_vec.push(3); +temp_vec +``` + +我们已经定义了一个宏,其可以接收任意数量和类型的参数,同时可以生成能够创建包含指定元素的 vector 的代码。 + +`macro_rules!` 中有一些奇怪的地方。在将来,会有第二种采用 `macro` 关键字的声明宏,其工作方式类似但修复了这些极端情况。在此之后,`macro_rules!` 实际上就过时(deprecated)了。在此基础之上,同时鉴于大多数 Rust 程序员 **使用** 宏而非 **编写** 宏的事实,此处不再深入探讨 `macro_rules!`。请查阅在线文档或其他资源,如 [“The Little Book of Rust Macros”][tlborm] 来更多地了解如何写宏。 + +[tlborm]: https://danielkeep.github.io/tlborm/book/index.html + +### 用于从属性生成代码的过程宏 + +第二种形式的宏被称为 **过程宏**(*procedural macros*),因为它们更像函数(一种过程类型)。过程宏接收 Rust 代码作为输入,在这些代码上进行操作,然后产生另一些代码作为输出,而非像声明式宏那样匹配对应模式然后以另一部分代码替换当前代码。 + +有三种类型的过程宏,不过它们的工作方式都类似。其一,其定义必须位于一种特殊类型的属于它们自己的 crate 中。这么做出于复杂的技术原因,将来我们希望能够消除这些限制。 + +其二,使用这些宏需采用类似示例 19-37 所示的代码形式,其中 `some_attribute` 是一个使用特定宏的占位符。 + +文件名: src/lib.rs + +```rust,ignore +use proc_macro; + +#[some_attribute] +pub fn some_name(input: TokenStream) -> TokenStream { +} +``` + +示例 19-37: 一个使用过程宏的例子 + +过程宏包含一个函数,这也是其得名的原因:“过程” 是 “函数” 的同义词。那么为何不叫 “函数宏” 呢?好吧,有一个过程宏是 “类函数” 的,叫成函数会产生混乱。无论如何,定义过程宏的函数接受一个 `TokenStream` 作为输入并产生一个 `TokenStream` 作为输出。这也就是宏的核心:宏所处理的源代码组成了输入 `TokenStream`,同时宏生成的代码是输出 `TokenStream`。最后,函数上有一个属性;这个属性表明过程宏的类型。在同一 crate 中可以有多种的过程宏。 + +考虑到这些宏是如此类似,我们会从自定义派生宏开始。接着会解释与其他形式宏的微小区别。 + +### 如何编写自定义 `derive` 宏 + +让我们创建一个 `hello_macro` crate,其包含名为 `HelloMacro` 的 trait 和关联函数 `hello_macro`。不同于让 crate 的用户为其每一个类型实现 `HelloMacro` trait,我们将会提供一个过程式宏以便用户可以使用 `#[derive(HelloMacro)]` 注解他们的类型来得到 `hello_macro` 函数的默认实现。该默认实现会打印 `Hello, Macro! My name is TypeName!`,其中 `TypeName` 为定义了 trait 的类型名。换言之,我们会创建一个 crate,使程序员能够写类似示例 19-38 中的代码。 + +文件名: src/main.rs + +```rust,ignore +use hello_macro::HelloMacro; +use hello_macro_derive::HelloMacro; + +#[derive(HelloMacro)] +struct Pancakes; + +fn main() { + Pancakes::hello_macro(); +} +``` + +示例 19-38: crate 用户所写的能够使用过程式宏的代码 + +运行该代码将会打印 `Hello, Macro! My name is Pancakes!` 第一步是像下面这样新建一个库 crate: + +```text +$ cargo new hello_macro --lib +``` + +接下来,会定义 `HelloMacro` trait 以及其关联函数: + +文件名: src/lib.rs + +```rust +pub trait HelloMacro { + fn hello_macro(); +} +``` + +现在有了一个包含函数的 trait 。此时,crate 用户可以实现该 trait 以达到其期望的功能,像这样: + +```rust,ignore +use hello_macro::HelloMacro; + +struct Pancakes; + +impl HelloMacro for Pancakes { + fn hello_macro() { + println!("Hello, Macro! My name is Pancakes!"); + } +} + +fn main() { + Pancakes::hello_macro(); +} +``` + +然而,他们需要为每一个他们想使用 `hello_macro` 的类型编写实现的代码块。我们希望为其节约这些工作。 + +另外,我们也无法为 `hello_macro` 函数提供一个能够打印实现了该 trait 的类型的名字的默认实现:Rust 没有反射的能力,因此其无法在运行时获取类型名。我们需要一个在运行时生成代码的宏。 + +下一步是定义过程式宏。在编写本部分时,过程式宏必须在其自己的 crate 内。该限制最终可能被取消。构造 crate 和其中宏的惯例如下:对于一个 `foo` 的包来说,一个自定义的派生过程宏的包被称为 `foo_derive` 。在 `hello_macro` 项目中新建名为 `hello_macro_derive` 的包。 + +```text +$ cargo new hello_macro_derive --lib +``` + +由于两个 crate 紧密相关,因此在 `hello_macro` 包的目录下创建过程式宏的 crate。如果改变在 `hello_macro` 中定义的 trait ,同时也必须改变在 `hello_macro_derive` 中实现的过程式宏。这两个包需要分别发布,编程人员如果使用这些包,则需要同时添加这两个依赖并将其引入作用域。我们也可以只用 `hello_macro` 包而将 `hello_macro_derive` 作为一个依赖,并重新导出过程式宏的代码。但我们组织项目的方式使编程人员使用 `hello_macro` 成为可能,即使他们无需 `derive` 的功能。 + +需要将 `hello_macro_derive` 声明为一个过程宏的 crate。同时也需要 `syn` 和 `quote` crate 中的功能,正如注释中所说,需要将其加到依赖中。为 `hello_macro_derive` 将下面的代码加入到 *Cargo.toml* 文件中。 + +文件名: hello_macro_derive/Cargo.toml + +```toml +[lib] +proc-macro = true + +[dependencies] +syn = "0.14.4" +quote = "0.6.3" +``` + +为定义一个过程式宏,请将示例 19-39 中的代码放在 `hello_macro_derive` crate 的 *src/lib.rs* 文件里面。注意这段代码在我们添加 `impl_hello_macro` 函数的定义之前是无法编译的。 + +文件名: hello_macro_derive/src/lib.rs + + + +> 在 Rust 1.31.0 时,`extern crate` 仍是必须的,请查看
+> https://github.com/rust-lang/rust/issues/54418
+> https://github.com/rust-lang/rust/pull/54658
+> https://github.com/rust-lang/rust/issues/55599 + +```rust,ignore +extern crate proc_macro; + +use crate::proc_macro::TokenStream; +use quote::quote; +use syn; + +#[proc_macro_derive(HelloMacro)] +pub fn hello_macro_derive(input: TokenStream) -> TokenStream { + // 构建 Rust 代码所代表的语法树 + // 以便可以进行操作 + let ast = syn::parse(input).unwrap(); + + // 构建 trait 实现 + impl_hello_macro(&ast) +} +``` + +示例 19-39: 大多数过程式宏处理 Rust 代码时所需的代码 + +注意在 19-39 中分离函数的方式,这将和你几乎所见到或创建的每一个过程宏都一样,因为这让编写一个过程式宏更加方便。在 `impl_hello_macro` 被调用的地方所选择做的什么依赖于该过程式宏的目的而有所不同。 + +现在,我们已经引入了 三个新的 crate:`proc_macro` 、 [`syn`] 和 [`quote`] 。Rust 自带 `proc_macro` crate,因此无需将其加到 *Cargo.toml* 文件的依赖中。`proc_macro` crate 是编译器用来读取和操作我们 Rust 代码的 API。`syn` crate 将字符串中的 Rust 代码解析成为一个可以操作的数据结构。`quote` 则将 `syn` 解析的数据结构反过来传入到 Rust 代码中。这些 crate 让解析任何我们所要处理的 Rust 代码变得更简单:为 Rust 编写整个的解析器并不是一件简单的工作。 + +[`syn`]: https://crates.io/crates/syn +[`quote`]: https://crates.io/crates/quote + +当用户在一个类型上指定 `#[derive(HelloMacro)]` 时,`hello_macro_derive` 函数将会被调用。原因在于我们已经使用 `proc_macro_derive` 及其指定名称对 `hello_macro_derive` 函数进行了注解:`HelloMacro` ,其匹配到 trait 名,这是大多数过程宏遵循的习惯。 + +该函数首先将来自 `TokenStream` 的 `input` 转换为一个我们可以解释和操作的数据结构。这正是 `syn` 派上用场的地方。`syn` 中的 `parse_derive_input` 函数获取一个 `TokenStream` 并返回一个表示解析出 Rust 代码的 `DeriveInput` 结构体。示例 19-40 展示了从字符串 `struct Pancakes;` 中解析出来的 `DeriveInput` 结构体的相关部分: + +```rust,ignore +DeriveInput { + // --snip-- + + ident: Ident { + ident: "Pancakes", + span: #0 bytes(95..103) + }, + data: Struct( + DataStruct { + struct_token: Struct, + fields: Unit, + semi_token: Some( + Semi + ) + } + ) +} +``` + +示例 19-40: 解析示例 19-38 中带有宏属性的代码时得到的 `DeriveInput` 实例 + +该结构体的字段展示了我们解析的 Rust 代码是一个类单元结构体,其 `ident`( identifier,表示名字)为 `Pancakes`。该结构体里面有更多字段描述了所有类型的 Rust 代码,查阅 [`syn` 中 `DeriveInput` 的文档][syn-docs] 以获取更多信息。 + +[syn-docs]: https://docs.rs/syn/0.14.4/syn/struct.DeriveInput.html + +此时,尚未定义 `impl_hello_macro` 函数,其用于构建所要包含在内的 Rust 新代码。但在此之前,注意其输出也是 `TokenStream`。所返回的 `TokenStream` 会被加到我们的 crate 用户所写的代码中,因此,当用户编译他们的 crate 时,他们会获取到我们所提供的额外功能。 + +你可能也注意到了,当调用 `parse_derive_input` 或 `parse` 失败时。在错误时 panic 对过程宏来说是必须的,因为 `proc_macro_derive` 函数必须返回 `TokenStream` 而不是 `Result`,以此来符合过程宏的 API。这里选择用 `unwrap` 来简化了这个例子;在生产代码中,则应该通过 `panic!` 或 `expect` 来提供关于发生何种错误的更加明确的错误信息。 + +现在我们有了将注解的 Rust 代码从 `TokenStream` 转换为 `DeriveInput` 实例的代码,让我们来创建在注解类型上实现 `HelloMacro` trait 的代码,如示例 19-41 所示。 + +文件名: hello_macro_derive/src/lib.rs + +```rust,ignore +fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream { + let name = &ast.ident; + let gen = quote! { + impl HelloMacro for #name { + fn hello_macro() { + println!("Hello, Macro! My name is {}", stringify!(#name)); + } + } + }; + gen.into() +} +``` + +示例 19-41: 使用解析过的 Rust 代码实现 `HelloMacro` trait + +我们得到一个包含以 `ast.ident` 作为注解类型名字(标识符)的 `Ident` 结构体实例。示例 19-40 中的结构体表明当 `impl_hello_macro` 函数运行于示例 19-38 中的代码上时 `ident` 字段的值是 `"Pancakes"`。因此,示例 19-41 中 `name` 变量会包含一个 `Ident` 结构体的实例,当打印时,会是字符串 `"Pancakes"`,也就是示例 19-38 中结构体的名称。 + +`quote!` 让我们可以编写希望返回的 Rust diamagnetic。`quote!` 宏执行的直接结果并不是编译器所期望的并需要转换为 `TokenStream`。为此需要调用 `into` 方法,它会消费这个中间表示(intermediate representation,IR)并返回所需的 `TokenStream` 类型值。 + +这个宏也提供了一些非常酷的模板机制;我们可以写 `#name` ,然后 `quote!` 会以 名为 `name` 的变量值来替换它。你甚至可以做些与这个正则宏任务类似的重复事情。查阅 [`quote` crate 的文档][quote-docs] 来获取详尽的介绍。 + +[quote-docs]: https://docs.rs/quote + +我们期望我们的过程式宏能够为通过 `#name` 获取到的用户注解类型生成 `HelloMacro` trait 的实现。该 trait 的实现有一个函数 `hello_macro` ,其函数体包括了我们期望提供的功能:打印 `Hello, Macro! My name is` 和注解的类型名。 + +此处所使用的 `stringify!` 为 Rust 内置宏。其接收一个 Rust 表达式,如 `1 + 2` , 然后在编译时将表达式转换为一个字符串常量,如 `"1 + 2"` 。这与 `format!` 或 `println!` 是不同的,它计算表达式并将结果转换为 `String` 。有一种可能的情况是,所输入的 `#name` 可能是一个需要打印的表达式,因此我们用 `stringify!` 。 `stringify!` 编译时也保留了一份将 `#name` 转换为字符串之后的内存分配。 + +此时,`cargo build` 应该都能成功编译 `hello_macro` 和 `hello_macro_derive` 。我们将这些 crate 连接到示例 19-38 的代码中来看看过程宏的行为!在 *projects* 目录下用 `cargo new pancakes` 命令新建一个二进制项目。需要将 `hello_macro` 和 `hello_macro_derive` 作为依赖加到 `pancakes` 包的 *Cargo.toml* 文件中去。如果你正将 `hello_macro` 和 `hello_macro_derive` 的版本发布到 *https://crates.io/* 上,其应为正规依赖;如果不是,则可以像下面这样将其指定为 `path` 依赖: + +```toml +[dependencies] +hello_macro = { path = "../hello_macro" } +hello_macro_derive = { path = "../hello_macro/hello_macro_derive" } +``` + +把示例 19-38 中的代码放在 *src/main.rs* ,然后执行 `cargo run`:其应该打印 `Hello, Macro! My name is Pancakes!`。其包含了该过程宏中 `HelloMacro` trait 的实现,而无需 `pancakes` crate 实现它;`#[derive(HelloMacro)]` 增加了该 trait 实现。 + +接下来,让我们探索一下其他类型的过程宏与自定义派生宏有何区别。 + +### 类属性宏 + +类属性宏与自定义派生宏相似,不同于为 `derive` 属性生成代码,它们允许你创建新的属性。它们也更为灵活;`derive` 只能用于结构体和枚举;属性还可以用于其它的项,比如函数。作为一个使用类属性宏的例子,可以创建一个名为 `route` 的属性用于注解 web 应用程序框架(web application framework)的函数: + +```rust,ignore +#[route(GET, "/")] +fn index() { +``` + +`#[route]` 属性将由框架本身定义为一个过程宏。其宏定义的函数签名看起来像这样: + +```rust,ignore +#[proc_macro_attribute] +pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream { +``` + +这里有两个 `TokenStream` 类型的参数;第一个用于属性内容本身,也就是 `GET, "/"` 部分。第二个是属性所标记的项,在本例中,是 `fn index() {}` 和剩下的函数体。 + +除此之外,类属性宏与自定义派生宏工作方式一致:创建 `proc-macro` crate 类型的 crate 并实现希望生成代码的函数! + +### 类函数宏 + +最后,类函数宏定义看起来像函数调用的宏。例如,`sql!` 宏可能像这样被调用: + +```rust,ignore +let sql = sql!(SELECT * FROM posts WHERE id=1); +``` + +这个宏会解析其中的 SQL 语句并检查其是否是句法正确的。该宏应该被定义为如此: + +```rust,ignore +#[proc_macro] +pub fn sql(input: TokenStream) -> TokenStream { +``` + +这类似于自定义派生宏的签名:获取括号中的 token,并返回希望生成的代码。 + +## 总结 + +好的!现在我们学习了 Rust 并不常用但在特定情况下你可能用得着的功能。我们介绍了很多复杂的主题,这样若你在错误信息提示或阅读他人代码时遇到他们,至少可以说之前已经见过这些概念和语法了。你可以使用本章作为一个解决方案的参考。 + +接下来,我们将再开始一个项目,将本书所学的所有内容付与实践! \ No newline at end of file diff --git a/src/ch20-00-final-project-a-web-server.md b/src/ch20-00-final-project-a-web-server.md index de5b28d..873ae3d 100644 --- a/src/ch20-00-final-project-a-web-server.md +++ b/src/ch20-00-final-project-a-web-server.md @@ -1,16 +1,16 @@ # 最后的项目: 构建多线程 web server -> [ch20-00-final-project-a-web-server.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch20-00-final-project-a-web-server.md) +> [ch20-00-final-project-a-web-server.md](https://github.com/rust-lang/book/blob/master/src/ch20-00-final-project-a-web-server.md) >
-> commit e2a38b44f3a7f796fa8000e558dc8dd2ddf340a3 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -这是一次漫长的旅途,不过我们做到了!这一章便是本书的结束。离别是如此甜蜜的悲伤。不过在我们结束之前,再来一起构建另一个项目,来展示最后几章所学,同时复习更早的章节。 +这是一次漫长的旅途,不过我们到达了本书的结束。在本章中,我们将一同构建另一个项目,来展示最后几章所学,同时复习更早的章节。 -作为最后的项目,我们将要实现一个只返回 “hello” 的 web server;它在浏览器中看起来就如图例 20-1 所示: +作为最后的项目,我们将要实现一个返回 “hello” 的 web server,它在浏览器中看起来就如图例 20-1 所示: ![hello from rust](img/trpl20-01.png) -图例 20-1: 我们最好将一起分享的项目 +图例 20-1: 我们最后将一起分享的项目 如下是我们将怎样构建此 web server 的计划: @@ -20,6 +20,6 @@ 4. 创建一个合适的 HTTP 响应 5. 通过线程池改善 server 的吞吐量 -不过在开始之前,需要提到一点:这里使用的方法并不是使用 Rust 构建 web server 最好的方法。*https://crates.io* 上有很多可用于生产环境的 crate,它们提供了比我们所要编写的更为完整的 web server 和线程池实现。 +不过在开始之前,需要提到一点细节:这里使用的方法并不是使用 Rust 构建 web server 最好的方法。*https://crates.io* 上有很多可用于生产环境的 crate,它们提供了比我们所要编写的更为完整的 web server 和线程池实现。 然而,本章的目的在于学习,而不是走捷径。因为 Rust 是一个系统编程语言,我们能够选择处理什么层次的抽象,并能够选择比其他语言可能或可用的层次更低的层次。因此我们将自己编写一个基础的 HTTP server 和线程池,以便学习将来可能用到的 crate 背后的通用理念和技术。 diff --git a/src/ch20-01-single-threaded.md b/src/ch20-01-single-threaded.md index 4ec672c..a3a4355 100644 --- a/src/ch20-01-single-threaded.md +++ b/src/ch20-01-single-threaded.md @@ -1,10 +1,10 @@ ## 构建单线程 web server -> [ch20-01-single-threaded.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch20-01-single-threaded.md) +> [ch20-01-single-threaded.md](https://github.com/rust-lang/book/blob/master/src/ch20-01-single-threaded.md) >
-> commit 90e6737d534cb66102674d183d2ef1966b190c2c +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -首先让我们创建一个可运行的单线程 web server,不过在开始之前,我们将快速了解一下构建 web server 所涉及到的协议。这些协议的细节超出了本书的范畴,不过一个简单的概括会提供你所需的信息。 +首先让我们创建一个可运行的单线程 web server,不过在开始之前,我们将快速了解一下构建 web server 所涉及到的协议。这些协议的细节超出了本书的范畴,不过一个简单的概括会提供我们所需的信息。 web server 中涉及到的两个主要协议是 **超文本传输协议**(*Hypertext Transfer Protocol*,*HTTP*)和 **传输控制协议**(*Transmission Control Protocol*,*TCP*)。这两者都是 **请求-响应**(*request-response*)协议,也就是说,有 **客户端**(*client*)来初始化请求,并有 **服务端**(*server*)监听请求并向客户端提供响应。请求与响应的内容由协议本身定义。 @@ -15,7 +15,7 @@ TCP 是一个底层协议,它描述了信息如何从一个 server 到另一 所以我们的 web server 所需做的第一件事便是能够监听 TCP 连接。标准库提供了 `std::net` 模块处理这些功能。让我们一如既往新建一个项目: ```text -$ cargo new hello --bin +$ cargo new hello Created binary (application) `hello` project $ cd hello ``` @@ -40,28 +40,16 @@ fn main() { 示例 20-1: 监听传入的流并在接收到流时打印信息 -`TcpListener` 用于监听 TCP 连接。我们选择监听地址 `127.0.0.1:7878`。将这个地址拆开,冒号之前的部分是一个代表本机的 IP 地址(这个地址在每台计算机上都相同,并不特指作者的计算机),而 `7878` 是端口。选择这个端口出于两个原因:通常 HTTP 接受这个端口而且 7878 在电话上打出来就是 "rust"(译者注:九宫格键盘上的英文)。注意连接 80 端口需要管理员权限;非管理员用户只能监听大于 1024 的端口。 +`TcpListener` 用于监听 TCP 连接。我们选择监听地址 `127.0.0.1:7878`。将这个地址拆开,冒号之前的部分是一个代表本机的 IP 地址(这个地址在每台计算机上都相同,并不特指作者的计算机),而 `7878` 是端口。选择这个端口出于两个原因:通常 HTTP 接受这个端口而且 7878 在电话上打出来就是 "rust"(译者注:九宫格键盘上的英文)。 在这个场景中 `bind` 函数类似于 `new` 函数,在这里它返回一个新的 `TcpListener` 实例。这个函数叫做 `bind` 是因为,在网络领域,连接到监听端口被称为 “绑定到一个端口”(“binding to a port”) -`bind` 函数返回 `Result`,这表明绑定可能会失败,例如,如果不是管理员尝试连接 80 端口,或是如果运行两个此程序的实例这样会有两个程序监听相同的端口,绑定会失败。因为我们是出于学习目的来编写一个基础的 server,将不用关心处理这类错误,使用 `unwrap` 在出现这些情况时直接停止程序。 +`bind` 函数返回 `Result`,这表明绑定可能会失败,例如,连接 80 端口需要管理员权限(非管理员用户只能监听大于 1024 的端口),所以如果不是管理员尝试连接 80 端口,则会绑定失败。另一个例子是如果运行两个此程序的实例这样会有两个程序监听相同的端口,绑定会失败。因为我们是出于学习目的来编写一个基础的 server,将不用关心处理这类错误,使用 `unwrap` 在出现这些情况时直接停止程序。 `TcpListener` 的 `incoming` 方法返回一个迭代器,它提供了一系列的流(更准确的说是 `TcpStream` 类型的流)。**流**(*stream*)代表一个客户端和服务端之间打开的连接。**连接**(*connection*)代表客户端连接服务端、服务端生成响应以及服务端关闭连接的全部请求 / 响应过程。为此,`TcpStream` 允许我们读取它来查看客户端发送了什么,并可以编写响应。总体来说,这个 `for` 循环会依次处理每个连接并产生一系列的流供我们处理。 - - - 目前为止,处理流的过程包含 `unwrap` 调用,如果出现任何错误会终止程序,如果没有任何错误,则打印出信息。下一个示例我们将为成功的情况增加更多功能。当客户端连接到服务端时 `incoming` 方法返回错误是可能的,因为我们实际上没有遍历连接,而是遍历 **连接尝试**(*connection attempts*)。连接可能会因为很多原因不能成功,大部分是操作系统相关的。例如,很多系统限制同时打开的连接数;新连接尝试产生错误,直到一些打开的连接关闭为止。 - - 让我们试试这段代码!首先在终端执行 `cargo run`,接着在浏览器中加载 `127.0.0.1:7878`。浏览器会显示出看起来类似于“连接重置”(“Connection reset”)的错误信息,因为 server 目前并没响应任何数据。但是如果我们观察终端,会发现当浏览器连接 server 时会打印出一系列的信息! ```text @@ -71,11 +59,11 @@ Connection established! Connection established! ``` -有时会看到对于一次浏览器请求会打印出多条信息;这可能是因为浏览器在请求页面的同时还请求了其他资源,比如出现在浏览器 tab 标签中的 `favicon.ico`。 +有时会看到对于一次浏览器请求会打印出多条信息;这可能是因为浏览器在请求页面的同时还请求了其他资源,比如出现在浏览器 tab 标签中的 *favicon.ico*。 这也可能是因为浏览器尝试多次连接 server,因为 server 没有响应任何数据。当 `stream` 在循环的结尾离开作用域并被丢弃,其连接将被关闭,作为 `drop` 实现的一部分。浏览器有时通过重连来处理关闭的连接,因为这些问题可能是暂时的。现在重要的是我们成功的处理了 TCP 连接! -记得当运行完特定版本的代码后使用 ctrl-C 来停止程序,并在做出最新的代码修改之后执行 `cargo run` 重启服务。 +记得当运行完特定版本的代码后使用 ctrl-C 来停止程序。并在做出最新的代码修改之后执行 `cargo run` 重启服务。 ### 读取请求 @@ -85,8 +73,8 @@ Connection established! ```rust,no_run use std::io::prelude::*; -use std::net::TcpListener; use std::net::TcpStream; +use std::net::TcpListener; fn main() { let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); @@ -111,18 +99,11 @@ fn handle_connection(mut stream: TcpStream) { 这里将 `std::io::prelude` 引入作用域来获取读写流所需的特定 trait。在 `main` 函数的 `for` 循环中,相比获取到连接时打印信息,现在调用新的 `handle_connection` 函数并向其传递 `stream`。 -在 `handle_connection` 中,`stream` 参数是可变的。 - -我们将从流中读取数据,所以它需要是可修改的。这是因为 `TcpStream` 实例在内部记录了所返回的数据。它可能读取了多于我们请求的数据并保存它们以备下一次请求数据。因此它需要是 `mut` 的因为其内部状态可能会改变;通常我们认为 “读取” 不需要可变性,不过在这个例子中则需要 `mut` 关键字。 - - - +在 `handle_connection` 中,`stream` 参数是可变的。这是因为 `TcpStream` 实例在内部记录了所返回的数据。它可能读取了多于我们请求的数据并保存它们以备下一次请求数据。因此它需要是 `mut` 的因为其内部状态可能会改变;通常我们认为 “读取” 不需要可变性,不过在这个例子中则需要 `mut` 关键字。 接下来,需要实际读取流。这里分两步进行:首先,在栈上声明一个 `buffer` 来存放读取到的数据。这里创建了一个 512 字节的缓冲区,它足以存放基本请求的数据并满足本章的目的需要。如果希望处理任意大小的请求,缓冲区管理将更为复杂,不过现在一切从简。接着将缓冲区传递给 `stream.read` ,它会从 `TcpStream` 中读取字节并放入缓冲区中。 -接下来将缓冲区中的字节转换为字符串并打印出来。`String::from_utf8_lossy` 函数获取一个 `&[u8]` 并产生一个 `String`。函数名的 “lossy” 部分来源于当其遇到无效的 UTF-8 序列时的行为:它使用 �,`U+FFFD REPLACEMENT CHARACTER`,来代替无效序列。你可能会在缓冲区的剩余部分看到这些替代字符,因为他们没有被请求数据填满。 +接下来将缓冲区中的字节转换为字符串并打印出来。`String::from_utf8_lossy` 函数获取一个 `&[u8]` 并产生一个 `String`。函数名的 “lossy” 部分来源于当其遇到无效的 UTF-8 序列时的行为:它使用 `�`,`U+FFFD REPLACEMENT CHARACTER`,来代替无效序列。你可能会在缓冲区的剩余部分看到这些替代字符,因为他们没有被请求数据填满。 让我们试一试!启动程序并再次在浏览器中发起请求。注意浏览器中仍然会出现错误页面,不过终端中程序的输出现在看起来像这样: @@ -143,7 +124,7 @@ Upgrade-Insecure-Requests: 1 ������������������������������������ ``` -根据使用的浏览器不同可能会出现稍微不同的数据。现在我们打印出了请求数据,可以通过观察 `Request: GET` 之后的路径来解释为何会从浏览器得到多个连接。如果重复的连接都是请求 `/`,就知道了浏览器尝试重复获取 `/` 因为它没有从程序得到响应。 +根据使用的浏览器不同可能会出现稍微不同的数据。现在我们打印出了请求数据,可以通过观察 `Request: GET` 之后的路径来解释为何会从浏览器得到多个连接。如果重复的连接都是请求 */*,就知道了浏览器尝试重复获取 */* 因为它没有从程序得到响应。 拆开请求数据来理解浏览器向程序请求了什么。 @@ -159,19 +140,11 @@ message-body 第一行叫做 **请求行**(*request line*),它存放了客户端请求了什么的信息。请求行的第一部分是所使用的 *method*,比如 `GET` 或 `POST`,这描述了客户端如何进行请求。这里客户端使用了 `GET` 请求。 - - - -`Request` 行接下来的部分是 `/`,它代表客户端请求的 **统一资源标识符**(*Uniform Resource Identifier*,*URI*) —— URI 大体上类似,但也不完全类似于 URL(**统一资源定位符**,*Uniform Resource Locators*)。URI 和 URL 之间的区别对于本章的目的来说并不重要,不过 HTTP 规范使用术语 URI,所以这里可以简单的将 URL 理解为 URI。 +请求行接下来的部分是 */*,它代表客户端请求的 **统一资源标识符**(*Uniform Resource Identifier*,*URI*) —— URI 大体上类似,但也不完全类似于 URL(**统一资源定位符**,*Uniform Resource Locators*)。URI 和 URL 之间的区别对于本章的目的来说并不重要,不过 HTTP 规范使用术语 URI,所以这里可以简单的将 URL 理解为 URI。 -最后,是客户端使用的 HTTP 版本,接着请求行以一个 CRLF 序列结尾。CRLF 序列也可以写作 `\r\n`:`\r` 是 **回车**(*carriage return*)而 `\n` 是 **换行**(*line feed*)(这些术语来自打字机时代!)。注意当 CRLF 被打印时,会看到开始了一个新行而不是 `\r\n`。 +最后,是客户端使用的 HTTP 版本,接着请求行以一个 **CRLF 序列**(CRLF 是**回车**,*carriage return* 和 **换行**,*line feed* 的缩写,这些术语来自打字机时代!)结尾。结尾。CRLF 序列也可以写作 `\r\n`:`\r` 是回车而 `\n` 是换行。CRLF 序列将请求行与其余的请求数据分开。注意当 CRLF 被打印时,会看到开始了一个新行而不是 `\r\n`。 - - - -观察目前运行程序所接收到的数据的请求行,可以看到 `GET` 是 method,`/` 是请求 URI,而 `HTTP/1.1` 是版本。 +观察目前运行程序所接收到的数据的请求行,可以看到 `GET` 是 method,*/* 是请求 URI,而 `HTTP/1.1` 是版本。 从 `Host:` 开始的其余的行是 headers;`GET` 请求没有 body。 @@ -191,15 +164,13 @@ message-body 第一行叫做 **状态行**(*status line*),它包含响应的 HTTP 版本、一个数字状态码用以总结请求的结果和一个描述之前状态码的文本原因短语。CRLF 序列之后是任意 header,另一个 CRLF 序列,和响应的 body。 -这里是一个使用 HTTP 1.1 版本的响应例子,其状态码为 `200`,原因短语为 `OK`,没有 header,也没有 body: +这里是一个使用 HTTP 1.1 版本的响应例子,其状态码为 200,原因短语为 OK,没有 header,也没有 body: ```text HTTP/1.1 200 OK\r\n\r\n ``` -状态码 200 是一个标准的成功响应。这些文本是一个微型的成功 HTTP 响应。让我们将这些文本写入流作为成功请求的响应! - -在 `handle_connection` 函数中,我们需要去掉打印请求数据的 `println!`,并替换为示例 20-3 中的代码: +状态码 200 是一个标准的成功响应。这些文本是一个微型的成功 HTTP 响应。让我们将这些文本写入流作为成功请求的响应!在 `handle_connection` 函数中,我们需要去掉打印请求数据的 `println!`,并替换为示例 20-3 中的代码: 文件名: src/main.rs @@ -220,26 +191,11 @@ fn handle_connection(mut stream: TcpStream) { 示例 20-3: 将一个微型成功 HTTP 响应写入流 - - 新代码中的第一行定义了变量 `response` 来存放将要返回的成功响应的数据。接着,在 `response` 上调用 `as_bytes`,因为 `stream` 的 `write` 方法获取一个 `&[u8]` 并直接将这些字节发送给连接。 - - - 因为 `write` 操作可能会失败,所以像之前那样对任何错误结果使用 `unwrap`。同理,在真实世界的应用中这里需要添加错误处理。最后,`flush` 会等待并阻塞程序执行直到所有字节都被写入连接中;`TcpStream` 包含一个内部缓冲区来最小化对底层操作系统的调用。 - - - -有了这些修改,运行我们的代码并进行请求!我们不再向终端打印任何数据,所以不会再看到除了 Cargo 以外的任何输出。不过当在浏览器中加载 `127.0.0.1:7878` 时,会得到一个空页面而不是错误。太棒了!我们刚刚手写了一个 HTTP 请求与响应。 +有了这些修改,运行我们的代码并进行请求!我们不再向终端打印任何数据,所以不会再看到除了 Cargo 以外的任何输出。不过当在浏览器中加载 *127.0.0.1:7878* 时,会得到一个空页面而不是错误。太棒了!我们刚刚手写了一个 HTTP 请求与响应。 ### 返回真正的 HTML @@ -263,25 +219,21 @@ small detail I don't think is worth going into in depth. /Carol --> 示例 20-4: 一个简单的 HTML 文件用来作为响应 -这是一个极小化的 HTML 5 文档,它有一个标题和一小段文本。为了在 server 接受请求时返回它,需要如示例 20-5 所示修改 `handle_connection` 来读取 HTML 文件,将其加入到响应的 body 中,并发送: +这是一个极小化的 HTML5 文档,它有一个标题和一小段文本。为了在 server 接受请求时返回它,需要如示例 20-5 所示修改 `handle_connection` 来读取 HTML 文件,将其加入到响应的 body 中,并发送: 文件名: src/main.rs ```rust # use std::io::prelude::*; # use std::net::TcpStream; -use std::fs::File; - +use std::fs; // --snip-- fn handle_connection(mut stream: TcpStream) { let mut buffer = [0; 512]; stream.read(&mut buffer).unwrap(); - let mut file = File::open("hello.html").unwrap(); - - let mut contents = String::new(); - file.read_to_string(&mut contents).unwrap(); + let contents = fs::read_to_string("hello.html").unwrap(); let response = format!("HTTP/1.1 200 OK\r\n\r\n{}", contents); @@ -296,20 +248,20 @@ fn handle_connection(mut stream: TcpStream) { 接下来,使用 `format!` 将文件内容加入到将要写入流的成功响应的 body 中。 -使用 `cargo run` 运行程序,在浏览器加载 `127.0.0.1:7878`,你应该会看到渲染出来的 HTML 文件! +使用 `cargo run` 运行程序,在浏览器加载 *127.0.0.1:7878*,你应该会看到渲染出来的 HTML 文件! -目前忽略了 `buffer` 中的请求数据并无条件的发送了 HTML 文件的内容。这意味着如果尝试在浏览器中请求 `127.0.0.1:7878/something-else` 也会得到同样的 HTML 响应。如此其作用是非常有限的,也不是大部分 server 所做的;让我们检查请求并只对格式良好(well-formed)的请求 `/` 发送 HTML 文件。 +目前忽略了 `buffer` 中的请求数据并无条件的发送了 HTML 文件的内容。这意味着如果尝试在浏览器中请求 *127.0.0.1:7878/something-else* 也会得到同样的 HTML 响应。如此其作用是非常有限的,也不是大部分 server 所做的;让我们检查请求并只对格式良好(well-formed)的请求 `/` 发送 HTML 文件。 ### 验证请求并有选择的进行响应 -目前我们的 web server 不管客户端请求什么都会返回相同的 HTML 文件。让我们增加在返回 HTML 文件前检查浏览器是否请求 `/`,并在其请求任何其他内容时返回错误的功能。为此需要如示例 20-6 那样修改 `handle_connection`。新代码接收到的请求的内容与已知的 `/` 请求的一部分做比较,并增加了 `if` 和 `else` 块来区别处理请求: +目前我们的 web server 不管客户端请求什么都会返回相同的 HTML 文件。让我们增加在返回 HTML 文件前检查浏览器是否请求 */*,并在其请求任何其他内容时返回错误的功能。为此需要如示例 20-6 那样修改 `handle_connection`。新代码接收到的请求的内容与已知的 */* 请求的一部分做比较,并增加了 `if` 和 `else` 块来区别处理请求: 文件名: src/main.rs ```rust # use std::io::prelude::*; # use std::net::TcpStream; -# use std::fs::File; +# use std::fs; // --snip-- fn handle_connection(mut stream: TcpStream) { @@ -319,48 +271,41 @@ fn handle_connection(mut stream: TcpStream) { let get = b"GET / HTTP/1.1\r\n"; if buffer.starts_with(get) { - let mut file = File::open("hello.html").unwrap(); - - let mut contents = String::new(); - file.read_to_string(&mut contents).unwrap(); + let contents = fs::read_to_string("hello.html").unwrap(); let response = format!("HTTP/1.1 200 OK\r\n\r\n{}", contents); stream.write(response.as_bytes()).unwrap(); stream.flush().unwrap(); } else { - // some other request + // 其他请求 } } ``` 示例 20-6: 匹配请求并区别处理 `/` 请求与其他请求 -首先,将与 `/` 请求相关的数据硬编码进变量 `get`。因为我们将原始字节读取进了缓冲区,所以在 `get` 的数据开头增加 `b""` 字节字符串语法将其转换为字节字符串。接着检查 `buffer` 是否以 `get` 中的字节开头。如果是,这就是一个格式良好的 `/` 请求,也就是 `if` 块中期望处理的成功情况,并会返回 HTML 文件内容的代码。 - +首先,将与 */* 请求相关的数据硬编码进变量 `get`。因为我们将原始字节读取进了缓冲区,所以在 `get` 的数据开头增加 `b""` 字节字符串语法将其转换为字节字符串。接着检查 `buffer` 是否以 `get` 中的字节开头。如果是,这就是一个格式良好的 */* 请求,也就是 `if` 块中期望处理的成功情况,并会返回 HTML 文件内容的代码。 如果 `buffer` **不** 以 `get` 中的字节开头,就说明接收的是其他请求。之后会在 `else` 块中增加代码来响应所有其他请求。 -现在如果运行代码并请求 `127.0.0.1:7878`,就会得到 *hello.html* 中的 HTML。如果进行任何其他请求,比如 `127.0.0.1:7878/something-else`,则会得到像运行示例 20-1 和 20-2 中代码那样的连接错误。 +现在如果运行代码并请求 *127.0.0.1:7878*,就会得到 *hello.html* 中的 HTML。如果进行任何其他请求,比如 *127.0.0.1:7878/something-else*,则会得到像运行示例 20-1 和 20-2 中代码那样的连接错误。 -现在向示例 20-7 的 `else` 块增加代码来返回一个带有 `404` 状态码的响应,这代表了所请求的内容没有找到。接着也会返回一个 HTML 向浏览器终端用户表明此意: +现在向示例 20-7 的 `else` 块增加代码来返回一个带有 404 状态码的响应,这代表了所请求的内容没有找到。接着也会返回一个 HTML 向浏览器终端用户表明此意: 文件名: src/main.rs ```rust # use std::io::prelude::*; # use std::net::TcpStream; -# use std::fs::File; +# use std::fs; # fn handle_connection(mut stream: TcpStream) { # if true { // --snip-- } else { let status_line = "HTTP/1.1 404 NOT FOUND\r\n\r\n"; - let mut file = File::open("404.html").unwrap(); - let mut contents = String::new(); - - file.read_to_string(&mut contents).unwrap(); + let contents = fs::read_to_string("404.html").unwrap(); let response = format!("{}{}", status_line, contents); @@ -370,9 +315,9 @@ fn handle_connection(mut stream: TcpStream) { # } ``` -示例 20-7: 对于任何不是 `/` 的请求返回 `404` 状态码的响应和错误页面 +示例 20-7: 对于任何不是 */* 的请求返回 `404` 状态码的响应和错误页面 -这里,响应的状态行有状态码 `404` 和原因短语 `NOT FOUND`。仍然没有返回任何 header,而其 body 将是 *404.html* 文件中的 HTML。需要在 *hello.html* 同级目录创建 *404.html* 文件作为错误页面;这一次也可以随意使用任何 HTML 或使用示例 20-8 中的示例 HTML: +这里,响应的状态行有状态码 404 和原因短语 `NOT FOUND`。仍然没有返回任何 header,而其 body 将是 *404.html* 文件中的 HTML。需要在 *hello.html* 同级目录创建 *404.html* 文件作为错误页面;这一次也可以随意使用任何 HTML 或使用示例 20-8 中的示例 HTML: 文件名: 404.html @@ -390,9 +335,9 @@ fn handle_connection(mut stream: TcpStream) { ``` -示例 20-8: 任何 `404` 响应所返回错误页面内容样例 +示例 20-8: 任何 404 响应所返回错误页面内容样例 -有了这些修改,再次运行 server。请求 `127.0.0.1:7878` 应该会返回 *hello.html* 的内容,而对于任何其他请求,比如 `127.0.0.1:7878/foo`,应该会返回 *404.html* 中的错误 HTML! +有了这些修改,再次运行 server。请求 *127.0.0.1:7878* 应该会返回 *hello.html* 的内容,而对于任何其他请求,比如 *127.0.0.1:7878/foo*,应该会返回 *404.html* 中的错误 HTML! ### 少量代码重构 @@ -403,7 +348,7 @@ fn handle_connection(mut stream: TcpStream) { ```rust # use std::io::prelude::*; # use std::net::TcpStream; -# use std::fs::File; +# use std::fs; // --snip-- fn handle_connection(mut stream: TcpStream) { @@ -419,10 +364,7 @@ fn handle_connection(mut stream: TcpStream) { ("HTTP/1.1 404 NOT FOUND\r\n\r\n", "404.html") }; - let mut file = File::open(filename).unwrap(); - let mut contents = String::new(); - - file.read_to_string(&mut contents).unwrap(); + let contents = fs::read_to_string(filename).unwrap(); let response = format!("{}{}", status_line, contents); @@ -437,6 +379,6 @@ fn handle_connection(mut stream: TcpStream) { 之前读取文件和写入响应的冗余代码现在位于 `if` 和 `else` 块之外,并会使用变量 `status_line` 和 `filename`。这样更易于观察这两种情况真正有何不同,还意味着如果需要改变如何读取文件或写入响应时只需要更新一处的代码。示例 20-9 中代码的行为与示例 20-8 完全一样。 -好极了!我们有了一个 40 行左右 Rust 代码的小而简单的 server,它对一个请求返回页面内容而对所有其他请求返回 `404` 响应。 +好极了!我们有了一个 40 行左右 Rust 代码的小而简单的 server,它对一个请求返回页面内容而对所有其他请求返回 404 响应。 目前 server 运行于单线程中,它一次只能处理一个请求。让我们模拟一些慢请求来看看这如何会成为一个问题,并进行修复以便 server 可以一次处理多个请求。 diff --git a/src/ch20-02-multithreaded.md b/src/ch20-02-multithreaded.md index 8d6683a..98e9f73 100644 --- a/src/ch20-02-multithreaded.md +++ b/src/ch20-02-multithreaded.md @@ -1,19 +1,14 @@ ## 将单线程 server 变为多线程 server -> [ch20-02-multithreaded.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch20-02-multithreaded.md) +> [ch20-02-multithreaded.md](https://github.com/rust-lang/book/blob/master/src/ch20-02-multithreaded.md) >
-> commit 1f0136399ba2f5540ecc301fab04bd36492e5554 - - - +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 目前 server 会依次处理每一个请求,意味着它在完成第一个连接的处理之前不会处理第二个连接。如果 server 正接收越来越多的请求,这类串行操作会使性能越来越差。如果一个请求花费很长时间来处理,随后而来的请求则不得不等待这个长请求结束,即便这些新请求可以很快就处理完。我们需要修复这种情况,不过首先让我们实际尝试一下这个问题。 ### 在当前 server 实现中模拟慢请求 -让我们看看一个慢请求如何影响当前 server 实现中的其他请求。示例 20-10 通过模拟慢响应实现了 `/sleep` 请求处理,它会使 server 在响应之前休眠五秒。 +让我们看看一个慢请求如何影响当前 server 实现中的其他请求。示例 20-10 通过模拟慢响应实现了 */sleep* 请求处理,它会使 server 在响应之前休眠五秒。 文件名: src/main.rs @@ -46,23 +41,18 @@ fn handle_connection(mut stream: TcpStream) { } ``` -示例 20-10: 通过识别 `/sleep` 并休眠五秒来模拟慢请求 +示例 20-10: 通过识别 */sleep* 并休眠五秒来模拟慢请求 -这段代码有些凌乱,不过对于模拟的目的来说已经足够!这里创建了第二个请求 `sleep`,我们会识别其数据。在 `if` 块之后增加了一个 `else if` 来检查 `/sleep` 请求,当接收到这个请求时,在渲染成功 HTML 页面之前会先休眠五秒。 +这段代码有些凌乱,不过对于模拟的目的来说已经足够。这里创建了第二个请求 `sleep`,我们会识别其数据。在 `if` 块之后增加了一个 `else if` 来检查 */sleep* 请求,当接收到这个请求时,在渲染成功 HTML 页面之前会先休眠五秒。 -现在就可以真切的看出我们的 server 有多么的原始;真实的库将会以更简洁的方式处理多请求识别问题。 +现在就可以真切的看出我们的 server 有多么的原始;真实的库将会以更简洁的方式处理多请求识别问题! -使用 `cargo run` 启动 server,并接着打开两个浏览器窗口:一个请求 `http://localhost:7878/` 而另一个请求 `http://localhost:7878/sleep`。如果像之前一样多次请求 `/`,会发现响应的比较快速。不过如果请求`/sleep` 之后在请求 `/`,就会看到 `/` 会等待直到 `sleep` 休眠完五秒之后才出现。 +使用 `cargo run` 启动 server,并接着打开两个浏览器窗口:一个请求 *http://127.0.0.1:7878/* 而另一个请求 *http://127.0.0.1:7878/sleep*。如果像之前一样多次请求 */*,会发现响应的比较快速。不过如果请求 */sleep* 之后在请求 */*,就会看到 */* 会等待直到 `sleep` 休眠完五秒之后才出现。 这里有多种办法来改变我们的 web server 使其避免所有请求都排在慢请求之后;我们将要实现的一个便是线程池。 ### 使用线程池改善吞吐量 - - - **线程池**(*thread pool*)是一组预先分配的等待或准备处理任务的线程。当程序收到一个新任务,线程池中的一个线程会被分配任务,这个线程会离开并处理任务。其余的线程则可用于处理在第一个线程处理任务的同时处理其他接收到的任务。当第一个线程处理完任务时,它会返回空闲线程池中等待处理新任务。线程池允许我们并发处理连接,增加 server 的吞吐量。 我们会将池中线程限制为较少的数量,以防拒绝服务(Denial of Service, DoS)攻击;如果程序为每一个接收的请求都新建一个线程,某人向 server 发起千万级的请求时会耗尽服务器的资源并导致所有请求的处理都被终止。 @@ -73,7 +63,7 @@ changing too much --> 在开始之前,让我们讨论一下线程池应用看起来怎样。当尝试设计代码时,首先编写客户端接口确实有助于指导代码设计。以期望的调用方式来构建 API 代码的结构,接着在这个结构之内实现功能,而不是先实现功能再设计公有 API。 -类似于第十二章项目中使用的测试驱动开发。这里将要使用编译器驱动开发(Compiler Driven Development)。我们将编写调用所期望的函数的代码,接着观察编译器错误告诉我们接下来需要修改什么使得代码可以工作。 +类似于第十二章项目中使用的测试驱动开发。这里将要使用编译器驱动开发(compiler-driven development)。我们将编写调用所期望的函数的代码,接着观察编译器错误告诉我们接下来需要修改什么使得代码可以工作。 #### 为每一个请求分配线程的代码结构 @@ -103,7 +93,7 @@ fn main() { 示例 20-11: 为每一个流新建一个线程 -正如第十六章讲到的,`thread::spawn` 会创建一个新线程并在其中运行闭包中的代码。如果运行这段代码并在在浏览器中加载 `/sleep`,接着在另两个浏览器标签页中加载 `/`,确实会发现 `/` 请求不必等待 `/sleep` 结束。不过正如之前提到的,这最终会使系统崩溃因为我们无限制的创建新线程。 +正如第十六章讲到的,`thread::spawn` 会创建一个新线程并在其中运行闭包中的代码。如果运行这段代码并在在浏览器中加载 */sleep*,接着在另两个浏览器标签页中加载 */*,确实会发现 */* 请求不必等待 */sleep* 结束。不过正如之前提到的,这最终会使系统崩溃因为我们无限制的创建新线程。 #### 为有限数量的线程创建一个类似的接口 @@ -142,10 +132,6 @@ fn main() { 这里使用 `ThreadPool::new` 来创建一个新的线程池,它有一个可配置的线程数的参数,在这里是四。这样在 `for` 循环中,`pool.execute` 有着类似 `thread::spawn` 的接口,它获取一个线程池运行于每一个流的闭包。`pool.execute` 需要实现为获取闭包并传递给池中的线程运行。这段代码还不能编译,不过通过尝试编译器会指导我们如何修复它。 - - - #### 采用编译器驱动构建 `ThreadPool` 结构体 继续并对示例 20-12 中的 *src/main.rs* 做出修改,并利用来自 `cargo check` 的编译器错误来驱动开发。下面是我们得到的第一个错误: @@ -178,7 +164,6 @@ pub struct ThreadPool; 文件名: src/bin/main.rs ```rust,ignore -extern crate hello; use hello::ThreadPool; ``` @@ -196,7 +181,7 @@ error[E0599]: no function or associated item named `new` found for type `hello::ThreadPool` ``` -好的,这告诉我们下一步是为 `ThreadPool` 创建一个叫做 `new` 的关联函数。我们还知道 `new` 需要有一个参数可以接受 `4`,而且 `new` 应该返回 `ThreadPool` 实例。让我们实现拥有此特征的最小化 `new` 函数: +这告诉我们下一步是为 `ThreadPool` 创建一个叫做 `new` 的关联函数。我们还知道 `new` 需要有一个参数可以接受 `4`,而且 `new` 应该返回 `ThreadPool` 实例。让我们实现拥有此特征的最小化 `new` 函数: 文件夹: src/lib.rs @@ -233,14 +218,6 @@ error[E0599]: no method named `execute` found for type `hello::ThreadPool` in th | ^^^^^^^ ``` - - - 现在有了一个警告和一个错误。暂时先忽略警告,发生错误是因为并没有 `ThreadPool` 上的 `execute` 方法。回忆 “为有限数量的线程创建一个类似的接口” 部分我们决定线程池应该有与 `thread::spawn` 类似的接口,同时我们将实现 `execute` 函数来获取传递的闭包并将其传递给池中的空闲线程执行。 我们会在 `ThreadPool` 上定义 `execute` 函数来获取一个闭包参数。回忆第十三章的 “使用带有泛型和 `Fn` trait 的闭包” 部分,闭包作为参数时可以使用三个不同的 trait:`Fn`、`FnMut` 和 `FnOnce`。我们需要决定这里应该使用哪种闭包。最终需要实现的类似于标准库的 `thread::spawn`,所以我们可以观察 `thread::spawn` 的签名在其参数中使用了何种 bound。查看文档会发现: @@ -254,11 +231,6 @@ pub fn spawn(f: F) -> JoinHandle `F` 是这里我们关心的参数;`T` 与返回值有关所以我们并不关心。考虑到 `spawn` 使用 `FnOnce` 作为 `F` 的 trait bound,这可能也是我们需要的,因为最终会将传递给 `execute` 的参数传给 `spawn`。因为处理请求的线程只会执行闭包一次,这也进一步确认了 `FnOnce` 是我们需要的 trait,这里符合 `FnOnce` 中 `Once` 的意思。 - - - `F` 还有 trait bound `Send` 和生命周期绑定 `'static`,这对我们的情况也是有意义的:需要 `Send` 来将闭包从一个线程转移到另一个线程,而 `'static` 是因为并不知道线程会执行多久。让我们编写一个使用带有这些 bound 的泛型参数 `F` 的 `ThreadPool` 的 `execute` 方法: 文件名: src/lib.rs @@ -279,7 +251,7 @@ impl ThreadPool { `FnOnce` trait 仍然需要之后的 `()`,因为这里的 `FnOnce` 代表一个没有参数也没有返回值的闭包。正如函数的定义,返回值类型可以从签名中省略,不过即便没有参数也需要括号。 -这里再一次增加了 `execute` 方法的最小化实现,它没有做任何工作。再次进行检查: +这里再一次增加了 `execute` 方法的最小化实现:它没有做任何工作,只是尝试让代码能够编译。再次进行检查: ```text $ cargo check @@ -308,25 +280,20 @@ warning: unused variable: `f` #### 在 `new` 中验证池中线程数量 -这里仍然存在警告是因为其并没有对 `new` 和 `execute` 的参数做任何操作。让我们用期望的行为来实现这些函数。以考虑 `new` 作为开始。 - -之前选择使用无符号类型作为 `size` 参数的类型,因为线程数为负的线程池没有意义。然而,线程数为零的线程池同样没有意义,不过零是一个完全有效的 `u32` 值。让我们增加在返回 `ThreadPool` 实例之前检查 `size` 是否大于零的代码,并使用 `assert!` 宏在得到零时 panic,如示例 20-13 所示: - - -在返回 `ThreadPool` 之前检查 `size` 是否大于零,并使用 `assert!` 宏在得到零时 panic,如列表 20-13 所示: +这里仍然存在警告是因为其并没有对 `new` 和 `execute` 的参数做任何操作。让我们用期望的行为来实现这些函数。以考虑 `new` 作为开始。之前选择使用无符号类型作为 `size` 参数的类型,因为线程数为负的线程池没有意义。然而,线程数为零的线程池同样没有意义,不过零是一个完全有效的 `u32` 值。让我们增加在返回 `ThreadPool` 实例之前检查 `size` 是否大于零的代码,并使用 `assert!` 宏在得到零时 panic,如示例 20-13 所示: 文件名: src/lib.rs ```rust # pub struct ThreadPool; impl ThreadPool { - /// Create a new ThreadPool. + /// 创建线程池。 /// - /// The size is the number of threads in the pool. + /// 线程池中线程的数量。 /// /// # Panics /// - /// The `new` function will panic if the size is zero. + /// `new` 函数在 size 为 0 时会 panic。 pub fn new(size: usize) -> ThreadPool { assert!(size > 0); @@ -339,19 +306,17 @@ impl ThreadPool { 示例 20-13: 实现 `ThreadPool::new` 在 `size` 为零时 panic -趁着这个机会我们用文档注释为 `ThreadPool` 增加了一些文档。注意这里遵循了良好的文档实践并增加了一个部分来提示函数会 panic 的情况,正如第十四章所讨论的。尝试运行 `cargo doc --open` 并点击 `ThreadPool` 结构体来查看生成的 `new` 的文档看起来如何! +这里用文档注释为 `ThreadPool` 增加了一些文档。注意这里遵循了良好的文档实践并增加了一个部分来提示函数会 panic 的情况,正如第十四章所讨论的。尝试运行 `cargo doc --open` 并点击 `ThreadPool` 结构体来查看生成的 `new` 的文档看起来如何! 相比像这里使用 `assert!` 宏,也可以让 `new` 像之前 I/O 项目中示例 12-9 中 `Config::new` 那样返回一个 `Result`,不过在这里我们选择创建一个没有任何线程的线程池应该是不可恢复的错误。如果你想做的更好,尝试编写一个采用如下签名的 `new` 版本来感受一下两者的区别: ```rust,ignore -fn new(size: usize) -> Result { +pub fn new(size: usize) -> Result { ``` #### 分配空间以储存线程 -现在有了一个有效的线程池线程数,就可以实际创建这些线程并在返回之前将他们储存在 `ThreadPool` 结构体中。 - -这引出了另一个问题:如何 “储存” 一个线程?让我们再看看 `thread::spawn` 的签名: +现在有了一个有效的线程池线程数,就可以实际创建这些线程并在返回之前将他们储存在 `ThreadPool` 结构体中。不过如何 “储存” 一个线程?让我们再看看 `thread::spawn` 的签名: ```rust,ignore pub fn spawn(f: F) -> JoinHandle @@ -366,7 +331,7 @@ pub fn spawn(f: F) -> JoinHandle 文件名: src/lib.rs -```rust,ignore +```rust,ignore,not_desired_behavior use std::thread; pub struct ThreadPool { @@ -403,32 +368,11 @@ impl ThreadPool { #### `Worker` 结构体负责从 `ThreadPool` 中将代码传递给线程 - - - - - 示例 20-14 的 `for` 循环中留下了一个关于创建线程的注释。如何实际创建线程呢?这是一个难题。标准库提供的创建线程的方法,`thread::spawn`,它期望获取一些一旦创建线程就应该执行的代码。然而,我们希望开始线程并使其等待稍后传递的代码。标准库的线程实现并没有包含这么做的方法;我们必须自己实现。 - - - 我们将要实现的行为是创建线程并稍后发送代码,这会在 `ThreadPool` 和线程间引入一个新数据类型来管理这种新行为。这个数据结构称为 `Worker`:这是一个池实现中的常见概念。想象一下在餐馆厨房工作的员工:员工等待来自客户的订单,他们负责接受这些订单并完成它们。 - - - -不同于在线程池中储存一个 `JoinHandle<()>` 实例的 vector,我们会储存 `Worker` 结构体的实例。每一个 `Worker` 会储存一个单独的 `JoinHandle<()>` 实例。接着会在 -`Worker` 上实现一个方法,它会获取需要允许代码的闭包并将其发送给已经运行的线程执行。我们还会赋予每一个 worker `id`,这样就可以在日志和调试中区别线程池中的不同 worker。 +不同于在线程池中储存一个 `JoinHandle<()>` 实例的 vector,我们会储存 `Worker` 结构体的实例。每一个 `Worker` 会储存一个单独的 `JoinHandle<()>` 实例。接着会在 `Worker` 上实现一个方法,它会获取需要允许代码的闭包并将其发送给已经运行的线程执行。我们还会赋予每一个 worker `id`,这样就可以在日志和调试中区别线程池中的不同 worker。 首先,让我们做出如此创建 `ThreadPool` 时所需的修改。在通过如下方式设置完 `Worker` 之后,我们会实现向线程发送闭包的代码: @@ -507,7 +451,7 @@ impl Worker { 4. `execute` 方法会在通道发送端发出期望执行的任务。 5. 在线程中,`Worker` 会遍历通道的接收端并执行任何接收到的任务。 -让我们以在 `ThreadPool::new` 中创建通道并让 `ThreadPool` 实例充当发送端开始,如示例 20-16 所示。`Job` 是将在通道中发出的类型;目前它是一个没有任何内容的结构体: +让我们以在 `ThreadPool::new` 中创建通道并让 `ThreadPool` 实例充当发送端开始,如示例 20-16 所示。`Job` 是将在通道中发出的类型,目前它是一个没有任何内容的结构体: 文件名: src/lib.rs @@ -569,7 +513,7 @@ impl ThreadPool { 文件名: src/lib.rs -```rust,ignore +```rust,ignore,does_not_compile impl ThreadPool { // --snip-- pub fn new(size: usize) -> ThreadPool { @@ -629,10 +573,6 @@ error[E0382]: use of moved value: `receiver` 这段代码尝试将 `receiver` 传递给多个 `Worker` 实例。这是不行的,回忆第十六章:Rust 所提供的通道实现是多 **生产者**,单 **消费者** 的。这意味着不能简单的克隆通道的消费端来解决问题。即便可以,那也不是我们希望使用的技术;我们希望通过在所有的 worker 中共享单一 `receiver`,在线程间分发任务。 - - - 另外,从通道队列中取出任务涉及到修改 `receiver`,所以这些线程需要一个能安全的共享和修改 `receiver` 的方式,否则可能导致竞争状态(参考第十六章)。 回忆一下第十六章讨论的线程安全智能指针,为了在多个线程间共享所有权并允许线程修改其值,需要使用 `Arc>`。`Arc` 使得多个 worker 拥有接收端,而 `Mutex` 则确保一次只有一个 worker 能从接收端得到任务。示例 20-18 展示了所需的修改: @@ -644,7 +584,6 @@ can you double check my edits and correct here? --> # use std::sync::mpsc; use std::sync::Arc; use std::sync::Mutex; - // --snip-- # pub struct ThreadPool { @@ -744,7 +683,7 @@ impl ThreadPool { 文件名: src/lib.rs -```rust,ignore +```rust,ignore,does_not_compile // --snip-- impl Worker { @@ -773,7 +712,7 @@ impl Worker { 如果锁定了互斥器,接着调用 `recv` 从通道中接收 `Job`。最后的 `unwrap` 也绕过了一些错误,这可能发生于持有通道发送端的线程停止的情况,类似于如果接收端关闭时 `send` 方法如何返回 `Err` 一样。 -调用 `recv` 会 **阻塞** 当前线程,所以如果还没有任务,其会等待直到有可用的任务。`Mutex` 确保一次只有一个 `Worker` 线程尝试请求任务。 +调用 `recv` 会阻塞当前线程,所以如果还没有任务,其会等待直到有可用的任务。`Mutex` 确保一次只有一个 `Worker` 线程尝试请求任务。 理论上这段代码应该能够编译。不幸的是,Rust 编译器仍不够完美,会给出如下错误: @@ -838,9 +777,7 @@ impl Worker { 接下来,为任何实现了 `FnOnce()` trait 的类型 `F` 实现 `FnBox` trait。这实际上意味着任何 `FnOnce()` 闭包都可以使用 `call_box` 方法。`call_box` 的实现使用 `(*self)()` 将闭包移动出 `Box` 并调用此闭包。 -现在我们需要 `Job` 类型别名是任何实现了新 trait `FnBox` 的 `Box`。这允许我们在得到 `Job` 值时使用 `Worker` 中的 `call_box`。为任何 `FnOnce()` 闭包都实现了 `FnBox` trait 意味着无需对实际在通道中发出的值做任何修改。 - -最后,对于 `Worker::new` 的线程中所运行的闭包,调用 `call_box` 而不是直接执行闭包。现在 Rust 就能够理解我们的行为是正确的了。 +现在我们需要 `Job` 类型别名是任何实现了新 trait `FnBox` 的 `Box`。这允许我们在得到 `Job` 值时使用 `Worker` 中的 `call_box`。为任何 `FnOnce()` 闭包都实现了 `FnBox` trait 意味着无需对实际在通道中发出的值做任何修改。现在 Rust 就能够理解我们的行为是正确的了。 这是非常狡猾且复杂的手段。无需过分担心他们并不是非常有道理;总有一天,这一切将是毫无必要的。 @@ -875,7 +812,7 @@ warning: field is never used: `thread` Finished dev [unoptimized + debuginfo] target(s) in 0.99 secs Running `target/debug/hello` - Worker 0 got a job; executing. +Worker 0 got a job; executing. Worker 2 got a job; executing. Worker 1 got a job; executing. Worker 3 got a job; executing. @@ -887,13 +824,15 @@ Worker 0 got a job; executing. Worker 2 got a job; executing. ``` -成功了!现在我们有了一个可以异步执行连接的线程池!它绝不会创建超过四个线程,所以当 server 收到大量请求时系统也不会负担过重。如果请求 `/sleep`,server 也能够通过另外一个线程处理其他请求。 +成功了!现在我们有了一个可以异步执行连接的线程池!它绝不会创建超过四个线程,所以当 server 收到大量请求时系统也不会负担过重。如果请求 */sleep*,server 也能够通过另外一个线程处理其他请求。 + +注意如果同时在多个浏览器窗口打开 */sleep*,它们可能会彼此间隔地加载 5 秒,因为一些浏览器处于缓存的原因会顺序执行相同请求的多个实例。这些限制并不是由于我们的 web server 造成的。 在学习了第十八章的 `while let` 循环之后,你可能会好奇为何不能如此编写 worker 线程: 文件名: src/lib.rs -```rust,ignore +```rust,ignore,not_desired_behavior // --snip-- impl Worker { @@ -916,6 +855,6 @@ impl Worker { 示例 20-22: 一个使用 `while let` 的 `Worker::new` 替代实现 -这段代码可以编译和运行,但是并不会产生所期望的线程行为:一个慢请求仍然会导致其他请求等待执行。如此的原因有些微妙:`Mutex` 结构体没有公有 `unlock` 方法,因为锁的所有权依赖 `lock` 方法返回的 `LockResult>` 中 `MutexGuard` 的生命周期。这允许借用检查器在编译时确保绝不会在没有持有锁的情况下访问由 `Mutex` 守护的资源,不过如果没有认真的思考 `MutexGuard` 的生命周期的话,也可能会导致比预期更久的持有锁。因为 `while` 表达式中的值在整个块一直处于作用域中,`job.call_box()` 调用的过程中其仍然持有锁,这意味着其他 worker 不能接收任务。 +这段代码可以编译和运行,但是并不会产生所期望的线程行为:一个慢请求仍然会导致其他请求等待执行。其原因有些微妙:`Mutex` 结构体没有公有 `unlock` 方法,因为锁的所有权依赖 `lock` 方法返回的 `LockResult>` 中 `MutexGuard` 的生命周期。这允许借用检查器在编译时确保绝不会在没有持有锁的情况下访问由 `Mutex` 守护的资源,不过如果没有认真的思考 `MutexGuard` 的生命周期的话,也可能会导致比预期更久的持有锁。因为 `while` 表达式中的值在整个块一直处于作用域中,`job.call_box()` 调用的过程中其仍然持有锁,这意味着其他 worker 不能接收任务。 相反通过使用 `loop` 并在循环块之内而不是之外获取锁和任务,`lock` 方法返回的 `MutexGuard` 在 `let job` 语句结束之后立刻就被丢弃了。这确保了 `recv` 调用过程中持有锁,而在 `job.call_box()` 调用前锁就被释放了,这就允许并发处理多个请求了。 \ No newline at end of file diff --git a/src/ch20-03-graceful-shutdown-and-cleanup.md b/src/ch20-03-graceful-shutdown-and-cleanup.md index c82f159..e07c243 100644 --- a/src/ch20-03-graceful-shutdown-and-cleanup.md +++ b/src/ch20-03-graceful-shutdown-and-cleanup.md @@ -1,8 +1,8 @@ ## 优雅停机与清理 -> [ch20-03-graceful-shutdown-and-cleanup.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch20-03-graceful-shutdown-and-cleanup.md) +> [ch20-03-graceful-shutdown-and-cleanup.md](https://github.com/rust-lang/book/blob/master/src/ch20-03-graceful-shutdown-and-cleanup.md) >
-> commit 1f0136399ba2f5540ecc301fab04bd36492e5554 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 示例 20-21 中的代码如期通过使用线程池异步的响应请求。这里有一些警告说 `workers`、`id` 和 `thread` 字段没有直接被使用,这提醒了我们并没有清理所有的内容。当使用不那么优雅的 ctrl-C 终止主线程时,所有其他线程也会立刻停止,即便它们正处于处理请求的过程中。 @@ -14,7 +14,7 @@ 文件名: src/lib.rs -```rust,ignore +```rust,ignore,does_not_compile impl Drop for ThreadPool { fn drop(&mut self) { for worker in &mut self.workers { @@ -223,7 +223,7 @@ impl Drop for ThreadPool { 示例 20-25:在对每个 worker 线程调用 `join` 之前向 worker 发送 `Message::Terminate` -现在遍历了 worker 两次,一次向每个 worker 发送一个 `Terminate` 消息,一个调用每个 worker 线程上的 `join`。如果尝试在同一循环中发送消息并立即 join 线程,则无法保证当前迭代的 worker 是从通道收到终止消息的 worker。 +现在遍历了 worker 两次,一次向每个 worker 发送一个 `Terminate` 消息,一个调用每个 worker 线程上的 `join`。如果尝试在同一循环中发送消息并立即 join 线程,则无法保证当前迭代的 worker 是从通道收到终止消息的 worker。 为了更好的理解为什么需要两个分开的循环,想象一下只有两个 worker 的场景。如果在一个单独的循环中遍历每个 worker,在第一次迭代中向通道发出终止消息并对第一个 worker 线程调用 `join`。我们会一直等待第一个 worker 结束,不过它永远也不会结束因为第二个线程接收了终止消息。死锁! @@ -282,7 +282,9 @@ Shutting down worker 3 这个特定的运行过程中一个有趣的地方在于:注意我们向通道中发出终止消息,而在任何线程收到消息之前,就尝试 join worker 0 了。worker 0 还没有收到终止消息,所以主线程阻塞直到 worker 0 结束。与此同时,每一个线程都收到了终止消息。一旦 worker 0 结束,主线程就等待其他 worker 结束,此时他们都已经收到终止消息并能够停止了。 -恭喜!现在我们完成了这个项目,也有了一个使用线程池异步响应请求的基础 web server。我们能对 server 执行优雅停机,它会清理线程池中的所有线程。如下是完整的代码参考: +恭喜!现在我们完成了这个项目,也有了一个使用线程池异步响应请求的基础 web server。我们能对 server 执行优雅停机,它会清理线程池中的所有线程。 + +如下是完整的代码参考: 文件名: src/bin/main.rs @@ -368,16 +370,16 @@ impl FnBox for F { } } -type Job = Box; +type Job = Box; impl ThreadPool { - /// Create a new ThreadPool. + /// 创建线程池。 /// - /// The size is the number of threads in the pool. + /// 线程池中线程的数量。 /// /// # Panics /// - /// The `new` function will panic if the size is zero. + /// `new` 函数在 size 为 0 时会 panic。 pub fn new(size: usize) -> ThreadPool { assert!(size > 0); @@ -469,8 +471,8 @@ impl Worker { - 为库的功能增加测试 - 将 `unwrap` 调用改为更健壮的错误处理 - 使用 `ThreadPool` 进行其他不同于处理网络请求的任务 -- 在 crates.io 寻找一个线程池 crate 并使用它实现一个类似的 web server,将其 API 和鲁棒性与我们的实现做对比 +- 在 *https://crates.io/* 寻找一个线程池 crate 并使用它实现一个类似的 web server,将其 API 和鲁棒性与我们的实现做对比 ## 总结 -好极了!你结束了本书的学习!由衷感谢你与我们一道加入这次 Rust 之旅。现在你已经准备好出发并实现自己的 Rust 项目并帮助他人了。请不要忘记我们的社区,这里有其他 Rustaceans 正乐于帮助你迎接 Rust 之路上的任何挑战。 \ No newline at end of file +好极了!你结束了本书的学习!由衷感谢你同我们一道加入这次 Rust 之旅。现在你已经准备好出发并实现自己的 Rust 项目并帮助他人了。请不要忘记我们的社区,这里有其他 Rustaceans 正乐于帮助你迎接 Rust 之路上的任何挑战。 \ No newline at end of file From de8b4cc84b69c2f2e1f8044198890f58153f2036 Mon Sep 17 00:00:00 2001 From: Lam <15622383059@163.com> Date: Mon, 10 Dec 2018 11:17:49 +0800 Subject: [PATCH 38/49] improve some translation and fix typo --- src/ch11-03-test-organization.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ch11-03-test-organization.md b/src/ch11-03-test-organization.md index ea13021..7114330 100644 --- a/src/ch11-03-test-organization.md +++ b/src/ch11-03-test-organization.md @@ -87,9 +87,9 @@ fn it_adds_two() { 示例 11-13:一个 `adder` crate 中函数的集成测试 -我们在顶部增加了 `extern crate adder`,这在单元测试中是不需要的。这是因为每一个 `tests` 目录中的测试文件都是完全独立的 crate,所以需要在每一个文件中导入库。 +与单元测试不同,我们需要在文件顶部添加 `use adder`。这是因为每一个 `tests` 目录中的测试文件都是完全独立的 crate,所以需要在每一个文件中导入库。 -并不需要将 *tests/integration_test.rs* 中的任何代码标注为 `#[cfg(test)]`。Cargo 对 `tests` 文件夹特殊处理并只会在运行 `cargo test` 时编译这个目录中的文件。现在就运行 `cargo test` 试试: +并不需要将 *tests/integration_test.rs* 中的任何代码标注为 `#[cfg(test)]`。 `tests` 文件夹在 Cargo 中是一个特殊的文件夹, Cargo 只会在运行 `cargo test` 时编译这个目录中的文件。现在就运行 `cargo test` 试试: ```text $ cargo test @@ -143,7 +143,7 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out 将每个集成测试文件当作其自己的 crate 来对待,这更有助于创建单独的作用域,这种单独的作用域能提供更类似与最终使用者使用 crate 的环境。然而,正如你在第七章中学习的如何将代码分为模块和文件的知识,*tests* 目录中的文件不能像 *src* 中的文件那样共享相同的行为。 -当你有一些在多个集成测试文件都会用到的帮助函数,而你尝试按照第七章 “将模块移动到其他文件” 部分的步骤将他们提取到一个通用的模块中时, *tests* 目录中不同文件的行为就会显得很明显。例如,如果我们创建了 *tests/common.rs* 文件并将一个名叫 `setup` 的函数放入其中,这里将放入一些我们希望能够在多个测试文件的多个测试函数中调用的代码: +当你有一些在多个集成测试文件都会用到的帮助函数,而你尝试按照第七章 “将模块移动到其他文件” 部分的步骤将他们提取到一个通用的模块中时, *tests* 目录中不同文件的行为就会显得很明显。例如,如果我们可以创建 一个*tests/common.rs* 文件并创建一个名叫 `setup` 的函数,我们希望这个函数能被多个测试文件的测试函数调用: 文件名: tests/common.rs @@ -181,9 +181,9 @@ running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ``` -我们并不希望 `common` 出现在测试结果中并显示 `running 0 tests` 。我们只是希望能够在其他集成测试文件中分享一些代码罢了。 +我们并不想要`common` 出现在测试结果中显示 `running 0 tests` 。我们只是希望其能被其他多个集成测试文件中调用罢了。 -为了避免 `common` 出现在测试输出中,我们将创建 *tests/common/mod.rs* ,而不是创建 *tests/common.rs* 。在第七章的 “模块文件系统规则” 部分,对于拥有子模块的模块文件使用了 *module_name/mod.rs* 命名规范,虽然这里 `common` 并没有子模块,但是这样命名告诉 Rust 不要将 `common` 看作一个集成测试文件。当将 `setup` 函数代码移动到 *tests/common/mod.rs* 并删除 *tests/common.rs* 文件之后,测试输出中将不会出现这一部分。*tests* 目录中的子目录不会被作为单独的 crate 编译或作为一个测试结果部分出现在测试输出中。 +为了不让 `common` 出现在测试输出中,我们将创建 *tests/common/mod.rs* ,而不是创建 *tests/common.rs* 。这是一种 Rust 的命名规范,这样命名告诉 Rust 不要将 `common` 看作一个集成测试文件。将 `setup` 函数代码移动到 *tests/common/mod.rs* 并删除 *tests/common.rs* 文件之后,测试输出中将不会出现这一部分。*tests* 目录中的子目录不会被作为单独的 crate 编译或作为一个测试结果部分出现在测试输出中。 一旦拥有了 *tests/common/mod.rs*,就可以将其作为模块以便在任何集成测试文件中使用。这里是一个 *tests/integration_test.rs* 中调用 `setup` 函数的 `it_adds_two` 测试的例子: From f2b39f5f641f88e474d5d70bc2f342310bde5264 Mon Sep 17 00:00:00 2001 From: Lam <15622383059@163.com> Date: Mon, 10 Dec 2018 15:39:38 +0800 Subject: [PATCH 39/49] improve some translation and fix typo in ch12 --- src/ch12-01-accepting-command-line-arguments.md | 6 +++--- src/ch12-02-reading-a-file.md | 6 +++--- ...h12-03-improving-error-handling-and-modularity.md | 12 ++++++------ src/ch12-04-testing-the-librarys-functionality.md | 2 +- src/ch12-05-working-with-environment-variables.md | 6 +++--- src/ch12-06-writing-to-stderr-instead-of-stdout.md | 8 ++++---- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/ch12-01-accepting-command-line-arguments.md b/src/ch12-01-accepting-command-line-arguments.md index 35c1ca6..e82a7fe 100644 --- a/src/ch12-01-accepting-command-line-arguments.md +++ b/src/ch12-01-accepting-command-line-arguments.md @@ -1,4 +1,4 @@ -## 接受命令行参数 +## 接受命令行参数 > [ch12-01-accepting-command-line-arguments.md](https://github.com/rust-lang/book/blob/master/src/ch12-01-accepting-command-line-arguments.md) >
@@ -45,9 +45,9 @@ fn main() { > > 注意 `std::env::args` 在其任何参数包含无效 Unicode 字符时会 panic。如果你需要接受包含无效 Unicode 字符的参数,使用 `std::env::args_os` 代替。这个函数返回 `OsString` 值而不是 `String` 值。这里出于简单考虑使用了 `std::env::args`,因为 `OsString` 值每个平台都不一样而且比 `String` 值处理起来更为复杂。 -在 `main` 函数的第一行,我们调用了 `env::args`,并立即使用 `collect` 来创建了一个包含迭代器所有值的 vector。`collect` 可以被用来创建很多类型的集合,所以这里显式注明 `args` 的类型来指定我们需要一个字符串 vector。虽然在 Rust 中我们很少会需要注明类型,`collect` 就是一个经常需要注明类型的函数,因为 Rust 不能推断出你想要什么类型的集合。 +在 `main` 函数的第一行,我们调用了 `env::args`,并立即使用 `collect` 来创建了一个包含迭代器所有值的 vector。`collect` 可以被用来创建很多类型的集合,所以这里显式注明 `args` 的类型来指定我们需要一个字符串 vector。虽然在 Rust 中我们很少会需要注明类型,然而 `collect` 是一个经常需要注明类型的函数,因为 Rust 不能推断出你想要什么类型的集合。 -最后,我们使用调试格式 `:?` 打印出 vector。让我们尝试不用参数运行代码,接着用两个参数: +最后,我们使用调试格式 `:?` 打印出 vector。让我们尝试分别用两种方式(不包含参数和包含参数)运行代码: ```text $ cargo run diff --git a/src/ch12-02-reading-a-file.md b/src/ch12-02-reading-a-file.md index d39a93e..8da0e3c 100644 --- a/src/ch12-02-reading-a-file.md +++ b/src/ch12-02-reading-a-file.md @@ -1,4 +1,4 @@ -## 读取文件 +## 读取文件 > [ch12-02-reading-a-file.md](https://github.com/rust-lang/book/blob/master/src/ch12-02-reading-a-file.md) >
@@ -49,11 +49,11 @@ fn main() { 示例 12-4:读取第二个参数所指定的文件内容 -首先,我们增加了更多的 `use` 语句来引入标准库中的相关部分:需要 `std::fs` 来处理文件。 +首先,我们增加了一个 `use` 语句来引入标准库中的相关部分:我们需要 `std::fs` 来处理文件。 在 `main` 中新增了一行语句:`fs::read_to_string` 接受 `filename`,打开文件,接着返回包含其内容的 `Result`。 -在这些代码之后,我们再次增加了临时的 `println!` 打印出读取文件后 `contents` 的值,这样就可以检查目前为止的程序能否工作。 +在这些代码之后,我们再次增加了临时的 `println!` 打印出读取文件之后 `contents` 的值,这样就可以检查目前为止的程序能否工作。 尝试运行这些代码,随意指定一个字符串作为第一个命令行参数(因为还未实现搜索功能的部分)而将 *poem.txt* 文件将作为第二个参数: diff --git a/src/ch12-03-improving-error-handling-and-modularity.md b/src/ch12-03-improving-error-handling-and-modularity.md index 873f38c..27bd0a1 100644 --- a/src/ch12-03-improving-error-handling-and-modularity.md +++ b/src/ch12-03-improving-error-handling-and-modularity.md @@ -1,4 +1,4 @@ -## 重构改进模块性和错误处理 +## 重构改进模块性和错误处理 > [ch12-03-improving-error-handling-and-modularity.md](https://github.com/rust-lang/book/blob/master/src/ch12-03-improving-error-handling-and-modularity.md) >
@@ -10,7 +10,7 @@ 这同时也关系到第二个问题:`search` 和 `filename` 是程序中的配置变量,而像 `f` 和 `contents` 则用来执行程序逻辑。随着 `main` 函数的增长,就需要引入更多的变量到作用域中,而当作用域中有更多的变量时,将更难以追踪每个变量的目的。最好能将配置变量组织进一个结构,这样就能使他们的目的更明确了。 -第三个问题是如果打开文件失败我们使用 `expect` 来打印出错误信息,不过这个错误信息只是说 `file not found`。除了缺少文件之外还有很多打开文件可能失败的方式:例如,文件可能存在,不过可能没有打开它的权限。如果我们现在就出于这种情况,打印出的 `file not found` 错误信息就给了用户错误的建议! +第三个问题是如果打开文件失败我们使用 `expect` 来打印出错误信息,不过这个错误信息只是说 `file not found`。除了缺少文件之外还有很多可以导致打开文件失败的方式:例如,文件可能存在,不过可能没有打开它的权限。如果我们现在就出于这种情况,打印出的 `file not found` 错误信息就给了用户错误的建议! 第四,我们不停的使用 `expect` 来处理不同的错误,如果用户没有指定足够的参数来运行程序,他们会从 Rust 得到 `index out of bounds` 错误,而这并不能明确的解释问题。如果所有的错误处理都位于一处这样将来的维护者在需要修改错误处理逻辑时就只需要考虑这一处代码。将所有的错误处理都放在一处也有助于确保我们打印的错误信息对终端用户来说是有意义的。 @@ -106,7 +106,7 @@ fn parse_config(args: &[String]) -> Config { 示例 12-6:重构 `parse_config` 返回一个 `Config` 结构体实例 -`parse_config` 的签名表明它现在返回一个 `Config` 值。在 `parse_config` 的函数体中,之前返回引用了 `args` 中 `String` 值的字符串 slice,现在我们选择定义 `Config` 来包含拥有所有权的 `String` 值。`main` 中的 `args` 变量是参数值的所有者并只允许 `parse_config` 函数借用他们,这意味着如果 `Config` 尝试获取 `args` 中值的所有权将违反 Rust 的借用规则。 +`parse_config` 的签名表明它现在返回一个 `Config` 值。在之前的 `parse_config` 函数体中,我们返回了引用 `args` 中 `String` 值的字符串 slice,现在我们定义 `Config` 来包含拥有所有权的 `String` 值。`main` 中的 `args` 变量是参数值的所有者并只允许 `parse_config` 函数借用他们,这意味着如果 `Config` 尝试获取 `args` 中值的所有权将违反 Rust 的借用规则。 还有许多不同的方式可以处理 `String` 的数据,而最简单但有些不太高效的方式是调用这些值的 `clone` 方法。这会生成 `Config` 实例可以拥有的数据的完整拷贝,不过会比储存字符串数据的引用消耗更多的时间和内存。不过拷贝数据使得代码显得更加直白因为无需管理引用的生命周期,所以在这种情况下牺牲一小部分性能来换取简洁性的取舍是值得的。 @@ -330,7 +330,7 @@ fn run(config: Config) -> Result<(), Box> { 这里我们做出了三个明显的修改。首先,将 `run` 函数的返回类型变为 `Result<(), Box>`。之前这个函数返回 unit 类型 `()`,现在它仍然保持作为 `Ok` 时的返回值。 -对于错误类型,使用了 **trait 对象** `Box`(在开头使用了 `use` 语句将 `std::error::Error` 引入作用域)。第十七章会涉及 trait 对象。目前只需知道 `Box` 意味着函数会返回实现了 `Error` trait 的类型,不过无需指定具体将会返回的值的类型。这提供了在不同的错误场景可能有不同类型的错误返回值的灵活性。这也就是 `dyn`,它是 “动态”(“dynamic”)的缩写。 +对于错误类型,使用了 **trait 对象** `Box`(在开头使用了 `use` 语句将 `std::error::Error` 引入作用域)。第十七章会涉及 trait 对象。目前只需知道 `Box` 意味着函数会返回实现了 `Error` trait 的类型,不过无需指定具体将会返回的值的类型。这提供了在不同的错误场景可能有不同类型的错误返回值的灵活性。这也就是 `dyn`,它是 “动态的”(“dynamic”)的缩写。 第二个改变是去掉了 `expect` 调用并替换为第九章讲到的 `?`。不同于遇到错误就 `panic!`,`?` 会从函数中返回错误值并让调用者来处理它。 @@ -434,8 +434,8 @@ fn main() { 示例 12-14:将 `minigrep` crate 引入 *src/main.rs* 的作用域中 -为了将库 crate 引入二进制 crate,我们使用 `extern crate minigrep`。接着增加 `use minigrep::Config` 将 `Config` 类型引入作用域,并使用 crate 名作为 `run` 函数的前缀。通过这些重构,所有功能应该能够联系在一起并运行了。运行 `cargo run` 来确保一切都正确的衔接在一起。 +为了将库 crate 引入二进制 crate,我们使用了 `use minigrep`。接着 `use minigrep::Config` 将 `Config` 类型引入作用域,并使用 crate 名称作为 `run` 函数的前缀。通过这些重构,所有功能应该能够联系在一起并运行了。运行 `cargo run` 来确保一切都正确的衔接在一起。 哇哦!这可有很多的工作,不过我们为将来的成功打下了基础。现在处理错误将更容易,同时代码也更加模块化。从现在开始几乎所有的工作都将在 *src/lib.rs* 中进行。 -让我们利用这些新创建的模块的优势来进行一些在旧代码中难以展开的工作,他们在新代码中却很简单:编写测试! +让我们利用这些新创建的模块的优势来进行一些在旧代码中难以展开的工作,这些工作在新代码中非常容易实现,那就是:编写测试! diff --git a/src/ch12-04-testing-the-librarys-functionality.md b/src/ch12-04-testing-the-librarys-functionality.md index ea9ab3c..62737be 100644 --- a/src/ch12-04-testing-the-librarys-functionality.md +++ b/src/ch12-04-testing-the-librarys-functionality.md @@ -1,4 +1,4 @@ -## 采用测试驱动开发完善库的功能 +## 采用测试驱动开发完善库的功能 > [ch12-04-testing-the-librarys-functionality.md](https://github.com/rust-lang/book/blob/master/src/ch12-04-testing-the-librarys-functionality.md) >
diff --git a/src/ch12-05-working-with-environment-variables.md b/src/ch12-05-working-with-environment-variables.md index 40daf8a..b90174e 100644 --- a/src/ch12-05-working-with-environment-variables.md +++ b/src/ch12-05-working-with-environment-variables.md @@ -1,10 +1,10 @@ -## 处理环境变量 +## 处理环境变量 > [ch12-05-working-with-environment-variables.md](https://github.com/rust-lang/book/blob/master/src/ch12-05-working-with-environment-variables.md) >
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -我们将增加一个额外的功能来改进 `minigrep`:一个通过环境变量启用的大小写不敏感搜索的选项。可以将其设计为一个命令行参数并要求用户每次需要时都加上它,不过相反我们将使用环境变量。这允许用户设置环境变量一次之后在整个终端会话中所有的搜索都将是大小写不敏感的。 +我们将增加一个额外的功能来改进 `minigrep`:用户可以通过设置环境变量来设置搜索是否是大小写敏感的 。当然,我们也可以将其设计为一个命令行参数并要求用户每次需要时都加上它,不过在这里我们将使用环境变量。这允许用户设置环境变量一次之后在整个终端会话中所有的搜索都将是大小写不敏感的。 ### 编写一个大小写不敏感 `search` 函数的失败测试 @@ -199,7 +199,7 @@ How dreary to be somebody! 看起来程序仍然能够工作!现在将 `CASE_INSENSITIVE` 设置为 `1` 并仍使用相同的查询 `to`。 -如果你使用 PowerShell,则需要用两句命令而不是一句来设置环境变量并运行程序: +如果你使用 PowerShell,则需要用两个命令来设置环境变量并运行程序: ```text $ $env:CASE_INSENSITIVE=1 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 3481d95..53ea29d 100644 --- a/src/ch12-06-writing-to-stderr-instead-of-stdout.md +++ b/src/ch12-06-writing-to-stderr-instead-of-stdout.md @@ -1,4 +1,4 @@ -## 将错误信息输出到标准错误而不是标准输出 +## 将错误信息输出到标准错误而不是标准输出 > [ch12-06-writing-to-stderr-instead-of-stdout.md](https://github.com/rust-lang/book/blob/master/src/ch12-06-writing-to-stderr-instead-of-stdout.md) >
@@ -10,11 +10,11 @@ ### 检查错误应该写入何处 -首先,让我们观察一下目前 `minigrep` 打印的所有内容都被写入了标准输出,包括应该被写入标准错误的错误信息。可以通过将标准输出流重定向到一个文件同时有意产生一个错误来做到这一点。我们没有重定向标准错误流,所以任何发送到标准错误的内容将会继续显示在屏幕上。 +首先,让我们观察一下目前 `minigrep` 打印的所有内容是如何被写入标准输出的,包括那些应该被写入标准错误的错误信息。可以通过将标准输出流重定向到一个文件同时有意产生一个错误来做到这一点。我们没有重定向标准错误流,所以任何发送到标准错误的内容将会继续显示在屏幕上。 命令行程序被期望将错误信息发送到标准错误流,这样即便选择将标准输出流重定向到文件中时仍然能看到错误信息。目前我们的程序并不符合期望;相反我们将看到它将错误信息输出保存到了文件中。 -展示这种行为的方式是通过 `>` 和文件名 *output.txt* 来与运行程序,这个文件是期望重定向标准输出流的位置。并不传递任何参数应该会产生一个错误: +我们通过 `>` 和文件名 *output.txt* 来运行程序,我们期望重定向标准输出流到该文件中。在这里,我们没有传递任何参数,所以会产生一个错误: ```text $ cargo run > output.txt @@ -26,7 +26,7 @@ $ cargo run > output.txt Problem parsing arguments: not enough arguments ``` -是的,错误信息被打印到了标准输出中。像这样的错误信息被打印到标准错误中将有用的多,并在重定向标准输出时只将成功运行的信息写入文件。我们将改变他们。 +是的,错误信息被打印到了标准输出中。像这样的错误信息被打印到标准错误中将会有用的多,并且只将成功运行的信息写入文件。接下来我们将对程序进行修改从而以这种方式进行输出。 ### 将错误打印到标准错误 From b3cbc346f87971477e7b5638a4b4cb996f5d3a56 Mon Sep 17 00:00:00 2001 From: KaiserY Date: Tue, 11 Dec 2018 01:10:23 +0800 Subject: [PATCH 40/49] check to appendix-05 --- src/appendix-04-useful-development-tools.md | 167 +++++++++++++++++++- src/appendix-05-editions.md | 24 ++- 2 files changed, 188 insertions(+), 3 deletions(-) diff --git a/src/appendix-04-useful-development-tools.md b/src/appendix-04-useful-development-tools.md index 9c0d95f..1d579df 100644 --- a/src/appendix-04-useful-development-tools.md +++ b/src/appendix-04-useful-development-tools.md @@ -1,5 +1,168 @@ -## 附录 E:实用开发工具 +## 附录 D:实用开发工具 > [appendix-04-useful-development-tools.md](https://github.com/rust-lang/book/blob/master/src/appendix-04-useful-development-tools.md) >
-> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f \ No newline at end of file +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f + +本附录,我们将讨论 Rust 项目提供的用于开发 Rust 代码的工具。 + +## 通过 `rustfmt` 自动格式化 + +`rustfmt` 工具根据社区代码风格格式化代码。很多项目使用 `rustfmt` 来避免编写 Rust 风格的争论:所有人都用这个工具格式化代码! + +`rustfmt` 工具的质量还未达到发布 1.0 版本的水平,不过目前有一个可用的预览版。请尝试使用并告诉我们它如何! + +安装 `rustfmt`: + +```text +$ rustup component add rustfmt-preview +``` + +这会提供 `rustfmt` 和 `cargo-fmt`,类似于 Rust 同时安装 `rustc` 和 `cargo`。为了格式化整个 Cargo 项目: + +```text +$ cargo fmt +``` + +运行此命令会格式化当前 crate 中所有的 Rust 代码。这应该只会改变代码风格,而不是代码语义。请查看 [该文档][rustfmt] 了解 `rustfmt` 的更多信息。 + +[rustfmt]: https://github.com/rust-lang-nursery/rustfmt + +## 通过 `rustfix` 修复代码 + +如果你编写过 Rust 代码,那么你可能见过编译器警告。例如,考虑如下代码: + +文件名: src/main.rs + +```rust +fn do_something() {} + +fn main() { + for i in 0..100 { + do_something(); + } +} +``` + +这里调用了 `do_something` 函数 100 次,不过从未在 `for` 循环体中使用变量 `i`。Rust 会警告说: + +```text +$ cargo build + Compiling myprogram v0.1.0 (file:///projects/myprogram) +warning: unused variable: `i` + --> src/main.rs:4:9 + | +4 | for i in 1..100 { + | ^ help: consider using `_i` instead + | + = note: #[warn(unused_variables)] on by default + + Finished dev [unoptimized + debuginfo] target(s) in 0.50s +``` + +警告中建议使用 `_i` 名称:下划线表明该变量有意不使用。我们可以通过 `cargo fix` 命令使用 `rustfix` 工具来自动采用该建议: + +```text +$ cargo fix + Checking myprogram v0.1.0 (file:///projects/myprogram) + Fixing src/main.rs (1 fix) + Finished dev [unoptimized + debuginfo] target(s) in 0.59s +``` + +如果再次查看 *src/main.rs*,会发现 `cargo fix` 修改了代码: + +文件名: src/main.rs + +```rust +fn do_something() {} + +fn main() { + for _i in 0..100 { + do_something(); + } +} +``` + +现在 `for` 循环变量变为 `_i`,警告也不再出现。 + +`cargo fix` 命令可以用于在不同 Rust 版本间迁移代码。版本在附录 E 中介绍。 + +## 通过 `clippy` 提供更多 lint 功能 + +`clippy` 工具是一系列 lint 的集合,用于捕捉常见错误和改进 Rust 代码。 + +`clippy` 工具的质量还未达到发布 1.0 版本的水平,不过目前有一个可用的预览版。请尝试使用并告诉我们它如何! + +安装 `clippy`: + +```text +$ rustup component add clippy-preview +``` + +对任何 Cargo 项目运行 clippy 的 lint: + +```text +$ cargo clippy +``` + +例如,如果程序使用了如 pi 这样数学常数的近似值,如下: + +文件名: src/main.rs + +```rust +fn main() { + let x = 3.1415; + let r = 8.0; + println!("the area of the circle is {}", x * r * r); +} +``` + +在此项目上运行 `cargo clippy` 会导致这个错误: + +```text +error: approximate value of `f{32, 64}::consts::PI` found. Consider using it directly + --> src/main.rs:2:13 + | +2 | let x = 3.1415; + | ^^^^^^ + | + = note: #[deny(clippy::approx_constant)] on by default + = help: for further information visit https://rust-lang-nursery.github.io/rust-clippy/v0.0.212/index.html#approx_constant +``` + +这告诉我们 Rust 定义了更为精确的常量,而如果使用了这些常量程序将更加准确。如下代码就不会导致 `clippy` 产生任何错误或警告: + +文件名: src/main.rs + +```rust +fn main() { + let x = std::f64::consts::PI; + let r = 8.0; + println!("the area of the circle is {}", x * r * r); +} +``` + +请查看 [其文档][clippy] 来了解 `clippy` 的更多信息。 + +[clippy]: https://github.com/rust-lang-nursery/rust-clippy + +## 使用 Rust Language Server 的 IDE 集成 + +为了帮助 IDE 集成,Rust 项目分发了 `rls`,其为 Rust Language Server 的缩写。这个工具采用 [Language Server Protocol][lsp],这是一个 IDE 与编程语言沟通的规格说明。`rls` 可以用于不同的客户端,比如 [Visual Studio: Code 的 Rust 插件][vscode]。 + +[lsp]: http://langserver.org/ +[vscode]: https://marketplace.visualstudio.com/items?itemName=rust-lang.rust + +`rls` 工具的质量还未达到发布 1.0 版本的水平,不过目前有一个可用的预览版。请尝试使用并告诉我们它如何! + +安装 `rls`: + +```text +$ rustup component add rls-preview +``` + +接着为特定的 IDE 安装 language server 支持,如比便会获得如自动补全、跳转到定义和 inline error 之类的功能。 + +请查看 [其文档][rls] 来了解 `rls` 的更多信息。 + +[rls]: https://github.com/rust-lang-nursery/rls \ No newline at end of file diff --git a/src/appendix-05-editions.md b/src/appendix-05-editions.md index 51bb588..3af7c35 100644 --- a/src/appendix-05-editions.md +++ b/src/appendix-05-editions.md @@ -2,4 +2,26 @@ > [appendix-05-editions.md](https://github.com/rust-lang/book/blob/master/src/appendix-05-editions.md) >
-> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f \ No newline at end of file +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f + +早在第一章,我们见过 `cargo new` 在 *Cargo.toml* 中增加了一些有关 `edition` 的元数据。本附录将解释其意义! + +Rust 语言和编译器有一个为期 6 周的发布循环。这意味着用户会稳定得到新功能的更新。其他编程语言发布大更新但不甚频繁;Rust 选择更为频繁的发布小更新。一段时间之后,所有这些小更新会日积月累。不过随着版本的发布,很难回顾说 “哇,从 Rust 1.10 到 Rust 1.31,Rust 的变化真大!” + +每两到三年,Rust 团队会生成一个新的 Rust **版本**(*edition*)。每一个版本会结合已经落地的功能,并提供一个清晰的带有完整更新文档和工具的功能包。新版本会作为常规的 6 周发布过程的一部分发布。 + +这为不同的人群提供了不同的功能: + +* 对于活跃的 Rust 用户,其将增量的修改与易于理解的功能包相结合。 +* 对于非用户,它表明发布了一些重大进展,这意味着 Rust 可能变得值得一试。 +* 对于 Rust 自身开发者,其提供了项目整体的集合点。 + +在本文档编写时,Rust 有两个版本:Rust 2015 和 Rust 2018。本书基于 Rust 2018 edition 编写。 + +*Cargo.toml* 中的 `edition` 字段表明代码应该使用哪个版本编译。如果该字段不存在,其默认为 `2015` 以提供后向兼容性。 + +每个项目都可以选择不同于默认的 2015 edition 的版本。这样,版本可能会包含不兼容的修改,比如新增关键字可能会与代码中的标识符冲突并导致错误。不过除非选择兼容这些修改,(旧)代码仍将能够编译,即便升级了 Rust 编译器的版本。所有 Rust 编译器都支持任何之前存在的编译器版本,并可以链接人恶化支持版本的 crate。编译器修改只影响最初的解析代码的过程。因此,如果你使用 Rust 2015 而某个依赖使用 Rust 2018,你的项目仍旧能够编译并使用该依赖。反之,若项目使用 Rust 2018 而依赖使用 Rust 2015 亦可工作。 + +有一点需要明确:大部分功能在所有版本中都能使用。开发者使用任何 Rust 版本将能继续接收最新稳定版的改进。然而在一些情况,主要是增加了新关键字的时候,则可能出现了只能用于新版本的功能。只需切换版本即可利用新版本的功能。 + +请查看 [Edition Guide](https://rust-lang-nursery.github.io/edition-guide/) 了解更多细节,这是一个完全介绍版本的书籍,包括如何通过 `cargo fix` 自动将代码迁移到新版本。 \ No newline at end of file From 37bc20245a26b3be31296a114b3291e74714f949 Mon Sep 17 00:00:00 2001 From: KaiserY Date: Tue, 11 Dec 2018 10:16:32 +0800 Subject: [PATCH 41/49] check to appendix-07 --- src/appendix-07-nightly-rust.md | 112 +++++++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 1 deletion(-) diff --git a/src/appendix-07-nightly-rust.md b/src/appendix-07-nightly-rust.md index fc48b22..23acb6c 100644 --- a/src/appendix-07-nightly-rust.md +++ b/src/appendix-07-nightly-rust.md @@ -2,4 +2,114 @@ > [appendix-07-nightly-rust.md](https://github.com/rust-lang/book/blob/master/src/appendix-07-nightly-rust.md) >
-> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f \ No newline at end of file +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f + +本附录介绍 Rust 是如何开发的以及这如何影响作为 Rust 开发者的你。 + +### 无停滞稳定 + +作为一个语言,Rust **十分** 注重代码的稳定性。我们希望 Rust 成为你代码坚实的基础,不过如果事物不断改变的话,这也将是不可能的。同时,当事物不再改变时,如果不能实验新功能的话,在发布之前我们也无法发现其中重大的缺陷。 + +对于这个问题我们的解决方案被称为 “无停滞稳定”(“stability without stagnation”),其指导性原则是:无需担心升级到最新的稳定版 Rust。每次升级应该是无痛的,并应带来新功能,更少的 bug 和更快的编译速度。 + +### Choo, Choo!(开车啦,逃)发布通道和发布时刻表(Riding the Trains) + +Rust 开发运行于一个 **发布时刻表**(*train schedule*)之上。也就是说,所有的开发工作都位于 Rust 仓库的 `master` 分支。发布采用 software release train 模型,其被用于 思科 IOS 等其它软件项目。Rust 有三个 **发布通道**(*release channel*): + +* Nightly +* Beta +* Stable (稳定版) + +大部分 Rust 开发者主要采用稳定版通道,不过希望实验新功能的开发者可能会使用 nightly 或 beta 版。 + +如下是一个开发和发布过程如何运转的例子:假设 Rust 团队正在进行 Rust 1.5 的发布工作。该版本发布于 2015 年 12 月,不过这里只是为了提供一个真实的版本。Rust 新增了一项功能:一个 `master` 分支的新提交。每天晚上,会产生一个新的 nightly 版本。每天都是发布版本的日子,而这些发布由发布基础设施自动完成。所以随着时间推移,发布轨迹看起来像这样,版本一天一发: + +```text +nightly: * - - * - - * +``` + +每 6 周时间,是准备发布新版本的时候了!Rust 仓库的 `beta` 分支会从用于 nightly 的 `master` 分支产生。现在,有了两个发布版本: + +```text +nightly: * - - * - - * + | +beta: * +``` + +大部分 Rust 用户不会主要使用 beta 版本,不过在 CI 系统中对 beta 版本进行测试能够帮助 Rust 发现可能的回归缺陷(regression)。同时,每天仍产生 nightly 发布: + +```text +nightly: * - - * - - * - - * - - * + | +beta: * +``` + +第一个 beta 版的 6 周后,是发布稳定版的时候了!`stable` 分支从 `beta` 分支生成: + +```text +nightly: * - - * - - * - - * - - * - - * - * - * + | +beta: * - - - - - - - - * + | +stable: * +``` + +好的!Rust 1.5 发布了!然而,我们忘了些东西:因为又过了 6 周,我们还需发布 **新版** Rust 的 beta 版,Rust 1.6。所以从 `stable` 生成 `beta` 分支后,新版的 `beta` 分之也再次从 `nightly` 生成: + +```text +nightly: * - - * - - * - - * - - * - - * - * - * + | | +beta: * - - - - - - - - * * + | +stable: * +``` + +这被称为 “train model”,因为每 6 周,一个版本 “离开车站”(“leaves the station”),不过从 beta 通道到达稳定通道还有一段旅程。 + +Rust 每 6周发布一个版本,如时钟版准确。如果你知道了某个 Rust 版本的发布时间,就可以知道下个版本的时间:6 周后。每 6 周发布版本的一个好的方面是下一班车会来得更快。如果特定版本碰巧缺失某个功能也无需担心:另一个版本很快就会到来!这有助于减少因临近发版时间而偷偷释出未经完善的功能的压力。 + +多亏了这个过程,你总是可以切换到下一版本的 Rut 并验证是否可以轻易的升级:如果 beta 版不能如期工作,你可以向 Rust 团队报告并在发布稳定版之前得到修复!beta 版造成的破坏是非常少见的,不过 `rustc` 也不过是一个软件,可能会存在 bug。 + +### 不稳定功能 + +这个发布模型中另一个值得注意的地方:不稳定功能(unstable features)。Rust 使用一个被称为 “功能标记”(“feature flags”)的技术来确定给定版本的某个功能是否启用。如果新功能正在积极底开发中,其提交到了 `master`,因此会出现在 nightly 版中,不过会位于一个 **功能标记** 之后。作为用户,如果你希望尝试这个正在开发的功能,则可以在源码中使用合适的标记来开启,不过必须使用 nightly 版。 + +如果使用的是 beta 或稳定版 Rust,则不能使用任何功能标记。这是在新功能被标记未永久稳定之前获得实用价值的关键。这既满足了希望实用最尖端技术的同学,那些坚持稳定版的同学也知道其代码不会被破坏。这就是无停滞稳定。 + +本书只包含稳定的功能,因为还在开发中的功能仍可能改变,当其进入稳定版时肯定会与编写本书的时候有所不同。你可以在网上获取 nightly 版的文档。 + +### Rustup 和 Rust Nightly 的职责 + +Rustup 使得改变不同发布通道的 Rust 更为简单,其在全局或分项目的层次工作。其默认会安装稳定版 Rust。例如为了安装 nightly: + +```text +$ rustup install nightly +``` + +你会发现 `rustup` 也安装了所有的 **工具链**(*toolchains*, Rust 和其相关组件)。如下是一位作者的 Windows 计算机上的例子: + +```powershell +> rustup toolchain list +stable-x86_64-pc-windows-msvc (default) +beta-x86_64-pc-windows-msvc +nightly-x86_64-pc-windows-msvc +``` + +如你所见,默认是稳定版。大部分 Rust 用户在大部分时间使用稳定版。你可能也会这么做,不过如果你关心最新的功能,可以为特定项目使用 nightly 版。为此,可以在项目目录使用 `rustup override` 来设置当前目录 `rustup`使用 nightly 工具链作: + +```text +$ cd ~/projects/needs-nightly +$ rustup override set nightly +``` + +现在,每次在 *~/projects/needs-nightly* 调用 `rustc` 或 `cargo`,`rustup` 会确保使用 nightly 版 Rust。在这有很多 Rust 项目时大有裨益! + +### RFC 过程和团队 + +那么你如何了解这些新功能呢?Rust 开发模式遵循一个 **Request For Comments (RFC) 过程**。如果你希望改进 Rust,可以编写一个提议,也就是 RFC。 + +任何人都可以编写 RFC 来改进 Rust,同时这些 RFC 会被 Rust 团队评审和讨论,他们由很多不同分工的子团队组成。这里是 [Rust 官网上](https://www.rust-lang.org/en-US/team.html) 所有团队的总列表,其包含了项目中每个领域的团队:语言设计、编译器实现、基础设施、文档等。个个团队会阅读相应的提议和评论,编写回复,并最终达成接受或回绝功能的一致。 + +如果功能被接受了,在 Rust 仓库会打开一个 issue,人们就可以实现它。实现功能的人当人可能不是最初提议功能的人!当实现完成后,其会合并到 `master` 分支并位于一个功能开关(feature gate)之后,正如 [“不稳定功能”(#unstable-features) 部分所讨论的。 + +在稍后的某个时间,一旦使用 nightly 版的 Rust 团队能够尝试这个功能了,团队成员会讨论这个功能,它如何在 nightly 中工作,并决定是否应该进入稳定版。如果决定继续推进,功能开关会移除,然后这个功能就被认为是稳定的了!并会在新的稳定版 Rust 中出现。 \ No newline at end of file From 4f95bca423fc9f50bf8b01f3aa623586f6678dd6 Mon Sep 17 00:00:00 2001 From: Lam <15622383059@163.com> Date: Wed, 12 Dec 2018 11:26:44 +0800 Subject: [PATCH 42/49] improve some translation and fix typo in ch13 --- src/ch13-01-closures.md | 26 ++++++++++++------------- src/ch13-02-iterators.md | 6 +++--- src/ch13-03-improving-our-io-project.md | 12 ++++++------ src/ch13-04-performance.md | 4 ++-- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/ch13-01-closures.md b/src/ch13-01-closures.md index 1914347..278f942 100644 --- a/src/ch13-01-closures.md +++ b/src/ch13-01-closures.md @@ -55,7 +55,7 @@ fn main() { 示例 13-2:`main` 函数包含了用于 `generate_workout` 函数的模拟用户输入和模拟随机数输入 -处于简单考虑这里硬编码了 `simulated_user_specified_value` 变量的值为 10 和 `simulated_random_number` 变量的值为 7;一个实际的程序会从 app 前端获取强度系数并使用 `rand` crate 来生成随机数,正如第二章的猜猜看游戏所做的那样。`main` 函数使用模拟的输入值调用 `generate_workout` 函数: +出于简单考虑这里硬编码了 `simulated_user_specified_value` 变量的值为 10 和 `simulated_random_number` 变量的值为 7;一个实际的程序会从 app 前端获取强度系数并使用 `rand` crate 来生成随机数,正如第二章的猜猜看游戏所做的那样。`main` 函数使用模拟的输入值调用 `generate_workout` 函数: 现在有了执行上下文,让我们编写算法。示例 13-3 中的 `generate_workout` 函数包含本例中我们最关心的 app 业务逻辑。本例中余下的代码修改都将在这个函数中进行: @@ -96,7 +96,7 @@ fn generate_workout(intensity: u32, random_number: u32) { 示例 13-3:程序的业务逻辑,它根据输入并调用 `simulated_expensive_calculation` 函数来打印出健身计划 -示例 13-3 中的代码有多处慢计算函数的调用。第一个 `if` 块调用了 `simulated_expensive_calculation` 两次,外部 `else` 中的 `if` 完全没有调用它,第二个 `else` 中的代码调用了它一次。 +示例 13-3 中的代码有多处调用了慢计算函数 `simulated_expensive_calculation` 。第一个 `if` 块调用了 `simulated_expensive_calculation` 两次, `else` 中的 `if` 没有调用它,而第二个 `else` 中的代码调用了它一次。 @@ -178,7 +178,7 @@ let expensive_closure = |num| { 闭包定义是 `expensive_closure` 赋值的 `=` 之后的部分。闭包的定义以一对竖线(`|`)开始,在竖线中指定闭包的参数;之所以选择这个语法是因为它与 Smalltalk 和 Ruby 的闭包定义类似。这个闭包有一个参数 `num`;如果有多于一个参数,可以使用逗号分隔,比如 `|param1, param2|`。 -参数之后是存放闭包体的大括号 —— 如果闭包体只有一行则大括号是可以省略的。大括号之后闭包的结尾,需要用于 `let` 语句的分号。闭包体的最后一行(`num`)返回的值将是调用闭包时返回的值,因为最后一行没有分号;正如函数体中的一样。 +参数之后是存放闭包体的大括号 —— 如果闭包体只有一行则大括号是可以省略的。大括号之后闭包的结尾,需要用于 `let` 语句的分号。因为闭包体的最后一行没有分号(正如函数体一样),所以其返回了值 `num` 。 注意这个 `let` 语句意味着 `expensive_closure` 包含一个匿名函数的 **定义**,不是调用匿名函数的 **返回值**。回忆一下使用闭包的原因是我们需要在一个位置定义代码,储存代码,并在之后的位置实际调用它;期望调用的代码现在储存在 `expensive_closure` 中。 @@ -223,7 +223,7 @@ fn generate_workout(intensity: u32, random_number: u32) { 现在耗时的计算只在一个地方被调用,并只会在需要结果的时候执行改代码。 -然而,我们又重新引入了示例 13-3 中的问题:仍然在第一个 `if` 块中调用了闭包两次,这会调用慢计算两次并使用户多等待一倍的时间。可以通过在 `if` 块中创建一个本地变量存放闭包调用的结果来解决这个问题,不过正因为使用了闭包还有另一个解决方案。稍后会回到这个方案上。首先讨论一下为何闭包定义中和所涉及的 trait 中没有类型注解。 +然而,我们又重新引入了示例 13-3 中的问题:仍然在第一个 `if` 块中调用了闭包两次,这调用了慢计算代码两次而使得用户需要多等待一倍的时间。可以通过在 `if` 块中创建一个本地变量存放闭包调用的结果来解决这个问题,不过闭包可以提供另外一种解决方案。我们稍后会讨论这个方案,不过目前让我们首先讨论一下为何闭包定义中和所涉及的 trait 中没有类型注解。 ### 闭包类型推断和注解 @@ -296,9 +296,9 @@ error[E0308]: mismatched types 幸运的是,还有另一个可用的方案。可以创建一个存放闭包和调用闭包结果的结构体。该结构体只会在需要结果时执行闭包,并会缓存结果值,这样余下的代码就不必再负责保存结果并可以复用该值。你可能见过这种模式被称 *memoization* 或 *lazy evaluation*。 -为了让结构体存放闭包,我们需要能够指定闭包的类型,因为结构体定义需要知道其每一个字段的类型。每一个闭包实例有其自己独有的匿名类型:也就是说,即便两个闭包有着相同的签名,他们的类型仍然可以被认为是不同。为了定义使用闭包的结构体、枚举或函数参数,需要像第十章讨论的那样使用泛型和 trait bound。 +为了让结构体存放闭包,我们需要指定闭包的类型,因为结构体定义需要知道其每一个字段的类型。每一个闭包实例有其自己独有的匿名类型:也就是说,即便两个闭包有着相同的签名,他们的类型仍然可以被认为是不同。为了定义使用闭包的结构体、枚举或函数参数,需要像第十章讨论的那样使用泛型和 trait bound。 -`Fn` 系列 trait 由标准库提供。所有的闭包都实现了 trait `Fn`、`FnMut` 或 `FnOnce` 中的一个。在“闭包会捕获其环境”部分捕获环境部分我们会讨论这些 trait 的区别;在这个例子中可以使用 `Fn` trait。 +`Fn` 系列 trait 由标准库提供。所有的闭包都实现了 trait `Fn`、`FnMut` 或 `FnOnce` 中的一个。在“闭包会捕获其环境”部分我们会讨论这些 trait 的区别;在这个例子中可以使用 `Fn` trait。 为了满足 `Fn` trait bound 我们增加了代表闭包所必须的参数和返回值类型的类型。在这个例子中,闭包有一个 `u32` 的参数并返回一个 `u32`,这样所指定的 trait bound 就是 `Fn(u32) -> u32`。 @@ -319,7 +319,7 @@ struct Cacher 结构体 `Cacher` 有一个泛型 `T` 的字段 `calculation`。`T` 的 trait bound 指定了 `T` 是一个使用 `Fn` 的闭包。任何我们希望储存到 `Cacher` 实例的 `calculation` 字段的闭包必须有一个 `u32` 参数(由 `Fn` 之后的括号的内容指定)并必须返回一个 `u32`(由 `->` 之后的内容)。 -> 注意:函数也都实现了这三个 `Fn` trait。如果不需要捕获环境中的值,则在需要实现 `Fn` trait 是可以使用函数而不是闭包。 +> 注意:函数也都实现了这三个 `Fn` trait。如果不需要捕获环境中的值,则可以使用实现了 `Fn` trait 的函数而不是闭包。 `value` 是 `Option` 类型的。在执行闭包之前,`value` 将是 `None`。如果使用 `Cacher` 的代码请求闭包的结果,这时会执行闭包并将结果储存在 `value` 字段的 `Some` 成员中。接着如果代码再次请求闭包的结果,这时不再执行闭包,而是会返回存放在 `Some` 成员中的结果。 @@ -442,7 +442,7 @@ fn generate_workout(intensity: u32, random_number: u32) { ### `Cacher` 实现的限制 -值缓存是一种更加广泛的实用行为,我们可能希望在代码中的其他闭包中也使用他们。然而,目前 `Cacher` 的实现存在一些小问题,这使得在不同上下文中复用变得很困难。 +值缓存是一种更加广泛的实用行为,我们可能希望在代码中的其他闭包中也使用他们。然而,目前 `Cacher` 的实现存在两个小问题,这使得在不同上下文中复用变得很困难。 第一个问题是 `Cacher` 实例假设对于 `value` 方法的任何 `arg` 参数值总是会返回相同的值。也就是说,这个 `Cacher` 的测试会失败: @@ -470,7 +470,7 @@ thread 'call_with_different_values' panicked at 'assertion failed: `(left == rig 这里的问题是第一次使用 1 调用 `c.value`,`Cacher` 实例将 `Some(1)` 保存进 `self.value`。在这之后,无论传递什么值调用 `value`,它总是会返回 1。 -尝试修改 `Cacher` 存放一个哈希 map 而不是单独一个值。哈希 map 的 key 将是传递进来的 `arg` 值,而 value 则是对应 key 调用闭包的结果值。相比之前检查 `self.value` 直接是 `Some` 还是 `None` 值,现在 `value` 会在哈希 map 中寻找 `arg`,如果存在就返回它。如果不存在,`Cacher` 会调用闭包并将结果值保存在哈希 map 对应 `arg` 值的位置。 +尝试修改 `Cacher` 存放一个哈希 map 而不是单独一个值。哈希 map 的 key 将是传递进来的 `arg` 值,而 value 则是对应 key 调用闭包的结果值。相比之前检查 `self.value` 直接是 `Some` 还是 `None` 值,现在 `value` 函数会在哈希 map 中寻找 `arg`,如果找到的话就返回其对应的值。如果不存在,`Cacher` 会调用闭包并将结果值保存在哈希 map 对应 `arg` 值的位置。 当前 `Cacher` 实现的第二个问题是它的应用被限制为只接受获取一个 `u32` 值并返回一个 `u32` 值的闭包。比如说,我们可能需要能够缓存一个获取字符串 slice 并返回 `usize` 值的闭包的结果。请尝试引入更多泛型参数来增加 `Cacher` 功能的灵活性。 @@ -527,7 +527,7 @@ error[E0434]: can't capture dynamic environment in a fn item; use the || { ... 编译器甚至会提示我们这只能用于闭包! -当闭包从环境中捕获一个值,闭包会在闭包体中储存这个值以供使用。这会使用内存并产生额外的开销,当执行不会捕获环境的更通用的代码场景中我们不希望有这些开销。因为函数从未允许捕获环境,定义和使用函数也就从不会有这些额外开销。 +当闭包从环境中捕获一个值,闭包会在闭包体中储存这个值以供使用。这会使用内存并产生额外的开销,在更一般的场景中,当我们不需要闭包来捕获环境时,我们不希望产生这些开销。因为函数从未允许捕获环境,定义和使用函数也就从不会有这些额外开销。 闭包可以通过三种方式捕获其环境,他们直接对应函数的三种获取参数的方式:获取所有权,可变借用和不可变借用。这三种捕获值的方式被编码为如下三个 `Fn` trait: @@ -557,7 +557,7 @@ fn main() { } ``` -这个例子并不能编译: +这个例子并不能编译,会产生以下错误: ```text error[E0382]: use of moved value: `x` @@ -575,6 +575,6 @@ error[E0382]: use of moved value: `x` `x` 被移动进了闭包,因为闭包使用 `move` 关键字定义。接着闭包获取了 `x` 的所有权,同时 `main` 就不再允许在 `println!` 语句中使用 `x` 了。去掉 `println!` 即可修复问题。 -大部分需要指定一个 `Fn` trait bound 的时候,可以从 `Fn` 开始,而编译器会根据闭包体中的情况告诉你是否需要 `FnMut` 或 `FnOnce`。 +大部分需要指定一个 `Fn` 系列 trait bound 的时候,可以从 `Fn` 开始,而编译器会根据闭包体中的情况告诉你是否需要 `FnMut` 或 `FnOnce`。 -为了展示闭包作为函数参数时捕获其环境的作用,让我们移动到下一个主题:迭代器。 +为了展示闭包作为函数参数时捕获其环境的作用,让我们继续下一个主题:迭代器。 diff --git a/src/ch13-02-iterators.md b/src/ch13-02-iterators.md index 9a18967..97f028a 100644 --- a/src/ch13-02-iterators.md +++ b/src/ch13-02-iterators.md @@ -6,7 +6,7 @@ 迭代器模式允许你对一个项的序列进行某些处理。**迭代器**(*iterator*)负责遍历序列中的每一项和决定序列何时结束的逻辑。当使用迭代器时,我们无需重新实现这些逻辑。 -在 Rust 中,迭代器是 **惰性的**(*lazy*),这意味着直到调用方法消费迭代器之前它都不会有效果。例如,示例 13-13 中的代码通过调用定义于 `Vec` 上的 `iter` 方法在一个 vector `v1` 上创建了一个迭代器。这段代码本身没有任何用处: +在 Rust 中,迭代器是**惰性的**(*lazy*),这意味着在调用方法使用迭代器之前它都不会有效果。例如,示例 13-13 中的代码通过调用定义于 `Vec` 上的 `iter` 方法在一个 vector `v1` 上创建了一个迭代器。这段代码本身没有任何用处: ```rust let v1 = vec![1, 2, 3]; @@ -16,7 +16,7 @@ let v1_iter = v1.iter(); 示例 13-13:创建一个迭代器 -一旦创建迭代器之后,可以选择用多种方式利用它。在示例 3-4 中,我们使用迭代器和 `for` 循环在每一个项上执行了一些代码,虽然直到现在为止我们一直掩盖了 `iter` 调用做了什么。 +一旦创建迭代器之后,可以选择用多种方式利用它。在示例 3-4 中,我们使用迭代器和 `for` 循环在每一个项上执行了一些代码,虽然直到现在为止我们一直没有具体讨论调用 `iter` 到底具体做了什么。 示例 13-14 中的例子将迭代器的创建和 `for` 循环中的使用分开。迭代器被储存在 `v1_iter` 变量中,而这时没有进行迭代。一旦 `for` 循环开始使用 `v1_iter`,接着迭代器中的每一个元素被用于循环的一次迭代,这会打印出其每一个值: @@ -197,7 +197,7 @@ fn filters_by_size() { `shoes_in_my_size` 函数获取一个鞋子 vector 的所有权和一个鞋子大小作为参数。它返回一个只包含指定大小鞋子的 vector。 -在 `shoes_in_my_size` 函数体中调用了 `into_iter` 来创建一个获取 vector 所有权的迭代器。接着调用 `filter` 将这个迭代器适配成只含有闭包返回 `true` 元素的新迭代器。 +`shoes_in_my_size` 函数调用了 `into_iter` 来创建一个获取 vector 所有权的迭代器。接着调用 `filter` 将这个迭代器适配成一个只含有那些闭包返回 `true` 的元素的新迭代器。 闭包从环境中捕获了 `shoe_size` 变量并使用其值与每一只鞋的大小作比较,只保留指定大小的鞋子。最终,调用 `collect` 将迭代器适配器返回的值收集进一个 vector 并返回。 diff --git a/src/ch13-03-improving-our-io-project.md b/src/ch13-03-improving-our-io-project.md index 626a17d..3c9adb1 100644 --- a/src/ch13-03-improving-our-io-project.md +++ b/src/ch13-03-improving-our-io-project.md @@ -31,11 +31,11 @@ impl Config { 示例 13-24:重现第十二章结尾的 `Config::new` 函数 -这时可以不必担心低效的 `clone` 调用了,因为将来可以去掉他们。好吧,就是现在! +那时我们说过不必担心低效的 `clone` 调用了,因为将来可以对他们进行改进。好吧,就是现在! 起初这里需要 `clone` 的原因是参数 `args` 中有一个 `String` 元素的 slice,而 `new` 函数并不拥有 `args`。为了能够返回 `Config` 实例的所有权,我们需要克隆 `Config` 中字段 `query` 和 `filename` 的值,这样 `Config` 实例就能拥有这些值。 -通过迭代器的新知识,我们可以将 `new` 函数改为获取一个有所有权的迭代器作为参数而不是借用 slice。我们将使用迭代器功能之前检查 slice 长度和索引特定位置的代码。这会明确 `Config::new` 的工作因为迭代器会负责访问这些值。 +在学习了迭代器之后,我们可以将 `new` 函数改为获取一个有所有权的迭代器作为参数而不是借用 slice。我们将使用迭代器功能之前检查 slice 长度和索引特定位置的代码。这会明确 `Config::new` 的工作因为迭代器会负责访问这些值。 一旦 `Config::new` 获取了迭代器的所有权并不再使用借用的索引操作,就可以将迭代器中的 `String` 值移动到 `Config` 中,而不是调用 `clone` 分配新的空间。 @@ -58,7 +58,7 @@ fn main() { } ``` -我们会修改第十二章结尾示例 12-24 中的 `main` 函数的开头为示例 13-25 中的代码。直到同步更新 `Config::new` 之前这些代码还不能编译: +修改第十二章结尾示例 12-24 中的 `main` 函数的开头为示例 13-25 中的代码。在更新 `Config::new` 之前这些代码还不能编译: 文件名: src/main.rs @@ -134,7 +134,7 @@ impl Config { ### 使用迭代器适配器来使代码更简明 -I/O 项目中其他可以利用迭代器优势的地方位于 `search` 函数,示例 13-28 中重现了第十二章结尾示例 12-19 中此函数的定义: +I/O 项目中其他可以利用迭代器的地方是 `search` 函数,示例 13-28 中重现了第十二章结尾示例 12-19 中此函数的定义: 文件名: src/lib.rs @@ -168,8 +168,8 @@ pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { 示例 13-29:在 `search` 函数实现中使用迭代器适配器 -回忆 `search` 函数的目的是返回所有 `contents` 中包含 `query` 的行。类似于示例 13-19 中的 `filter` 例子,可以使用 `filter` 适配器只保留 `line.contains(query)` 返回 `true` 的那些行。接着使用 `collect` 将匹配行收集到另一个 vector 中。这样就容易多了!请随意对 `search_case_insensitive` 函数做出同样的使用迭代器方法的修改。 +回忆 `search` 函数的目的是返回所有 `contents` 中包含 `query` 的行。类似于示例 13-19 中的 `filter` 例子,可以使用 `filter` 适配器只保留 `line.contains(query)` 返回 `true` 的那些行。接着使用 `collect` 将匹配行收集到另一个 vector 中。这样就容易多了!尝试对 `search_case_insensitive` 函数做出同样的使用迭代器方法的修改吧。 -接下来的逻辑问题就是在代码中应该选择哪种风格:示例 13-28 中的原始实现,或者是示例 13-29 中使用迭代器的版本。大部分 Rust 程序员倾向于使用迭代器风格。开始这有点难以理解,不过一旦你对不同迭代器的工作方式有了感觉之后,迭代器可能会更容易理解。相比摆弄不同的循环并创建新 vector,(迭代器)代码则更关注循环的目的。这抽象出了那些老生常谈的代码,这样就更容易看清代码所特有的概念,比如迭代器中每个元素必须面对的过滤条件。 +接下来的逻辑问题就是在代码中应该选择哪种风格:是使用示例 13-28 中的原始实现还是使用示例 13-29 中使用迭代器的版本?大部分 Rust 程序员倾向于使用迭代器风格。开始这有点难以理解,不过一旦你对不同迭代器的工作方式有了感觉之后,迭代器可能会更容易理解。相比摆弄不同的循环并创建新 vector,(迭代器)代码则更关注循环的目的。这抽象出了那些老生常谈的代码,这样就更容易看清代码所特有的概念,比如迭代器中每个元素必须面对的过滤条件。 不过这两种实现真的完全等同吗?直觉上的假设是更底层的循环会更快一些。让我们聊聊性能吧。 diff --git a/src/ch13-04-performance.md b/src/ch13-04-performance.md index 28e95ba..c9ba7f7 100644 --- a/src/ch13-04-performance.md +++ b/src/ch13-04-performance.md @@ -4,7 +4,7 @@ >
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -为了决定使用哪个实现,我们需要知道哪个版本的 `search` 函数更快:直接使用 `for` 循环的版本还是使用迭代器的版本。 +为了决定使用哪个实现,我们需要知道哪个版本的 `search` 函数更快一些:是直接使用 `for` 循环的版本还是使用迭代器的版本。 我们运行了一个性能测试,通过将阿瑟·柯南·道尔的“福尔摩斯探案集”的全部内容加载进 `String` 并寻找其中的单词 “the”。如下是 `for` 循环版本和迭代器版本的 `search` 函数的性能测试结果: @@ -15,7 +15,7 @@ test bench_search_iter ... bench: 19,234,900 ns/iter (+/- 657,200) 结果迭代器版本还要稍微快一点!这里我们将不会查看性能测试的代码,我们的目的并不是为了证明他们是完全等同的,而是得出一个怎样比较这两种实现方式性能的基本思路。 -对于一个更全面的性能测试,将会检查不同长度的文本、不同的搜索单词、不同长度的单词和所有其他的可变情况。这里所要表达的是:迭代器,作为一个高级的抽象,被编译成了与手写的底层代码大体一致性能代码。迭代器是 Rust 的 **零成本抽象**(*zero-cost abstractions*)之一,它意味着抽象并不会强加运行时开销,它与本贾尼·斯特劳斯特卢普,C++ 的设计和实现者所定义的 **零开销**(*zero-overhead*)如出一辙: +对于一个更全面的性能测试,将会检查不同长度的文本、不同的搜索单词、不同长度的单词和所有其他的可变情况。这里所要表达的是:迭代器,作为一个高级的抽象,被编译成了与手写的底层代码大体一致性能代码。迭代器是 Rust 的 **零成本抽象**(*zero-cost abstractions*)之一,它意味着抽象并不会引入运行时开销,它与本贾尼·斯特劳斯特卢普(C++ 的设计和实现者)在 “Foundations of C++”(2012) 中所定义的 **零开销**(*zero-overhead*)如出一辙: > In general, C++ implementations obey the zero-overhead principle: What you don’t use, you don’t pay for. And further: What you do use, you couldn’t hand code any better. > From feb446e33126bdde90f088b620aa9ceee537954e Mon Sep 17 00:00:00 2001 From: Lam <15622383059@163.com> Date: Wed, 12 Dec 2018 14:52:13 +0800 Subject: [PATCH 43/49] improve some translation and fix typo in ch14 --- src/ch14-00-more-about-cargo.md | 2 +- src/ch14-02-publishing-to-crates-io.md | 16 ++++++++-------- src/ch14-03-cargo-workspaces.md | 2 +- src/ch14-05-extending-cargo.md | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/ch14-00-more-about-cargo.md b/src/ch14-00-more-about-cargo.md index 781ec68..28f131a 100644 --- a/src/ch14-00-more-about-cargo.md +++ b/src/ch14-00-more-about-cargo.md @@ -12,4 +12,4 @@ * 从 [crates.io](https://crates.io) 安装二进制文件 * 使用自定义的命令来扩展 Cargo -相比本章能够涉及的工作 Cargo 甚至还可以做到更多,关于其功能的全部解释,请查看 [文档](http://doc.rust-lang.org/cargo/) \ No newline at end of file +Cargo 的功能不止本章所介绍的,关于其功能的全部解释,请查看 [文档](http://doc.rust-lang.org/cargo/) \ 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 07a2093..d74936c 100644 --- a/src/ch14-02-publishing-to-crates-io.md +++ b/src/ch14-02-publishing-to-crates-io.md @@ -98,20 +98,20 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ### 使用 `pub use` 导出合适的公有 API -第七章介绍了如何使用 `mod` 关键字来将代码组织进模块中,如何使用 `pub` 关键字将项变为公有,和如何使用 `use` 关键字将项引入作用域。然而对你开发来说很有道理的结果可能对用户来说就不太方便了。你可能希望将结构组织进有多个层次的层级中,不过想要使用被定义在很深层级中的类型的人可能很难发现这些类型是否存在。他们也可能会厌烦使用 `use my_crate::some_module::another_module::UsefulType;` 而不是 `use my_crate::UsefulType;` 来使用类型。 +第七章介绍了如何使用 `mod` 关键字来将代码组织进模块中,如何使用 `pub` 关键字将项变为公有,和如何使用 `use` 关键字将项引入作用域。然而你开发时候使用的文件架构可能并不方便用户。你的结构可能是一个包含多个层级的分层结构,不过这对于用于来说并不方便。这是因为想要使用被定义在很深层级中的类型的人可能很难发现这些类型的存在。他们也可能会厌烦使用 `use my_crate::some_module::another_module::UsefulType;` 而不是 `use my_crate::UsefulType;` 来使用类型。 公有 API 的结构是你发布 crate 时主要需要考虑的。crate 用户没有你那么熟悉其结构,并且如果模块层级过大他们可能会难以找到所需的部分。 -好消息是,如果结果对于用户来说 **不是** 很方便,你也无需重新安排内部组织:你可以选择使用 `pub use` 重导出(re-export)项来使公有结构不同于私有结构。重导出获取位于一个位置的公有项并将其公开到另一个位置,好像它就定义在这个新位置一样。 +好消息是,即使文件结构对于用户来说 **不是** 很方便,你也无需重新安排内部组织:你可以选择使用 `pub use` 重导出(re-export)项来使公有结构不同于私有结构。重导出获取位于一个位置的公有项并将其公开到另一个位置,好像它就定义在这个新位置一样。 -例如,假设我们创建了一个模块化的充满艺术化气息的库 `art`。在这个库中是一个包含两个枚举 `PrimaryColor` 和 `SecondaryColor` 的模块 `kinds`,以及一个包含函数 `mix` 的模块 `utils`,如示例 14-3 所示: +例如,假设我们创建了一个描述美术信息的库 `art`。这个库中包含了一个有两个枚举 `PrimaryColor` 和 `SecondaryColor` 的模块 `kinds`,以及一个包含函数 `mix` 的模块 `utils`,如示例 14-3 所示: 文件名: src/lib.rs ```rust,ignore //! # Art //! -//! 一个模块化的充满艺术化气息的库。 +//! 一个描述美术信息的库。 pub mod kinds { /// 采用 RGB 色彩模式的主要颜色。 @@ -148,7 +148,7 @@ pub mod utils { 图 14-3:包含 `kinds` 和 `utils` 模块的库 `art` 的文档首页 -注意 `PrimaryColor` 和 `SecondaryColor` 类型没有在首页中列出,`mix` 函数也没有。必须点击 `kinds` 或 `utils` 才能看到他们。 +注意 `PrimaryColor` 和 `SecondaryColor` 类型、以及 `mix` 函数都没有在首页中列出。我们必须点击 `kinds` 或 `utils` 才能看到他们。 另一个依赖这个库的 crate 需要 `use` 语句来导入 `art` 中的项,这包含指定其当前定义的模块结构。示例 14-4 展示了一个使用 `art` crate 中 `PrimaryColor` 和 `mix` 项的 crate 的例子: @@ -176,7 +176,7 @@ fn main() { ```rust,ignore //! # Art //! -//! 一个模块化的充满艺术化气息的库。 +//! 一个描述美术信息的库。 pub use kinds::PrimaryColor; pub use kinds::SecondaryColor; @@ -216,7 +216,7 @@ fn main() { 对于有很多嵌套模块的情况,使用 `pub use` 将类型重导出到顶级结构对于使用 crate 的人来说将会是大为不同的体验。 -创建一个有用的公有 API 结构更像是一门艺术而非科学,你可以反复检视他们来找出最适合用户的 API。选择 `pub use` 提供了解耦组织 crate 内部结构和与终端用户体现的灵活性。观察一些你所安装的 crate 的代码来看看其内部结构是否不同于公有 API。 +创建一个有用的公有 API 结构更像是一门艺术而非科学,你可以反复检视他们来找出最适合用户的 API。`pub use` 提供了解耦组织 crate 内部结构和与终端用户体现的灵活性。观察一些你所安装的 crate 的代码来看看其内部结构是否不同于公有 API。 ### 创建 Crates.io 账号 @@ -292,7 +292,7 @@ license = "MIT OR Apache-2.0" 现在我们创建了一个账号,保存了 API token,为 crate 选择了一个名字,并指定了所需的元数据,你已经准备好发布了!发布 crate 会上传特定版本的 crate 到 [crates.io](https://crates.io) 以供他人使用。 -发布 crate 时请多加小心,因为发布是 **永久性的**(*permanent*)。对应版本不可能被覆盖,其代码也不可能被删除。[crates.io](https://crates.io) 的一个主要目标是作为一个代码的永久文档服务器,这样所有依赖 [crates.io](https://crates.io) 中 crate 的项目都能一直正常工作。允许删除版本将不可能满足这个目标。然而,可以被发布的版本号却没有限制。 +发布 crate 时请多加小心,因为发布是 **永久性的**(*permanent*)。对应版本不可能被覆盖,其代码也不可能被删除。[crates.io](https://crates.io) 的一个主要目标是作为一个存储代码的永久文档服务器,这样所有依赖 [crates.io](https://crates.io) 中的 crate 的项目都能一直正常工作。而允许删除版本没办法达成这个目标。然而,可以被发布的版本号却没有限制。 再次运行 `cargo publish` 命令。这次它应该会成功: diff --git a/src/ch14-03-cargo-workspaces.md b/src/ch14-03-cargo-workspaces.md index 3ca037b..4c4463a 100644 --- a/src/ch14-03-cargo-workspaces.md +++ b/src/ch14-03-cargo-workspaces.md @@ -108,7 +108,7 @@ add-one = { path = "../add-one" } 工作空间中的 crate 不必相互依赖,所以仍需显式地表明工作空间中 crate 的依赖关系。 -接下来,在 `adder` crate 中使用 `add-one` crate 的函数 `add_one`。打开 *adder/src/main.rs* 在顶部增加一行 `extern crate` 将新 `add-one` 库 crate 引入作用域。接着修改 `main` 函数来调用 `add_one` 函数,如示例 14-7 所示: +接下来,在 `adder` crate 中使用 `add-one` crate 的函数 `add_one`。打开 *adder/src/main.rs* 在顶部增加一行 `use` 将新 `add-one` 库 crate 引入作用域。接着修改 `main` 函数来调用 `add_one` 函数,如示例 14-7 所示: 文件名: adder/src/main.rs diff --git a/src/ch14-05-extending-cargo.md b/src/ch14-05-extending-cargo.md index 1e88eb1..9d66dcd 100644 --- a/src/ch14-05-extending-cargo.md +++ b/src/ch14-05-extending-cargo.md @@ -4,7 +4,7 @@ >
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -Cargo 被设计为可以通过新的子命令而无须修改 Cargo 自身来进行扩展。如果 `$PATH` 中有类似 `cargo-something` 的二进制文件,就可以通过 `cargo something` 来像 Cargo 子命令一样运行它。像这样的自定义命令也可以运行 `cargo --list` 来展示出来。能够通过 `cargo install` 向 Cargo 安装扩展并可以如内建 Cargo 工具那样运行他们是 Cargo 设计上的一个非常方便的优点! +Cargo 的设计使得开发者可以通过新的子命令来对 Cargo 进行扩展,而无需修改 Cargo 本身。如果 `$PATH` 中有类似 `cargo-something` 的二进制文件,就可以通过 `cargo something` 来像 Cargo 子命令一样运行它。像这样的自定义命令也可以运行 `cargo --list` 来展示出来。能够通过 `cargo install` 向 Cargo 安装扩展并可以如内建 Cargo 工具那样运行他们是 Cargo 设计上的一个非常方便的优点! ## 总结 From 9627aa9965576cbfa984cbd7fbf80897b0d0da7c Mon Sep 17 00:00:00 2001 From: Lam <15622383059@163.com> Date: Wed, 12 Dec 2018 15:53:59 +0800 Subject: [PATCH 44/49] improve some translation and fix typo in ch15 --- src/ch15-05-interior-mutability.md | 2 +- src/ch15-06-reference-cycles.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ch15-05-interior-mutability.md b/src/ch15-05-interior-mutability.md index d169bcc..d9b7626 100644 --- a/src/ch15-05-interior-mutability.md +++ b/src/ch15-05-interior-mutability.md @@ -242,7 +242,7 @@ impl Messenger for MockMessenger { } ``` -示例 15-26:在同一作用域中创建两个可变引用并观察 `RefCell` panic +示例 15-23:在同一作用域中创建两个可变引用并观察 `RefCell` panic 这里为 `borrow_mut` 返回的 `RefMut` 智能指针创建了 `one_borrow` 变量。接着用相同的方式在变量 `two_borrow` 创建了另一个可变借用。这会在相同作用域中创建一个可变引用,这是不允许的。当运行库的测试时,示例 15-23 编译时不会有任何错误,不过测试会失败: diff --git a/src/ch15-06-reference-cycles.md b/src/ch15-06-reference-cycles.md index f948b4d..53d51a6 100644 --- a/src/ch15-06-reference-cycles.md +++ b/src/ch15-06-reference-cycles.md @@ -4,7 +4,7 @@ >
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -Rust 的内存安全保证使其难以意外的制造永远也不会被清理的内存(被称为 **内存泄露**(*memory leak*)),但并不是不可能。完全的避免内存泄露并不是同在编译时拒绝数据竞争一样为 Rust 的保证之一,这意味着内存泄露在 Rust 被认为是内存安全的。这一点可以通过 `Rc` 和 `RefCell` 看出:有可能会创建个个项之间相互引用的引用。这会造成内存泄露,因为每一项的引用计数将永远也到不了 0,其值也永远也不会被丢弃。 +Rust 的内存安全保证使其难以意外地制造永远也不会被清理的内存(被称为 **内存泄露**(*memory leak*)),但并不是不可能。完全地避免内存泄露并不是同在编译时拒绝数据竞争一样为 Rust 的保证之一,这意味着内存泄露在 Rust 被认为是内存安全的。这一点可以通过 `Rc` 和 `RefCell` 看出:有可能会创建个个项之间相互引用的引用。这会造成内存泄露,因为每一项的引用计数将永远也到不了 0,其值也永远也不会被丢弃。 ### 制造引用循环 From 3d3da5f510260445c6e325f789b5ab79b64cd34e Mon Sep 17 00:00:00 2001 From: Lam <15622383059@163.com> Date: Thu, 13 Dec 2018 11:47:53 +0800 Subject: [PATCH 45/49] a little revision --- src/ch16-01-threads.md | 2 +- src/ch17-01-what-is-oo.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ch16-01-threads.md b/src/ch16-01-threads.md index 54c2de9..7dd70d3 100644 --- a/src/ch16-01-threads.md +++ b/src/ch16-01-threads.md @@ -1,6 +1,6 @@ ## 使用线程同时运行代码 -> [ch16-01-threads.md](https://github.com/rust-lang/book/blob/master/src/ch16-00-concurrency.md) +> [ch16-01-threads.md](https://github.com/rust-lang/book/blob/master/src/ch16-01-threads.md) >
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f diff --git a/src/ch17-01-what-is-oo.md b/src/ch17-01-what-is-oo.md index ec35577..bb8a67d 100644 --- a/src/ch17-01-what-is-oo.md +++ b/src/ch17-01-what-is-oo.md @@ -8,7 +8,7 @@ ### 对象包含数据和行为 -Erich Gamma、Richard Helm、Ralph Johnson和 John Vlissides(Addison-Wesley Professional, 1994)的 *Design Patterns: Elements of Reusable Object-Oriented Software* 被俗称为 `The Gang of Four book`,是面向对象编程模式的目录。它这样定义面向对象编程: +由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides(Addison-Wesley Professional, 1994)编写的书 *Design Patterns: Elements of Reusable Object-Oriented Software* 被俗称为 `The Gang of Four`,它是面向对象编程模式的目录。它这样定义面向对象编程: > Object-oriented programs are made up of objects. An *object* packages both > data and the procedures that operate on that data. The procedures are From 42d8c6b00f9c5c8c0fc99636e410d12f23774471 Mon Sep 17 00:00:00 2001 From: Jianbing Fang Date: Fri, 14 Dec 2018 15:45:03 +0800 Subject: [PATCH 46/49] Fix typo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change "分号" to "冒号" --- src/ch07-02-modules-and-use-to-control-scope-and-privacy.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ch07-02-modules-and-use-to-control-scope-and-privacy.md b/src/ch07-02-modules-and-use-to-control-scope-and-privacy.md index 44c1ce5..e722319 100644 --- a/src/ch07-02-modules-and-use-to-control-scope-and-privacy.md +++ b/src/ch07-02-modules-and-use-to-control-scope-and-privacy.md @@ -89,7 +89,7 @@ crate * **绝对路径**(*absolute path*)从 crate 根开始,以 crate 名或者字面值 `crate` 开头。 * **相对路径**(*relative path*)从当前模块开始,以 `self`、`super` 或当前模块的标识符开头。 -绝对路径和相对路径都后跟一个或多个由双分号(`::`)分割的标识符。 +绝对路径和相对路径都后跟一个或多个由双冒号(`::`)分割的标识符。 如何在示例 7-2 的 `main` 函数中调用 `clarinet` 函数呢?也就是说,`clarinet` 函数的路径是什么呢?在示例 7-4 中稍微简化了代码,移除了一些模块,并展示了两种在 `main` 中调用 `clarinet` 函数的方式。这个例子还不能编译,我们会解释为什么。 @@ -586,7 +586,7 @@ use std::io; // ---snip--- ``` -可以使用嵌套的路径将同样的项在一行中引入而不是两行,这么做需要指定路径的相同部分,接着是两个分号,接着是大括号中的各自不同的路径部分,如示例 7-22 所示。 +可以使用嵌套的路径将同样的项在一行中引入而不是两行,这么做需要指定路径的相同部分,接着是两个冒号,接着是大括号中的各自不同的路径部分,如示例 7-22 所示。 文件名: src/main.rs @@ -698,4 +698,4 @@ pub fn clarinet() { Rust 提供了将包组织进 crate、将 crate 组织进模块和通过指定绝对或相对路径从一个模块引用另一个模块中定义的项的方式。可以通过 `use` 语句将路径引入作用域,这样在多次使用时可以使用更短的路径。模块定义的代码默认是私有的,不过可以选择增加 `pub` 关键字使其定义变为公有。 -接下来,让我们看看一些标准库提供的集合数据类型,你可以利用它们编写出漂亮整洁的代码。 \ No newline at end of file +接下来,让我们看看一些标准库提供的集合数据类型,你可以利用它们编写出漂亮整洁的代码。 From 7f538f1f2c6fca1e78dc4306b66b7501e3a12511 Mon Sep 17 00:00:00 2001 From: KaiserY Date: Sun, 16 Dec 2018 14:46:49 +0800 Subject: [PATCH 47/49] fix typo --- ferris.js | 8 ++++---- src/ch00-00-introduction.md | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ferris.js b/ferris.js index 5d014f3..8917365 100644 --- a/ferris.js +++ b/ferris.js @@ -1,19 +1,19 @@ var ferrisTypes = [ { attr: 'does_not_compile', - title: 'This code does not compile!' + title: '这些代码不能编译!' }, { attr: 'panics', - title: 'This code panics!' + title: '这些代码会 panic!' }, { attr: 'unsafe', - title: 'This code block contains unsafe code.' + title: '这些代码块包含不安全(unsafe)代码。' }, { attr: 'not_desired_behavior', - title: 'This code does not produce the desired behavior.' + title: '这些代码不会产生期望的行为。' } ] diff --git a/src/ch00-00-introduction.md b/src/ch00-00-introduction.md index e23ba86..974f626 100644 --- a/src/ch00-00-introduction.md +++ b/src/ch00-00-introduction.md @@ -84,7 +84,7 @@ Rust 语言也希望能支持很多其他用户,这里提及的只是最大的 |------------------------------------------------------------------------|--------------------------------------------------| | | 这些代码不能编译! | | | 这些代码会 panic! | -| | 这些代码块包含不安全(unsafe)代码 | +| | 这些代码块包含不安全(unsafe)代码。 | | | 这些代码不会产生期望的行为。 | 在大部分情况,我们会指引你将任何不能编译的代码纠正为正确版本。 From a588d4568a2cc25ceb2e5f9c06c54d6ee87ad802 Mon Sep 17 00:00:00 2001 From: Peng Guanwen Date: Sun, 30 Dec 2018 15:44:47 +0800 Subject: [PATCH 48/49] =?UTF-8?q?=E8=B0=83=E6=95=B4=E8=BF=87=E9=95=BF?= =?UTF-8?q?=E7=9A=84=E8=AF=AD=E5=8F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ch15-06-reference-cycles.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ch15-06-reference-cycles.md b/src/ch15-06-reference-cycles.md index 53d51a6..f051160 100644 --- a/src/ch15-06-reference-cycles.md +++ b/src/ch15-06-reference-cycles.md @@ -4,7 +4,7 @@ >
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -Rust 的内存安全保证使其难以意外地制造永远也不会被清理的内存(被称为 **内存泄露**(*memory leak*)),但并不是不可能。完全地避免内存泄露并不是同在编译时拒绝数据竞争一样为 Rust 的保证之一,这意味着内存泄露在 Rust 被认为是内存安全的。这一点可以通过 `Rc` 和 `RefCell` 看出:有可能会创建个个项之间相互引用的引用。这会造成内存泄露,因为每一项的引用计数将永远也到不了 0,其值也永远也不会被丢弃。 +Rust 的内存安全保证使其难以意外地制造永远也不会被清理的内存(被称为 **内存泄露**(*memory leak*)),但并不是不可能。与在编译时拒绝数据竞争不同, Rust 并不保证完全地避免内存泄露,这意味着内存泄露在 Rust 被认为是内存安全的。这一点可以通过 `Rc` 和 `RefCell` 看出:有可能会创建个个项之间相互引用的引用。这会造成内存泄露,因为每一项的引用计数将永远也到不了 0,其值也永远也不会被丢弃。 ### 制造引用循环 From 84fed4947ecfb193aa9d0374245db9ca9454d6ad Mon Sep 17 00:00:00 2001 From: Rrrandom Date: Thu, 3 Jan 2019 23:01:11 +0800 Subject: [PATCH 49/49] fix typo `10-26` -> `10-27` --- src/ch10-03-lifetime-syntax.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ch10-03-lifetime-syntax.md b/src/ch10-03-lifetime-syntax.md index 51a6b1a..78f75c3 100644 --- a/src/ch10-03-lifetime-syntax.md +++ b/src/ch10-03-lifetime-syntax.md @@ -345,7 +345,7 @@ fn first_word(s: &str) -> &str { } ``` -示例 10-27:示例 4-9 定义了一个没有使用生命周期注解的函数,即便其参数和返回值都是引用 +示例 10-26:示例 4-9 定义了一个没有使用生命周期注解的函数,即便其参数和返回值都是引用 这个函数没有生命周期注解却能编译是由于一些历史原因:在早期版本(pre-1.0)的 Rust 中,这的确是不能编译的。每一个引用都必须有明确的生命周期。那时的函数签名将会写成这样: