diff --git a/README.md b/README.md index 0564a6d..ba6dcdd 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,4 @@ 还在施工中:目前翻译到第十六章 -目前正在解决代码排版问题:已检查到第六章 \ No newline at end of file +目前正在解决代码排版问题:已检查到第十一章第一部分 \ No newline at end of file diff --git a/docs/ch01-01-installation.html b/docs/ch01-01-installation.html index 51d1e12..01d3a34 100644 --- a/docs/ch01-01-installation.html +++ b/docs/ch01-01-installation.html @@ -71,7 +71,7 @@

ch01-01-installation.md
-commit 4f2dc564851dc04b271a2260c834643dfd86c724

+commit c1b95a18dbcbb06aadf07c03759f27d88ccf62cf

使用 Rust 的第一步是安装。你需要网络连接来执行本章的命令,因为我们要从网上下载 Rust。

我们将会展示很多使用终端的命令,并且这些代码都以$开头。并不需要真正输入$,它们在这里代表每行指令的开头。在网上会看到很多使用这个惯例的教程和例子:$代表以常规用户运行命令,#代表需要用管理员运行的命令。没有以$(或#)的行通常是之前命令的输出。

@@ -88,6 +88,10 @@ commit 4f2dc564851dc04b271a2260c834643dfd86c724

本书其余 Windows 相关的命令假设你使用cmd作为你的 shell。如果你使用不同的 shell,可能能够执行 Linux 和 Mac 用户相同的命令。如果都不行,请查看所使用的 shell 的文档。

自定义安装

如果有理由倾向于不使用 rustup.rs,请查看Rust 安装页面获取其他选择。

+

更新

+

一旦安装完 Rust,更新到最新版本是简单的。在 shell 中运行更新脚本:

+
$ rustup update
+

卸载

卸载 Rust 同安装一样简单。在 shell 中运行卸载脚本

$ rustup self uninstall
diff --git a/docs/ch07-01-mod-and-the-filesystem.html b/docs/ch07-01-mod-and-the-filesystem.html
index 24d0e20..b738715 100644
--- a/docs/ch07-01-mod-and-the-filesystem.html
+++ b/docs/ch07-01-mod-and-the-filesystem.html
@@ -71,7 +71,7 @@
 

ch07-01-mod-and-the-filesystem.md
-commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

+commit 6fc32eabcd09f7a130094767abadb691dfcdddf7

我们将通过使用 Cargo 创建一个新项目来开始我们的模块之旅,不过不再创建一个二进制 crate,而是创建一个库 crate:一个其他人可以作为依赖导入的项目。第二章我们见过的rand就是这样的 crate。

我们将创建一个提供一些通用网络功能的项目的骨架结构;我们将专注于模块和函数的组织,而不担心函数体中的具体代码。这个项目叫做communicator。Cargo 默认会创建一个库 crate 除非指定其他项目类型,所以如果不像一直以来那样加入--bin参数则项目将会是一个库:

diff --git a/docs/ch07-02-controlling-visibility-with-pub.html b/docs/ch07-02-controlling-visibility-with-pub.html index 818f13b..0b687a3 100644 --- a/docs/ch07-02-controlling-visibility-with-pub.html +++ b/docs/ch07-02-controlling-visibility-with-pub.html @@ -69,9 +69,9 @@

使用pub控制可见性

-

ch07-02-controlling-visibility-with-pub.md +

ch07-02-controlling-visibility-with-pub.md
-commit e2a129961ae346f726f8b342455ec2255cdfed68

+commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

我们通过将networknetwork::server的代码分别移动到 src/network/mod.rssrc/network/server.rs 文件中解决了列表 7-4 中出现的错误信息。现在,cargo build能够构建我们的项目,不过仍然有一些警告信息,表示client::connectnetwork::connectnetwork::server::connect函数没有被使用:

warning: function is never used: `connect`, #[warn(dead_code)] on by default
@@ -92,7 +92,7 @@ warning: function is never used: `connect`, #[warn(dead_code)] on by default
 1 | fn connect() {
   | ^
 
-

那么为什么会出现这些错误信息呢?我们构建的是一个库,它的函数的目的是被用户使用,而不一定要被项目自身使用,所以不应该担心这些函数是未被使用的。创建他们的意义就在于被另一个项目而不是被自己使用。

+

那么为什么会出现这些错误信息呢?我们构建的是一个库,它的函数的目的是被用户使用,而不一定要被项目自身使用,所以不应该担心这些connect函数是未使用的。创建他们的意义就在于被另一个项目而不是被自己使用。

为了理解为什么这个程序出现了这些警告,尝试作为另一个项目来使用这个connect库,从外部调用他们。为此,通过创建一个包含这些代码的 src/main.rs 文件,在与库 crate 相同的目录创建一个二进制 crate:

Filename: src/main.rs

extern crate communicator;
@@ -112,7 +112,7 @@ fn main() {
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 

啊哈!这告诉了我们client模块是私有的,这也正是那些警告的症结所在。这也是我们第一次在 Rust 上下文中涉及到公有私有的概念。Rust 所有代码的默认状态是私有的:除了自己之外别人不允许使用这些代码。如果不在自己的项目中使用一个私有函数,因为程序自身是唯一允许使用这个函数的代码,Rust 会警告说函数未被使用。

-

一旦我们指定一个像client::connect的函数为公有,不光二进制 crate 中的函数调用会被允许,函数未被使用的警告也会消失。将其标记为公有让 Rust 知道了我们意在使函数在我们程序的外部被使用。现在这个可能的理论上的外部可用性使 Rust 认为这个函数“已经被使用”。因此。当某项被标记为公有,Rust 不再要求它在程序自身被使用并停止警告某项未被使用。

+

一旦我们指定一个像client::connect的函数为公有,不光二进制 crate 中的函数调用是允许的,函数未被使用的警告也会消失。将其标记为公有让 Rust 知道了我们意在使函数在我们程序的外部被使用。现在这个可能的理论上的外部可用性使得 Rust 认为这个函数“已经被使用”。因此。当某项被标记为公有,Rust 不再要求它在程序自身被使用并停止警告某项未被使用。

标记函数为公有

为了告诉 Rust 某项为公有,在想要标记为公有的项的声明开头加上pub关键字。现在我们将致力于修复client::connect未被使用的警告,以及二进制 crate 中“模块client是私有的”的错误。像这样修改 src/lib.rs 使client模块公有:

Filename: src/lib.rs

@@ -190,8 +190,7 @@ pub mod network;

私有性示例

让我们看看更多例子作为练习。创建一个新的库项目并在新项目的 src/lib.rs 输入列表 7-5 中的代码:

-
-Filename: src/lib.rs +

Filename: src/lib.rs

mod outermost {
     pub fn middle_function() {}
 
@@ -211,11 +210,8 @@ fn try_me() {
     outermost::inside::secret_function();
 }
 
-
-

Listing 7-5: Examples of private and public functions, some of which are -incorrect

-
-
+

Listing 7-5: Examples of private and public functions, +some of which are incorrect

在尝试编译这些代码之前,猜测一下try_me函数的哪一行会出错。接着编译项目来看看是否猜对了,然后继续阅读后面关于错误的讨论!

检查错误

try_me函数位于项目的根模块。叫做outermost的模块是私有的,不过第二条私有性规则说明try_me函数允许访问outermost模块,因为outermost位于当前(根)模块,try_me也是。

diff --git a/docs/ch07-03-importing-names-with-use.html b/docs/ch07-03-importing-names-with-use.html index 29ffa0a..201ebea 100644 --- a/docs/ch07-03-importing-names-with-use.html +++ b/docs/ch07-03-importing-names-with-use.html @@ -69,13 +69,12 @@

导入命名

-

ch07-03-importing-names-with-use.md +

ch07-03-importing-names-with-use.md
-commit e2a129961ae346f726f8b342455ec2255cdfed68

+commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

我们已经讲到了如何使用模块名称作为调用的一部分,来调用模块中的函数,如列表 7-6 中所示的nested_modules函数调用。

-
-Filename: src/main.rs +

Filename: src/main.rs

pub mod a {
     pub mod series {
         pub mod of {
@@ -88,11 +87,8 @@ fn main() {
     a::series::of::nested_modules();
 }
 
-
-

Listing 7-6: Calling a function by fully specifying its enclosing module’s -namespaces

-
-
+

Listing 7-6: Calling a function by fully specifying its +enclosing module’s namespaces

如你所见,指定函数的完全限定名称可能会非常冗长。所幸 Rust 有一个关键字使得这些调用显得更简洁。

使用use的简单导入

Rust 的use关键字的工作是缩短冗长的函数调用,通过将想要调用的函数所在的模块引入到作用域中。这是一个将a::series::of模块导入一个二进制 crate 的根作用域的例子:

@@ -175,7 +171,7 @@ mod tests { } }
-

第十二章会更详细的解释测试,不过其部分内容现在应该可以理解了:有一个叫做tests的模块紧邻其他模块,同时包含一个叫做it_works的函数。即便存在一些特殊注解,tests也不过是另外一个模块!所以我们的模块层次结构看起来像这样:

+

第十一章会更详细的解释测试,不过其部分内容现在应该可以理解了:有一个叫做tests的模块紧邻其他模块,同时包含一个叫做it_works的函数。即便存在一些特殊注解,tests也不过是另外一个模块!所以我们的模块层次结构看起来像这样:

communicator
  ├── client
  ├── network
diff --git a/docs/ch08-00-common-collections.html b/docs/ch08-00-common-collections.html
index 81c59d3..229f818 100644
--- a/docs/ch08-00-common-collections.html
+++ b/docs/ch08-00-common-collections.html
@@ -69,9 +69,9 @@
                 

通用集合类型

-

ch08-00-common-collections.md +

ch08-00-common-collections.md
-commit 0d229cc5a3da341196e15a6761735b2952281569

+commit e6d6caab41471f7115a621029bd428a812c5260e

Rust 标准库中包含一系列被称为集合collections)的非常有用的数据结构。大部分其他数据类型都代表一个特定的值,不过集合可以包含多个值。不同于内建的数组和元组类型,这些集合指向的数据是储存在堆上的,这意味着数据的数量不必在编译时就可知并且可以随着程序的运行增长或缩小。每种集合都有着不同能力和代价,而为所处的场景选择合适的集合则是你将要始终发展的技能。在这一章里,我们将详细的了解三个在 Rust 程序中被广泛使用的集合:

    @@ -79,8 +79,8 @@ commit 0d229cc5a3da341196e15a6761735b2952281569

  • 字符串string)是一个字符的集合。我们之前见过String类型,现在将详细介绍它。
  • 哈希 maphash map)允许我们将值与一个特定的键(key)相关联。这是一个叫做 map 的更通用的数据结构的特定实现。
-

对于标准库提供的其他类型的集合,请查看文档

-

我们将讨论如何创建和更新 vector、字符串和哈希 map,以及他们何以如此特殊。

+

对于标准库提供的其他类型的集合,请查看文档

+

我们将讨论如何创建和更新 vector、字符串和哈希 map,以及他们有什么不同。

diff --git a/docs/ch08-01-vectors.html b/docs/ch08-01-vectors.html index 45aae71..1517d30 100644 --- a/docs/ch08-01-vectors.html +++ b/docs/ch08-01-vectors.html @@ -69,11 +69,11 @@

vector

-

ch08-01-vectors.md +

ch08-01-vectors.md
-commit 0d229cc5a3da341196e15a6761735b2952281569

+commit 4f2dc564851dc04b271a2260c834643dfd86c724

-

我们要讲到的第一个类型是Vec<T>,也被称为 vector。vector 允许我们在一个单独的数据结构中储存多于一个值,它在内存中彼此相邻的排列所有的值。vector 只能储存相同类型的值。他们在拥有一系列的场景下非常实用,例如文件中的文本行或是购物车中商品的价格。

+

我们要讲到的第一个类型是Vec<T>,也被称为 vector。vector 允许我们在一个单独的数据结构中储存多于一个值,它在内存中彼此相邻的排列所有的值。vector 只能储存相同类型的值。他们在拥有一系列项的场景下非常实用,例如文件中的文本行或是购物车中商品的价格。

新建 vector

为了创建一个新的,空的 vector,可以调用Vec::new函数:

let v: Vec<i32> = Vec::new();
diff --git a/docs/ch08-02-strings.html b/docs/ch08-02-strings.html
index 3717eaf..010c60b 100644
--- a/docs/ch08-02-strings.html
+++ b/docs/ch08-02-strings.html
@@ -69,9 +69,9 @@
                 

字符串

-

ch08-02-strings.md +

ch08-02-strings.md
-commit 65f52921e21ad2e1c79d620fcfd01bde3ee30571

+commit d362dadae60a7cc3212b107b9e9562769b0f20e3

第四章已经讲过一些字符串的内容,不过现在让我们更深入地了解一下它。字符串是新晋 Rustacean 们通常会被困住的领域。这是由于三方面内容的结合:Rust 倾向于确保暴露出可能的错误,字符串是比很多程序员所想象的要更为复杂的数据结构,以及 UTF-8。所有这些结合起来对于来自其他语言背景的程序员就可能显得很困难了。

字符串出现在集合章节的原因是,字符串是作为字节的集合外加一些方法实现的,当这些字节被解释为文本时,这些方法提供了实用的功能。在这一部分,我们会讲到String那些任何集合类型都有的操作,比如创建、更新和读取。也会讨论String于其他集合不一样的地方,例如索引String是很复杂的,由于人和计算机理解String数据的不同方式。

@@ -112,7 +112,7 @@ let hello = "Hola";

更新字符串

String的大小可以增长其内容也可以改变,就像可以放入更多数据来改变Vec的内容一样。另外,String实现了+运算符作为级联运算符以便于使用。

-

附加字符串

+

使用 push 附加字符串

可以通过push_str方法来附加字符串 slice,从而使String变长:

let mut s = String::from("foo");
 s.push_str("bar");
@@ -137,7 +137,7 @@ let s3 = s1 + &s2; // Note that s1 has been moved here and can no longer be
 
fn add(self, s: &str) -> String {
 

这并不是标准库中实际的签名;那个add使用泛型定义。这里的签名使用具体类型代替了泛型,这也正是当使用String值调用这个方法会发生的。第十章会讨论泛型。这个签名提供了理解+运算那奇怪的部分的线索。

-

首先,s2使用了&,意味着我们使用第二个字符串的引用与第一个字符串相加。这是因为add函数的s参数:只能将&strString相加,不能将两个String值相加。回忆之前第四章我们讲到&String是如何被强转为&str的:写成&s2的话String将会被强转成一个合适的类型&str。又因为方法没有获取参数的所有权,所以s2在这个操作后仍然有效。

+

首先,s2使用了&,意味着我们使用第二个字符串的引用与第一个字符串相加。这是因为add函数的s参数:只能将&strString相加,不能将两个String值相加。不过等一下——正如add的第二个参数所指定的,&s2的类型是&String而不是&str。那么为什么代码还能编译呢?之所以能够在add调用中使用&s2是因为&String可以被强转coerced)成 &str——当add函数被调用时,Rust 使用了一个被成为解引用强制多态deref coercion)的技术,你可以将其理解为它把&s2变成了&s2[..]以供add函数使用。第十五章会更深入的讨论解引用强制多态。因为add没有获取参数的所有权,所以s2在这个操作后仍然是有效的String

其次,可以发现签名中add获取了self的所有权,因为self没有使用&。这意味着上面例子中的s1的所有权将被移动到add调用中,之后就不再有效。所以虽然let s3 = s1 + &s2;看起来就像它会复制两个字符串并创建一个新的字符串,而实际上这个语句会获取s1的所有权,附加上从s2中拷贝的内容,并返回结果的所有权。换句话说,它看起来好像生成了很多拷贝不过实际上并没有:这个实现比拷贝要更高效。

如果想要级联多个字符串,+的行为就显得笨重了:

let s1 = String::from("tic");
@@ -167,7 +167,7 @@ satisfied [--explain E0277]
   |>             ^^^^^
 note: the type `std::string::String` cannot be indexed by `_`
 
-

错误和提示说明了全部问题:Rust 的字符串不支持索引。那么接下来的问题是,为什么不支持呢?为了回答这个问题,我们必须先聊一聊 Rust 如何在内存中储存字符串。

+

错误和提示说明了全部问题:Rust 的字符串不支持索引。那么接下来的问题是,为什么不支持呢?为了回答这个问题,我们必须先聊一聊 Rust 是如何在内存中储存字符串的。

内部表示

String是一个Vec<u8>的封装。让我们看看之前一些正确编码的字符串的例子。首先是这一个:

let len = String::from("Hola").len();
diff --git a/docs/ch08-03-hash-maps.html b/docs/ch08-03-hash-maps.html
index 1605034..f1492c8 100644
--- a/docs/ch08-03-hash-maps.html
+++ b/docs/ch08-03-hash-maps.html
@@ -69,9 +69,9 @@
                 

哈希 map

-

ch08-03-hash-maps.md +

ch08-03-hash-maps.md
-commit 0d229cc5a3da341196e15a6761735b2952281569

+commit 4f2dc564851dc04b271a2260c834643dfd86c724

最后要介绍的常用集合类型是哈希 maphash map)。HashMap<K, V>类型储存了一个键类型K对应一个值类型V的映射。它通过一个哈希函数hashing function)来实现映射,它决定了如何将键和值放入内存中。很多编程语言支持这种数据结构,不过通常有不同的名字:哈希、map、对象、哈希表或者关联数组,仅举几例。

哈希 map 可以用于需要任何类型作为键来寻找数据的情况,而不是像 vector 那样通过索引。例如,在一个游戏中,你可以将每个团队的分数记录到哈希 map 中,其中键是队伍的名字而值是每个队伍的分数。给出一个队名,就能得到他们的得分。

@@ -192,7 +192,7 @@ println!("{:?}", map);
  • 使用哈希 map 和 vector,创建一个文本接口来允许用户向公司的部门中增加员工的名字。例如,“Add Sally to Engineering”或“Add Amir to Sales”。接着让用户获取一个部门的所有员工的列表,或者公司每个部门的所有员工按照字母顺排序的列表。
  • 标准库 API 文档中描述的这些类型的方法将有助于你进行这些练习!

    -

    我们已经开始解除可能会有失败操作的复杂程序了,这也意味着接下来是一个了解错误处理的绝佳时机!

    +

    我们已经开始接触可能会有失败操作的复杂程序了,这也意味着接下来是一个了解错误处理的绝佳时机!

    diff --git a/docs/ch09-00-error-handling.html b/docs/ch09-00-error-handling.html index 2b4852d..ede643f 100644 --- a/docs/ch09-00-error-handling.html +++ b/docs/ch09-00-error-handling.html @@ -69,11 +69,11 @@

    错误处理

    -

    ch09-00-error-handling.md +

    ch09-00-error-handling.md
    -commit fc825966fabaa408067eb2df3aa45e4fa6644fb6

    +commit 4f2dc564851dc04b271a2260c834643dfd86c724

    -

    Rust 对可靠性的执着也扩展到了错误处理。错误对于软件来说是不可避免的,所以 Rust 有很多功能来处理当现错误的情况。在很多情况下,Rust 要求你承认出错的可能性可能性并在编译代码之前就采取行动。通过确保不会只有在将代码部署到生产环境之后才会发现错误来使得程序更可靠。

    +

    Rust 对可靠性的执着也扩展到了错误处理。错误对于软件来说是不可避免的,所以 Rust 有很多功能来处理当现错误的情况。在很多情况下,Rust 要求你承认出错的可能性并在编译代码之前就采取行动。通过确保不会只有在将代码部署到生产环境之后才会发现错误来使得程序更可靠。

    Rust 将错误组合成两个主要类别:可恢复错误recoverable)和不可恢复错误unrecoverable)。可恢复错误通常代表向用户报告错误和重试操作是合理的情况,比如未找到文件。不可恢复错误通常是 bug 的同义词,比如尝试访问超过数组结尾的位置。

    大部分语言并不区分这两类错误,并采用类似异常这样方式统一处理他们。Rust 并没有异常。相反,对于可恢复错误有Result<T, E>值和panic!,它在遇到不可恢复错误时停止程序执行。这一章会首先介绍panic!调用,接着会讲到如何返回Result<T, E>。最后,我们会讨论当决定是尝试从错误中恢复还是停止执行时需要顾及的权衡考虑。

    diff --git a/docs/ch09-01-unrecoverable-errors-with-panic.html b/docs/ch09-01-unrecoverable-errors-with-panic.html index 0c3715b..58b36e1 100644 --- a/docs/ch09-01-unrecoverable-errors-with-panic.html +++ b/docs/ch09-01-unrecoverable-errors-with-panic.html @@ -69,11 +69,11 @@

    panic!与不可恢复的错误

    -

    ch09-01-unrecoverable-errors-with-panic.md +

    ch09-01-unrecoverable-errors-with-panic.md
    -commit 380e6ee57c251f5ffa8df4c58b3949405448d914

    +commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

    -

    突然有一天,糟糕的事情发生了,而你对此束手无策。对于这种情况,Rust 有`panic!宏。当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据,并接着退出。出现这种情况的场景通常是检测到一些类型的 bug 而且程序员并不清楚该如何处理它。

    +

    突然有一天,糟糕的事情发生了,而你对此束手无策。对于这种情况,Rust 有panic!宏。当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据,然后接着退出。出现这种情况的场景通常是检测到一些类型的 bug 而且程序员并不清楚该如何处理它。

    Panic 中的栈展开与终止

    当出现panic!时,程序默认会开始展开unwinding),这意味着 Rust 会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的过程有很多工作。另一种选择是直接终止abort),这会不清理数据就退出程序。那么程序所使用的内存需要由操作系统来清理。如果你需要项目的最终二进制文件越小越好,可以由 panic 时展开切换为终止,通过在 Cargo.toml[profile]部分增加panic = 'abort'。例如,如果你想要在发布模式中 panic 时直接终止:

    @@ -98,7 +98,7 @@ error: Process didn't exit successfully: `target/debug/panic` (exit code: 101)

    最后三行包含panic!造成的错误信息。第一行显示了 panic 提供的信息并指明了源码中 panic 出现的位置:src/main.rs:2 表明这是 src/main.rs 文件的第二行。

    在这个例子中,被指明的那一行是我们代码的一部分,而且查看这一行的话就会发现panic!宏的调用。换句话说,panic!可能会出现在我们的代码调用的代码中。错误信息报告的文件名和行号可能指向别人代码中的panic!宏调用,而不是我们代码中最终导致panic!的那一行。可以使用panic!被调用的函数的 backtrace 来寻找(我们代码中出问题的地方)。

    -

    使用panic!backtrace

    +

    使用panic!的 backtrace

    让我们来看看另一个因为我们代码中的 bug 引起的别的库中panic!的例子,而不是直接的宏调用:

    Filename: src/main.rs

    fn main() {
    @@ -121,7 +121,6 @@ error: Process didn't exit successfully: `target/debug/panic` (exit code: 101)
     

    这指向了一个不是我们编写的文件,libcollections/vec.rs。这是标准库中Vec<T>的实现。这是当对 vector v使用[]libcollections/vec.rs 中会执行的代码,也是真正出现panic!的地方。

    接下来的几行提醒我们可以设置RUST_BACKTRACE环境变量来得到一个 backtrace 来调查究竟是什么导致了错误。让我们来试试看。列表 9-1 显示了其输出:

    -
    $ RUST_BACKTRACE=1 cargo run
         Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
          Running `target/debug/panic`
    @@ -148,11 +147,8 @@ core::ops::Index<usize>>::index::h1204ab053b688140
       13:        0x109226b16 - std::rt::lang_start::h1204ab053b688140
       14:        0x1092206e9 - main
     
    -
    -

    Listing 9-1: The backtrace generated by a call to panic! displayed when the -environment variable RUST_BACKTRACE is set

    -
    -
    +

    Listing 9-1: The backtrace generated by a call to +panic! displayed when the environment variable RUST_BACKTRACE is set

    这里有大量的输出!backtrace 第 11 行指向了我们程序中引起错误的行:src/main.rs 的第四行。backtrace 是一个执行到目前位置所有被调用的函数的列表。Rust 的 backtrace 跟其他语言中的一样:阅读 backtrace 的关键是从头开始读直到发现你编写的文件。这就是问题的发源地。这一行往上是你的代码调用的代码;往下则是调用你的代码的代码。这些行可能包含核心 Rust 代码,标准库代码或用到的 crate 代码。

    如果你不希望我们的程序 panic,第一个提到我们编写的代码行的位置是你应该开始调查的,以便查明是什么值如何在这个地方引起了 panic。在上面的例子中,我们故意编写会 panic 的代码来演示如何使用 backtrace,修复这个 panic 的方法就是不要尝试在一个只包含三个项的 vector 中请求索引是 100 的元素。当将来你得代码出现了 panic,你需要搞清楚在这特定的场景下代码中执行了什么操作和什么值导致了 panic,以及应当如何处理才能避免这个问题。

    本章的后面会再次回到panic!并讲到何时应该何时不应该使用这个方式。接下来,我们来看看如何使用Result来从错误中恢复。

    diff --git a/docs/ch09-02-recoverable-errors-with-result.html b/docs/ch09-02-recoverable-errors-with-result.html index fe609a4..34341be 100644 --- a/docs/ch09-02-recoverable-errors-with-result.html +++ b/docs/ch09-02-recoverable-errors-with-result.html @@ -69,12 +69,12 @@

    Result与可恢复的错误

    -

    ch09-01-unrecoverable-errors-with-panic.md +

    ch09-01-unrecoverable-errors-with-panic.md
    -commit 0c1d55ef48e5f6cf6a3b221f5b6dd4c922130bb1

    +commit e6d6caab41471f7115a621029bd428a812c5260e

    -

    大部分错误并没有严重到需要程序完全停止执行。有时,一个函数会因为一个容易理解并回应的原因失败。例如,如果尝试打开一个文件不过由于文件并不存在而操作就失败,这是我们可能想要创建这个文件而不是终止进程。

    -

    回忆一下第二章“使用Result类型来处理潜在的错误”部分中的那个Result枚举,它定义有如下连个成员,OkErr

    +

    大部分错误并没有严重到需要程序完全停止执行。有时,一个函数会因为一个容易理解并做出反映的原因失败。例如,如果尝试打开一个文件不过由于文件并不存在而操作就失败,这是我们可能想要创建这个文件而不是终止进程。

    +

    回忆一下第二章“使用Result类型来处理潜在的错误”部分中的那个Result枚举,它定义有如下两个成员,OkErr

    enum Result<T, E> {
         Ok(T),
         Err(E),
    @@ -82,18 +82,14 @@ commit 0c1d55ef48e5f6cf6a3b221f5b6dd4c922130bb1

    TE是泛型类型参数;第十章会详细介绍泛型。现在你需要知道的就是T代表成功时返回的Ok成员中的数据的类型,而E代表失败时返回的Err成员中的错误的类型。因为Result有这些泛型类型参数,我们可以将Result类型和标准库中为其定义的函数用于很多不同的场景,这些情况中需要返回的成功值和失败值可能会各不相同。

    让我们调用一个返回Result的函数,因为它可能会失败:如列表 9-2 所示打开一个文件:

    -
    -Filename: src/main.rs +

    Filename: src/main.rs

    use std::fs::File;
     
     fn main() {
         let f = File::open("hello.txt");
     }
     
    -
    -

    Listing 9-2: Opening a file

    -
    -
    +

    Listing 9-2: Opening a file

    如何知道File::open返回一个Result呢?我们可以查看标准库 API 文档,或者可以直接问编译器!如果给f某个我们知道不是函数返回值类型的类型注解,接着尝试编译代码,编译器会告诉我们类型不匹配。然后错误信息会告诉我们f的类型应该是什么,为此我们将let f语句改为:

    let f: u32 = File::open("hello.txt");
     
    @@ -112,8 +108,7 @@ fn main() {

    这个返回值类型说明File::open调用可能会成功并返回一个可以进行读写的文件句柄。这个函数也可能会失败:例如,文件可能并不存在,或者可能没有访问文件的权限。File::open需要一个方式告诉我们是成功还是失败,并同时提供给我们文件句柄或错误信息。而这些信息正是Result枚举可以提供的。

    File::open成功的情况下,变量f的值将会是一个包含文件句柄的Ok实例。在失败的情况下,f会是一个包含更多关于出现了何种错误信息的Err实例。

    我们需要在列表 9-2 的代码中增加根据File::open返回值进行不同处理的逻辑。列表 9-3 展示了一个处理Result的基本工具:第六章学习过的match表达式。

    -
    -Filename: src/main.rs +

    Filename: src/main.rs

    use std::fs::File;
     
     fn main() {
    @@ -127,11 +122,8 @@ fn main() {
         };
     }
     
    -
    -

    Listing 9-3: Using a match expression to handle the Result variants we -might have

    -
    -
    +

    Listing 9-3: Using a match expression to handle the +Result variants we might have

    注意与Option枚举一样,Result枚举和其成员也被导入到了 prelude 中,所以就不需要在match分支中的OkErr之前指定Result::

    这里我们告诉 Rust 当结果是Ok,返回Ok成员中的file值,然后将这个文件句柄赋值给变量fmatch之后,我们可以利用这个文件句柄来进行读写。

    match的另一个分支处理从File::open得到Err值的情况。在这种情况下,我们选择调用panic!宏。如果当前目录没有一个叫做 hello.txt 的文件,当运行这段代码时会看到如下来自panic!宏的输出:

    @@ -140,8 +132,7 @@ Os { code: 2, message: "No such file or directory" } }', src/main.rs:8

    匹配不同的错误

    列表 9-3 中的代码不管File::open是因为什么原因失败都会panic!。我们真正希望的是对不同的错误原因采取不同的行为:如果File::open因为文件不存在而失败,我们希望创建这个文件并返回新文件的句柄。如果File::open因为任何其他原因失败,例如没有打开文件的权限,我们仍然希望像列表 9-3 那样panic!。让我们看看列表 9-4,其中match增加了另一个分支:

    -
    -Filename: src/main.rs +

    Filename: src/main.rs

    use std::fs::File;
     use std::io::ErrorKind;
     
    @@ -170,12 +161,10 @@ fn main() {
         };
     }
     
    -
    -

    Listing 9-4: Handling different kinds of errors in different ways

    -
    -
    +

    Listing 9-4: Handling different kinds of errors in +different ways

    File::open返回的Err成员中的值类型io::Error,它是一个标准库中提供的结构体。这个结构体有一个返回io::ErrorKind值的kind方法可供调用。io::ErrorKind是一个标准库提供的枚举,它的成员对应io操作可能导致的不同错误类型。我们感兴趣的成员是ErrorKind::NotFound,它代表尝试打开的文件并不存在。

    -

    if error.kind() == ErrorKind::NotFound条件被称作 match guard:它是一个进一步完善match分支模式的额外的条件。这个条件必须为真才能使分支的代码被执行;否则,模式匹配会继续并考虑match中的下一个分支。模式中的ref是必须的,这样error就不会被移动到 guard 条件中而只是仅仅引用它。第十八章会详细解释为什么在模式中使用ref而不是&来获取一个引用。简而言之,在模式的上下文中,&匹配一个引用并返回它的值,而ref匹配一个值并返回一个引用。

    +

    条件if error.kind() == ErrorKind::NotFound被称作 match guard:它是一个进一步完善match分支模式的额外的条件。这个条件必须为真才能使分支的代码被执行;否则,模式匹配会继续并考虑match中的下一个分支。模式中的ref是必须的,这样error就不会被移动到 guard 条件中而只是仅仅引用它。第十八章会详细解释为什么在模式中使用ref而不是&来获取一个引用。简而言之,在模式的上下文中,&匹配一个引用并返回它的值,而ref匹配一个值并返回一个引用。

    在 match guard 中我们想要检查的条件是error.kind()是否是ErrorKind枚举的NotFound成员。如果是,尝试用File::create创建文件。然而File::create也可能会失败,我们还需要增加一个内部match语句。当文件不能被打开,会打印出一个不同的错误信息。外部match的最后一个分支保持不变这样对任何除了文件不存在的错误会使程序 panic。

    失败时 panic 的捷径:unwrapexpect

    match能够胜任它的工作,不过它可能有点冗长并且并不总是能很好的表明意图。Result<T, E>类型定义了很多辅助方法来处理各种情况。其中之一叫做unwrap,它的实现就类似于列表 9-3 中的match语句。如果Result值是成员Okunwrap会返回Ok中的值。如果Result是成员Errunwrap会为我们调用panic!

    @@ -205,7 +194,6 @@ fn main() {

    传播错误

    当编写一个其实现会调用一些可能会失败的操作的函数时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理。这被称为传播propagating)错误,这样能更好的控制代码调用,因为比起你代码所拥有的上下文,调用者可能拥有更多信息或逻辑来决定应该如何处理错误。

    例如,列表 9-5 展示了一个从文件中读取用户名的函数。如果文件不存在或不能读取,这个函数会将这些错误返回给调用它的代码:

    -
    use std::io;
     use std::io::Read;
     use std::fs::File;
    @@ -226,19 +214,17 @@ fn read_username_from_file() -> Result<String, io::Error> {
         }
     }
     
    -
    -

    Listing 9-5: A function that returns errors to the calling code using match

    -
    -
    +

    Listing 9-5: A function that returns errors to the +calling code using match

    首先让我们看看函数的返回值:Result<String, io::Error>。这意味着函数返回一个Result<T, E>类型的值,其中泛型参数T的具体类型是String,而E的具体类型是io::Error。如果这个函数没有出任何错误成功返回,函数的调用者会收到一个包含StringOk值————函数从文件中读取到的用户名。如果函数遇到任何错误,函数的调用者会收到一个Err值,它储存了一个包含更多这个问题相关信息的io::Error实例。我们选择io::Error作为函数的返回值是因为它正好是函数体中那两个可能会失败的操作的错误返回值:File::open函数和read_to_string方法。

    函数体以File::open函数开头。接着使用match处理返回值Result,类似于列表 9-3 中的match,唯一的区别是不再当Err时调用panic!,而是提早返回并将File::open返回的错误值作为函数的错误返回值传递给调用者。如果File::open成功了,我们将文件句柄储存在变量f中并继续。

    接着我们在变量s中创建了一个新String并调用文件句柄fread_to_string方法来将文件的内容读取到s中。read_to_string方法也返回一个Result因为它也可能会失败:哪怕是File::open已经成功了。所以我们需要另一个match来处理这个Result:如果read_to_string成功了,那么这个函数就成功了,并返回文件中的用户名,它现在位于被封装进Oks中。如果read_to_string失败了,则像之前处理File::open的返回值的match那样返回错误值。并不需要显式的调用return,因为这是函数的最后一个表达式。

    调用这个函数的代码最终会得到一个包含用户名的Ok值,亦或一个包含io::ErrorErr值。我们无从得知调用者会如何处理这些值。例如,如果他们得到了一个Err值,他们可能会选择panic!并使程序崩溃、使用一个默认的用户名或者从文件之外的地方寻找用户名。我们没有足够的信息知晓调用者具体会如何尝试,所以将所有的成功或失败信息向上传播,让他们选择合适处理方法。

    这种传播错误的模式在 Rust 是如此的常见,以至于有一个更简便的专用语法:?

    传播错误的捷径:?

    -

    列表 9-6 展示了一个read_username_from_file的实现,它实现了与列表 9-5 中的代码相同的功能,不过这个实现是使用了问号运算符:

    -
    +

    列表 9-6 展示了一个read_username_from_file的实现,它实现了与列表 9-5 中的代码相同的功能,不过这个实现是使用了问号运算符的:

    use std::io;
    +use std::io::Read;
     use std::fs::File;
     
     fn read_username_from_file() -> Result<String, io::Error> {
    @@ -248,10 +234,8 @@ fn read_username_from_file() -> Result<String, io::Error> {
         Ok(s)
     }
     
    -
    -

    Listing 9-6: A function that returns errors to the calling code using ?

    -
    -
    +

    Listing 9-6: A function that returns errors to the +calling code using ?

    Result值之后的?被定义为与列表 9-5 中定义的处理Result值的match表达式有着完全相同的工作方式。如果Result的值是Ok,这个表达式将会返回Ok中的值而程序将继续执行。如果值是ErrErr中的值将作为整个函数的返回值,就好像使用了return关键字一样,这样错误值就被传播给了调用者。

    在列表 9-6 的上下文中,File::open调用结尾的?将会把Ok中的值返回给变量f。如果出现了错误,?会提早返回整个函数并将任何Err值传播给调用者。同理也适用于read_to_string调用结尾的?

    ?消除了大量样板代码并使得函数的实现更简单。我们甚至可以在?之后直接使用链式方法调用来进一步缩短代码:

    diff --git a/docs/ch09-03-to-panic-or-not-to-panic.html b/docs/ch09-03-to-panic-or-not-to-panic.html index 13d3978..191ff77 100644 --- a/docs/ch09-03-to-panic-or-not-to-panic.html +++ b/docs/ch09-03-to-panic-or-not-to-panic.html @@ -69,11 +69,11 @@

    panic!还是不panic!

    -

    ch09-03-to-panic-or-not-to-panic.md +

    ch09-03-to-panic-or-not-to-panic.md
    -commit 0c1d55ef48e5f6cf6a3b221f5b6dd4c922130bb1

    +commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

    -

    那么,该如何决定何时应该panic!以及何时应该返回Result呢?如果代码 panic,就没有恢复的可能。你可以选择对任何错误场景都调用panic!,不管是否有可能恢复,不过这样就你代替调用者决定了这是不可恢复的。选择返回Result值的话,就将选择权交给了调用者,而不是代替他们做出决定。调用者可能会选择以符合他们场景的方式尝试恢复,或者也可能干脆就认为Err是不可恢复的,所以他们也可能会调用panic!并将可恢复的错误变成了不可恢复的错误。因此返回Result是定义可能会失败的函数的一个好的默认选择。

    +

    那么,该如何决定何时应该panic!以及何时应该返回Result呢?如果代码 panic,就没有恢复的可能。你可以选择对任何错误场景都调用panic!,不管是否有可能恢复,不过这样就是你代替调用者决定了这是不可恢复的。选择返回Result值的话,就将选择权交给了调用者,而不是代替他们做出决定。调用者可能会选择以符合他们场景的方式尝试恢复,或者也可能干脆就认为Err是不可恢复的,所以他们也可能会调用panic!并将可恢复的错误变成了不可恢复的错误。因此返回Result是定义可能会失败的函数的一个好的默认选择。

    有一些情况 panic 比返回Result更为合适,不过他们并不常见。让我们讨论一下为何在示例、代码原型和测试中,以及那些人们认为不会失败而编译器不这么看的情况下, panic 是合适的,最后会总结一些在库代码中如何决定是否要 panic 的通用指导原则。

    示例、代码原型和测试:非常适合 panic

    当你编写一个示例来展示一些概念时,在拥有健壮的错误处理代码的同时也会使得例子不那么明确。例如,调用一个类似unwrap这样可能panic!的方法可以被理解为一个你实际希望程序处理错误方式的占位符,它根据其余代码运行方式可能会各不相同。

    @@ -87,7 +87,7 @@ let home = "127.0.0.1".parse::<IpAddr>().unwrap();

    我们通过解析一个硬编码的字符来创建一个IpAddr实例。可以看出127.0.0.1是一个有效的 IP 地址,所以这里使用unwrap是没有问题的。然而,拥有一个硬编码的有效的字符串也不能改变parse方法的返回值类型:它仍然是一个Result值,而编译器仍然就好像还是有可能出现Err成员那样要求我们处理Result,因为编译器还没有智能到可以识别出这个字符串总是一个有效的 IP 地址。如果 IP 地址字符串来源于用户而不是硬编码进程序中的话,那么就确实有失败的可能性,这时就绝对需要我们以一种更健壮的方式处理Result了。

    错误处理指导原则

    -

    在当有可能会导致有害状态的情况下建议使用panic!————在这里,有害状态是指当一些假设、保证、协议或不可变形被打破的状态,例如无效的值、自相矛盾的值或者被传递了不存在的值————外加如下几种情况:

    +

    在当有可能会导致有害状态的情况下建议使用panic!——在这里,有害状态是指当一些假设、保证、协议或不可变性被打破的状态,例如无效的值、自相矛盾的值或者被传递了不存在的值——外加如下几种情况:

    标准库 API 文档中描述的这些类型的方法将有助于你进行这些练习!

    -

    我们已经开始解除可能会有失败操作的复杂程序了,这也意味着接下来是一个了解错误处理的绝佳时机!

    +

    我们已经开始接触可能会有失败操作的复杂程序了,这也意味着接下来是一个了解错误处理的绝佳时机!

    错误处理

    -

    ch09-00-error-handling.md +

    ch09-00-error-handling.md
    -commit fc825966fabaa408067eb2df3aa45e4fa6644fb6

    +commit 4f2dc564851dc04b271a2260c834643dfd86c724

    -

    Rust 对可靠性的执着也扩展到了错误处理。错误对于软件来说是不可避免的,所以 Rust 有很多功能来处理当现错误的情况。在很多情况下,Rust 要求你承认出错的可能性可能性并在编译代码之前就采取行动。通过确保不会只有在将代码部署到生产环境之后才会发现错误来使得程序更可靠。

    +

    Rust 对可靠性的执着也扩展到了错误处理。错误对于软件来说是不可避免的,所以 Rust 有很多功能来处理当现错误的情况。在很多情况下,Rust 要求你承认出错的可能性并在编译代码之前就采取行动。通过确保不会只有在将代码部署到生产环境之后才会发现错误来使得程序更可靠。

    Rust 将错误组合成两个主要类别:可恢复错误recoverable)和不可恢复错误unrecoverable)。可恢复错误通常代表向用户报告错误和重试操作是合理的情况,比如未找到文件。不可恢复错误通常是 bug 的同义词,比如尝试访问超过数组结尾的位置。

    大部分语言并不区分这两类错误,并采用类似异常这样方式统一处理他们。Rust 并没有异常。相反,对于可恢复错误有Result<T, E>值和panic!,它在遇到不可恢复错误时停止程序执行。这一章会首先介绍panic!调用,接着会讲到如何返回Result<T, E>。最后,我们会讨论当决定是尝试从错误中恢复还是停止执行时需要顾及的权衡考虑。

    panic!与不可恢复的错误

    -

    ch09-01-unrecoverable-errors-with-panic.md +

    ch09-01-unrecoverable-errors-with-panic.md
    -commit 380e6ee57c251f5ffa8df4c58b3949405448d914

    +commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

    -

    突然有一天,糟糕的事情发生了,而你对此束手无策。对于这种情况,Rust 有`panic!宏。当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据,并接着退出。出现这种情况的场景通常是检测到一些类型的 bug 而且程序员并不清楚该如何处理它。

    +

    突然有一天,糟糕的事情发生了,而你对此束手无策。对于这种情况,Rust 有panic!宏。当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据,然后接着退出。出现这种情况的场景通常是检测到一些类型的 bug 而且程序员并不清楚该如何处理它。

    Panic 中的栈展开与终止

    当出现panic!时,程序默认会开始展开unwinding),这意味着 Rust 会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的过程有很多工作。另一种选择是直接终止abort),这会不清理数据就退出程序。那么程序所使用的内存需要由操作系统来清理。如果你需要项目的最终二进制文件越小越好,可以由 panic 时展开切换为终止,通过在 Cargo.toml[profile]部分增加panic = 'abort'。例如,如果你想要在发布模式中 panic 时直接终止:

    @@ -4044,7 +4040,7 @@ error: Process didn't exit successfully: `target/debug/panic` (exit code: 101)

    最后三行包含panic!造成的错误信息。第一行显示了 panic 提供的信息并指明了源码中 panic 出现的位置:src/main.rs:2 表明这是 src/main.rs 文件的第二行。

    在这个例子中,被指明的那一行是我们代码的一部分,而且查看这一行的话就会发现panic!宏的调用。换句话说,panic!可能会出现在我们的代码调用的代码中。错误信息报告的文件名和行号可能指向别人代码中的panic!宏调用,而不是我们代码中最终导致panic!的那一行。可以使用panic!被调用的函数的 backtrace 来寻找(我们代码中出问题的地方)。

    -

    使用panic!backtrace

    +

    使用panic!的 backtrace

    让我们来看看另一个因为我们代码中的 bug 引起的别的库中panic!的例子,而不是直接的宏调用:

    Filename: src/main.rs

    fn main() {
    @@ -4067,7 +4063,6 @@ error: Process didn't exit successfully: `target/debug/panic` (exit code: 101)
     

    这指向了一个不是我们编写的文件,libcollections/vec.rs。这是标准库中Vec<T>的实现。这是当对 vector v使用[]libcollections/vec.rs 中会执行的代码,也是真正出现panic!的地方。

    接下来的几行提醒我们可以设置RUST_BACKTRACE环境变量来得到一个 backtrace 来调查究竟是什么导致了错误。让我们来试试看。列表 9-1 显示了其输出:

    -
    $ RUST_BACKTRACE=1 cargo run
         Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
          Running `target/debug/panic`
    @@ -4094,22 +4089,19 @@ core::ops::Index<usize>>::index::h1204ab053b688140
       13:        0x109226b16 - std::rt::lang_start::h1204ab053b688140
       14:        0x1092206e9 - main
     
    -
    -

    Listing 9-1: The backtrace generated by a call to panic! displayed when the -environment variable RUST_BACKTRACE is set

    -
    -
    +

    Listing 9-1: The backtrace generated by a call to +panic! displayed when the environment variable RUST_BACKTRACE is set

    这里有大量的输出!backtrace 第 11 行指向了我们程序中引起错误的行:src/main.rs 的第四行。backtrace 是一个执行到目前位置所有被调用的函数的列表。Rust 的 backtrace 跟其他语言中的一样:阅读 backtrace 的关键是从头开始读直到发现你编写的文件。这就是问题的发源地。这一行往上是你的代码调用的代码;往下则是调用你的代码的代码。这些行可能包含核心 Rust 代码,标准库代码或用到的 crate 代码。

    如果你不希望我们的程序 panic,第一个提到我们编写的代码行的位置是你应该开始调查的,以便查明是什么值如何在这个地方引起了 panic。在上面的例子中,我们故意编写会 panic 的代码来演示如何使用 backtrace,修复这个 panic 的方法就是不要尝试在一个只包含三个项的 vector 中请求索引是 100 的元素。当将来你得代码出现了 panic,你需要搞清楚在这特定的场景下代码中执行了什么操作和什么值导致了 panic,以及应当如何处理才能避免这个问题。

    本章的后面会再次回到panic!并讲到何时应该何时不应该使用这个方式。接下来,我们来看看如何使用Result来从错误中恢复。

    Result与可恢复的错误

    -

    ch09-01-unrecoverable-errors-with-panic.md +

    ch09-01-unrecoverable-errors-with-panic.md
    -commit 0c1d55ef48e5f6cf6a3b221f5b6dd4c922130bb1

    +commit e6d6caab41471f7115a621029bd428a812c5260e

    -

    大部分错误并没有严重到需要程序完全停止执行。有时,一个函数会因为一个容易理解并回应的原因失败。例如,如果尝试打开一个文件不过由于文件并不存在而操作就失败,这是我们可能想要创建这个文件而不是终止进程。

    -

    回忆一下第二章“使用Result类型来处理潜在的错误”部分中的那个Result枚举,它定义有如下连个成员,OkErr

    +

    大部分错误并没有严重到需要程序完全停止执行。有时,一个函数会因为一个容易理解并做出反映的原因失败。例如,如果尝试打开一个文件不过由于文件并不存在而操作就失败,这是我们可能想要创建这个文件而不是终止进程。

    +

    回忆一下第二章“使用Result类型来处理潜在的错误”部分中的那个Result枚举,它定义有如下两个成员,OkErr

    enum Result<T, E> {
         Ok(T),
         Err(E),
    @@ -4117,18 +4109,14 @@ commit 0c1d55ef48e5f6cf6a3b221f5b6dd4c922130bb1

    TE是泛型类型参数;第十章会详细介绍泛型。现在你需要知道的就是T代表成功时返回的Ok成员中的数据的类型,而E代表失败时返回的Err成员中的错误的类型。因为Result有这些泛型类型参数,我们可以将Result类型和标准库中为其定义的函数用于很多不同的场景,这些情况中需要返回的成功值和失败值可能会各不相同。

    让我们调用一个返回Result的函数,因为它可能会失败:如列表 9-2 所示打开一个文件:

    -
    -Filename: src/main.rs +

    Filename: src/main.rs

    use std::fs::File;
     
     fn main() {
         let f = File::open("hello.txt");
     }
     
    -
    -

    Listing 9-2: Opening a file

    -
    -
    +

    Listing 9-2: Opening a file

    如何知道File::open返回一个Result呢?我们可以查看标准库 API 文档,或者可以直接问编译器!如果给f某个我们知道不是函数返回值类型的类型注解,接着尝试编译代码,编译器会告诉我们类型不匹配。然后错误信息会告诉我们f的类型应该是什么,为此我们将let f语句改为:

    let f: u32 = File::open("hello.txt");
     
    @@ -4147,8 +4135,7 @@ fn main() {

    这个返回值类型说明File::open调用可能会成功并返回一个可以进行读写的文件句柄。这个函数也可能会失败:例如,文件可能并不存在,或者可能没有访问文件的权限。File::open需要一个方式告诉我们是成功还是失败,并同时提供给我们文件句柄或错误信息。而这些信息正是Result枚举可以提供的。

    File::open成功的情况下,变量f的值将会是一个包含文件句柄的Ok实例。在失败的情况下,f会是一个包含更多关于出现了何种错误信息的Err实例。

    我们需要在列表 9-2 的代码中增加根据File::open返回值进行不同处理的逻辑。列表 9-3 展示了一个处理Result的基本工具:第六章学习过的match表达式。

    -
    -Filename: src/main.rs +

    Filename: src/main.rs

    use std::fs::File;
     
     fn main() {
    @@ -4162,11 +4149,8 @@ fn main() {
         };
     }
     
    -
    -

    Listing 9-3: Using a match expression to handle the Result variants we -might have

    -
    -
    +

    Listing 9-3: Using a match expression to handle the +Result variants we might have

    注意与Option枚举一样,Result枚举和其成员也被导入到了 prelude 中,所以就不需要在match分支中的OkErr之前指定Result::

    这里我们告诉 Rust 当结果是Ok,返回Ok成员中的file值,然后将这个文件句柄赋值给变量fmatch之后,我们可以利用这个文件句柄来进行读写。

    match的另一个分支处理从File::open得到Err值的情况。在这种情况下,我们选择调用panic!宏。如果当前目录没有一个叫做 hello.txt 的文件,当运行这段代码时会看到如下来自panic!宏的输出:

    @@ -4175,8 +4159,7 @@ Os { code: 2, message: "No such file or directory" } }', src/main.rs:8

    匹配不同的错误

    列表 9-3 中的代码不管File::open是因为什么原因失败都会panic!。我们真正希望的是对不同的错误原因采取不同的行为:如果File::open因为文件不存在而失败,我们希望创建这个文件并返回新文件的句柄。如果File::open因为任何其他原因失败,例如没有打开文件的权限,我们仍然希望像列表 9-3 那样panic!。让我们看看列表 9-4,其中match增加了另一个分支:

    -
    -Filename: src/main.rs +

    Filename: src/main.rs

    use std::fs::File;
     use std::io::ErrorKind;
     
    @@ -4205,12 +4188,10 @@ fn main() {
         };
     }
     
    -
    -

    Listing 9-4: Handling different kinds of errors in different ways

    -
    -
    +

    Listing 9-4: Handling different kinds of errors in +different ways

    File::open返回的Err成员中的值类型io::Error,它是一个标准库中提供的结构体。这个结构体有一个返回io::ErrorKind值的kind方法可供调用。io::ErrorKind是一个标准库提供的枚举,它的成员对应io操作可能导致的不同错误类型。我们感兴趣的成员是ErrorKind::NotFound,它代表尝试打开的文件并不存在。

    -

    if error.kind() == ErrorKind::NotFound条件被称作 match guard:它是一个进一步完善match分支模式的额外的条件。这个条件必须为真才能使分支的代码被执行;否则,模式匹配会继续并考虑match中的下一个分支。模式中的ref是必须的,这样error就不会被移动到 guard 条件中而只是仅仅引用它。第十八章会详细解释为什么在模式中使用ref而不是&来获取一个引用。简而言之,在模式的上下文中,&匹配一个引用并返回它的值,而ref匹配一个值并返回一个引用。

    +

    条件if error.kind() == ErrorKind::NotFound被称作 match guard:它是一个进一步完善match分支模式的额外的条件。这个条件必须为真才能使分支的代码被执行;否则,模式匹配会继续并考虑match中的下一个分支。模式中的ref是必须的,这样error就不会被移动到 guard 条件中而只是仅仅引用它。第十八章会详细解释为什么在模式中使用ref而不是&来获取一个引用。简而言之,在模式的上下文中,&匹配一个引用并返回它的值,而ref匹配一个值并返回一个引用。

    在 match guard 中我们想要检查的条件是error.kind()是否是ErrorKind枚举的NotFound成员。如果是,尝试用File::create创建文件。然而File::create也可能会失败,我们还需要增加一个内部match语句。当文件不能被打开,会打印出一个不同的错误信息。外部match的最后一个分支保持不变这样对任何除了文件不存在的错误会使程序 panic。

    失败时 panic 的捷径:unwrapexpect

    match能够胜任它的工作,不过它可能有点冗长并且并不总是能很好的表明意图。Result<T, E>类型定义了很多辅助方法来处理各种情况。其中之一叫做unwrap,它的实现就类似于列表 9-3 中的match语句。如果Result值是成员Okunwrap会返回Ok中的值。如果Result是成员Errunwrap会为我们调用panic!

    @@ -4240,7 +4221,6 @@ fn main() {

    传播错误

    当编写一个其实现会调用一些可能会失败的操作的函数时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理。这被称为传播propagating)错误,这样能更好的控制代码调用,因为比起你代码所拥有的上下文,调用者可能拥有更多信息或逻辑来决定应该如何处理错误。

    例如,列表 9-5 展示了一个从文件中读取用户名的函数。如果文件不存在或不能读取,这个函数会将这些错误返回给调用它的代码:

    -
    use std::io;
     use std::io::Read;
     use std::fs::File;
    @@ -4261,19 +4241,17 @@ fn read_username_from_file() -> Result<String, io::Error> {
         }
     }
     
    -
    -

    Listing 9-5: A function that returns errors to the calling code using match

    -
    -
    +

    Listing 9-5: A function that returns errors to the +calling code using match

    首先让我们看看函数的返回值:Result<String, io::Error>。这意味着函数返回一个Result<T, E>类型的值,其中泛型参数T的具体类型是String,而E的具体类型是io::Error。如果这个函数没有出任何错误成功返回,函数的调用者会收到一个包含StringOk值————函数从文件中读取到的用户名。如果函数遇到任何错误,函数的调用者会收到一个Err值,它储存了一个包含更多这个问题相关信息的io::Error实例。我们选择io::Error作为函数的返回值是因为它正好是函数体中那两个可能会失败的操作的错误返回值:File::open函数和read_to_string方法。

    函数体以File::open函数开头。接着使用match处理返回值Result,类似于列表 9-3 中的match,唯一的区别是不再当Err时调用panic!,而是提早返回并将File::open返回的错误值作为函数的错误返回值传递给调用者。如果File::open成功了,我们将文件句柄储存在变量f中并继续。

    接着我们在变量s中创建了一个新String并调用文件句柄fread_to_string方法来将文件的内容读取到s中。read_to_string方法也返回一个Result因为它也可能会失败:哪怕是File::open已经成功了。所以我们需要另一个match来处理这个Result:如果read_to_string成功了,那么这个函数就成功了,并返回文件中的用户名,它现在位于被封装进Oks中。如果read_to_string失败了,则像之前处理File::open的返回值的match那样返回错误值。并不需要显式的调用return,因为这是函数的最后一个表达式。

    调用这个函数的代码最终会得到一个包含用户名的Ok值,亦或一个包含io::ErrorErr值。我们无从得知调用者会如何处理这些值。例如,如果他们得到了一个Err值,他们可能会选择panic!并使程序崩溃、使用一个默认的用户名或者从文件之外的地方寻找用户名。我们没有足够的信息知晓调用者具体会如何尝试,所以将所有的成功或失败信息向上传播,让他们选择合适处理方法。

    这种传播错误的模式在 Rust 是如此的常见,以至于有一个更简便的专用语法:?

    传播错误的捷径:?

    -

    列表 9-6 展示了一个read_username_from_file的实现,它实现了与列表 9-5 中的代码相同的功能,不过这个实现是使用了问号运算符:

    -
    +

    列表 9-6 展示了一个read_username_from_file的实现,它实现了与列表 9-5 中的代码相同的功能,不过这个实现是使用了问号运算符的:

    use std::io;
    +use std::io::Read;
     use std::fs::File;
     
     fn read_username_from_file() -> Result<String, io::Error> {
    @@ -4283,10 +4261,8 @@ fn read_username_from_file() -> Result<String, io::Error> {
         Ok(s)
     }
     
    -
    -

    Listing 9-6: A function that returns errors to the calling code using ?

    -
    -
    +

    Listing 9-6: A function that returns errors to the +calling code using ?

    Result值之后的?被定义为与列表 9-5 中定义的处理Result值的match表达式有着完全相同的工作方式。如果Result的值是Ok,这个表达式将会返回Ok中的值而程序将继续执行。如果值是ErrErr中的值将作为整个函数的返回值,就好像使用了return关键字一样,这样错误值就被传播给了调用者。

    在列表 9-6 的上下文中,File::open调用结尾的?将会把Ok中的值返回给变量f。如果出现了错误,?会提早返回整个函数并将任何Err值传播给调用者。同理也适用于read_to_string调用结尾的?

    ?消除了大量样板代码并使得函数的实现更简单。我们甚至可以在?之后直接使用链式方法调用来进一步缩短代码:

    @@ -4335,11 +4311,11 @@ error message. /Carol -->

    现在我们讨论过了调用panic!或返回Result的细节,是时候返回他们各自适合哪些场景的话题了。

    panic!还是不panic!

    -

    ch09-03-to-panic-or-not-to-panic.md +

    ch09-03-to-panic-or-not-to-panic.md
    -commit 0c1d55ef48e5f6cf6a3b221f5b6dd4c922130bb1

    +commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

    -

    那么,该如何决定何时应该panic!以及何时应该返回Result呢?如果代码 panic,就没有恢复的可能。你可以选择对任何错误场景都调用panic!,不管是否有可能恢复,不过这样就你代替调用者决定了这是不可恢复的。选择返回Result值的话,就将选择权交给了调用者,而不是代替他们做出决定。调用者可能会选择以符合他们场景的方式尝试恢复,或者也可能干脆就认为Err是不可恢复的,所以他们也可能会调用panic!并将可恢复的错误变成了不可恢复的错误。因此返回Result是定义可能会失败的函数的一个好的默认选择。

    +

    那么,该如何决定何时应该panic!以及何时应该返回Result呢?如果代码 panic,就没有恢复的可能。你可以选择对任何错误场景都调用panic!,不管是否有可能恢复,不过这样就是你代替调用者决定了这是不可恢复的。选择返回Result值的话,就将选择权交给了调用者,而不是代替他们做出决定。调用者可能会选择以符合他们场景的方式尝试恢复,或者也可能干脆就认为Err是不可恢复的,所以他们也可能会调用panic!并将可恢复的错误变成了不可恢复的错误。因此返回Result是定义可能会失败的函数的一个好的默认选择。

    有一些情况 panic 比返回Result更为合适,不过他们并不常见。让我们讨论一下为何在示例、代码原型和测试中,以及那些人们认为不会失败而编译器不这么看的情况下, panic 是合适的,最后会总结一些在库代码中如何决定是否要 panic 的通用指导原则。

    示例、代码原型和测试:非常适合 panic

    当你编写一个示例来展示一些概念时,在拥有健壮的错误处理代码的同时也会使得例子不那么明确。例如,调用一个类似unwrap这样可能panic!的方法可以被理解为一个你实际希望程序处理错误方式的占位符,它根据其余代码运行方式可能会各不相同。

    @@ -4353,7 +4329,7 @@ let home = "127.0.0.1".parse::<IpAddr>().unwrap();

    我们通过解析一个硬编码的字符来创建一个IpAddr实例。可以看出127.0.0.1是一个有效的 IP 地址,所以这里使用unwrap是没有问题的。然而,拥有一个硬编码的有效的字符串也不能改变parse方法的返回值类型:它仍然是一个Result值,而编译器仍然就好像还是有可能出现Err成员那样要求我们处理Result,因为编译器还没有智能到可以识别出这个字符串总是一个有效的 IP 地址。如果 IP 地址字符串来源于用户而不是硬编码进程序中的话,那么就确实有失败的可能性,这时就绝对需要我们以一种更健壮的方式处理Result了。

    错误处理指导原则

    -

    在当有可能会导致有害状态的情况下建议使用panic!————在这里,有害状态是指当一些假设、保证、协议或不可变形被打破的状态,例如无效的值、自相矛盾的值或者被传递了不存在的值————外加如下几种情况:

    +

    在当有可能会导致有害状态的情况下建议使用panic!——在这里,有害状态是指当一些假设、保证、协议或不可变性被打破的状态,例如无效的值、自相矛盾的值或者被传递了不存在的值——外加如下几种情况:

    • 有害状态并不包含预期会偶尔发生的错误
    • 之后的代码的运行依赖于不再处于这种有害状态
    • @@ -4386,7 +4362,6 @@ let home = "127.0.0.1".parse::<IpAddr>().unwrap();

      if表达式检查了值是否超出范围,告诉用户出了什么问题,并调用continue开始下一次循环,请求另一个猜测。if表达式之后,就可以在知道guess在 1 到 100 之间的情况下与秘密数字作比较了。

      然而,这并不是一个理想的解决方案:程序只处理 1 到 100 之间的值是绝对不可取的,而且如果有很多函数都有这样的要求,在每个函数中都有这样的检查将是非常冗余的(并可能潜在的影响性能)。

      相反我们可以创建一个新类型来将验证放入创建其实例的函数中,而不是到处重复这些检查。这样就可以安全的在函数签名中使用新类型并相信他们接收到的值。列表 9-8 中展示了一个定义Guess类型的方法,只有在new函数接收到 1 到 100 之间的值时才会创建Guess的实例:

      -
      struct Guess {
           value: u32,
       }
      @@ -4407,11 +4382,8 @@ impl Guess {
           }
       }
       
      -
      -

      Listing 9-8: A Guess type that will only continue with values between 1 and -100

      -
      -
      +

      Listing 9-8: A Guess type that will only continue with +values between 1 and 100

      首先,我们定义了一个包含u32类型字段value的结构体Guess。这里是储存猜测值的地方。

      接着在Guess上实现了一个叫做new的关联函数来创建Guess的实例。new定义为接收一个u32类型的参数value并返回一个Guessnew函数中代码的测试确保了其值是在 1 到 100 之间的。如果value没有通过测试则调用panic!,这会警告调用这个函数的程序员有一个需要修改的 bug,因为创建一个value超出范围的Guess将会违反Guess::new所遵循的契约。Guess::new会出现 panic 的条件应该在其公有 API 文档中被提及;第十四章会涉及到在 API 文档中表明panic!可能性的相关规则。如果value通过了测试,我们新建一个Guess,其字段value将被设置为参数value的值,接着返回这个Guess

      接着,我们实现了一个借用了self的方法value,它没有任何其他参数并返回一个u32。这类方法有时被称为 getter,因为它的目的就是返回对应字段的数据。这样的公有方法是必要的,因为Guess结构体的value字段是私有的。私有的字段value是很重要的,这样使用Guess结构体的代码将不允许直接设置value的值:调用者必须使用Guess::new方法来创建一个Guess的实例,这就确保了不会存在一个value没有通过Guess::new函数的条件检查的Guess

      @@ -4421,20 +4393,19 @@ impl Guess {

      现在我们已经见识过了标准库中OptionResult泛型枚举的能力了,让我们聊聊泛型是如何工作的,以及如果在你的代码中利用他们。

      泛型、trait 和生命周期

      -

      ch10-00-generics.md +

      ch10-00-generics.md
      -commit b335da755592f286fd97a64d98f0ca3be6a59327

      +commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

      每一个编程语言都有高效的处理重复概念的工具;在 Rust 中工具之一就是泛型generics)。泛型是具体类型或其他属性的抽象替代。我们可以表达泛型的属性,比如他们的行为或如何与其他泛型相关联,而不需要在编写和编译代码时知道他们在这里实际上代表什么。

      -

      同理为了编写一份可以用于多种具体值的代码,函数并不知道其参数为何值,这时就可以让函数获取泛型而不是像i32String这样的具体值。我们已经使用过第六章的Option<T>,第八章的Vec<T>HashMap<K, V>,以及第九章的Result<T, E>这些泛型了。本章会探索如何使用泛型定义我们自己自己的类型、函数和方法。

      +

      同理为了编写一份可以用于多种具体值的代码,函数并不知道其参数为何值,这时就可以让函数获取泛型而不是像i32String这样的具体值。我们已经使用过第六章的Option<T>,第八章的Vec<T>HashMap<K, V>,以及第九章的Result<T, E>这些泛型了。本章会探索如何使用泛型定义我们自己自己的类型、函数和方法!

      首先,我们将回顾一下提取函数以减少代码重复的机制。接着使用一个只在参数类型上不同的泛型函数来实现相同的功能。我们也会讲到结构体和枚举定义中的泛型。

      之后,我们讨论 traits,这是一个定义泛型行为的方法。trait 可以与泛型结合来将泛型限制为拥有特定行为的类型,而不是任意类型。

      最后介绍生命周期lifetimes),它是一类允许我们向编译器提供引用如何相互关联的泛型。Rust 的生命周期功能允许在很多场景下借用值同时仍然使编译器能够检查这些引用的有效性。

      提取函数来减少重复

      在介绍泛型语法之前,首先来回顾一个不使用泛型的处理重复的技术:提取一个函数。当熟悉了这个技术以后,我们将使用相同的机制来提取一个泛型函数!如同你识别出可以提取到函数中重复代码那样,你也会开始识别出能够使用泛型的重复代码。

      考虑一下这个寻找列表中最大值的小程序,如列表 10-1 所示:

      -
      -Filename: src/main.rs +

      Filename: src/main.rs

      fn main() {
           let numbers = vec![34, 50, 25, 100, 65];
       
      @@ -4450,14 +4421,11 @@ commit b335da755592f286fd97a64d98f0ca3be6a59327

      # assert_eq!(largest, 100); }
      -
      -

      Listing 10-1: Code to find the largest number in a list of numbers

      -
      -
      +

      Listing 10-1: Code to find the largest number in a list +of numbers

      这段代码获取一个整型列表,存放在变量numbers中。它将列表的第一项放入了变量largest中。接着遍历了列表中的所有数字,如果当前值大于largest中储存的值,将largest替换为这个值。如果当前值小于目前为止的最大值,largest保持不变。当列表中所有值都被考虑到之后,largest将会是最大值,在这里也就是 100。

      如果需要在两个不同的列表中寻找最大值,我们可以重复列表 10-1 中的代码这样程序中就会存在两段相同逻辑的代码,如列表 10-2 所示:

      -
      -Filename: src/main.rs +

      Filename: src/main.rs

      fn main() {
           let numbers = vec![34, 50, 25, 100, 65];
       
      @@ -4484,10 +4452,8 @@ commit b335da755592f286fd97a64d98f0ca3be6a59327

      println!("The largest number is {}", largest); }
      -
      -

      Listing 10-2: Code to find the largest number in two lists of numbers

      -
      -
      +

      Listing 10-2: Code to find the largest number in two +lists of numbers

      虽然代码能够执行,但是重复的代码是冗余且已于出错的,并且意味着当更新逻辑时需要修改多处地方的代码。

      @@ -4495,8 +4461,7 @@ commit b335da755592f286fd97a64d98f0ca3be6a59327

      为了消除重复,我们可以创建一层抽象,在这个例子中将表现为一个获取任意整型列表作为参数并对其进行处理的函数。这将增加代码的简洁性并让我们将表达和推导寻找列表中最大值的这个概念与使用这个概念的特定位置相互独。 立。

      在列表 10-3 的程序中将寻找最大值的代码提取到了一个叫做largest的函数中。这个程序可以找出两个不同数字列表的最大值,不过列表 10-1 中的代码只存在于一个位置:

      -
      -Filename: src/main.rs +

      Filename: src/main.rs

      fn largest(list: &[i32]) -> i32 {
           let mut largest = list[0];
       
      @@ -4523,10 +4488,8 @@ fn main() {
       #    assert_eq!(result, 6000);
       }
       
      -
      -

      Listing 10-3: Abstracted code to find the largest number in two lists

      -
      -
      +

      Listing 10-3: Abstracted code to find the largest number +in two lists

      这个函数有一个参数list,它代表会传递给函数的任何具体i32值的 slice。函数定义中的list代表任何&[i32]。当调用largest函数时,其代码实际上运行于我们传递的特定值上。

      从列表 10-2 到列表 10-3 中涉及的机制经历了如下几步:

        @@ -4538,7 +4501,7 @@ fn main() {

        如果我们有两个函数,一个寻找一个i32值的 slice 中的最大项而另一个寻找char值的 slice 中的最大项该怎么办?该如何消除重复呢?让我们拭目以待!

        泛型数据类型

        -

        ch10-01-syntax.md +

        ch10-01-syntax.md
        commit 55d9e75ffec92e922273c997026bb10613a76578

        @@ -4546,8 +4509,7 @@ commit 55d9e75ffec92e922273c997026bb10613a76578

        在函数定义中使用泛型

        定义函数时可以在函数签名的参数数据类型和返回值中使用泛型。以这种方式编写的代码将更灵活并能向函数调用者提供更多功能,同时不引入重复代码。

        回到largest函数上,列表 10-4 中展示了两个提供了相同的寻找 slice 中最大值功能的函数。第一个是从列表 10-3 中提取的寻找 slice 中i32最大值的函数。第二个函数寻找 slice 中char的最大值:

        -
        -Filename: src/main.rs +

        Filename: src/main.rs

        fn largest_i32(list: &[i32]) -> i32 {
             let mut largest = list[0];
         
        @@ -4586,21 +4548,17 @@ fn main() {
         #    assert_eq!(result, 'y');
         }
         
        -
        -

        Listing 10-4: Two functions that differ only in their names and the types in -their signatures

        -
        -
        -

        这里largest_i32largest_char有着完全相同的函数体,所以能够将这两个函数变成一个来减少重复就太好了。所幸通过引入一个泛型参数就能实现。

        -

        为了参数化要定义的函数的签名中的类型,我们需要像给函数的值参数起名那样为这类型参数起一个名字。这里选择了名称T。任何标识符抖可以作为类型参数名,选择T是因为 Rust 的类型命名规范是骆驼命名法(CamelCase)。另外泛型类型参数的规范也倾向于简短,经常仅仅是一个字母。T作为“type”是大部分 Rust 程序员的首选。

        +

        Listing 10-4: Two functions that differ only in their +names and the types in their signatures

        +

        这里largest_i32largest_char有着完全相同的函数体,所以能够将这两个函数变成一个来减少重复就太好了。所幸通过引入一个泛型参数就能实现!

        +

        为了参数化要定义的函数的签名中的类型,我们需要像给函数的值参数起名那样为这类型参数起一个名字。这里选择了名称T。任何标识符抖可以作为类型参数名,选择T是因为 Rust 的类型命名规范是骆驼命名法(CamelCase)。另外泛型类型参数的规范也倾向于简短,经常仅仅是一个字母。T作为“type”的缩写是大部分 Rust 程序员的首选。

        当需要再函数体中使用一个参数时,必须再函数签名中声明这个参数以便编译器能知道函数体中这个名称的意义。同理,当在函数签名中使用一个类型参数时,必须在使用它之前就声明它。类型参数声明位于函数名称与参数列表中间的尖括号中。

        我们将要定义的泛型版本的largest函数的签名看起来像这样:

        fn largest<T>(list: &[T]) -> T {
         

        这可以理解为:函数largest有泛型类型T。它有一个参数list,它的类型是一个T值的 slice。largest函数将会返回一个与T相同类型的值。

        列表 10-5 展示一个在签名中使用了泛型的统一的largest函数定义,并向我们展示了如何对i32值的 slice 或char值的 slice 调用largest函数。注意这些代码还不能编译!

        -
        -Filename: src/main.rs +

        Filename: src/main.rs

        fn largest<T>(list: &[T]) -> T {
             let mut largest = list[0];
         
        @@ -4625,11 +4583,8 @@ fn main() {
             println!("The largest char is {}", result);
         }
         
        -
        -

        Listing 10-5: A definition of the largest function that uses generic type -parameters but doesn't compile yet

        -
        -
        +

        Listing 10-5: A definition of the largest function that +uses generic type parameters but doesn't compile yet

        如果现在就尝试编译这些代码,会出现如下错误:

        error[E0369]: binary operation `>` cannot be applied to type `T`
           |
        @@ -4648,8 +4603,7 @@ not-yet-compiling example here, which I know isn't ideal either. Let us know
         what you think. /Carol -->
         

        结构体定义中的泛型

        同样也可以使用<>语法来定义拥有一个或多个泛型参数类型字段的结构体。列表 10-6 展示了如何定义和使用一个可以存放任何类型的xy坐标值的结构体Point

        -
        -Filename: src/main.rs +

        Filename: src/main.rs

        struct Point<T> {
             x: T,
             y: T,
        @@ -4660,14 +4614,11 @@ fn main() {
             let float = Point { x: 1.0, y: 4.0 };
         }
         
        -
        -

        Listing 10-6: A Point struct that holds x and y values of type T

        -
        -
        +

        Listing 10-6: A Point struct that holds x and y +values of type T

        其语法类似于函数定义中的泛型应用。首先,必须在结构体名称后面的尖括号中声明泛型参数的名称。接着在结构体定义中可以指定具体数据类型的位置使用泛型类型。

        注意Point的定义中是使用了要给泛型类型,我们想要表达的是结构体Point对于一些类型T是泛型的,而且无论这个泛型是什么,字段xy都是相同类型的。如果尝试创建一个有不同类型值的Point的实例,像列表 10-7 中的代码就不能编译:

        -
        -Filename: src/main.rs +

        Filename: src/main.rs

        struct Point<T> {
             x: T,
             y: T,
        @@ -4677,11 +4628,8 @@ fn main() {
             let wont_work = Point { x: 5, y: 4.0 };
         }
         
        -
        -

        Listing 10-7: The fields x and y must be the same type because both have -the same generic data type T

        -
        -
        +

        Listing 10-7: The fields x and y must be the same +type because both have the same generic data type T

        尝试编译会得到如下错误:

        error[E0308]: mismatched types
          -->
        @@ -4695,8 +4643,7 @@ the same generic data type T

        当我们将 5 赋值给x,编译器就知道这个Point实例的泛型类型T是一个整型。接着我们将y指定为 4.0,而它被定义为与x有着相同的类型,所以出现了类型不匹配的错误。

        如果想要一个xy可以有不同类型且仍然是泛型的Point结构体,我们可以使用多个泛型类型参数。在列表 10-8 中,我们修改Point的定义为拥有两个泛型类型TU。其中字段xT类型的,而字段yU类型的:

        -
        -Filename: src/main.rs +

        Filename: src/main.rs

        struct Point<T, U> {
             x: T,
             y: U,
        @@ -4708,11 +4655,8 @@ fn main() {
             let integer_and_float = Point { x: 5, y: 4.0 };
         }
         
        -
        -

        Listing 10-8: A Point generic over two types so that x and y may be -values of different types

        -
        -
        +

        Listing 10-8: A Point generic over two types so that +x and y may be values of different types

        现在所有这些Point实例都是被允许的了!你可以在定义中使用任意多的泛型类型参数,不过太多的话代码将难以阅读和理解。如果你处于一个需要很多泛型类型的位置,这可能是一个需要重新组织代码并分隔成一些更小部分的信号。

        枚举定义中的泛型数据类型

        类似于结构体,枚举也可以在其成员中存放泛型数据类型。第六章我们使用过了标准库提供的Option<T>枚举,现在这个定义看起来就更容易理解了。让我们再看看:

        @@ -4732,8 +4676,7 @@ values of different types

        当发现代码中有多个只有存放的值的类型有所不同的结构体或枚举定义时,你就应该像之前的函数定义中那样引入泛型类型来减少重复。

        方法定义中的枚举数据类型

        可以像第五章介绍的那样来为其定义中带有泛型的结构体或枚举实现方法。列表 10-9 中展示了列表 10-6 中定义的结构体Point<T>。接着我们在Point<T>上定义了一个叫做x的方法来返回字段x中数据的引用:

        -
        -Filename: src/main.rs +

        Filename: src/main.rs

        struct Point<T> {
             x: T,
             y: T,
        @@ -4751,22 +4694,19 @@ fn main() {
             println!("p.x = {}", p.x());
         }
         
        -
        -

        Listing 10-9: Implementing a method named x on the Point<T> struct that -will return a reference to the x field, which is of type T.

        -
        -
        +

        Listing 10-9: Implementing a method named x on the +Point<T> struct that will return a reference to the x field, which is of +type T.

        注意必须在impl后面声明T,这样就可以在Point<T>上实现的方法中使用它了。

        结构体定义中的泛型类型参数并不总是与结构体方法签名中使用的泛型是同一类型。列表 10-10 中在列表 10-8 中的结构体Point<T, U>上定义了一个方法mixup。这个方法获取另一个Point作为参数,而它可能与调用mixupself是不同的Point类型。这个方法用selfPoint类型的x值(类型T)和参数的Point类型的y值(类型W)来创建一个新Point类型的实例:

        -
        -Filename: src/main.rs +

        Filename: src/main.rs

        struct Point<T, U> {
             x: T,
             y: U,
         }
         
         impl<T, U> Point<T, U> {
        -    fn mixup<V, W>(&self, other: &Point<V, W>) -> Point<T, W> {
        +    fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
                 Point {
                     x: self.x,
                     y: other.y,
        @@ -4783,11 +4723,8 @@ fn main() {
             println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
         }
         
        -
        -

        Listing 10-10: Methods that use different generic types than their struct's -definition

        -
        -
        +

        Listing 10-10: Methods that use different generic types +than their struct's definition

        main函数中,定义了一个有i32类型的x(其值为5)和f64y(其值为10.4)的Pointp2则是一个有着字符串 slice 类型的x(其值为"Hello")和char类型的y(其值为c)的Point。在p1上以p2调用mixup会返回一个p3,它会有一个i32类型的x,因为x来自p1,并拥有一个char类型的y,因为y来自p2println!会打印出p3.x = 5, p3.y = c

        注意泛型参数TU声明于impl之后,因为他们于结构体定义相对应。而泛型参数VW声明于fn mixup之后,因为他们只是相对于方法本身的。

        泛型代码的性能

        @@ -4819,35 +4756,30 @@ fn main() {

        我们可以使用泛型来编写不重复的代码,而 Rust 会将会为每一个实例编译其特定类型的代码。这意味着在使用泛型时没有运行时开销;当代码运行,它的执行效率就跟好像手写每个具体定义的重复代码一样。这个单态化过程正是 Rust 泛型在运行时极其高效的原因。

        trait:定义共享的行为

        -

        ch10-02-traits.md +

        ch10-02-traits.md
        -commit 709eb1eaca48864fafd9263042f5f9d9d6ffe08d

        +commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

        trait 允许我们进行另一种抽象:他们让我们可以抽象类型所通用的行为。trait 告诉 Rust 编译器某个特定类型拥有可能与其他类型共享的功能。在使用泛型类型参数的场景中,可以使用 trait bounds 在编译时指定泛型可以是任何实现了某个 trait 的类型,并由此在这个场景下拥有我们希望的功能。

        注意:trait 类似于其他语言中的常被称为接口interfaces)的功能,虽然有一些不同。

        定义 trait

        -

        一个类型的行为由其可供调用的方法构成。如果可以对不同类型调用相同的方法的话,这些类型就可以共享相同的行为了。trait 定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必须行为的集合。

        +

        一个类型的行为由其可供调用的方法构成。如果可以对不同类型调用相同的方法的话,这些类型就可以共享相同的行为了。trait 定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必需的行为的集合。

        例如,这里有多个存放了不同类型和属性文本的结构体:结构体NewsArticle用于存放发生于世界各地的新闻故事,而结构体Tweet最多只能存放 140 个字符的内容,以及像是否转推或是否是对推友的回复这样的元数据。

        我们想要创建一个多媒体聚合库用来显示可能储存在NewsArticleTweet实例中的数据的总结。每一个结构体都需要的行为是他们是能够被总结的,这样的话就可以调用实例的summary方法来请求总结。列表 10-11 中展示了一个表现这个概念的Summarizable trait 的定义:

        -
        -Filename: lib.rs +

        Filename: lib.rs

        pub trait Summarizable {
             fn summary(&self) -> String;
         }
         
        -
        -

        Listing 10-11: Definition of a Summarizable trait that consists of the -behavior provided by a summary method

        -
        -
        +

        Listing 10-11: Definition of a Summarizable trait that +consists of the behavior provided by a summary method

        使用trait关键字来定义一个 trait,后面是 trait 的名字,在这个例子中是Summarizable。在大括号中声明描述实现这个 trait 的类型所需要的行为的方法签名,在这个例子中是是fn summary(&self) -> String。在方法签名后跟分号而不是在大括号中提供其实现。接着每一个实现这个 trait 的类型都需要提供其自定义行为的方法体,编译器也会确保任何实现Summarizable trait 的类型都拥有与这个签名的定义完全一致的summary方法。

        trait 体中可以有多个方法,一行一个方法签名且都以分号结尾。

        为类型实现 trait

        现在我们定义了Summarizable trait,接着就可以在多媒体聚合库中需要拥有这个行为的类型上实现它了。列表 10-12 中展示了NewsArticle结构体上Summarizable trait 的一个实现,它使用标题、作者和创建的位置作为summary的返回值。对于Tweet结构体,我们选择将summary定义为用户名后跟推文的全部文本作为返回值,并假设推文内容已经被限制为 140 字符以内。

        -
        -Filename: lib.rs +

        Filename: lib.rs

        # pub trait Summarizable {
         #     fn summary(&self) -> String;
         # }
        @@ -4878,11 +4810,8 @@ impl Summarizable for Tweet {
             }
         }
         
        -
        -

        Listing 10-12: Implementing the Summarizable trait on the NewsArticle and -Tweet types

        -
        -
        +

        Listing 10-12: Implementing the Summarizable trait on +the NewsArticle and Tweet types

        在类型上实现 trait 类似与实现与 trait 无关的方法。区别在于impl关键字之后,我们提供需要实现 trait 的名称,接着是for和需要实现 trait 的类型的名称。在impl块中,使用 trait 定义中的方法签名,不过不再后跟分号,而是需要在大括号中编写函数体来为特定类型实现 trait 方法所拥有的行为。

        一旦实现了 trait,我们就可以用与NewsArticleTweet实例的非 trait 方法一样的方式调用 trait 方法了:

        let tweet = Tweet {
        @@ -4896,8 +4825,7 @@ println!("1 new tweet: {}", tweet.summary());
         

        这会打印出1 new tweet: horse_ebooks: of course, as you probably already know, people

        注意因为列表 10-12 中我们在相同的lib.rs力定义了Summarizable trait 和NewsArticleTweet类型,所以他们是位于同一作用域的。如果这个lib.rs是对应aggregator crate 的,而别人想要利用我们 crate 的功能外加为其WeatherForecast结构体实现Summarizable trait,在实现Summarizable trait 之前他们首先就需要将其导入其作用域中,如列表 10-13 所示:

        -
        -Filename: lib.rs +

        Filename: lib.rs

        extern crate aggregator;
         
         use aggregator::Summarizable;
        @@ -4916,29 +4844,22 @@ impl Summarizable for WeatherForecast {
             }
         }
         
        -
        -

        Listing 10-13: Bringing the Summarizable trait from our aggregator crate -into scope in another crate

        -
        -
        +

        Listing 10-13: Bringing the Summarizable trait from our +aggregator crate into scope in another crate

        另外这段代码假设Summarizable是一个公有 trait,这是因为列表 10-11 中trait之前使用了pub关键字。

        trait 实现的一个需要注意的限制是:只能在 trait 或对应类型位于我们 crate 本地的时候为其实现 trait。换句话说,不允许对外部类型实现外部 trait。例如,不能Vec上实现Display trait,因为DisplayVec都定义于标准库中。允许在像Tweet这样作为我们aggregatorcrate 部分功能的自定义类型上实现标准库中的 trait Display。也允许在aggregatorcrate中为Vec实现Summarizable,因为Summarizable定义与此。这个限制是我们称为 orphan rule 的一部分,如果你感兴趣的可以在类型理论中找到它。简单来说,它被称为 orphan rule 是因为其父类型不存在。没有这条规则的话,两个 crate 可以分别对相同类型是实现相同的 trait,因而这两个实现会相互冲突:Rust 将无从得知应该使用哪一个。因为 Rust 强制执行 orphan rule,其他人编写的代码不会破坏你代码,反之亦是如此。

        默认实现

        有时为 trait 中的某些或全部提供默认的行为,而不是在每个类型的每个实现中都定义自己的行为是很有用的。这样当为某个特定类型实现 trait 时,可以选择保留或重载每个方法的默认行为。

        列表 10-14 中展示了如何为Summarize trait 的summary方法指定一个默认的字符串值,而不是像列表 10-11 中那样只是定义方法签名:

        -
        -Filename: lib.rs +

        Filename: lib.rs

        pub trait Summarizable {
             fn summary(&self) -> String {
                 String::from("(Read more...)")
             }
         }
         
        -
        -

        Listing 10-14: Definition of a Summarizable trait with a default -implementation of the summary method

        -
        -
        +

        Listing 10-14: Definition of a Summarizable trait with +a default implementation of the summary method

        如果想要对NewsArticle实例使用这个默认实现,而不是像列表 10-12 中那样定义一个自己的实现,则可以指定一个空的impl块:

        impl Summarizable for NewsArticle {}
         
        @@ -5034,8 +4955,7 @@ error[E0507]: cannot move out of borrowed content

        错误的核心是cannot move out of type [T], a non-copy array,对于非泛型版本的largest函数,我们只尝试了寻找最大的i32char。正如第四章讨论过的,像i32char这样的类型是已知大小的并可以储存在栈上,所以他们实现了Copy trait。当我们将largest函数改成使用泛型后,现在list参数的类型就有可能是没有实现Copy trait 的,这意味着我们可能不能将list[0]的值移动到largest变量中。

        如果只想对实现了Copy的类型调用这些带啊吗,可以在T的 trait bounds 中增加Copy!列表 10-15 中展示了一个可以编译的泛型版本的largest函数的完整代码,只要传递给largest的 slice 值的类型实现了PartialOrdCopy这两个 trait,例如i32char

        -
        -Filename: src/main.rs +

        Filename: src/main.rs

        use std::cmp::PartialOrd;
         
         fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
        @@ -5062,26 +4982,23 @@ fn main() {
             println!("The largest char is {}", result);
         }
         
        -
        -

        Listing 10-15: A working definition of the largest function that works on any -generic type that implements the PartialOrd and Copy traits

        -
        -
        +

        Listing 10-15: A working definition of the largest +function that works on any generic type that implements the PartialOrd and +Copy traits

        如果并不希望限制largest函数只能用于实现了Copy trait 的类型,我们可以在T的 trait bounds 中指定Clone而不是Copy,并克隆 slice 的每一个值使得largest函数拥有其所有权。但是使用clone函数潜在意味着更多的堆分配,而且堆分配在涉及大量数据时可能会相当缓慢。另一种largest的实现方式是返回 slice 中一个T值的引用。如果我们将函数返回值从T改为&T并改变函数体使其能够返回一个引用,我们将不需要任何CloneCopy的 trait bounds 而且也不会有任何的堆分配。尝试自己实现这种替代解决方式吧!

        trait 和 trait bounds 让我们使用泛型类型参数来减少重复,并仍然能够向编译器明确指定泛型类型需要拥有哪些行为。因为我们向编译器提供了 trait bounds 信息,它就可以检查代码中所用到的具体类型是否提供了正确的行为。在动态类型语言中,如果我们尝试调用一个类型并没有实现的方法,会在运行时出现错误。Rust 将这些错误移动到了编译时,甚至在代码能够运行之前就强迫我们修复错误。另外,我们也无需编写运行时检查行为的代码,因为在编译时就已经检查过了,这样相比其他那些不愿放弃泛型灵活性的语言有更好的性能。

        这里还有一种泛型,我们一直在使用它甚至都没有察觉它的存在,这就是生命周期lifetimes)。不同于其他泛型帮助我们确保类型拥有期望的行为,生命周期则有助于确保引用在我们需要他们的时候一直有效。让我们学习生命周期是如何做到这些的。

        生命周期与引用有效性

        -

        ch10-03-lifetime-syntax.md +

        ch10-03-lifetime-syntax.md
        -commit d7a4e99554da53619dd71044273535ba0186f40a

        +commit c49e5ee8859f8eb8f8867cbeafbdf5b802aa5894

        当在第四章讨论引用时,我们遗漏了一个重要的细节:Rust 中的每一个引用都有其生命周期,也就是引用保持有效的作用域。大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以多种不同方式向关联的情况,所以 Rust 需要我们使用泛型生命周期参数来注明他们的关系,这样就能确保运行时实际使用的引用绝对是有效的。

        好吧,这有点不太寻常,而且也不同于其他语言中使用的工具。生命周期,从某种意义上说,是 Rust 最与众不同的功能。

        生命周期是一个很广泛的话题,本章不可能涉及到它全部的内容,所以这里我们会讲到一些通常你可能会遇到的生命周期语法以便你熟悉这个概念。第十九章会包含生命周期所有功能的更高级的内容。

        生命周期避免了悬垂引用

        生命周期的主要目标是避免悬垂引用,它会导致程序引用了并非其期望引用的数据。考虑一下列表 10-16 中的程序,它有一个外部作用域和一个内部作用域,外部作用域声明了一个没有初值的变量r,而内部作用域声明了一个初值为 5 的变量x。在内部作用域中,我们尝试将r的值设置为一个x的引用。接着在内部作用域结束后,尝试打印出r的值:

        -
        {
             let r;
         
        @@ -5093,10 +5010,8 @@ commit d7a4e99554da53619dd71044273535ba0186f40a

        println!("r: {}", r); }
        -
        -

        Listing 10-16: An attempt to use a reference whose value has gone out of scope

        -
        -
        +

        Listing 10-16: An attempt to use a reference whose value +has gone out of scope

        未初始化变量不能被使用

        接下来的一些例子中声明了没有初始值的变量,以便这些变量存在于外部作用域。这看起来好像和 Rust 不允许存在空值相冲突。然而这是可以的,如果我们尝试在给它一个值之前使用这个变量,会出现一个编译时错误。请自行尝试!

        @@ -5115,7 +5030,6 @@ commit d7a4e99554da53619dd71044273535ba0186f40a

        变量x并没有“存在的足够久”。为什么呢?好吧,x在到达第 7 行的大括号的结束时就离开了作用域,这也是内部作用域的结尾。不过r在外部作用域也是有效的;作用域越大我们就说它“存在的越久”。如果 Rust 允许这段代码工作,r将会引用在x离开作用域时被释放的内存,这时尝试对r做任何操作都会不能正常工作。那么 Rust 是如何决定这段代码是不被允许的呢?

        借用检查器

        编译器的这一部分叫做借用检查器borrow checker),它比较作用域来确保所有的借用都是有效的。列表 10-17 展示了与列表 10-16 相同的例子不过带有变量声明周期的注释:

        -
        {
             let r;         // -------+-- 'a
                            //        |
        @@ -5129,11 +5043,8 @@ commit d7a4e99554da53619dd71044273535ba0186f40a

        // -------+ }
        -
        -

        Listing 10-17: Annotations of the lifetimes of x and r, named 'a and 'b -respectively

        -
        -
        +

        Listing 10-17: Annotations of the lifetimes of r and +x, named 'a and 'b respectively

        @@ -5143,7 +5054,6 @@ the text art comments work or should we make an SVG diagram that has nicer looking arrows and labels? /Carol -->

        我们将r的声明周期标记为'a而将x的生命周期标记为'b。如你所见,内部的'b块要比外部的生命周期'a小得多。在编译时,Rust 比较这两个生命周期的大小,并发现r拥有声明周期'a,不过它引用了一个拥有生命周期'b的对象。程序被拒绝编译,因为生命周期'b比生命周期'a要小:引用者没有比被引用者存在的更久。

        让我们看看列表 10-18 中这个并没有产生悬垂引用且可以正常编译的例子:

        -
        {
             let x = 5;            // -----+-- 'b
                                   //      |
        @@ -5153,18 +5063,14 @@ looking arrows and labels? /Carol -->
                                   // --+  |
         }                         // -----+
         
        -
        -

        Listing 10-18: A valid reference because the data has a longer lifetime than -the reference

        -
        -
        +

        Listing 10-18: A valid reference because the data has a +longer lifetime than the reference

        x拥有生命周期 'b,在这里它比 'a要大。这就意味着r可以引用x:Rust 知道r中的引用在x有效的时候也会一直有效。

        现在我们已经在一个具体的例子中展示了引用的声明周期位于何处,并讨论了 Rust 如何分析生命周期来保证引用总是有效的,接下来让我们聊聊在函数的上下文中参数和返回值的泛型生命周期。

        函数中的泛型生命周期

        让我们来编写一个返回两个字符串 slice 中最长的那一个的函数。我们希望能够通过传递两个字符串 slice 来调用这个函数,并希望返回一个字符串 slice。一旦我们实现了longest函数,列表 10-19 中的代码应该会打印出The longest string is abcd

        -

        Filename: src/main.rs

        -
        fn main() {
        +
        fn main() {
             let string1 = String::from("abcd");
             let string2 = "xyz";
         
        @@ -5172,11 +5078,8 @@ the reference

        println!("The longest string is {}", result); }
        -
        -

        Listing 10-19: A main function that calls the longest function to find the -longest of two string slices

        -
        -
        +

        Listing 10-19: A main function that calls the longest +function to find the longest of two string slices

        注意函数期望获取字符串 slice(如第四章所讲到的这是引用)因为我们并不希望longest函数获取其参数的引用。我们希望函数能够接受String的 slice(也就是变量string1的类型)和字符串字面值(也就是变量string2包含的值)。

        参考之前第四章中的“字符串 slice 作为参数”部分中更多关于为什么上面例子中的参数正是我们想要的讨论。

        如果尝试像列表 10-20 中那样实现longest函数,它并不能编译:

        -
        -Filename: src/main.rs +

        Filename: src/main.rs

        fn longest(x: &str, y: &str) -> &str {
             if x.len() > y.len() {
                 x
        @@ -5206,11 +5108,9 @@ interested to know if rereading Chapter 4 clears up that confusion.
             }
         }
         
        -
        -

        Listing 10-20: An implementation of the longest function that returns the -longest of two string slices, but does not yet compile

        -
        -
        +

        Listing 10-20: An implementation of the longest +function that returns the longest of two string slices, but does not yet +compile

        将会出现如下有关生命周期的错误:

        error[E0106]: missing lifetime specifier
            |
        @@ -5233,8 +5133,7 @@ longest of two string slices, but does not yet compile

        生命周期注解本身没有多少意义:生命周期注解告诉 Rust 多个引用的泛型生命周期参数如何相互联系。如果函数有一个生命周期'ai32的引用的参数first,还有另一个同样是生命周期'ai32的引用的参数second,这两个生命周期注解有相同的名称意味着firstsecond必须与这相同的泛型生命周期存在得一样久。

        函数签名中的生命周期注解

        来看看我们编写的longest函数的上下文中的生命周期。就像泛型类型参数,泛型生命周期参数需要声明在函数名和参数列表间的加括号中。这里我们想要告诉 Rust 关于参数中的引用和返回值之间的限制是他们都必须拥有相同的生命周期,就像列表 10-21 中在每个引用中都加上了'a那样:

        -
        -Filename: src/main.rs +

        Filename: src/main.rs

        fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
             if x.len() > y.len() {
                 x
        @@ -5243,19 +5142,16 @@ longest of two string slices, but does not yet compile

        } }
        -
        -

        Listing 10-21: The longest function definition that specifies all the -references in the signature must have the same lifetime, 'a

        -
        -
        +

        Listing 10-21: The longest function definition that +specifies all the references in the signature must have the same lifetime, +'a

        这段代码能够编译并会产生我们想要使用列表 10-19 中的main函数得到的结果。

        现在函数签名表明对于某些生命周期'a,函数会获取两个参数,他们都是与生命周期'a存在的一样长的字符串 slice。函数会返回一个同样也与生命周期'a存在的一样长的字符串 slice。这就是我们告诉 Rust 需要其保证的协议。

        通过在函数签名中指定生命周期参数,我们不会改变任何参数或返回值的生命周期,不过我们说过任何不坚持这个协议的类型都将被借用检查器拒绝。这个函数并不知道(或需要知道)xy具体会存在多久,不过只需要知道一些可以使用'a替代的作用域将会满足这个签名。

        当在函数中使用生命周期注解时,这些注解出现在函数签名中,而不存在于函数体中的任何代码中。这是因为 Rust 能够分析函数中代码而不需要任何协助,不过当函数引用或被函数之外的代码引用时,参数或返回值的生命周期可能在每次函数被调用时都不同。这可能会产生惊人的消耗并且对于 Rust 来说经常都是不可能分析的。在这种情况下,我们需要自己标注生命周期。

        当具体的引用被传递给longest时,具体被'a所替代的生命周期是x的作用域与y的作用域相重叠的那一部分。因为作用域总是嵌套的,所以换一种说法就是泛型生命周期'a的具体生命周期等同于xy的生命周期中较小的那一个。因为我们用相同的生命周期参数标注了返回的引用值,所以返回的引用值就能保证在xy中较短的那个生命周期结束之前保持有效。

        让我们如何通过传递拥有不同具体生命周期的引用来观察他们是如何限制longest函数的使用的。列表 10-22 是一个应该在任何编程语言中都很直观的例子:string1直到外部作用域结束都是有效的,string2则在内部作用域中是有效的,而result则引用了一些直到外部作用域结束都是有效的值。借用检查器赞同这些代码;它能够编译和运行,并打印出The longest string is long string is long

        -
        -Filename: src/main.rs +

        Filename: src/main.rs

        # fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
         #     if x.len() > y.len() {
         #         x
        @@ -5274,23 +5170,11 @@ fn main() {
             }
         }
         
        -
        -

        Listing 10-22: Using the longest function with references to String values -that have different concrete lifetimes

        -
        -
        +

        Listing 10-22: Using the longest function with +references to String values that have different concrete lifetimes

        接下来,让我们尝试一个result的引用的生命周期必须比两个参数的要短的例子。将result变量的声明从内部作用域中移动出来,不过将resultstring2变量的赋值语句一同放在内部作用域里。接下来,我们将使用resultprintln!移动到内部作用域之外,就在其结束之后。注意列表 10-23 中的代码不能编译:

        -
        -Filename: src/main.rs -
        # fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
        -#     if x.len() > y.len() {
        -#         x
        -#     } else {
        -#         y
        -#     }
        -# }
        -#
        -fn main() {
        +

        Filename: src/main.rs

        +
        fn main() {
             let string1 = String::from("long string is long");
             let result;
             {
        @@ -5300,11 +5184,8 @@ fn main() {
             println!("The longest string is {}", result);
         }
         
        -
        -

        Listing 10-23: Attempting to use result after string2 has gone out of scope -won't compile

        -
        -
        +

        Listing 10-23: Attempting to use result after string2 +has gone out of scope won't compile

        如果尝试编译会出现如下错误:

        error: `string2` does not live long enough
            |
        @@ -5352,8 +5233,7 @@ at 1:44...
         

        从结果上看,生命周期语法是关于如何联系函数不同参数和返回值的生命周期的。一旦他们形成了某种联系,Rust 就有了足够的信息来允许内存安全的操作并阻止会产生悬垂指针亦或是违反内存安全的行为。

        结构体定义中的生命周期注解

        目前为止,我们只定义过有所有权类型的结构体。也可以定义存放引用的结构体,不过需要为结构体定义中的每一个引用添加生命周期注解。列表 10-24 中有一个存放了一个字符串 slice 的结构体ImportantExcerpt

        -
        -Filename: src/main.rs +

        Filename: src/main.rs

        struct ImportantExcerpt<'a> {
             part: &'a str,
         }
        @@ -5366,17 +5246,13 @@ fn main() {
             let i = ImportantExcerpt { part: first_sentence };
         }
         
        -
        -

        Listing 10-24: A struct that holds a reference, so its definition needs a -lifetime annotation

        -
        -
        +

        Listing 10-24: A struct that holds a reference, so its +definition needs a lifetime annotation

        这个结构体有一个字段,part,它存放了一个字符串 slice,这是一个引用。类似于泛型参数类型,必须在结构体名称后面的尖括号中声明泛型生命周期参数,以便在结构体定义中使用生命周期参数。

        这里的main函数创建了一个ImportantExcerpt的实例,它存放了变量novel所拥有的String的第一个句子的引用。

        生命周期省略

        在这一部分,我们知道了每一个引用都有一个生命周期,而且需要为使用了引用的函数或结构体指定生命周期。然而,第四章的“字符串 slice”部分有一个函数,我们在列表 10-25 中再次展示它,没有生命周期注解却能成功编译:

        -
        -Filename: src/lib.rs +

        Filename: src/lib.rs

        fn first_word(s: &str) -> &str {
             let bytes = s.as_bytes();
         
        @@ -5389,11 +5265,9 @@ lifetime annotation

        &s[..] }
        -
        -

        Listing 10-25: A function we defined in Chapter 4 that compiled without -lifetime annotations, even though the parameter and return type are references

        -
        -
        +

        Listing 10-25: A function we defined in Chapter 4 that +compiled without lifetime annotations, even though the parameter and return +type are references

        这个函数没有生命周期注解却能编译是由于一些历史原因:在早期 1.0 之前的版本的 Rust 中,这的确是不能编译的。每一个引用都必须有明确的生命周期。那时的函数签名将会写成这样:

        fn first_word<'a>(s: &'a str) -> &'a str {
         
        @@ -5497,9 +5371,9 @@ fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann

        你可能不会相信,这个领域还有更多需要学习的内容:第十七章会讨论 trait 对象,这是另一种使用 trait 的方式。第十九章会涉及到生命周期注解更复杂的场景。第二十章讲解一些高级的类型系统功能。不过接下来,让我们聊聊如何在 Rust 中编写测试,来确保代码的所有功能能像我们希望的那样工作!

        测试

        -

        ch11-00-testing.md +

        ch11-00-testing.md
        -commit c9fd8eb1da7a79deee97020e8ad49af8ded78f9c

        +commit b7ab6668bbcb73b93c6464d8354c94a8e6c90395

        Program testing can be a very effective way to show the presence of bugs, but it is hopelessly inadequate for showing their absence.

        @@ -5507,23 +5381,35 @@ commit c9fd8eb1da7a79deee97020e8ad49af8ded78f9c

        软件测试是证明 bug 存在的有效方法,而证明它们不存在时则显得令人绝望的不足。

        Edsger W. Dijkstra,【谦卑的程序员】(1972)

        -

        Rust 是一个非常注重正确性的编程语言,不过正确性是一个难以证明的复杂主题。Rust 在其类型系统上下了很大的功夫,来确保程序像我们希望的那样运行,不过它并不有助于所有情况。为此,Rust 也包含为语言自身编写软件测试的支持。

        -

        例如,我们可以编写一个叫做add_two的函数,它的签名有一个整型参数并返回一个整型值。我们可以实现并编译这个函数,而 Rust 也会进行所有的类型检查和借用检查,正如我们之前见识过的那样。Rust 所不能检查的是,我们实现的这个函数是否返回了参数值加二后的值,而不是加 10 或者减 50!这也就是测试出场的地方。例如可以编写传递3add_two函数并检查我们是否得到了5。任何时候修改了代码我们都可以运行测试来确保没有改变任何现有测试所指定的行为。

        -

        测试是一项技能,而且我们也不能期望在一本书的一个章节中就涉及到编写好的测试的所有内容。然而我们可以讨论的是 Rust 测试功能的机制。我们会讲到编写测试时会用到的注解和宏,Rust 提供用来运行测试的默认行为和选项,以及如何将测试组织成单元测试和集成测试。

        +

        程序的正确性意味着代码如我们期望的那样运行。Rust 是一个非常注重正确性的编程语言,不过正确性是一个难以证明的复杂主题。Rust 的类型系统在此问题上下了很大的功夫,不过它不可能捕获所有类型的错误。为此,Rust 也包含为语言自身编写软件测试的支持。

        +

        例如,我们可以编写一个叫做add_two的将传递给它的值加二的函数。它的签名有一个整型参数并返回一个整型值。当实现和编译这个函数时,Rust 会进行所有目前我们已经见过的的类型检查和借用检查。例如,这些检查会确保我们不会传递String或无效的引用给这个函数。Rust 所不能检查的是这个函数是否会准确的完成我们期望的工作:返回参数加二后的值,而不是比如说参数加 10 或减 50 的值!这也就是测试出场的地方。

        +

        我们可以编写测试断言,比如说,当传递3add_two函数时,应该得到5。当对代码进行修改时可以运行测试来确保任何现存的正确行为没有被改变。

        +

        测试是一项复杂的技能,而且我们也不能期望在一本书的一个章节中就涉及到编写好的测试的所有内容,所以这里仅仅讨论 Rust 测试功能的机制。我们会讲到编写测试时会用到的注解和宏,Rust 提供用来运行测试的默认行为和选项,以及如何将测试组织成单元测试和集成测试。

        编写测试

        -

        ch11-01-writing-tests.md +

        ch11-01-writing-tests.md
        -commit 77370c073661548dd56bbcb43cc64713585acbba

        +commit 55b294f20fc846a13a9be623bf322d8b364cee77

        -

        测试是一种使用特定功能的 Rust 函数,它用来验证非测试的代码按照期望的方式运行。我们讨论过的任何 Rust 代码规则都适用于测试!让我们看看 Rust 提供的具体用来编写测试的功能:test属性、一些宏和should_panic属性。

        -

        test属性

        -

        作为最简单例子,Rust 中的测试就是一个带有test属性注解的函数。让我们使用 Cargo 来创建一个新的库项目adder

        +

        测试用来验证非测试的代码按照期望的方式运行的 Rust 函数。测试函数体通常包括一些设置,运行需要测试的代码,接着断言其结果是我们所期望的。让我们看看 Rust 提供的具体用来编写测试的功能:test属性、一些宏和should_panic属性。

        +

        测试函数剖析

        +

        作为最简单例子,Rust 中的测试就是一个带有test属性注解的函数。属性(attribute)是关于 Rust 代码片段的元数据:第五章中结构体中用到的derive属性就是一个例子。为了将一个函数变成测试函数,需要在fn行之前加上#[test]。当使用cargo test命令运行测试函数时,Rust 会构建一个测试执行者二进制文件用来运行标记了test属性的函数并报告每一个测试是通过还是失败。

        + + +

        第七章当使用 Cargo 新建一个库项目时,它会自动为我们生成一个测试模块和一个测试函数。这有助于我们开始编写测试,因为这样每次开始新项目时不必去查找测试函数的具体结构和语法了。同时可以额外增加任意多的测试函数以及测试模块!

        +

        我们将先通过对自动生成的测试模板做一些试验来探索测试如何工作的一些方面内容,而不实际测试任何代码。接着会写一些真实的测试来调用我们编写的代码并断言他们的行为是正确的。

        +

        让我们创建一个新的库项目adder

        $ cargo new adder
              Created library `adder` project
         $ cd adder
         
        -

        Cargo 在创建新的库项目时自动生成一个简单的测试。这是src/lib.rs中的内容:

        +

        adder 库中src/lib.rs的内容应该看起来像这样:

        Filename: src/lib.rs

        #[cfg(test)]
         mod tests {
        @@ -5532,14 +5418,18 @@ mod tests {
             }
         }
         
        -

        现在让我们暂时忽略tests模块和#[cfg(test)]注解并只关注函数。注意它之前的#[test]:这个属性表明这是一个测试函数。这个函数目前没有任何内容,所以绝对是可以通过的!使用cargo test来运行测试:

        +

        Listing 11-1: The test module and function generated +automatically for us by cargo new

        +

        现在让我们暂时忽略tests模块和#[cfg(test)]注解并只关注函数。注意fn行之前的#[test]:这个属性表明这是一个测试函数,这样测试执行者就知道将其作为测试处理。也可以在tests模块中拥有非测试的函数来帮助我们建立通用场景或进行常见操作,所以需要使用#[test]属性标明哪些函数是测试。

        +

        这个函数目前没有任何内容,这意味着没有代码会使测试失败;一个空的测试是可以通过的!让我们运行一下看看它是否通过了。

        +

        cargo test命令会运行项目中所有的测试,如列表 11-2 所示:

        $ cargo test
            Compiling adder v0.1.0 (file:///projects/adder)
             Finished debug [unoptimized + debuginfo] target(s) in 0.22 secs
        -     Running target/debug/deps/adder-abcabcabc
        +     Running target/debug/deps/adder-ce99bcc2479f4607
         
         running 1 test
        -test it_works ... ok
        +test tests::it_works ... ok
         
         test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
         
        @@ -5549,6 +5439,149 @@ running 0 tests
         
         test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
         
        +

        Listing 11-2: The output from running the one +automatically generated test

        +

        Cargo 编译并运行了测试。在CompilingFinishedRunning这几行之后,可以看到running 1 test这一行。下一行显示了生成的测试函数的名称,它是it_works,以及测试的运行结果,ok。接着可以看到全体测试运行结果的总结:test result: ok.意味着所有测试都通过了。1 passed; 0 failed表示通过或失败的测试数量。

        +

        这里并没有任何被标记为忽略的测试,所以总结表明0 ignored。在下一部分关于运行测试的不同方式中会讨论忽略测试。0 measured统计是针对测试性能的性能测试的。性能测试(benchmark tests)在编写本书时,仍只属于开发版 Rust(nightly Rust)。请查看附录 D 来了解更多开发版 Rust 的信息。

        +

        测试输出中以Doc-tests adder开头的下一部分是所有文档测试的结果。现在并没有任何文档测试,不过 Rust 会编译任何出现在 API 文档中的代码示例。这个功能帮助我们使文档和代码保持同步!在第十四章的“文档注释”部分会讲到如何编写文档测试。现在我们将忽略Doc-tests部分的输出。

        + + +

        让我们改变测试的名称并看看这如何改变测试的输出。给it_works函数起个不同的名字,比如exploration,像这样:

        +

        Filename: src/lib.rs

        +
        #[cfg(test)]
        +mod tests {
        +    #[test]
        +    fn exploration() {
        +    }
        +}
        +
        +

        并再次运行cargo test。现在输出中将出现exploration而不是it_works

        +
        running 1 test
        +test tests::exploration ... ok
        +
        +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
        +
        +

        让我们增加另一个测试,不过这一次是一个会失败的测试!当测试函数中出现 panic 时测试就失败了。第九章讲到了最简单的造成 panic 的方法:调用panic!宏!写入新函数后 src/lib.rs 现在看起来如列表 11-3 所示:

        +

        Filename: src/lib.rs

        +
        #[cfg(test)]
        +mod tests {
        +    #[test]
        +    fn exploration() {
        +    }
        +
        +    #[test]
        +    fn another() {
        +        panic!("Make this test fail");
        +    }
        +}
        +
        +

        Listing 11-3: Adding a second test; one that will fail +since we call the panic! macro

        +

        再次cargo test运行测试。输出应该看起来像列表 11-4,它表明exploration测试通过了而another失败了:

        +
        running 2 tests
        +test tests::exploration ... ok
        +test tests::another ... FAILED
        +
        +failures:
        +
        +---- tests::another stdout ----
        +    thread 'tests::another' panicked at 'Make this test fail', src/lib.rs:9
        +note: Run with `RUST_BACKTRACE=1` for a backtrace.
        +
        +failures:
        +    tests::another
        +
        +test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
        +
        +error: test failed
        +
        +

        Listing 11-4: Test results when one test passes and one +test fails

        +

        test tests::another这一行是FAILED而不是ok了。在单独测试结果和总结之间多了两个新的部分:第一个部分显示了测试失败的详细原因。在这个例子中,another因为panicked at 'Make this test fail'而失败,这位于 src/lib.rs 的第 9 行。下一部分仅仅列出了所有失败的测试,这在很有多测试和很多失败测试的详细输出时很有帮助。可以使用失败测试的名称来只运行这个测试,这样比较方便调试;下一部分会讲到更多运行测试的方法。

        +

        最后是总结行:总体上讲,一个测试结果是FAILED的。有一个测试通过和一个测试失败。

        +

        现在我们见过不同场景中测试结果是什么样子的了,再来看看除了panic!之外一些在测试中有帮助的宏吧。

        +

        使用assert!宏来检查结果

        +

        assert!宏由标准库提供,在希望确保测试中一些条件为true时非常有用。需要向assert!宏提供一个计算为布尔值的参数。如果值是trueassert!什么也不做同时测试会通过。如果值为falseassert!调用panic!宏,这会导致测试失败。这是一个帮助我们检查代码是否以期望的方式运行的宏。

        + + +

        回忆一下第五章中,列表 5-9 中有一个Rectangle结构体和一个can_hold方法,在列表 11-5 中再次使用他们。将他们放进 src/lib.rs 而不是 src/main.rs 并使用assert!宏编写一些测试。

        + +

        Filename: src/lib.rs

        +
        #[derive(Debug)]
        +pub struct Rectangle {
        +    length: u32,
        +    width: u32,
        +}
        +
        +impl Rectangle {
        +    pub fn can_hold(&self, other: &Rectangle) -> bool {
        +        self.length > other.length && self.width > other.width
        +    }
        +}
        +
        +

        Listing 11-5: The Rectangle struct and its can_hold +method from Chapter 5

        +

        can_hold方法返回一个布尔值,这意味着它完美符合assert!宏的使用场景。在列表 11-6 中,让我们编写一个can_hold方法的测试来作为练习,这里创建一个长为 8 宽为 7 的Rectangle实例,并假设它可以放得下另一个长为5 宽为 1 的Rectangle实例:

        +

        Filename: src/lib.rs

        +
        #[cfg(test)]
        +mod tests {
        +    use super::*;
        +
        +    #[test]
        +    fn larger_can_hold_smaller() {
        +        let larger = Rectangle { length: 8, width: 7 };
        +        let smaller = Rectangle { length: 5, width: 1 };
        +
        +        assert!(larger.can_hold(&smaller));
        +    }
        +}
        +
        +

        Listing 11-6: A test for can_hold that checks that a +larger rectangle indeed holds a smaller rectangle

        +

        注意在tests模块中新增加了一行:use super::*;tests是一个普通的模块,它遵循第七章介绍的通常的可见性规则。因为这是一个内部模块,需要将外部模块中被测试的代码引入到内部模块的作用域中。这里选择使用全局导入使得外部模块定义的所有内容在tests模块中都是可用的。

        +

        我们将测试命名为larger_can_hold_smaller,并创建所需的两个Rectangle实例。接着调用assert!宏并传递larger.can_hold(&smaller)调用的结果作为参数。这个表达式预期会返回true,所以测试应该通过。让我们拭目以待!

        +
        running 1 test
        +test tests::larger_can_hold_smaller ... ok
        +
        +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
        +
        +

        它确实通过了!再来增加另一个测试,这一回断言一个更小的矩形不能放下一个更大的矩形:

        +

        Filename: src/lib.rs

        +
        #[cfg(test)]
        +mod tests {
        +    use super::*;
        +
        +    #[test]
        +    fn larger_can_hold_smaller() {
        +        let larger = Rectangle { length: 8, width: 7 };
        +        let smaller = Rectangle { length: 5, width: 1 };
        +
        +        assert!(larger.can_hold(&smaller));
        +    }
        +
        +    #[test]
        +    fn smaller_can_hold_larger() {
        +        let larger = Rectangle { length: 8, width: 7 };
        +        let smaller = Rectangle { length: 5, width: 1 };
        +
        +        assert!(!smaller.can_hold(&larger));
        +    }
        +}
        +

        Cargo 编译并运行了测试。这里有两部分输出:本章我们将关注第一部分。第二部分是文档测试的输出,第十四章会介绍他们。现在注意看这一行:

        test it_works ... ok
         
        diff --git a/src/PREFACE.md b/src/PREFACE.md index 0564a6d..ba6dcdd 100644 --- a/src/PREFACE.md +++ b/src/PREFACE.md @@ -2,4 +2,4 @@ 还在施工中:目前翻译到第十六章 -目前正在解决代码排版问题:已检查到第六章 \ No newline at end of file +目前正在解决代码排版问题:已检查到第十一章第一部分 \ No newline at end of file diff --git a/src/ch01-01-installation.md b/src/ch01-01-installation.md index 4d95449..38a6f01 100644 --- a/src/ch01-01-installation.md +++ b/src/ch01-01-installation.md @@ -2,7 +2,7 @@ > [ch01-01-installation.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch01-01-installation.md) >
        -> commit 4f2dc564851dc04b271a2260c834643dfd86c724 +> commit c1b95a18dbcbb06aadf07c03759f27d88ccf62cf 使用 Rust 的第一步是安装。你需要网络连接来执行本章的命令,因为我们要从网上下载 Rust。 @@ -34,6 +34,15 @@ Rust is installed now. Great! 如果有理由倾向于不使用 rustup.rs,请查看[Rust 安装页面](https://www.rust-lang.org/install.html)获取其他选择。 + +### 更新 + +一旦安装完 Rust,更新到最新版本是简单的。在 shell 中运行更新脚本: + +``` +$ rustup update +``` + ### 卸载 卸载 Rust 同安装一样简单。在 shell 中运行卸载脚本 diff --git a/src/ch07-01-mod-and-the-filesystem.md b/src/ch07-01-mod-and-the-filesystem.md index 47b3491..945188c 100644 --- a/src/ch07-01-mod-and-the-filesystem.md +++ b/src/ch07-01-mod-and-the-filesystem.md @@ -2,7 +2,7 @@ > [ch07-01-mod-and-the-filesystem.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch07-01-mod-and-the-filesystem.md) >
        -> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56 +> commit 6fc32eabcd09f7a130094767abadb691dfcdddf7 我们将通过使用 Cargo 创建一个新项目来开始我们的模块之旅,不过不再创建一个二进制 crate,而是创建一个库 crate:一个其他人可以作为依赖导入的项目。第二章我们见过的`rand`就是这样的 crate。 diff --git a/src/ch07-02-controlling-visibility-with-pub.md b/src/ch07-02-controlling-visibility-with-pub.md index e86f8e7..85d5685 100644 --- a/src/ch07-02-controlling-visibility-with-pub.md +++ b/src/ch07-02-controlling-visibility-with-pub.md @@ -1,8 +1,8 @@ ## 使用`pub`控制可见性 -> [ch07-02-controlling-visibility-with-pub.md](https://github.com/rust-lang/book/blob/master/src/ch07-02-controlling-visibility-with-pub.md) +> [ch07-02-controlling-visibility-with-pub.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch07-02-controlling-visibility-with-pub.md) >
        -> commit e2a129961ae346f726f8b342455ec2255cdfed68 +> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56 我们通过将`network`和`network::server`的代码分别移动到 *src/network/mod.rs* 和 *src/network/server.rs* 文件中解决了列表 7-4 中出现的错误信息。现在,`cargo build`能够构建我们的项目,不过仍然有一些警告信息,表示`client::connect`、`network::connect`和`network::server::connect`函数没有被使用: @@ -26,7 +26,7 @@ warning: function is never used: `connect`, #[warn(dead_code)] on by default | ^ ``` -那么为什么会出现这些错误信息呢?我们构建的是一个库,它的函数的目的是被**用户**使用,而不一定要被项目自身使用,所以不应该担心这些函数是未被使用的。创建他们的意义就在于被另一个项目而不是被自己使用。 +那么为什么会出现这些错误信息呢?我们构建的是一个库,它的函数的目的是被**用户**使用,而不一定要被项目自身使用,所以不应该担心这些`connect`函数是未使用的。创建他们的意义就在于被另一个项目而不是被自己使用。 为了理解为什么这个程序出现了这些警告,尝试作为另一个项目来使用这个`connect`库,从外部调用他们。为此,通过创建一个包含这些代码的 *src/main.rs* 文件,在与库 crate 相同的目录创建一个二进制 crate: @@ -58,7 +58,7 @@ error: module `client` is private 啊哈!这告诉了我们`client`模块是私有的,这也正是那些警告的症结所在。这也是我们第一次在 Rust 上下文中涉及到**公有**和**私有**的概念。Rust 所有代码的默认状态是私有的:除了自己之外别人不允许使用这些代码。如果不在自己的项目中使用一个私有函数,因为程序自身是唯一允许使用这个函数的代码,Rust 会警告说函数未被使用。 -一旦我们指定一个像`client::connect`的函数为公有,不光二进制 crate 中的函数调用会被允许,函数未被使用的警告也会消失。将其标记为公有让 Rust 知道了我们意在使函数在我们程序的外部被使用。现在这个可能的理论上的外部可用性使 Rust 认为这个函数“已经被使用”。因此。当某项被标记为公有,Rust 不再要求它在程序自身被使用并停止警告某项未被使用。 +一旦我们指定一个像`client::connect`的函数为公有,不光二进制 crate 中的函数调用是允许的,函数未被使用的警告也会消失。将其标记为公有让 Rust 知道了我们意在使函数在我们程序的外部被使用。现在这个可能的理论上的外部可用性使得 Rust 认为这个函数“已经被使用”。因此。当某项被标记为公有,Rust 不再要求它在程序自身被使用并停止警告某项未被使用。 ### 标记函数为公有 @@ -173,7 +173,6 @@ warning: function is never used: `connect`, #[warn(dead_code)] on by default 让我们看看更多例子作为练习。创建一个新的库项目并在新项目的 *src/lib.rs* 输入列表 7-5 中的代码: -
        Filename: src/lib.rs ```rust,ignore @@ -197,13 +196,8 @@ fn try_me() { } ``` -
        - -Listing 7-5: Examples of private and public functions, some of which are -incorrect - -
        -
        +Listing 7-5: Examples of private and public functions, +some of which are incorrect 在尝试编译这些代码之前,猜测一下`try_me`函数的哪一行会出错。接着编译项目来看看是否猜对了,然后继续阅读后面关于错误的讨论! diff --git a/src/ch07-03-importing-names-with-use.md b/src/ch07-03-importing-names-with-use.md index 1d33301..0cc8cde 100644 --- a/src/ch07-03-importing-names-with-use.md +++ b/src/ch07-03-importing-names-with-use.md @@ -1,12 +1,11 @@ ## 导入命名 -> [ch07-03-importing-names-with-use.md](https://github.com/rust-lang/book/blob/master/src/ch07-03-importing-names-with-use.md) +> [ch07-03-importing-names-with-use.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch07-03-importing-names-with-use.md) >
        -> commit e2a129961ae346f726f8b342455ec2255cdfed68 +> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56 我们已经讲到了如何使用模块名称作为调用的一部分,来调用模块中的函数,如列表 7-6 中所示的`nested_modules`函数调用。 -
        Filename: src/main.rs ```rust @@ -23,13 +22,8 @@ fn main() { } ``` -
        - -Listing 7-6: Calling a function by fully specifying its enclosing module’s -namespaces - -
        -
        +Listing 7-6: Calling a function by fully specifying its +enclosing module’s namespaces 如你所见,指定函数的完全限定名称可能会非常冗长。所幸 Rust 有一个关键字使得这些调用显得更简洁。 @@ -138,7 +132,7 @@ mod tests { } ``` -第十二章会更详细的解释测试,不过其部分内容现在应该可以理解了:有一个叫做`tests`的模块紧邻其他模块,同时包含一个叫做`it_works`的函数。即便存在一些特殊注解,`tests`也不过是另外一个模块!所以我们的模块层次结构看起来像这样: +第十一章会更详细的解释测试,不过其部分内容现在应该可以理解了:有一个叫做`tests`的模块紧邻其他模块,同时包含一个叫做`it_works`的函数。即便存在一些特殊注解,`tests`也不过是另外一个模块!所以我们的模块层次结构看起来像这样: ``` communicator diff --git a/src/ch08-00-common-collections.md b/src/ch08-00-common-collections.md index d8fce89..57e5529 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/src/ch08-00-common-collections.md) +> [ch08-00-common-collections.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch08-00-common-collections.md) >
        -> commit 0d229cc5a3da341196e15a6761735b2952281569 +> commit e6d6caab41471f7115a621029bd428a812c5260e Rust 标准库中包含一系列被称为**集合**(*collections*)的非常有用的数据结构。大部分其他数据类型都代表一个特定的值,不过集合可以包含多个值。不同于内建的数组和元组类型,这些集合指向的数据是储存在堆上的,这意味着数据的数量不必在编译时就可知并且可以随着程序的运行增长或缩小。每种集合都有着不同能力和代价,而为所处的场景选择合适的集合则是你将要始终发展的技能。在这一章里,我们将详细的了解三个在 Rust 程序中被广泛使用的集合: @@ -12,6 +12,6 @@ Rust 标准库中包含一系列被称为**集合**(*collections*)的非常 对于标准库提供的其他类型的集合,请查看[文档][collections]。 -[collections]: ../std/collections +[collections]: https://doc.rust-lang.org/std/collections -我们将讨论如何创建和更新 vector、字符串和哈希 map,以及他们何以如此特殊。 \ No newline at end of file +我们将讨论如何创建和更新 vector、字符串和哈希 map,以及他们有什么不同。 \ No newline at end of file diff --git a/src/ch08-01-vectors.md b/src/ch08-01-vectors.md index f61f186..3a86c92 100644 --- a/src/ch08-01-vectors.md +++ b/src/ch08-01-vectors.md @@ -1,10 +1,10 @@ ## vector -> [ch08-01-vectors.md](https://github.com/rust-lang/book/blob/master/src/ch08-01-vectors.md) +> [ch08-01-vectors.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch08-01-vectors.md) >
        -> commit 0d229cc5a3da341196e15a6761735b2952281569 +> commit 4f2dc564851dc04b271a2260c834643dfd86c724 -我们要讲到的第一个类型是`Vec`,也被称为 *vector*。vector 允许我们在一个单独的数据结构中储存多于一个值,它在内存中彼此相邻的排列所有的值。vector 只能储存相同类型的值。他们在拥有一系列的场景下非常实用,例如文件中的文本行或是购物车中商品的价格。 +我们要讲到的第一个类型是`Vec`,也被称为 *vector*。vector 允许我们在一个单独的数据结构中储存多于一个值,它在内存中彼此相邻的排列所有的值。vector 只能储存相同类型的值。他们在拥有一系列项的场景下非常实用,例如文件中的文本行或是购物车中商品的价格。 ### 新建 vector diff --git a/src/ch08-02-strings.md b/src/ch08-02-strings.md index 811681d..c68d058 100644 --- a/src/ch08-02-strings.md +++ b/src/ch08-02-strings.md @@ -1,8 +1,8 @@ ## 字符串 -> [ch08-02-strings.md](https://github.com/rust-lang/book/blob/master/src/ch08-02-strings.md) +> [ch08-02-strings.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch08-02-strings.md) >
        -> commit 65f52921e21ad2e1c79d620fcfd01bde3ee30571 +> commit d362dadae60a7cc3212b107b9e9562769b0f20e3 第四章已经讲过一些字符串的内容,不过现在让我们更深入地了解一下它。字符串是新晋 Rustacean 们通常会被困住的领域。这是由于三方面内容的结合:Rust 倾向于确保暴露出可能的错误,字符串是比很多程序员所想象的要更为复杂的数据结构,以及 UTF-8。所有这些结合起来对于来自其他语言背景的程序员就可能显得很困难了。 @@ -67,7 +67,7 @@ let hello = "Hola"; `String`的大小可以增长其内容也可以改变,就像可以放入更多数据来改变`Vec`的内容一样。另外,`String`实现了`+`运算符作为级联运算符以便于使用。 -#### 附加字符串 +#### 使用 push 附加字符串 可以通过`push_str`方法来附加字符串 slice,从而使`String`变长: @@ -111,7 +111,7 @@ fn add(self, s: &str) -> String { 这并不是标准库中实际的签名;那个`add`使用泛型定义。这里的签名使用具体类型代替了泛型,这也正是当使用`String`值调用这个方法会发生的。第十章会讨论泛型。这个签名提供了理解`+`运算那奇怪的部分的线索。 -首先,`s2`使用了`&`,意味着我们使用第二个字符串的**引用**与第一个字符串相加。这是因为`add`函数的`s`参数:只能将`&str`和`String`相加,不能将两个`String`值相加。回忆之前第四章我们讲到`&String`是如何被强转为`&str`的:写成`&s2`的话`String`将会被强转成一个合适的类型`&str`。又因为方法没有获取参数的所有权,所以`s2`在这个操作后仍然有效。 +首先,`s2`使用了`&`,意味着我们使用第二个字符串的**引用**与第一个字符串相加。这是因为`add`函数的`s`参数:只能将`&str`和`String`相加,不能将两个`String`值相加。不过等一下——正如`add`的第二个参数所指定的,`&s2`的类型是`&String`而不是`&str`。那么为什么代码还能编译呢?之所以能够在`add`调用中使用`&s2`是因为`&String`可以被**强转**(*coerced*)成 `&str`——当`add`函数被调用时,Rust 使用了一个被成为**解引用强制多态**(*deref coercion*)的技术,你可以将其理解为它把`&s2`变成了`&s2[..]`以供`add`函数使用。第十五章会更深入的讨论解引用强制多态。因为`add`没有获取参数的所有权,所以`s2`在这个操作后仍然是有效的`String`。 其次,可以发现签名中`add`获取了`self`的所有权,因为`self`**没有**使用`&`。这意味着上面例子中的`s1`的所有权将被移动到`add`调用中,之后就不再有效。所以虽然`let s3 = s1 + &s2;`看起来就像它会复制两个字符串并创建一个新的字符串,而实际上这个语句会获取`s1`的所有权,附加上从`s2`中拷贝的内容,并返回结果的所有权。换句话说,它看起来好像生成了很多拷贝不过实际上并没有:这个实现比拷贝要更高效。 @@ -157,7 +157,7 @@ satisfied [--explain E0277] note: the type `std::string::String` cannot be indexed by `_` ``` -错误和提示说明了全部问题:Rust 的字符串不支持索引。那么接下来的问题是,为什么不支持呢?为了回答这个问题,我们必须先聊一聊 Rust 如何在内存中储存字符串。 +错误和提示说明了全部问题:Rust 的字符串不支持索引。那么接下来的问题是,为什么不支持呢?为了回答这个问题,我们必须先聊一聊 Rust 是如何在内存中储存字符串的。 #### 内部表示 diff --git a/src/ch08-03-hash-maps.md b/src/ch08-03-hash-maps.md index 99cedc4..7e74a56 100644 --- a/src/ch08-03-hash-maps.md +++ b/src/ch08-03-hash-maps.md @@ -1,8 +1,8 @@ ## 哈希 map -> [ch08-03-hash-maps.md](https://github.com/rust-lang/book/blob/master/src/ch08-03-hash-maps.md) +> [ch08-03-hash-maps.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch08-03-hash-maps.md) >
        -> commit 0d229cc5a3da341196e15a6761735b2952281569 +> commit 4f2dc564851dc04b271a2260c834643dfd86c724 最后要介绍的常用集合类型是**哈希 map**(*hash map*)。`HashMap`类型储存了一个键类型`K`对应一个值类型`V`的映射。它通过一个**哈希函数**(*hashing function*)来实现映射,它决定了如何将键和值放入内存中。很多编程语言支持这种数据结构,不过通常有不同的名字:哈希、map、对象、哈希表或者关联数组,仅举几例。 @@ -176,4 +176,4 @@ vector、字符串和哈希 map 会在你的程序需要储存、访问和修改 标准库 API 文档中描述的这些类型的方法将有助于你进行这些练习! -我们已经开始解除可能会有失败操作的复杂程序了,这也意味着接下来是一个了解错误处理的绝佳时机! \ No newline at end of file +我们已经开始接触可能会有失败操作的复杂程序了,这也意味着接下来是一个了解错误处理的绝佳时机! \ No newline at end of file diff --git a/src/ch09-00-error-handling.md b/src/ch09-00-error-handling.md index 0b07078..dff54bf 100644 --- a/src/ch09-00-error-handling.md +++ b/src/ch09-00-error-handling.md @@ -1,10 +1,10 @@ # 错误处理 -> [ch09-00-error-handling.md](https://github.com/rust-lang/book/blob/master/src/ch09-00-error-handling.md) +> [ch09-00-error-handling.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch09-00-error-handling.md) >
        -> commit fc825966fabaa408067eb2df3aa45e4fa6644fb6 +> commit 4f2dc564851dc04b271a2260c834643dfd86c724 -Rust 对可靠性的执着也扩展到了错误处理。错误对于软件来说是不可避免的,所以 Rust 有很多功能来处理当现错误的情况。在很多情况下,Rust 要求你承认出错的可能性可能性并在编译代码之前就采取行动。通过确保不会只有在将代码部署到生产环境之后才会发现错误来使得程序更可靠。 +Rust 对可靠性的执着也扩展到了错误处理。错误对于软件来说是不可避免的,所以 Rust 有很多功能来处理当现错误的情况。在很多情况下,Rust 要求你承认出错的可能性并在编译代码之前就采取行动。通过确保不会只有在将代码部署到生产环境之后才会发现错误来使得程序更可靠。 Rust 将错误组合成两个主要类别:**可恢复错误**(*recoverable*)和**不可恢复错误**(*unrecoverable*)。可恢复错误通常代表向用户报告错误和重试操作是合理的情况,比如未找到文件。不可恢复错误通常是 bug 的同义词,比如尝试访问超过数组结尾的位置。 diff --git a/src/ch09-01-unrecoverable-errors-with-panic.md b/src/ch09-01-unrecoverable-errors-with-panic.md index aed47ed..264daa1 100644 --- a/src/ch09-01-unrecoverable-errors-with-panic.md +++ b/src/ch09-01-unrecoverable-errors-with-panic.md @@ -1,10 +1,10 @@ ## `panic!`与不可恢复的错误 -> [ch09-01-unrecoverable-errors-with-panic.md](https://github.com/rust-lang/book/blob/master/src/ch09-01-unrecoverable-errors-with-panic.md) +> [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) >
        -> commit 380e6ee57c251f5ffa8df4c58b3949405448d914 +> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56 -突然有一天,糟糕的事情发生了,而你对此束手无策。对于这种情况,Rust 有`panic!宏。当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据,并接着退出。出现这种情况的场景通常是检测到一些类型的 bug 而且程序员并不清楚该如何处理它。 +突然有一天,糟糕的事情发生了,而你对此束手无策。对于这种情况,Rust 有`panic!`宏。当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据,然后接着退出。出现这种情况的场景通常是检测到一些类型的 bug 而且程序员并不清楚该如何处理它。 > ### Panic 中的栈展开与终止 > @@ -41,7 +41,7 @@ error: Process didn't exit successfully: `target/debug/panic` (exit code: 101) 在这个例子中,被指明的那一行是我们代码的一部分,而且查看这一行的话就会发现`panic!`宏的调用。换句话说,`panic!`可能会出现在我们的代码调用的代码中。错误信息报告的文件名和行号可能指向别人代码中的`panic!`宏调用,而不是我们代码中最终导致`panic!`的那一行。可以使用`panic!`被调用的函数的 backtrace 来寻找(我们代码中出问题的地方)。 -### 使用`panic!`backtrace +### 使用`panic!`的 backtrace 让我们来看看另一个因为我们代码中的 bug 引起的别的库中`panic!`的例子,而不是直接的宏调用: @@ -76,8 +76,6 @@ error: Process didn't exit successfully: `target/debug/panic` (exit code: 101) 接下来的几行提醒我们可以设置`RUST_BACKTRACE`环境变量来得到一个 backtrace 来调查究竟是什么导致了错误。让我们来试试看。列表 9-1 显示了其输出: -
        - ``` $ RUST_BACKTRACE=1 cargo run Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs @@ -106,13 +104,8 @@ core::ops::Index>::index::h1204ab053b688140 14: 0x1092206e9 - main ``` -
        - -Listing 9-1: The backtrace generated by a call to `panic!` displayed when the -environment variable `RUST_BACKTRACE` is set - -
        -
        +Listing 9-1: The backtrace generated by a call to +`panic!` displayed when the environment variable `RUST_BACKTRACE` is set 这里有大量的输出!backtrace 第 11 行指向了我们程序中引起错误的行:*src/main.rs* 的第四行。backtrace 是一个执行到目前位置所有被调用的函数的列表。Rust 的 backtrace 跟其他语言中的一样:阅读 backtrace 的关键是从头开始读直到发现你编写的文件。这就是问题的发源地。这一行往上是你的代码调用的代码;往下则是调用你的代码的代码。这些行可能包含核心 Rust 代码,标准库代码或用到的 crate 代码。 diff --git a/src/ch09-02-recoverable-errors-with-result.md b/src/ch09-02-recoverable-errors-with-result.md index 0063168..7d43eff 100644 --- a/src/ch09-02-recoverable-errors-with-result.md +++ b/src/ch09-02-recoverable-errors-with-result.md @@ -1,12 +1,14 @@ ## `Result`与可恢复的错误 -> [ch09-01-unrecoverable-errors-with-panic.md](https://github.com/rust-lang/book/blob/master/src/ch09-02-recoverable-errors-with-result.md) +> [ch09-01-unrecoverable-errors-with-panic.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch09-02-recoverable-errors-with-result.md) >
        -> commit 0c1d55ef48e5f6cf6a3b221f5b6dd4c922130bb1 +> commit e6d6caab41471f7115a621029bd428a812c5260e -大部分错误并没有严重到需要程序完全停止执行。有时,一个函数会因为一个容易理解并回应的原因失败。例如,如果尝试打开一个文件不过由于文件并不存在而操作就失败,这是我们可能想要创建这个文件而不是终止进程。 +大部分错误并没有严重到需要程序完全停止执行。有时,一个函数会因为一个容易理解并做出反映的原因失败。例如,如果尝试打开一个文件不过由于文件并不存在而操作就失败,这是我们可能想要创建这个文件而不是终止进程。 -回忆一下第二章“使用`Result`类型来处理潜在的错误”部分中的那个`Result`枚举,它定义有如下连个成员,`Ok`和`Err`: +回忆一下第二章“使用`Result`类型来处理潜在的错误”部分中的那个`Result`枚举,它定义有如下两个成员,`Ok`和`Err`: + +[handle_failure]: ch02-00-guessing-game-tutorial.html#handling-potential-failure-with-the-result-type ```rust enum Result { @@ -19,7 +21,6 @@ enum Result { 让我们调用一个返回`Result`的函数,因为它可能会失败:如列表 9-2 所示打开一个文件: -
        Filename: src/main.rs ```rust @@ -30,12 +31,7 @@ fn main() { } ``` -
        - -Listing 9-2: Opening a file - -
        -
        +Listing 9-2: Opening a file 如何知道`File::open`返回一个`Result`呢?我们可以查看标准库 API 文档,或者可以直接问编译器!如果给`f`某个我们知道**不是**函数返回值类型的类型注解,接着尝试编译代码,编译器会告诉我们类型不匹配。然后错误信息会告诉我们`f`的类型**应该**是什么,为此我们将`let f`语句改为: @@ -65,7 +61,6 @@ error[E0308]: mismatched types 我们需要在列表 9-2 的代码中增加根据`File::open`返回值进行不同处理的逻辑。列表 9-3 展示了一个处理`Result`的基本工具:第六章学习过的`match`表达式。 -
        Filename: src/main.rs ```rust,should_panic @@ -83,13 +78,8 @@ fn main() { } ``` -
        - -Listing 9-3: Using a `match` expression to handle the `Result` variants we -might have - -
        -
        +Listing 9-3: Using a `match` expression to handle the +`Result` variants we might have 注意与`Option`枚举一样,`Result`枚举和其成员也被导入到了 prelude 中,所以就不需要在`match`分支中的`Ok`和`Err`之前指定`Result::`。 @@ -106,7 +96,6 @@ Os { code: 2, message: "No such file or directory" } }', src/main.rs:8 列表 9-3 中的代码不管`File::open`是因为什么原因失败都会`panic!`。我们真正希望的是对不同的错误原因采取不同的行为:如果`File::open`因为文件不存在而失败,我们希望创建这个文件并返回新文件的句柄。如果`File::open`因为任何其他原因失败,例如没有打开文件的权限,我们仍然希望像列表 9-3 那样`panic!`。让我们看看列表 9-4,其中`match`增加了另一个分支: -
        Filename: src/main.rs ```rust,ignore @@ -139,16 +128,12 @@ fn main() { } ``` -
        - -Listing 9-4: Handling different kinds of errors in different ways - -
        -
        +Listing 9-4: Handling different kinds of errors in +different ways `File::open`返回的`Err`成员中的值类型`io::Error`,它是一个标准库中提供的结构体。这个结构体有一个返回`io::ErrorKind`值的`kind`方法可供调用。`io::ErrorKind`是一个标准库提供的枚举,它的成员对应`io`操作可能导致的不同错误类型。我们感兴趣的成员是`ErrorKind::NotFound`,它代表尝试打开的文件并不存在。 -`if error.kind() == ErrorKind::NotFound`条件被称作 *match guard*:它是一个进一步完善`match`分支模式的额外的条件。这个条件必须为真才能使分支的代码被执行;否则,模式匹配会继续并考虑`match`中的下一个分支。模式中的`ref`是必须的,这样`error`就不会被移动到 guard 条件中而只是仅仅引用它。第十八章会详细解释为什么在模式中使用`ref`而不是`&`来获取一个引用。简而言之,在模式的上下文中,`&`匹配一个引用并返回它的值,而`ref`匹配一个值并返回一个引用。 +条件`if error.kind() == ErrorKind::NotFound`被称作 *match guard*:它是一个进一步完善`match`分支模式的额外的条件。这个条件必须为真才能使分支的代码被执行;否则,模式匹配会继续并考虑`match`中的下一个分支。模式中的`ref`是必须的,这样`error`就不会被移动到 guard 条件中而只是仅仅引用它。第十八章会详细解释为什么在模式中使用`ref`而不是`&`来获取一个引用。简而言之,在模式的上下文中,`&`匹配一个引用并返回它的值,而`ref`匹配一个值并返回一个引用。 在 match guard 中我们想要检查的条件是`error.kind()`是否是`ErrorKind`枚举的`NotFound`成员。如果是,尝试用`File::create`创建文件。然而`File::create`也可能会失败,我们还需要增加一个内部`match`语句。当文件不能被打开,会打印出一个不同的错误信息。外部`match`的最后一个分支保持不变这样对任何除了文件不存在的错误会使程序 panic。 @@ -196,8 +181,6 @@ thread 'main' panicked at 'Failed to open hello.txt: Error { repr: Os { code: 例如,列表 9-5 展示了一个从文件中读取用户名的函数。如果文件不存在或不能读取,这个函数会将这些错误返回给调用它的代码: -
        - ```rust use std::io; use std::io::Read; @@ -220,12 +203,8 @@ fn read_username_from_file() -> Result { } ``` -
        - -Listing 9-5: A function that returns errors to the calling code using `match` - -
        -
        +Listing 9-5: A function that returns errors to the +calling code using `match` 首先让我们看看函数的返回值:`Result`。这意味着函数返回一个`Result`类型的值,其中泛型参数`T`的具体类型是`String`,而`E`的具体类型是`io::Error`。如果这个函数没有出任何错误成功返回,函数的调用者会收到一个包含`String`的`Ok`值————函数从文件中读取到的用户名。如果函数遇到任何错误,函数的调用者会收到一个`Err`值,它储存了一个包含更多这个问题相关信息的`io::Error`实例。我们选择`io::Error`作为函数的返回值是因为它正好是函数体中那两个可能会失败的操作的错误返回值:`File::open`函数和`read_to_string`方法。 @@ -239,12 +218,11 @@ Listing 9-5: A function that returns errors to the calling code using `match` ### 传播错误的捷径:`?` -列表 9-6 展示了一个`read_username_from_file`的实现,它实现了与列表 9-5 中的代码相同的功能,不过这个实现是使用了问号运算符: - -
        +列表 9-6 展示了一个`read_username_from_file`的实现,它实现了与列表 9-5 中的代码相同的功能,不过这个实现是使用了问号运算符的: ```rust use std::io; +use std::io::Read; use std::fs::File; fn read_username_from_file() -> Result { @@ -255,12 +233,8 @@ fn read_username_from_file() -> Result { } ``` -
        - -Listing 9-6: A function that returns errors to the calling code using `?` - -
        -
        +Listing 9-6: A function that returns errors to the +calling code using `?` `Result`值之后的`?`被定义为与列表 9-5 中定义的处理`Result`值的`match`表达式有着完全相同的工作方式。如果`Result`的值是`Ok`,这个表达式将会返回`Ok`中的值而程序将继续执行。如果值是`Err`,`Err`中的值将作为整个函数的返回值,就好像使用了`return`关键字一样,这样错误值就被传播给了调用者。 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 feaa21b..3dd77da 100644 --- a/src/ch09-03-to-panic-or-not-to-panic.md +++ b/src/ch09-03-to-panic-or-not-to-panic.md @@ -1,10 +1,10 @@ ## `panic!`还是不`panic!` -> [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) +> [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) >
        -> commit 0c1d55ef48e5f6cf6a3b221f5b6dd4c922130bb1 +> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56 -那么,该如何决定何时应该`panic!`以及何时应该返回`Result`呢?如果代码 panic,就没有恢复的可能。你可以选择对任何错误场景都调用`panic!`,不管是否有可能恢复,不过这样就你代替调用者决定了这是不可恢复的。选择返回`Result`值的话,就将选择权交给了调用者,而不是代替他们做出决定。调用者可能会选择以符合他们场景的方式尝试恢复,或者也可能干脆就认为`Err`是不可恢复的,所以他们也可能会调用`panic!`并将可恢复的错误变成了不可恢复的错误。因此返回`Result`是定义可能会失败的函数的一个好的默认选择。 +那么,该如何决定何时应该`panic!`以及何时应该返回`Result`呢?如果代码 panic,就没有恢复的可能。你可以选择对任何错误场景都调用`panic!`,不管是否有可能恢复,不过这样就是你代替调用者决定了这是不可恢复的。选择返回`Result`值的话,就将选择权交给了调用者,而不是代替他们做出决定。调用者可能会选择以符合他们场景的方式尝试恢复,或者也可能干脆就认为`Err`是不可恢复的,所以他们也可能会调用`panic!`并将可恢复的错误变成了不可恢复的错误。因此返回`Result`是定义可能会失败的函数的一个好的默认选择。 有一些情况 panic 比返回`Result`更为合适,不过他们并不常见。让我们讨论一下为何在示例、代码原型和测试中,以及那些人们认为不会失败而编译器不这么看的情况下, panic 是合适的,最后会总结一些在库代码中如何决定是否要 panic 的通用指导原则。 @@ -30,7 +30,7 @@ let home = "127.0.0.1".parse::().unwrap(); ### 错误处理指导原则 -在当有可能会导致有害状态的情况下建议使用`panic!`————在这里,有害状态是指当一些假设、保证、协议或不可变形被打破的状态,例如无效的值、自相矛盾的值或者被传递了不存在的值————外加如下几种情况: +在当有可能会导致有害状态的情况下建议使用`panic!`——在这里,有害状态是指当一些假设、保证、协议或不可变性被打破的状态,例如无效的值、自相矛盾的值或者被传递了不存在的值——外加如下几种情况: * 有害状态并不包含**预期**会偶尔发生的错误 * 之后的代码的运行依赖于不再处于这种有害状态 @@ -75,8 +75,6 @@ loop { 相反我们可以创建一个新类型来将验证放入创建其实例的函数中,而不是到处重复这些检查。这样就可以安全的在函数签名中使用新类型并相信他们接收到的值。列表 9-8 中展示了一个定义`Guess`类型的方法,只有在`new`函数接收到 1 到 100 之间的值时才会创建`Guess`的实例: -
        - ```rust struct Guess { value: u32, @@ -99,13 +97,8 @@ impl Guess { } ``` -
        - -Listing 9-8: A `Guess` type that will only continue with values between 1 and -100 - -
        -
        +Listing 9-8: A `Guess` type that will only continue with +values between 1 and 100 首先,我们定义了一个包含`u32`类型字段`value`的结构体`Guess`。这里是储存猜测值的地方。 diff --git a/src/ch10-00-generics.md b/src/ch10-00-generics.md index 8294a6f..2d96130 100644 --- a/src/ch10-00-generics.md +++ b/src/ch10-00-generics.md @@ -1,12 +1,12 @@ # 泛型、trait 和生命周期 -> [ch10-00-generics.md](https://github.com/rust-lang/book/blob/master/src/ch10-00-generics.md) +> [ch10-00-generics.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch10-00-generics.md) >
        -> commit b335da755592f286fd97a64d98f0ca3be6a59327 +> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56 每一个编程语言都有高效的处理重复概念的工具;在 Rust 中工具之一就是**泛型**(*generics*)。泛型是具体类型或其他属性的抽象替代。我们可以表达泛型的属性,比如他们的行为或如何与其他泛型相关联,而不需要在编写和编译代码时知道他们在这里实际上代表什么。 -同理为了编写一份可以用于多种具体值的代码,函数并不知道其参数为何值,这时就可以让函数获取泛型而不是像`i32`或`String`这样的具体值。我们已经使用过第六章的`Option`,第八章的`Vec`和`HashMap`,以及第九章的`Result`这些泛型了。本章会探索如何使用泛型定义我们自己自己的类型、函数和方法。 +同理为了编写一份可以用于多种具体值的代码,函数并不知道其参数为何值,这时就可以让函数获取泛型而不是像`i32`或`String`这样的具体值。我们已经使用过第六章的`Option`,第八章的`Vec`和`HashMap`,以及第九章的`Result`这些泛型了。本章会探索如何使用泛型定义我们自己自己的类型、函数和方法! 首先,我们将回顾一下提取函数以减少代码重复的机制。接着使用一个只在参数类型上不同的泛型函数来实现相同的功能。我们也会讲到结构体和枚举定义中的泛型。 @@ -20,7 +20,6 @@ 考虑一下这个寻找列表中最大值的小程序,如列表 10-1 所示: -
        Filename: src/main.rs ```rust @@ -40,18 +39,13 @@ fn main() { } ``` -
        - -Listing 10-1: Code to find the largest number in a list of numbers - -
        -
        +Listing 10-1: Code to find the largest number in a list +of numbers 这段代码获取一个整型列表,存放在变量`numbers`中。它将列表的第一项放入了变量`largest`中。接着遍历了列表中的所有数字,如果当前值大于`largest`中储存的值,将`largest`替换为这个值。如果当前值小于目前为止的最大值,`largest`保持不变。当列表中所有值都被考虑到之后,`largest`将会是最大值,在这里也就是 100。 如果需要在两个不同的列表中寻找最大值,我们可以重复列表 10-1 中的代码这样程序中就会存在两段相同逻辑的代码,如列表 10-2 所示: -
        Filename: src/main.rs ```rust @@ -82,12 +76,8 @@ fn main() { } ``` -
        - -Listing 10-2: Code to find the largest number in *two* lists of numbers - -
        -
        +Listing 10-2: Code to find the largest number in *two* +lists of numbers 虽然代码能够执行,但是重复的代码是冗余且已于出错的,并且意味着当更新逻辑时需要修改多处地方的代码。 @@ -100,7 +90,6 @@ Listing 10-2: Code to find the largest number in *two* lists of numbers 在列表 10-3 的程序中将寻找最大值的代码提取到了一个叫做`largest`的函数中。这个程序可以找出两个不同数字列表的最大值,不过列表 10-1 中的代码只存在于一个位置: -
        Filename: src/main.rs ```rust @@ -131,12 +120,8 @@ fn main() { } ``` -
        - -Listing 10-3: Abstracted code to find the largest number in two lists - -
        -
        +Listing 10-3: Abstracted code to find the largest number +in two lists 这个函数有一个参数`list`,它代表会传递给函数的任何具体`i32`值的 slice。函数定义中的`list`代表任何`&[i32]`。当调用`largest`函数时,其代码实际上运行于我们传递的特定值上。 diff --git a/src/ch10-01-syntax.md b/src/ch10-01-syntax.md index 4a58343..d2cd428 100644 --- a/src/ch10-01-syntax.md +++ b/src/ch10-01-syntax.md @@ -1,6 +1,6 @@ ## 泛型数据类型 -> [ch10-01-syntax.md](https://github.com/rust-lang/book/blob/master/src/ch10-01-syntax.md) +> [ch10-01-syntax.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch10-01-syntax.md) >
        > commit 55d9e75ffec92e922273c997026bb10613a76578 @@ -12,7 +12,6 @@ 回到`largest`函数上,列表 10-4 中展示了两个提供了相同的寻找 slice 中最大值功能的函数。第一个是从列表 10-3 中提取的寻找 slice 中`i32`最大值的函数。第二个函数寻找 slice 中`char`的最大值: -
        Filename: src/main.rs ```rust @@ -55,17 +54,12 @@ fn main() { } ``` -
        +Listing 10-4: Two functions that differ only in their +names and the types in their signatures -Listing 10-4: Two functions that differ only in their names and the types in -their signatures +这里`largest_i32`和`largest_char`有着完全相同的函数体,所以能够将这两个函数变成一个来减少重复就太好了。所幸通过引入一个泛型参数就能实现! -
        -
        - -这里`largest_i32`和`largest_char`有着完全相同的函数体,所以能够将这两个函数变成一个来减少重复就太好了。所幸通过引入一个泛型参数就能实现。 - -为了参数化要定义的函数的签名中的类型,我们需要像给函数的值参数起名那样为这类型参数起一个名字。这里选择了名称`T`。任何标识符抖可以作为类型参数名,选择`T`是因为 Rust 的类型命名规范是骆驼命名法(CamelCase)。另外泛型类型参数的规范也倾向于简短,经常仅仅是一个字母。`T`作为“type”是大部分 Rust 程序员的首选。 +为了参数化要定义的函数的签名中的类型,我们需要像给函数的值参数起名那样为这类型参数起一个名字。这里选择了名称`T`。任何标识符抖可以作为类型参数名,选择`T`是因为 Rust 的类型命名规范是骆驼命名法(CamelCase)。另外泛型类型参数的规范也倾向于简短,经常仅仅是一个字母。`T`作为“type”的缩写是大部分 Rust 程序员的首选。 当需要再函数体中使用一个参数时,必须再函数签名中声明这个参数以便编译器能知道函数体中这个名称的意义。同理,当在函数签名中使用一个类型参数时,必须在使用它之前就声明它。类型参数声明位于函数名称与参数列表中间的尖括号中。 @@ -79,8 +73,6 @@ fn largest(list: &[T]) -> T { 列表 10-5 展示一个在签名中使用了泛型的统一的`largest`函数定义,并向我们展示了如何对`i32`值的 slice 或`char`值的 slice 调用`largest`函数。注意这些代码还不能编译! - -
        Filename: src/main.rs ```rust,ignore @@ -109,13 +101,9 @@ fn main() { } ``` -
        +Listing 10-5: A definition of the `largest` function that +uses generic type parameters but doesn't compile yet -Listing 10-5: A definition of the `largest` function that uses generic type -parameters but doesn't compile yet - -
        -
        如果现在就尝试编译这些代码,会出现如下错误: @@ -142,7 +130,6 @@ what you think. /Carol --> 同样也可以使用`<>`语法来定义拥有一个或多个泛型参数类型字段的结构体。列表 10-6 展示了如何定义和使用一个可以存放任何类型的`x`和`y`坐标值的结构体`Point`: -
        Filename: src/main.rs ```rust @@ -157,19 +144,13 @@ fn main() { } ``` -
        - -Listing 10-6: A `Point` struct that holds `x` and `y` values of type `T` - -
        -
        +Listing 10-6: A `Point` struct that holds `x` and `y` +values of type `T` 其语法类似于函数定义中的泛型应用。首先,必须在结构体名称后面的尖括号中声明泛型参数的名称。接着在结构体定义中可以指定具体数据类型的位置使用泛型类型。 注意`Point`的定义中是使用了要给泛型类型,我们想要表达的是结构体`Point`对于一些类型`T`是泛型的,而且无论这个泛型是什么,字段`x`和`y`**都是**相同类型的。如果尝试创建一个有不同类型值的`Point`的实例,像列表 10-7 中的代码就不能编译: - -
        Filename: src/main.rs ```rust,ignore @@ -183,13 +164,8 @@ fn main() { } ``` -
        - -Listing 10-7: The fields `x` and `y` must be the same type because both have -the same generic data type `T` - -
        -
        +Listing 10-7: The fields `x` and `y` must be the same +type because both have the same generic data type `T` 尝试编译会得到如下错误: @@ -209,7 +185,6 @@ error[E0308]: mismatched types 如果想要一个`x`和`y`可以有不同类型且仍然是泛型的`Point`结构体,我们可以使用多个泛型类型参数。在列表 10-8 中,我们修改`Point`的定义为拥有两个泛型类型`T`和`U`。其中字段`x`是`T`类型的,而字段`y`是`U`类型的: -
        Filename: src/main.rs ```rust @@ -225,13 +200,8 @@ fn main() { } ``` -
        - -Listing 10-8: A `Point` generic over two types so that `x` and `y` may be -values of different types - -
        -
        +Listing 10-8: A `Point` generic over two types so that +`x` and `y` may be values of different types 现在所有这些`Point`实例都是被允许的了!你可以在定义中使用任意多的泛型类型参数,不过太多的话代码将难以阅读和理解。如果你处于一个需要很多泛型类型的位置,这可能是一个需要重新组织代码并分隔成一些更小部分的信号。 @@ -265,7 +235,6 @@ enum Result { 可以像第五章介绍的那样来为其定义中带有泛型的结构体或枚举实现方法。列表 10-9 中展示了列表 10-6 中定义的结构体`Point`。接着我们在`Point`上定义了一个叫做`x`的方法来返回字段`x`中数据的引用: -
        Filename: src/main.rs ```rust @@ -287,19 +256,14 @@ fn main() { } ``` -
        - -Listing 10-9: Implementing a method named `x` on the `Point` struct that -will return a reference to the `x` field, which is of type `T`. - -
        -
        +Listing 10-9: Implementing a method named `x` on the +`Point` struct that will return a reference to the `x` field, which is of +type `T`. 注意必须在`impl`后面声明`T`,这样就可以在`Point`上实现的方法中使用它了。 结构体定义中的泛型类型参数并不总是与结构体方法签名中使用的泛型是同一类型。列表 10-10 中在列表 10-8 中的结构体`Point`上定义了一个方法`mixup`。这个方法获取另一个`Point`作为参数,而它可能与调用`mixup`的`self`是不同的`Point`类型。这个方法用`self`的`Point`类型的`x`值(类型`T`)和参数的`Point`类型的`y`值(类型`W`)来创建一个新`Point`类型的实例: -
        Filename: src/main.rs ```rust @@ -309,7 +273,7 @@ struct Point { } impl Point { - fn mixup(&self, other: &Point) -> Point { + fn mixup(self, other: Point) -> Point { Point { x: self.x, y: other.y, @@ -327,13 +291,8 @@ fn main() { } ``` -
        - -Listing 10-10: Methods that use different generic types than their struct's -definition - -
        -
        +Listing 10-10: Methods that use different generic types +than their struct's definition 在`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`。 @@ -358,7 +317,6 @@ let float = Some(5.0); 编译器生成的单态化版本的代码看起来像这样,并包含将泛型`Option`替换为编译器创建的具体定义后的用例代码: - Filename: src/main.rs ```rust diff --git a/src/ch10-02-traits.md b/src/ch10-02-traits.md index f92e3b6..45444bf 100644 --- a/src/ch10-02-traits.md +++ b/src/ch10-02-traits.md @@ -1,8 +1,8 @@ ## trait:定义共享的行为 -> [ch10-02-traits.md](https://github.com/rust-lang/book/blob/master/src/ch10-02-traits.md) +> [ch10-02-traits.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch10-02-traits.md) >
        -> commit 709eb1eaca48864fafd9263042f5f9d9d6ffe08d +> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56 trait 允许我们进行另一种抽象:他们让我们可以抽象类型所通用的行为。*trait* 告诉 Rust 编译器某个特定类型拥有可能与其他类型共享的功能。在使用泛型类型参数的场景中,可以使用 *trait bounds* 在编译时指定泛型可以是任何实现了某个 trait 的类型,并由此在这个场景下拥有我们希望的功能。 @@ -10,13 +10,12 @@ trait 允许我们进行另一种抽象:他们让我们可以抽象类型所 ### 定义 trait -一个类型的行为由其可供调用的方法构成。如果可以对不同类型调用相同的方法的话,这些类型就可以共享相同的行为了。trait 定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必须行为的集合。 +一个类型的行为由其可供调用的方法构成。如果可以对不同类型调用相同的方法的话,这些类型就可以共享相同的行为了。trait 定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必需的行为的集合。 例如,这里有多个存放了不同类型和属性文本的结构体:结构体`NewsArticle`用于存放发生于世界各地的新闻故事,而结构体`Tweet`最多只能存放 140 个字符的内容,以及像是否转推或是否是对推友的回复这样的元数据。 我们想要创建一个多媒体聚合库用来显示可能储存在`NewsArticle`或`Tweet`实例中的数据的总结。每一个结构体都需要的行为是他们是能够被总结的,这样的话就可以调用实例的`summary`方法来请求总结。列表 10-11 中展示了一个表现这个概念的`Summarizable` trait 的定义: -
        Filename: lib.rs ```rust @@ -25,13 +24,8 @@ pub trait Summarizable { } ``` -
        - -Listing 10-11: Definition of a `Summarizable` trait that consists of the -behavior provided by a `summary` method - -
        -
        +Listing 10-11: Definition of a `Summarizable` trait that +consists of the behavior provided by a `summary` method 使用`trait`关键字来定义一个 trait,后面是 trait 的名字,在这个例子中是`Summarizable`。在大括号中声明描述实现这个 trait 的类型所需要的行为的方法签名,在这个例子中是是`fn summary(&self) -> String`。在方法签名后跟分号而不是在大括号中提供其实现。接着每一个实现这个 trait 的类型都需要提供其自定义行为的方法体,编译器也会确保任何实现`Summarizable` trait 的类型都拥有与这个签名的定义完全一致的`summary`方法。 @@ -41,7 +35,6 @@ trait 体中可以有多个方法,一行一个方法签名且都以分号结 现在我们定义了`Summarizable` trait,接着就可以在多媒体聚合库中需要拥有这个行为的类型上实现它了。列表 10-12 中展示了`NewsArticle`结构体上`Summarizable` trait 的一个实现,它使用标题、作者和创建的位置作为`summary`的返回值。对于`Tweet`结构体,我们选择将`summary`定义为用户名后跟推文的全部文本作为返回值,并假设推文内容已经被限制为 140 字符以内。 -
        Filename: lib.rs ```rust @@ -76,13 +69,8 @@ impl Summarizable for Tweet { } ``` -
        - -Listing 10-12: Implementing the `Summarizable` trait on the `NewsArticle` and -`Tweet` types - -
        -
        +Listing 10-12: Implementing the `Summarizable` trait on +the `NewsArticle` and `Tweet` types 在类型上实现 trait 类似与实现与 trait 无关的方法。区别在于`impl`关键字之后,我们提供需要实现 trait 的名称,接着是`for`和需要实现 trait 的类型的名称。在`impl`块中,使用 trait 定义中的方法签名,不过不再后跟分号,而是需要在大括号中编写函数体来为特定类型实现 trait 方法所拥有的行为。 @@ -103,7 +91,6 @@ println!("1 new tweet: {}", tweet.summary()); 注意因为列表 10-12 中我们在相同的`lib.rs`力定义了`Summarizable` trait 和`NewsArticle`与`Tweet`类型,所以他们是位于同一作用域的。如果这个`lib.rs`是对应`aggregator` crate 的,而别人想要利用我们 crate 的功能外加为其`WeatherForecast`结构体实现`Summarizable` trait,在实现`Summarizable` trait 之前他们首先就需要将其导入其作用域中,如列表 10-13 所示: -
        Filename: lib.rs ```rust,ignore @@ -126,13 +113,8 @@ impl Summarizable for WeatherForecast { } ``` -
        - -Listing 10-13: Bringing the `Summarizable` trait from our `aggregator` crate -into scope in another crate - -
        -
        +Listing 10-13: Bringing the `Summarizable` trait from our +`aggregator` crate into scope in another crate 另外这段代码假设`Summarizable`是一个公有 trait,这是因为列表 10-11 中`trait`之前使用了`pub`关键字。 @@ -144,7 +126,6 @@ trait 实现的一个需要注意的限制是:只能在 trait 或对应类型 列表 10-14 中展示了如何为`Summarize` trait 的`summary`方法指定一个默认的字符串值,而不是像列表 10-11 中那样只是定义方法签名: -
        Filename: lib.rs ```rust @@ -155,13 +136,8 @@ pub trait Summarizable { } ``` -
        - -Listing 10-14: Definition of a `Summarizable` trait with a default -implementation of the `summary` method - -
        -
        +Listing 10-14: Definition of a `Summarizable` trait with +a default implementation of the `summary` method 如果想要对`NewsArticle`实例使用这个默认实现,而不是像列表 10-12 中那样定义一个自己的实现,则可以指定一个空的`impl`块: @@ -201,7 +177,6 @@ pub trait Summarizable { 为了使用这个版本的`Summarizable`,只需在实现 trait 时定义`author_summary`即可: - ```rust,ignore impl Summarizable for Tweet { fn author_summary(&self) -> String { @@ -304,7 +279,6 @@ error[E0507]: cannot move out of borrowed content 如果只想对实现了`Copy`的类型调用这些带啊吗,可以在`T`的 trait bounds 中增加`Copy`!列表 10-15 中展示了一个可以编译的泛型版本的`largest`函数的完整代码,只要传递给`largest`的 slice 值的类型实现了`PartialOrd`和`Copy`这两个 trait,例如`i32`和`char`: -
        Filename: src/main.rs ```rust @@ -335,13 +309,9 @@ fn main() { } ``` -
        - -Listing 10-15: A working definition of the `largest` function that works on any -generic type that implements the `PartialOrd` and `Copy` traits - -
        -
        +Listing 10-15: A working definition of the `largest` +function that works on any generic type that implements the `PartialOrd` and +`Copy` traits 如果并不希望限制`largest`函数只能用于实现了`Copy` trait 的类型,我们可以在`T`的 trait bounds 中指定`Clone`而不是`Copy`,并克隆 slice 的每一个值使得`largest`函数拥有其所有权。但是使用`clone`函数潜在意味着更多的堆分配,而且堆分配在涉及大量数据时可能会相当缓慢。另一种`largest`的实现方式是返回 slice 中一个`T`值的引用。如果我们将函数返回值从`T`改为`&T`并改变函数体使其能够返回一个引用,我们将不需要任何`Clone`或`Copy`的 trait bounds 而且也不会有任何的堆分配。尝试自己实现这种替代解决方式吧! diff --git a/src/ch10-03-lifetime-syntax.md b/src/ch10-03-lifetime-syntax.md index 30238e8..252f9f7 100644 --- a/src/ch10-03-lifetime-syntax.md +++ b/src/ch10-03-lifetime-syntax.md @@ -1,8 +1,8 @@ ## 生命周期与引用有效性 -> [ch10-03-lifetime-syntax.md](https://github.com/rust-lang/book/blob/master/src/ch10-03-lifetime-syntax.md) +> [ch10-03-lifetime-syntax.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch10-03-lifetime-syntax.md) >
        -> commit d7a4e99554da53619dd71044273535ba0186f40a +> commit c49e5ee8859f8eb8f8867cbeafbdf5b802aa5894 当在第四章讨论引用时,我们遗漏了一个重要的细节:Rust 中的每一个引用都有其**生命周期**,也就是引用保持有效的作用域。大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以多种不同方式向关联的情况,所以 Rust 需要我们使用泛型生命周期参数来注明他们的关系,这样就能确保运行时实际使用的引用绝对是有效的。 @@ -14,8 +14,6 @@ 生命周期的主要目标是避免悬垂引用,它会导致程序引用了并非其期望引用的数据。考虑一下列表 10-16 中的程序,它有一个外部作用域和一个内部作用域,外部作用域声明了一个没有初值的变量`r`,而内部作用域声明了一个初值为 5 的变量`x`。在内部作用域中,我们尝试将`r`的值设置为一个`x`的引用。接着在内部作用域结束后,尝试打印出`r`的值: -
        - ```rust,ignore { let r; @@ -29,12 +27,8 @@ } ``` -
        - -Listing 10-16: An attempt to use a reference whose value has gone out of scope - -
        -
        +Listing 10-16: An attempt to use a reference whose value +has gone out of scope > ### 未初始化变量不能被使用 > @@ -60,8 +54,6 @@ error: `x` does not live long enough 编译器的这一部分叫做**借用检查器**(*borrow checker*),它比较作用域来确保所有的借用都是有效的。列表 10-17 展示了与列表 10-16 相同的例子不过带有变量声明周期的注释: -
        - ```rust,ignore { let r; // -------+-- 'a @@ -77,13 +69,9 @@ error: `x` does not live long enough } ``` -
        +Listing 10-17: Annotations of the lifetimes of `r` and +`x`, named `'a` and `'b` respectively -Listing 10-17: Annotations of the lifetimes of `x` and `r`, named `'a` and `'b` -respectively - -
        -
        让我们看看列表 10-18 中这个并没有产生悬垂引用且可以正常编译的例子: -
        - ```rust { let x = 5; // -----+-- 'b @@ -110,13 +96,8 @@ looking arrows and labels? /Carol --> } // -----+ ``` -
        - -Listing 10-18: A valid reference because the data has a longer lifetime than -the reference - -
        -
        +Listing 10-18: A valid reference because the data has a +longer lifetime than the reference `x`拥有生命周期 `'b`,在这里它比 `'a`要大。这就意味着`r`可以引用`x`:Rust 知道`r`中的引用在`x`有效的时候也会一直有效。 @@ -126,11 +107,9 @@ the reference 让我们来编写一个返回两个字符串 slice 中最长的那一个的函数。我们希望能够通过传递两个字符串 slice 来调用这个函数,并希望返回一个字符串 slice。一旦我们实现了`longest`函数,列表 10-19 中的代码应该会打印出`The longest string is abcd`: -
        - Filename: src/main.rs -```rust +```rust,ignore fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; @@ -140,13 +119,8 @@ fn main() { } ``` -
        - -Listing 10-19: A `main` function that calls the `longest` function to find the -longest of two string slices - -
        -
        +Listing 10-19: A `main` function that calls the `longest` +function to find the longest of two string slices 注意函数期望获取字符串 slice(如第四章所讲到的这是引用)因为我们并不希望`longest`函数获取其参数的引用。我们希望函数能够接受`String`的 slice(也就是变量`string1`的类型)和字符串字面值(也就是变量`string2`包含的值)。 @@ -171,7 +145,6 @@ interested to know if rereading Chapter 4 clears up that confusion. 如果尝试像列表 10-20 中那样实现`longest`函数,它并不能编译: -
        Filename: src/main.rs ```rust,ignore @@ -184,13 +157,9 @@ fn longest(x: &str, y: &str) -> &str { } ``` -
        - -Listing 10-20: An implementation of the `longest` function that returns the -longest of two string slices, but does not yet compile - -
        -
        +Listing 10-20: An implementation of the `longest` +function that returns the longest of two string slices, but does not yet +compile 将会出现如下有关生命周期的错误: @@ -228,7 +197,6 @@ error[E0106]: missing lifetime specifier 来看看我们编写的`longest`函数的上下文中的生命周期。就像泛型类型参数,泛型生命周期参数需要声明在函数名和参数列表间的加括号中。这里我们想要告诉 Rust 关于参数中的引用和返回值之间的限制是他们都必须拥有相同的生命周期,就像列表 10-21 中在每个引用中都加上了`'a`那样: -
        Filename: src/main.rs ```rust @@ -241,13 +209,9 @@ fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { } ``` -
        - -Listing 10-21: The `longest` function definition that specifies all the -references in the signature must have the same lifetime, `'a` - -
        -
        +Listing 10-21: The `longest` function definition that +specifies all the references in the signature must have the same lifetime, +`'a` 这段代码能够编译并会产生我们想要使用列表 10-19 中的`main`函数得到的结果。 @@ -261,7 +225,6 @@ references in the signature must have the same lifetime, `'a` 让我们如何通过传递拥有不同具体生命周期的引用来观察他们是如何限制`longest`函数的使用的。列表 10-22 是一个应该在任何编程语言中都很直观的例子:`string1`直到外部作用域结束都是有效的,`string2`则在内部作用域中是有效的,而`result`则引用了一些直到外部作用域结束都是有效的值。借用检查器赞同这些代码;它能够编译和运行,并打印出`The longest string is long string is long`: -
        Filename: src/main.rs ```rust @@ -284,28 +247,14 @@ fn main() { } ``` -
        - -Listing 10-22: Using the `longest` function with references to `String` values -that have different concrete lifetimes - -
        -
        +Listing 10-22: Using the `longest` function with +references to `String` values that have different concrete lifetimes 接下来,让我们尝试一个`result`的引用的生命周期必须比两个参数的要短的例子。将`result`变量的声明从内部作用域中移动出来,不过将`result`和`string2`变量的赋值语句一同放在内部作用域里。接下来,我们将使用`result`的`println!`移动到内部作用域之外,就在其结束之后。注意列表 10-23 中的代码不能编译: -
        Filename: src/main.rs -```rust -# fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { -# if x.len() > y.len() { -# x -# } else { -# y -# } -# } -# +```rust,ignore fn main() { let string1 = String::from("long string is long"); let result; @@ -317,13 +266,8 @@ fn main() { } ``` -
        - -Listing 10-23: Attempting to use `result` after `string2` has gone out of scope -won't compile - -
        -
        +Listing 10-23: Attempting to use `result` after `string2` +has gone out of scope won't compile 如果尝试编译会出现如下错误: @@ -395,7 +339,6 @@ at 1:44... 目前为止,我们只定义过有所有权类型的结构体。也可以定义存放引用的结构体,不过需要为结构体定义中的每一个引用添加生命周期注解。列表 10-24 中有一个存放了一个字符串 slice 的结构体`ImportantExcerpt`: -
        Filename: src/main.rs ```rust @@ -412,13 +355,8 @@ fn main() { } ``` -
        - -Listing 10-24: A struct that holds a reference, so its definition needs a -lifetime annotation - -
        -
        +Listing 10-24: A struct that holds a reference, so its +definition needs a lifetime annotation 这个结构体有一个字段,`part`,它存放了一个字符串 slice,这是一个引用。类似于泛型参数类型,必须在结构体名称后面的尖括号中声明泛型生命周期参数,以便在结构体定义中使用生命周期参数。 @@ -428,7 +366,6 @@ lifetime annotation 在这一部分,我们知道了每一个引用都有一个生命周期,而且需要为使用了引用的函数或结构体指定生命周期。然而,第四章的“字符串 slice”部分有一个函数,我们在列表 10-25 中再次展示它,没有生命周期注解却能成功编译: -
        Filename: src/lib.rs ```rust @@ -445,13 +382,9 @@ fn first_word(s: &str) -> &str { } ``` -
        - -Listing 10-25: A function we defined in Chapter 4 that compiled without -lifetime annotations, even though the parameter and return type are references - -
        -
        +Listing 10-25: A function we defined in Chapter 4 that +compiled without lifetime annotations, even though the parameter and return +type are references 这个函数没有生命周期注解却能编译是由于一些历史原因:在早期 1.0 之前的版本的 Rust 中,这的确是不能编译的。每一个引用都必须有明确的生命周期。那时的函数签名将会写成这样: diff --git a/src/ch11-00-testing.md b/src/ch11-00-testing.md index 4c3e683..c203a62 100644 --- a/src/ch11-00-testing.md +++ b/src/ch11-00-testing.md @@ -1,9 +1,8 @@ # 测试 -> [ch11-00-testing.md](https://github.com/rust-lang/book/blob/master/src/ch11-00-testing.md) +> [ch11-00-testing.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch11-00-testing.md) >
        -> commit c9fd8eb1da7a79deee97020e8ad49af8ded78f9c - +> commit b7ab6668bbcb73b93c6464d8354c94a8e6c90395 > Program testing can be a very effective way to show the presence of bugs, but it is hopelessly inadequate for showing their absence. > @@ -13,8 +12,10 @@ > > Edsger W. Dijkstra,【谦卑的程序员】(1972) -Rust 是一个非常注重正确性的编程语言,不过正确性是一个难以证明的复杂主题。Rust 在其类型系统上下了很大的功夫,来确保程序像我们希望的那样运行,不过它并不有助于所有情况。为此,Rust 也包含为语言自身编写软件测试的支持。 +程序的正确性意味着代码如我们期望的那样运行。Rust 是一个非常注重正确性的编程语言,不过正确性是一个难以证明的复杂主题。Rust 的类型系统在此问题上下了很大的功夫,不过它不可能捕获所有类型的错误。为此,Rust 也包含为语言自身编写软件测试的支持。 -例如,我们可以编写一个叫做`add_two`的函数,它的签名有一个整型参数并返回一个整型值。我们可以实现并编译这个函数,而 Rust 也会进行所有的类型检查和借用检查,正如我们之前见识过的那样。Rust 所**不能**检查的是,我们实现的这个函数是否返回了参数值加二后的值,而不是加 10 或者减 50!这也就是测试出场的地方。例如可以编写传递`3`给`add_two`函数并检查我们是否得到了`5`。任何时候修改了代码我们都可以运行测试来确保没有改变任何现有测试所指定的行为。 +例如,我们可以编写一个叫做`add_two`的将传递给它的值加二的函数。它的签名有一个整型参数并返回一个整型值。当实现和编译这个函数时,Rust 会进行所有目前我们已经见过的的类型检查和借用检查。例如,这些检查会确保我们不会传递`String`或无效的引用给这个函数。Rust 所**不能**检查的是这个函数是否会准确的完成我们期望的工作:返回参数加二后的值,而不是比如说参数加 10 或减 50 的值!这也就是测试出场的地方。 -测试是一项技能,而且我们也不能期望在一本书的一个章节中就涉及到编写好的测试的所有内容。然而我们可以讨论的是 Rust 测试功能的机制。我们会讲到编写测试时会用到的注解和宏,Rust 提供用来运行测试的默认行为和选项,以及如何将测试组织成单元测试和集成测试。 \ No newline at end of file +我们可以编写测试断言,比如说,当传递`3`给`add_two`函数时,应该得到`5`。当对代码进行修改时可以运行测试来确保任何现存的正确行为没有被改变。 + +测试是一项复杂的技能,而且我们也不能期望在一本书的一个章节中就涉及到编写好的测试的所有内容,所以这里仅仅讨论 Rust 测试功能的机制。我们会讲到编写测试时会用到的注解和宏,Rust 提供用来运行测试的默认行为和选项,以及如何将测试组织成单元测试和集成测试。 \ No newline at end of file diff --git a/src/ch11-01-writing-tests.md b/src/ch11-01-writing-tests.md index e638de6..d7fe508 100644 --- a/src/ch11-01-writing-tests.md +++ b/src/ch11-01-writing-tests.md @@ -1,14 +1,30 @@ ## 编写测试 -> [ch11-01-writing-tests.md](https://github.com/rust-lang/book/blob/master/src/ch11-01-writing-tests.md) +> [ch11-01-writing-tests.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch11-01-writing-tests.md) >
        -> commit 77370c073661548dd56bbcb43cc64713585acbba +> commit 55b294f20fc846a13a9be623bf322d8b364cee77 -测试是一种使用特定功能的 Rust 函数,它用来验证非测试的代码按照期望的方式运行。我们讨论过的任何 Rust 代码规则都适用于测试!让我们看看 Rust 提供的具体用来编写测试的功能:`test`属性、一些宏和`should_panic`属性。 +测试用来验证非测试的代码按照期望的方式运行的 Rust 函数。测试函数体通常包括一些设置,运行需要测试的代码,接着断言其结果是我们所期望的。让我们看看 Rust 提供的具体用来编写测试的功能:`test`属性、一些宏和`should_panic`属性。 -### `test`属性 +### 测试函数剖析 -作为最简单例子,Rust 中的测试就是一个带有`test`属性注解的函数。让我们使用 Cargo 来创建一个新的库项目`adder`: +作为最简单例子,Rust 中的测试就是一个带有`test`属性注解的函数。属性(attribute)是关于 Rust 代码片段的元数据:第五章中结构体中用到的`derive`属性就是一个例子。为了将一个函数变成测试函数,需要在`fn`行之前加上`#[test]`。当使用`cargo test`命令运行测试函数时,Rust 会构建一个测试执行者二进制文件用来运行标记了`test`属性的函数并报告每一个测试是通过还是失败。 + + + + + +第七章当使用 Cargo 新建一个库项目时,它会自动为我们生成一个测试模块和一个测试函数。这有助于我们开始编写测试,因为这样每次开始新项目时不必去查找测试函数的具体结构和语法了。同时可以额外增加任意多的测试函数以及测试模块! + +我们将先通过对自动生成的测试模板做一些试验来探索测试如何工作的一些方面内容,而不实际测试任何代码。接着会写一些真实的测试来调用我们编写的代码并断言他们的行为是正确的。 + +让我们创建一个新的库项目`adder`: ``` $ cargo new adder @@ -16,7 +32,7 @@ $ cargo new adder $ cd adder ``` -Cargo 在创建新的库项目时自动生成一个简单的测试。这是`src/lib.rs`中的内容: +adder 库中`src/lib.rs`的内容应该看起来像这样: Filename: src/lib.rs @@ -29,16 +45,23 @@ mod tests { } ``` -现在让我们暂时忽略`tests`模块和`#[cfg(test)]`注解并只关注函数。注意它之前的`#[test]`:这个属性表明这是一个测试函数。这个函数目前没有任何内容,所以绝对是可以通过的!使用`cargo test`来运行测试: +Listing 11-1: The test module and function generated +automatically for us by `cargo new` + +现在让我们暂时忽略`tests`模块和`#[cfg(test)]`注解并只关注函数。注意`fn`行之前的`#[test]`:这个属性表明这是一个测试函数,这样测试执行者就知道将其作为测试处理。也可以在`tests`模块中拥有非测试的函数来帮助我们建立通用场景或进行常见操作,所以需要使用`#[test]`属性标明哪些函数是测试。 + +这个函数目前没有任何内容,这意味着没有代码会使测试失败;一个空的测试是可以通过的!让我们运行一下看看它是否通过了。 + +`cargo test`命令会运行项目中所有的测试,如列表 11-2 所示: ``` $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished debug [unoptimized + debuginfo] target(s) in 0.22 secs - Running target/debug/deps/adder-abcabcabc + Running target/debug/deps/adder-ce99bcc2479f4607 running 1 test -test it_works ... ok +test tests::it_works ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured @@ -49,6 +72,209 @@ running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured ``` +Listing 11-2: The output from running the one +automatically generated test + +Cargo 编译并运行了测试。在`Compiling`、`Finished`和`Running`这几行之后,可以看到`running 1 test`这一行。下一行显示了生成的测试函数的名称,它是`it_works`,以及测试的运行结果,`ok`。接着可以看到全体测试运行结果的总结:`test result: ok.`意味着所有测试都通过了。`1 passed; 0 failed`表示通过或失败的测试数量。 + +这里并没有任何被标记为忽略的测试,所以总结表明`0 ignored`。在下一部分关于运行测试的不同方式中会讨论忽略测试。`0 measured`统计是针对测试性能的性能测试的。性能测试(benchmark tests)在编写本书时,仍只属于开发版 Rust(nightly Rust)。请查看附录 D 来了解更多开发版 Rust 的信息。 + +测试输出中以`Doc-tests adder`开头的下一部分是所有文档测试的结果。现在并没有任何文档测试,不过 Rust 会编译任何出现在 API 文档中的代码示例。这个功能帮助我们使文档和代码保持同步!在第十四章的“文档注释”部分会讲到如何编写文档测试。现在我们将忽略`Doc-tests`部分的输出。 + + + + +让我们改变测试的名称并看看这如何改变测试的输出。给`it_works`函数起个不同的名字,比如`exploration`,像这样: + +Filename: src/lib.rs + +```rust +#[cfg(test)] +mod tests { + #[test] + fn exploration() { + } +} +``` + +并再次运行`cargo test`。现在输出中将出现`exploration`而不是`it_works`: + +``` +running 1 test +test tests::exploration ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured +``` + +让我们增加另一个测试,不过这一次是一个会失败的测试!当测试函数中出现 panic 时测试就失败了。第九章讲到了最简单的造成 panic 的方法:调用`panic!`宏!写入新函数后 `src/lib.rs` 现在看起来如列表 11-3 所示: + +Filename: src/lib.rs + +```rust +#[cfg(test)] +mod tests { + #[test] + fn exploration() { + } + + #[test] + fn another() { + panic!("Make this test fail"); + } +} +``` + +Listing 11-3: Adding a second test; one that will fail +since we call the `panic!` macro + + +再次`cargo test`运行测试。输出应该看起来像列表 11-4,它表明`exploration`测试通过了而`another`失败了: + + +```text +running 2 tests +test tests::exploration ... ok +test tests::another ... FAILED + +failures: + +---- tests::another stdout ---- + thread 'tests::another' panicked at 'Make this test fail', src/lib.rs:9 +note: Run with `RUST_BACKTRACE=1` for a backtrace. + +failures: + tests::another + +test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured + +error: test failed +``` + +Listing 11-4: Test results when one test passes and one +test fails + +`test tests::another`这一行是`FAILED`而不是`ok`了。在单独测试结果和总结之间多了两个新的部分:第一个部分显示了测试失败的详细原因。在这个例子中,`another`因为`panicked at 'Make this test fail'`而失败,这位于 *src/lib.rs* 的第 9 行。下一部分仅仅列出了所有失败的测试,这在很有多测试和很多失败测试的详细输出时很有帮助。可以使用失败测试的名称来只运行这个测试,这样比较方便调试;下一部分会讲到更多运行测试的方法。 + +最后是总结行:总体上讲,一个测试结果是`FAILED`的。有一个测试通过和一个测试失败。 + +现在我们见过不同场景中测试结果是什么样子的了,再来看看除了`panic!`之外一些在测试中有帮助的宏吧。 + +### 使用`assert!`宏来检查结果 + +`assert!`宏由标准库提供,在希望确保测试中一些条件为`true`时非常有用。需要向`assert!`宏提供一个计算为布尔值的参数。如果值是`true`,`assert!`什么也不做同时测试会通过。如果值为`false`,`assert!`调用`panic!`宏,这会导致测试失败。这是一个帮助我们检查代码是否以期望的方式运行的宏。 + + + + + +回忆一下第五章中,列表 5-9 中有一个`Rectangle`结构体和一个`can_hold`方法,在列表 11-5 中再次使用他们。将他们放进 *src/lib.rs* 而不是 *src/main.rs* 并使用`assert!`宏编写一些测试。 + + + +Filename: src/lib.rs + +```rust +#[derive(Debug)] +pub struct Rectangle { + length: u32, + width: u32, +} + +impl Rectangle { + pub fn can_hold(&self, other: &Rectangle) -> bool { + self.length > other.length && self.width > other.width + } +} +``` + +Listing 11-5: The `Rectangle` struct and its `can_hold` +method from Chapter 5 + +`can_hold`方法返回一个布尔值,这意味着它完美符合`assert!`宏的使用场景。在列表 11-6 中,让我们编写一个`can_hold`方法的测试来作为练习,这里创建一个长为 8 宽为 7 的`Rectangle`实例,并假设它可以放得下另一个长为5 宽为 1 的`Rectangle`实例: + +Filename: src/lib.rs + +```rust +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn larger_can_hold_smaller() { + let larger = Rectangle { length: 8, width: 7 }; + let smaller = Rectangle { length: 5, width: 1 }; + + assert!(larger.can_hold(&smaller)); + } +} +``` + +Listing 11-6: A test for `can_hold` that checks that a +larger rectangle indeed holds a smaller rectangle + +注意在`tests`模块中新增加了一行:`use super::*;`。`tests`是一个普通的模块,它遵循第七章介绍的通常的可见性规则。因为这是一个内部模块,需要将外部模块中被测试的代码引入到内部模块的作用域中。这里选择使用全局导入使得外部模块定义的所有内容在`tests`模块中都是可用的。 + +我们将测试命名为`larger_can_hold_smaller`,并创建所需的两个`Rectangle`实例。接着调用`assert!`宏并传递`larger.can_hold(&smaller)`调用的结果作为参数。这个表达式预期会返回`true`,所以测试应该通过。让我们拭目以待! + +``` +running 1 test +test tests::larger_can_hold_smaller ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured +``` + +它确实通过了!再来增加另一个测试,这一回断言一个更小的矩形不能放下一个更大的矩形: + +Filename: src/lib.rs + +```rust +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn larger_can_hold_smaller() { + let larger = Rectangle { length: 8, width: 7 }; + let smaller = Rectangle { length: 5, width: 1 }; + + assert!(larger.can_hold(&smaller)); + } + + #[test] + fn smaller_can_hold_larger() { + let larger = Rectangle { length: 8, width: 7 }; + let smaller = Rectangle { length: 5, width: 1 }; + + assert!(!smaller.can_hold(&larger)); + } +} +``` + + + + + + + + + + + + Cargo 编译并运行了测试。这里有两部分输出:本章我们将关注第一部分。第二部分是文档测试的输出,第十四章会介绍他们。现在注意看这一行: ```text