结构体
-@@ -212,7 +212,7 @@ fn area(rectangle: &Rectangle) -> u32 {ch05-00-structs.md
commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56
这里我们定义了一个结构体并称其为Rectangle
。在{}
中定义了字段length
和width
,都是u32
类型的。接着在main
中,我们创建了一个长度为 50 和宽度为 30 的Rectangle
的具体实例。
函数area
现在被定义为接收一个名叫rectangle
的参数,它的类型是一个结构体Rectangle
实例的不可变借用。第四章讲到过,我们希望借用结构体而不是获取它的所有权这样main
函数就可以保持rect1
的所有权并继续使用它,所以这就是为什么在函数签名和调用的地方会有&
。
area
函数访问Rectangle
的length
和width
字段。area
的签名现在明确的表明了我们的意图:通过其length
和width
字段,计算一个Rectangle
的面积,。这表明了长度和宽度是相互联系的,并为这些值提供了描述性的名称而不是使用元组的索引值0
和1
。这是明确性的胜利。
area
函数访问Rectangle
的length
和width
字段。area
的签名现在明确的表明了我们的意图:通过其length
和width
字段,计算一个Rectangle
的面积,。这表明了长度和宽度是相互联系的,并为这些值提供了描述性的名称而不是使用元组的索引值0
和1
。结构体胜在更清晰明了。
通过衍生 trait 增加实用功能
如果能够在调试程序时打印出Rectangle
实例来查看其所有字段的值就更好了。列表 5-5 尝试像往常一样使用println!
宏:
Filename: src/main.rs
diff --git a/docs/ch05-01-method-syntax.html b/docs/ch05-01-method-syntax.html index 3c19190..9529046 100644 --- a/docs/ch05-01-method-syntax.html +++ b/docs/ch05-01-method-syntax.html @@ -47,7 +47,7 @@方法语法
-@@ -103,7 +103,7 @@ structch05-01-method-syntax.md
commit 8c1c1a55d5c0f9bc3c866ee79b267df9dc5c04e2
为了使函数定义于Rectangle
的上下文中,我们开始了一个impl
块(impl
是 implementation 的缩写)。接着将函数移动到impl
大括号中,并将签名中的第一个(在这里也是唯一一个)参数和函数体中其他地方的对应参数改成self
。然后在main
中将我们调用area
方法并传递rect1
作为参数的地方,改成使用方法语法在Rectangle
实例上调用area
方法。方法语法获取一个实例并加上一个点号后跟方法名、括号以及任何参数。
在area
的签名中,开始使用&self
来替代rectangle: &Rectangle
,因为该方法位于impl Rectangle
上下文中所以 Rust 知道self
的类型是Rectangle
。注意仍然需要在self
前面加上&
,就像&Rectangle
一样。方法可以选择获取self
的所有权,像我们这里一样不可变的借用self
,或者可变的借用self
,就跟其他别的参数一样。
这里选择&self
跟在函数版本中使用&Rectangle
出于同样的理由:我们并不想获取所有权,只希望能够读取结构体中的数据,而不是写入。如果想要能够在方法中改变调用方法的实例的话,需要将抵押给参数改为&mut self
。通过仅仅使用self
作为第一个参数来使方法获取实例的所有权,不过这是很少见的;这通常用在当方法将self
转换成别的实例的时候,同时我们想要防止调用者在转换之后使用原始的实例。
这里选择&self
跟在函数版本中使用&Rectangle
出于同样的理由:我们并不想获取所有权,只希望能够读取结构体中的数据,而不是写入。如果想要能够在方法中改变调用方法的实例的话,需要将第一个参数改为&mut self
。通过仅仅使用self
作为第一个参数来使方法获取实例的所有权,不过这是很少见的;这通常用在当方法将self
转换成别的实例的时候,同时我们想要防止调用者在转换之后使用原始的实例。
使用方法而不是函数,除了使用了方法语法和不需要在每个函数签名中重复self
类型外,其主要好处在于组织性。我将某个类型实例能做的所有事情都一起放入impl
块中,而不是让将来的用户在我们的代码中到处寻找Rectangle
的功能。
@@ -130,7 +130,7 @@ struct p1.distance(&p2); (&p1).distance(&p2); -第一行看起来简洁的多。这种自动引用的行为之所以能行得通是因为方法有一个明确的接收者————
+self
的类型。在给出接收者和方法名的前提下,Rust 可以明确的计算出方法是仅仅读取(所以需要&self
),做出修改(所以是&mut self
)或者是获取所有权(所以是self
)。Rust 这种使得借用对方法接收者来说是隐式的做法是其所有权系统人体工程学实践的一大部分。第一行看起来简洁的多。这种自动引用的行为之所以能行得通是因为方法有一个明确的接收者————
self
的类型。在给出接收者和方法名的前提下,Rust 可以明确的计算出方法是仅仅读取(所以需要&self
),做出修改(所以是&mut self
)或者是获取所有权(所以是self
)。Rust 这种使得借用对方法接收者来说是隐式的做法是其所有权系统程序员友好性实现的一大部分。
带有更多参数的方法
diff --git a/docs/ch06-00-enums.html b/docs/ch06-00-enums.html index 0057196..524c9d6 100644 --- a/docs/ch06-00-enums.html +++ b/docs/ch06-00-enums.html @@ -47,7 +47,7 @@枚举和模式匹配
-diff --git a/docs/ch06-01-defining-an-enum.html b/docs/ch06-01-defining-an-enum.html index bfc2c27..0e19f5c 100644 --- a/docs/ch06-01-defining-an-enum.html +++ b/docs/ch06-01-defining-an-enum.html @@ -47,7 +47,7 @@ch06-00-enums.md
commit 4f2dc564851dc04b271a2260c834643dfd86c724
定义枚举
--ch06-01-defining-an-enum.md
commit e6d6caab41471f7115a621029bd428a812c5260e
让我们通过一用代码来表现的场景,来看看为什么这里枚举是有用的而且比结构体更合适。比如我们要处理 IP 地。目前被广泛使用的两个主要 IP 标准:IPv4(version four)和 IPv6(version six)。这是我们的程序只可能会遇到两种 IP 地址:我们可以枚举出所有可能的值,这也正是它名字的由来。
-任何一个 IP 地址要么是 IPv4 的要么是 IPv6 的而不能两者都是。IP 地址的这个特性使得枚举数据结构非常适合这个场景,因为枚举值尽可能是其一个成员。IPv4 和 IPv6 从根本上讲都是 IP 地址,所以当代码在处理申请任何类型的 IP 地址的场景时应该把他们当作相同的类型。
+让我们通过一用代码来表现的场景,来看看为什么这里枚举是有用的而且比结构体更合适。比如我们要处理 IP 地。目前被广泛使用的两个主要 IP 标准:IPv4(version four)和 IPv6(version six)。这是我们的程序只可能会遇到两种 IP 地址:所以可以枚举出所有可能的值,这也正是它名字的由来。
+任何一个 IP 地址要么是 IPv4 的要么是 IPv6 的而不能两者都是。IP 地址的这个特性使得枚举数据结构非常适合这个场景,因为枚举值只可能是其中一个成员。IPv4 和 IPv6 从根本上讲仍是 IP 地址,所以当代码在处理申请任何类型的 IP 地址的场景时应该把他们当作相同的类型。
可以通过在代码中定义一个IpAddrKind
枚举来表现这个概念并列出可能的 IP 地址类型,V4
和V6
。这被称为枚举的成员(variants):
enum IpAddrKind {
V4,
@@ -111,8 +111,7 @@ fn route(ip_type: IpAddrKind) { }
route(IpAddrKind::V4);
route(IpAddrKind::V6);
-使用枚举甚至还有更多优势。进一步考虑一下我们的 IP 地址类型,目前没有一个储存实际 IP 地址数据的方法;只知道它是什么类型的。考虑到已经在第五章学习过结构体了,你可以想如列表 6-1 那样修改这个问题:
- +
这里我们定义了一个有两个字段的结构体IpAddr
:kind
字段是IpAddrKind
(之前定义的枚举)类型的而address
字段是String
类型的。这里有两个结构体的实例。第一个,home
,它的kind
的值是IpAddrKind::V4
与之相关联的地址数据是127.0.0.1
。第二个实例,loopback
,kind
的值是IpAddrKind
的另一个成员,V6
,关联的地址是::1
。我们使用了要给结构体来将kind
和address
打包在一起,现在枚举成员就与值相关联了。
我们可以使用一种更简洁的方式来表达相同的概念,仅仅使用枚举并将数据直接放进每一个枚举成员而不是将枚举作为结构体的一部分。IpAddr
枚举的新定义表明了V4
和V6
成员都关联了String
值:
enum IpAddr {
@@ -160,7 +156,7 @@ let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
-这些代码展示了使用枚举来储存两种不同 IP 地址的几种可能的选择。然而,事实证明储存和编码 IP 地址实在是太常见了以致标准库提供了一个可供使用的定义!让我们看看标准库如何定义IpAddr
的:它正有着跟我们定义和使用的一样的枚举和成员,不过它将成员种的地址数据嵌入到了两个不同形式的结构体中,他们对不同的成员的定义是不同的:
这些代码展示了使用枚举来储存两种不同 IP 地址的几种可能的选择。然而,事实证明储存和编码 IP 地址实在是太常见了以致标准库提供了一个可供使用的定义!让我们看看标准库如何定义IpAddr
的:它正有着跟我们定义和使用的一样的枚举和成员,不过它将成员中的地址数据嵌入到了两个不同形式的结构体中,他们对不同的成员的定义是不同的:
struct Ipv4Addr {
// details elided
}
@@ -174,10 +170,9 @@ enum IpAddr {
V6(Ipv6Addr),
}
-这些代码展示了可以将任意类型的数据放入枚举成员中:例如字符串、数字类型或者结构体。甚至可以包含另一个枚举!另外,标准库中的类型通常并不比你可能设想出来的要复杂多少。
+这些代码展示了可以将任意类型的数据放入枚举成员中:例如字符串、数字类型或者结构体。甚至可以包含另一个枚举!另外,标准库中的类型通常并不比你设想出来的要复杂多少。
注意虽然标准库中包含一个IpAddr
的定义,仍然可以创建和使用我们自己的定义而不会有冲突,因为我们并没有将标准库中的定义引入作用域。第七章会讲到如何导入类型。
来看看列表 6-2 中的另一个枚举的例子:它的成员中内嵌了多种多样的类型:
- +
这个枚举有四个含有不同类型的成员:
Quit
没有关联任何数据。
@@ -197,7 +189,7 @@ types of values
Write
包含单独一个String
。ChangeColor
包含三个i32
。
定义一个像列表 6-2 中的枚举类似于定义不同类型的结构体,除了枚举不使用struct
关键字而且所有成员都被组合在一起位于Message
下。如下这些结构体可以包含与之前枚举成员中相同的数据:
定义一个像列表 6-2 中的枚举类似于定义不同类型的结构体,除了枚举不使用struct
关键字而且所有成员都被组合在一起位于Message
下之外。如下这些结构体可以包含与之前枚举成员中相同的数据:
struct QuitMessage; // unit struct
struct MoveMessage {
x: i32,
@@ -228,14 +220,22 @@ m.call();
让我们看看标准库中的另一个非常常见和实用的枚举:Option
。
Option
枚举和其相对空值的优势
在之前的部分,我们看到了IpAddr
枚举如何利用 Rust 的类型系统编码更多信息而不单单是程序中的数据。这一部分探索一个Option
的案例分析,它是标准库定义的另一个枚举。Option
类型应用广泛因为它编码了一个非常普遍的场景,就是一个值可能是某个值或者什么都不是。从类型系统的角度来表达这个概念就意味着编译器需要检查是否处理了所有应该处理的情况,这样就可以避免在其他编程语言中非常常见的 bug。
-编程语言的设计经常从其包含功能的角度考虑问题,但是从不包含的功能的角度思考也很重要。Rust 并没有很多其他语言中有的空值功能。空值(Null )是一个值它代表没有值。在有空值的语言中,变量总是这两种状态之一:空值和非空值。
+编程语言的设计经常从其包含功能的角度考虑问题,但是从其所没有的功能的角度思考也很重要。Rust 并没有很多其他语言中有的空值功能。空值(Null )是一个值,它代表没有值。在有空值的语言中,变量总是这两种状态之一:空值和非空值。
在“Null References: The Billion Dollar Mistake”中,Tony Hoare,null 的发明者,曾经说到:
+I call it my billion-dollar mistake. At that time, I was designing the first
+comprehensive type system for references in an object-oriented language. My
+goal was to ensure that all use of references should be absolutely safe, with
+checking performed automatically by the compiler. But I couldn't resist the
+temptation to put in a null reference, simply because it was so easy to
+implement. This has led to innumerable errors, vulnerabilities, and system
+crashes, which have probably caused a billion dollars of pain and damage in
+the last forty years.
我称之为我万亿美元的错误。当时,我在在一个面向对象语言设计第一个综合性的面向引用的类型系统。我的目标是通过编译器的自动检查来保证所有引用的应有都应该是绝对安全的。不过我未能抗拒引入一个空引用的诱惑,仅仅是因为它是这么的容易实现。这引发了无数错误、漏洞和系统崩溃,在之后的四十多年中造成了数以万计美元的苦痛和伤害。
空值的为题在于当你尝试像一个非空值那样使用一个空值,会出现某种形式的错误。因为空和非空的属性是无处不在的,非常容易出现这类错误。
然而,空值尝试表达的概念仍然是有意义的:空值是一个因为某种原因目前无效或缺失的值。
-问题不在于实际的概念而在于具体的实现。为此,Rust 并没有空值,不过它确实拥有一个可以编码存在或不存在概念的枚举。这个枚举是Option<T>
,而且它定义于标准库中,如下:
+问题不在于具体的概念而在于特定的实现。为此,Rust 并没有空值,不过它确实拥有一个可以编码存在或不存在概念的枚举。这个枚举是Option<T>
,而且它定义于标准库中,如下:
enum Option<T> {
Some(T),
None,
@@ -266,9 +266,9 @@ not satisfied
|
哇哦!事实上,错误信息意味着 Rust 不知道该如何将Option<i8>
与i8
相加。当在 Rust 中拥有一个像i8
这样类型的值时,编译器确保它总是有一个有效的值。我们可以自信使用而无需判空。只有当使用Option<i8>
(或者任何用到的类型)是需要担心可能没有一个值,而编译器会确保我们在使用值之前处理为空的情况。
-换句话说,在对Option<T>
进行T
的运算之前必须转为T
。通常这能帮助我们捕获空值最常见的问题之一:假设某值不为空但实际上为空。
-无需担心错过非空值的假设(和处理)让我们对代码更加有信心,为了拥有一个可能为空的值,必须显式的将其放入对应类型的Option<T>
中。接着,当使用这个值时,必须明确的处理值为空的情况。任何地方一个值不是Option<T>
类型的话,可以安全的假设它的值不为空。这是 Rust 的一个有意为之的设计选择,来限制空值的泛滥和增加 Rust 代码的安全性。
-那么当有一个Option<T>
的值时,如何从Some
成员中取出T
的值来使用它呢?Option<T>
枚举拥有大量用于各种情况的方法:你可以查看相关代码。熟悉Option<T>
的方法将对你的 Rust 之旅提供巨大的帮助。
+换句话说,在对Option<T>
进行T
的运算之前必须转为T
。通常这能帮助我们捕获空值最常见的问题之一:假设某值不为空但实际上为空的情况。
+无需担心错过存在非空值的假设让我们对代码更加有信心,为了拥有一个可能为空的值,必须显式的将其放入对应类型的Option<T>
中。接着,当使用这个值时,必须明确的处理值为空的情况。任何地方一个值不是Option<T>
类型的话,可以安全的假设它的值不为空。这是 Rust 的一个有意为之的设计选择,来限制空值的泛滥和增加 Rust 代码的安全性。
+那么当有一个Option<T>
的值时,如何从Some
成员中取出T
的值来使用它呢?Option<T>
枚举拥有大量用于各种情况的方法:你可以查看相关代码。熟悉Option<T>
的方法将对你的 Rust 之旅提供巨大的帮助。
总的来说,为了使用Option<T>
值,需要编写处理每个成员的代码。我们想要一些代码只当拥有Some(T)
值时运行,这些代码允许使用其中的T
。也希望一些代码当在None
值时运行,这些代码并没有一个可用的T
值。match
表达式就是这么一个处理枚举的控制流结构:它会根据枚举的成员运行不同的代码,这些代码可以使用匹配到的值中的数据。
match
控制流运算符
--ch06-02-match.md
+commit 64090418c23d615facfe49a8d548ad9baea6b097
-commit 396e2db4f7de2e5e7869b1f8bc905c45c631ad7d
Rust 有一个叫做match
的极为强大的控制流运算符,它允许我们将一个值与一系列的模式相比较并根据匹配的模式执行代码。模式可由字面值、变量、通配符和许多其他内容构成;第十八章会讲到所有不同种类的模式以及他们的作用。match
的力量来源于模式的表现力以及编译器检查,它确保了所有可能的情况都得到处理。
把match
表达式想象成某种硬币分啦机:硬币滑入有着不同大小孔洞的轨道,每一个硬币都会掉入符合它大小的孔洞。同样地,值也会检查match
的每一个模式,并且在遇到第一个“符合”的模式时,值会进入相关联的代码块并在执行中被使用。
Rust 有一个叫做match
的极为强大的控制流运算符,它允许我们将一个值与一系列的模式相比较并根据匹配的模式执行代码。模式可由字面值、变量、通配符和许多其他内容构成;第十八章会涉及到所有不同种类的模式以及他们的作用。match
的力量来源于模式的表现力以及编译器检查,它确保了所有可能的情况都得到处理。
把match
表达式想象成某种硬币分类器:硬币滑入有着不同大小孔洞的轨道,每一个硬币都会掉入符合它大小的孔洞。同样地,值也会检查match
的每一个模式,并且在遇到第一个“符合”的模式时,值会进入相关联的代码块并在执行中被使用。
因为刚刚提到了硬币,让我们用他们来作为一个使用match
的例子!我们可以编写一个函数来获取一个未知的(美国)硬币,并以一种类似验钞机的方式,确定它是何种硬币并返回它的美分值,如列表 6-3 中所示:
拆开value_in_cents
函数中的match
来看。首先,我们列出match
关键字后跟一个表达式,在这个例子中是coin
的值。这看起来非常像if
使用的表达式,不过这里有一个非常大的区别:对于if
,表达式必须返回一个布尔值。而这里它可以是任何类型的。例子中的coin
的类型是列表 6-3 中定义的Coin
枚举。
接下来是match
的分支。一个分支有两个部分:一个模式和一些代码。第一个分支的模式是值Coin::Penny
而之后的=>
运算符将模式和将要运行的代码分开。这里的代码就仅仅是值1
。每一个分支之间使用逗号分隔。
当match
表达式执行时,它将结果值按顺序与每一个分支的模式相比较,如果模式匹配了这个值,这个模式相关联的代码将被执行。如果模式并不匹配这个值,将继续执行下一个分支,非常像一个硬币分类器。可以拥有任意多的分支:列表 6-3 中的match
有四个分支。
每个分支相关联的代码是一个表达式,而表达式的结果值将作为整个match
表达式的返回值。
如果分支代码较短的话可以不适用大括号,正如列表 6-3 中的每个分支都只是返回一个值。如果想要在分支中运行多行代码,可以使用大括号。例如,如下代码在每次使用Coin::Penny
调用时都会打印出“Lucky penny!”,同时仍然返回代码块最后的值,1
:
如果分支代码较短的话通常不使用大括号,正如列表 6-3 中的每个分支都只是返回一个值。如果想要在分支中运行多行代码,可以使用大括号。例如,如下代码在每次使用Coin::Penny
调用时都会打印出“Lucky penny!”,同时仍然返回代码块最后的值,1
:
# enum Coin {
# Penny,
# Nickel,
@@ -123,8 +120,7 @@ fn value_in_cents(coin: Coin) -> i32 {
绑定值的模式
匹配分支的另一个有用的功能是可以绑定匹配的模式的部分值。这也就是如何从枚举成员中提取值。
-作为一个例子,让我们修改枚举的一个成员来存放数据。1999 年到 2008 年间,美帝在 25 美分的硬币的一侧为 50 个州每一个都印刷了不同的设计。其他的硬币都没有这种区分州的设计,所以只有这些 25 美分硬币有特殊的价值。可以将这些信息加入我们的enum
,通过改变Quarter
成员来包含一个State
值,列表 6-4 中完成了这些修改:
想象一下我们的一个朋友尝试收集所有 50 个州的 25 美分硬币。在根据硬币类型分类零钱的同时,也可以报告出每个 25 美分硬币所对应的州名称,这样如何我们的朋友没有的话,他可以把它加入收藏。
-在这些代码的匹配表达式中,我们在匹配Coin::Quarter
成员的分支的模式中增加了一个叫做state
的变量。当匹配到Coin::Quarter
时,变量state
将会绑定 25 美分硬币所对应州的值。接着在代码那个分支中使用state
,如下:
+
想象一下我们的一个朋友尝试收集所有 50 个州的 25 美分硬币。在根据硬币类型分类零钱的同时,也可以报告出每个 25 美分硬币所对应的州名称,这样如果我们的朋友没有的话,他可以把它加入收藏。
+在这些代码的匹配表达式中,我们在匹配Coin::Quarter
成员的分支的模式中增加了一个叫做state
的变量。当匹配到Coin::Quarter
时,变量state
将会绑定 25 美分硬币所对应州的值。接着在那个分支的代码中使用state
,如下:
# #[derive(Debug)]
# enum UsState {
# Alabama,
@@ -174,9 +167,8 @@ fn value_in_cents(coin: Coin) -> i32 {
如果调用value_in_cents(Coin::Quarter(UsState::Alaska))
,coin
将是Coin::Quarter(UsState::Alaska)
。当将值与每个分支相比较时,没有分支会匹配知道遇到Coin::Quarter(state)
。这时,state
绑定的将会是值UsState::Alaska
。接着就可以在println!
表达式中使用这个绑定了,像这样就可以获取Coin
枚举的Quarter
成员中内部的州的值。
匹配Option<T>
在之前的部分在使用Option<T>
时我们想要从Some
中取出其内部的T
值;也可以像处理Coin
枚举那样使用match
处理Option<T>
!与其直接比较硬币,我们将比较Option<T>
的成员,不过match
表达式的工作方式保持不变。
-比如想要编写一个函数,它获取一个Option<i32>
并且如果其中有一个值,将其加一。如果其中没有值,函数应该返回None
值并不尝试执行任何操作。
+比如我们想要编写一个函数,它获取一个Option<i32>
并且如果其中有一个值,将其加一。如果其中没有值,函数应该返回None
值并不尝试执行任何操作。
编写这个函数非常简单,得益于match
,它将看起来像列表 6-5 中这样:
-
+
匹配Some(T)
更仔细的检查plus_one
的第一行操作。当调用plus_one(five)
时,plus_one
函数体中的x
将会是值Some(5)
。接着将其与每个分支比较。
None => None,
@@ -205,7 +195,7 @@ let none = plus_one(None);
None => None,
匹配上了!这里没有值来加一,所以程序结束并返回=>
右侧的值None
,因为第一个分支就匹配到了,其他的分支将不再比较。
-将match
与枚举相结合在很多场景中都是有用的。你会在 Rust 代码中看到很多这样的模式:match
一个枚举,绑定其中的值到一个变量,接着根据其值执行代码。这在一开有点复杂,不过一旦习惯了,你将希望所有语言都拥有它!这一直是用户的最爱。
+将match
与枚举相结合在很多场景中都是有用的。你会在 Rust 代码中看到很多这样的模式:match
一个枚举,绑定其中的值到一个变量,接着根据其值执行代码。这在一开有点复杂,不过一旦习惯了,你会希望所有语言都拥有它!这一直都是用户的最爱。
匹配是穷尽的
match
还有另一方面需要讨论。考虑一下plus_one
函数的这个版本:
fn plus_one(x: Option<i32>) -> Option<i32> {
diff --git a/docs/ch06-03-if-let.html b/docs/ch06-03-if-let.html
index e0b13e6..43874eb 100644
--- a/docs/ch06-03-if-let.html
+++ b/docs/ch06-03-if-let.html
@@ -47,7 +47,7 @@
@@ -69,23 +69,19 @@
if let
简单控制流
-ch06-03-if-let.md
-commit 396e2db4f7de2e5e7869b1f8bc905c45c631ad7d
+commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56
if let
语法让我们以一种不那么冗长的方式结合if
和let
,来处理匹配一个模式的值而忽略其他的值。考虑列表 6-6 中的程序,它匹配一个Option<u8>
值并只希望当值是三时执行代码:
-
+
我们想要对Some(3)
匹配进行操作不过不想处理任何其他Some<u8>
值或None
值。为了满足match
表达式(穷尽性)的要求,必须在处理完这唯一的成员后加上_ => ()
,这样也要增加很多样板代码。
不过我们可以使用if let
这种更短的方式编写。如下代码与列表 6-6 中的match
行为一致:
# let some_u8_value = Some(0u8);
@@ -141,7 +137,7 @@ if let Coin::Quarter(state) = coin {
总结
现在我们涉及到了如何使用枚举来创建有一系列可列举值的自定义类型。我们也展示了标准库的Option<T>
类型是如何帮助你利用类型系统来避免出错。当枚举值包含数据时,你可以根据需要处理多少情况来选择使用match
或if let
来获取并使用这些值。
你的 Rust 程序现在能够使用结构体和枚举在自己的作用域内表现其内容了。在你的 API 中使用自定义类型保证了类型安全:编译器会确保你的函数只会得到它期望的类型的值。
-为了向你的用户提供一个组织良好的 API,它使用直观且只向用户暴露他们确实需要的部分,那么让我们转向 Rust 的模块系统吧。
+为了向你的用户提供一个组织良好的 API,它使用起来很直观并且只向用户暴露他们确实需要的部分,那么现在就让我们转向 Rust 的模块系统吧。
diff --git a/docs/ch07-00-modules.html b/docs/ch07-00-modules.html
index 00cac28..a49fcd9 100644
--- a/docs/ch07-00-modules.html
+++ b/docs/ch07-00-modules.html
@@ -47,7 +47,7 @@
@@ -69,9 +69,9 @@
模块
-ch07-00-modules.md
-commit e2a129961ae346f726f8b342455ec2255cdfed68
+commit 4f2dc564851dc04b271a2260c834643dfd86c724
在你刚开始编写 Rust 程序时,代码可能仅仅位于main
函数里。随着代码数量的增长,最终你会将功能移动到其他函数中,为了复用也为了更好的组织。通过将代码分隔成更小的块,每一个块代码自身就更易于理解。不过当你发现自己有太多的函数了该怎么办呢?Rust 有一个模块系统来处理编写可复用代码同时保持代码组织度的问题。
就跟你将代码行提取到一个函数中一样,也可以将函数(和其他类似结构体和枚举的代码)提取到不同模块中。模块(module)是一个包含函数或类型定义的命名空间,你可以选择这些定义是能(公有)还是不能(私有)在其模块外可见。这是一个模块如何工作的概括:
diff --git a/docs/ch07-01-mod-and-the-filesystem.html b/docs/ch07-01-mod-and-the-filesystem.html
index 9a574bb..c1e14bf 100644
--- a/docs/ch07-01-mod-and-the-filesystem.html
+++ b/docs/ch07-01-mod-and-the-filesystem.html
@@ -47,7 +47,7 @@
@@ -69,11 +69,11 @@
mod
和文件系统
-ch07-01-mod-and-the-filesystem.md
+
ch07-01-mod-and-the-filesystem.md
-commit e2a129961ae346f726f8b342455ec2255cdfed68
+commit 6fc32eabcd09f7a130094767abadb691dfcdddf7
-我们将通过使用 Cargo 创建一个新项目来开始我们的模块之旅,不过我们不再创建一个二进制 crate,而是创建一个库 crate:一个其他人可以作为依赖导入的项目。第二章我们见过的rand
就是这样的 crate。
+我们将通过使用 Cargo 创建一个新项目来开始我们的模块之旅,不过不再创建一个二进制 crate,而是创建一个库 crate:一个其他人可以作为依赖导入的项目。第二章我们见过的rand
就是这样的 crate。
我们将创建一个提供一些通用网络功能的项目的骨架结构;我们将专注于模块和函数的组织,而不担心函数体中的具体代码。这个项目叫做communicator
。Cargo 默认会创建一个库 crate 除非指定其他项目类型,所以如果不像一直以来那样加入--bin
参数则项目将会是一个库:
$ cargo new communicator
$ cd communicator
@@ -91,7 +91,7 @@ mod tests {
因为没有 src/main.rs 文件,所以没有可供 Cargo 的cargo run
执行的东西。因此,我们将使用cargo build
命令只是编译库 crate 的代码。
我们将学习根据编写代码的意图来选择不同的织库项目代码组织来适应多种场景。
模块定义
-对于communicator
网络库,首先我们要定义一个叫做network
的模块,它包含一个叫做connect
的函数定义。Rust 中所有模块的定义以关键字mod
开始。在 src/lib.rs 文件的开头在测试代码的上面增加这些代码:
+对于communicator
网络库,首先要定义一个叫做network
的模块,它包含一个叫做connect
的函数定义。Rust 中所有模块的定义以关键字mod
开始。在 src/lib.rs 文件的开头在测试代码的上面增加这些代码:
Filename: src/lib.rs
mod network {
fn connect() {
@@ -100,8 +100,7 @@ mod tests {
mod
关键字的后面是模块的名字,network
,接着是位于大括号中的代码块。代码块中的一切都位于network
命名空间中。在这个例子中,只有一个函数,connect
。如果想要在network
模块外面的代码中调用这个函数,需要指定模块名并使用命名空间语法::
,像这样:network::connect()
,而不是只是connect()
。
也可以在 src/lib.rs 文件中同时存在多个模块。例如,再拥有一个client
模块,它也有一个叫做connect
的函数,如列表 7-1 中所示那样增加这个模块:
-
-现在我们有了network::connect
函数和client::connect
函数。他们可能有着完全不同的功能,同时他们也不会彼此冲突因为他们位于不同的模块。
+
+现在我们有了network::connect
函数和client::connect
函数。他们可能有着完全不同的功能,同时他们也不会彼此冲突,因为他们位于不同的模块。
虽然在这个例子中,我们构建了一个库,但是 src/lib.rs 并没有什么特殊意义。也可以在 src/main.rs 中使用子模块。事实上,也可以将模块放入其他模块中。这有助于随着模块的增长,将相关的功能组织在一起并又保持各自独立。如何选择组织代码依赖于如何考虑代码不同部分之间的关系。例如,对于库的用户来说,client
模块和它的函数connect
可能放在network
命名空间里显得更有道理,如列表 7-2 所示:
-
+
在 src/lib.rs 文件中,将现有的mod network
和mod client
的定义替换为client
模块作为network
的一个内部模块。现在我们有了network::connect
和network::client::connect
函数:又一次,这两个connect
函数也不相冲突,因为他们在不同的命名空间中。
这样,模块之间形成了一个层次结构。src/lib.rs 的内容位于最顶层,而其子模块位于较低的层次。这是列表 7-1 中的例子以这种方式考虑的组织结构:
communicator
@@ -149,8 +142,7 @@ in src/lib.rs
可以看到列表 7-2 中,client
是network
的子模块,而不是它的同级模块。更为负责的项目可以有很多的模块,所以他们需要符合逻辑地组合在一起以便记录他们。在项目中“符合逻辑”的意义全凭你得理解和库的用户对你项目领域的认识。利用我们这里讲到的技术来创建同级模块和嵌套的模块将是你会喜欢的结构。
将模块移动到其他文件
位于层级结构中的模块,非常类似计算机领域的另一个我们非常熟悉的结构:文件系统!我们可以利用 Rust 的模块系统连同多个文件一起分解 Rust 项目,这样就不是所有的内容都落到 src/lib.rs 中了。作为例子,我们将从列表 7-3 中的代码开始:
-
+
这是模块层次结构:
communicator
├── client
└── network
└── server
+如果这些模块有很多函数,而这些函数又很长,将难以在文件中寻找我们需要的代码。因为这些函数被嵌套进一个或多个模块中,同时函数中的代码也会开始变长。这就有充分的理由将client
、network
和server
每一个模块从 src/lib.rs 抽出并放入他们自己的文件中。
+让我们开始把client
模块提取到另一个文件中。首先,将 src/lib.rs 中的client
模块代码替换为如下:
+Filename: src/lib.rs
+mod client;
+
+mod network {
+ fn connect() {
+ }
+
+ mod server {
+ fn connect() {
+ }
+ }
+}
+
这里我们仍然定义了client
模块,不过去掉了大括号和client
模块中的定义并替换为一个分号,这使得 Rust 知道去其他地方寻找模块中定义的代码。
那么现在需要创建对应模块名的外部文件。在 src/ 目录创建一个 client.rs 文件,接着打开它并输入如下内容,它是上一步client
模块中被去掉的connect
函数:
Filename: src/client.rs
fn connect() {
}
-注意这个文件中并不需要一个mod
声明;因为已经在 src/lib.rs 中已经使用mod
声明了client
模块。这个文件仅仅提供client
模块的内容。如果在这里加上一个mod client
,那么就等于给client
模块增加了一个叫做client
的子模块!
+注意这个文件中并不需要一个mod
声明;因为已经在 src/lib.rs 中已经使用mod
声明了client
模块。这个文件仅仅提供client
模块的内容。如果在这里加上一个mod client
,那么就等于给client
模块增加了一个叫做client
的子模块了!
Rust 默认只知道 src/lib.rs 中的内容。如果想要对项目加入更多文件,我们需要在 src/lib.rs 中告诉 Rust 去寻找其他文件;这就是为什么mod client
需要被定义在 src/lib.rs 而不是在 src/client.rs。
现在,一切应该能成功编译,虽然会有一些警告。记住使用cargo build
而不是cargo run
因为这是一个库 crate 而不是二进制 crate:
$ cargo build
@@ -225,7 +229,7 @@ mod server {
}
注意这个模块文件中我们也使用了一个mod
声明;这是因为我们希望server
成为network
的一个子模块。
-现在再次运行cargo build
。成功!不过我们还需要再提取出另一个模块:server
。因为这是一个子模块————也就是模块中的模块————目前的将模块提取到对应名字的文件中的策略就不管用了。如果我们仍这么尝试则会出现错误。对 src/network.rs 的第一个修改是用mod server;
替换server
模块的内容:
+现在再次运行cargo build
。成功!不过我们还需要再提取出另一个模块:server
。因为这是一个子模块——也就是模块中的模块——目前的将模块提取到对应名字的文件中的策略就不管用了。如果我们仍这么尝试则会出现错误。对 src/network.rs 的第一个修改是用mod server;
替换server
模块的内容:
Filename: src/network.rs
fn connect() {
}
@@ -238,7 +242,6 @@ mod server;
}
当尝试运行cargo build
时,会出现如列表 7-4 中所示的错误:
-
+
这个错误说明“不能在这个位置新声明一个模块”并指出 src/network.rs 中的mod server;
这一行。看来 src/network.rs 与 src/lib.rs 在某些方面是不同的;让我们继续阅读以理解这是为什么。
列表 7-4 中间的记录事实上是非常有帮助的,因为它指出了一些我们还未讲到的操作:
-note: maybe move this module `network` to its own directory via `network/mod.rs`
+note: maybe move this module `network` to its own directory via `network/mod.rs`
我们可以按照记录所建议的去操作,而不是继续使用之前的与模块同名的文件的模式:
diff --git a/docs/ch07-02-controlling-visibility-with-pub.html b/docs/ch07-02-controlling-visibility-with-pub.html
index 818f13b..6f70929 100644
--- a/docs/ch07-02-controlling-visibility-with-pub.html
+++ b/docs/ch07-02-controlling-visibility-with-pub.html
@@ -47,7 +47,7 @@
@@ -69,9 +69,9 @@
使用pub
控制可见性
-ch07-02-controlling-visibility-with-pub.md
+
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
函数没有被使用:
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 中的代码:
-
+
在尝试编译这些代码之前,猜测一下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..a35ffb4 100644
--- a/docs/ch07-03-importing-names-with-use.html
+++ b/docs/ch07-03-importing-names-with-use.html
@@ -47,7 +47,7 @@
@@ -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
函数调用。
-
+
如你所见,指定函数的完全限定名称可能会非常冗长。所幸 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..90d5a08 100644
--- a/docs/ch08-00-common-collections.html
+++ b/docs/ch08-00-common-collections.html
@@ -47,7 +47,7 @@
@@ -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
类型,现在将详细介绍它。
- 哈希 map(hash map)允许我们将值与一个特定的键(key)相关联。这是一个叫做 map 的更通用的数据结构的特定实现。
-对于标准库提供的其他类型的集合,请查看文档。
-我们将讨论如何创建和更新 vector、字符串和哈希 map,以及他们何以如此特殊。
+对于标准库提供的其他类型的集合,请查看文档。
+我们将讨论如何创建和更新 vector、字符串和哈希 map,以及他们有什么不同。
diff --git a/docs/ch08-01-vectors.html b/docs/ch08-01-vectors.html
index 45aae71..b97d598 100644
--- a/docs/ch08-01-vectors.html
+++ b/docs/ch08-01-vectors.html
@@ -47,7 +47,7 @@
@@ -69,11 +69,11 @@
vector
-ch08-01-vectors.md
-commit 0d229cc5a3da341196e15a6761735b2952281569
+commit 6c24544ba718bce0755bdaf03423af86280051d5
-我们要讲到的第一个类型是Vec<T>
,也被称为 vector。vector 允许我们在一个单独的数据结构中储存多于一个值,它在内存中彼此相邻的排列所有的值。vector 只能储存相同类型的值。他们在拥有一系列的场景下非常实用,例如文件中的文本行或是购物车中商品的价格。
+我们要讲到的第一个类型是Vec<T>
,也被称为 vector。vector 允许我们在一个单独的数据结构中储存多于一个值,它在内存中彼此相邻的排列所有的值。vector 只能储存相同类型的值。他们在拥有一系列项的场景下非常实用,例如文件中的文本行或是购物车中商品的价格。
新建 vector
为了创建一个新的,空的 vector,可以调用Vec::new
函数:
let v: Vec<i32> = Vec::new();
@@ -159,7 +159,9 @@ let row = vec![
SpreadsheetCell::Float(10.12),
];
-Rust 在编译时就必须准确的知道 vector 中类型的原因是它需要知道储存每个元素到底需要多少内存。第二个优点是可以准确的知道这个 vector 中允许什么类型。如果 Rust 允许 vector 存放任意类型,那么当对 vector 元素执行操作时一个或多个类型的值就有可能会造成错误。使用枚举外加match
意味着 Rust 能在编译时就保证总是会处理所有可能的情况,正如第六章讲到的那样。
+
+Rust 在编译时就必须准确的知道 vector 中类型的原因是它需要知道储存每个元素到底需要多少内存。第二个好处是可以准确的知道这个 vector 中允许什么类型。如果 Rust 允许 vector 存放任意类型,那么当对 vector 元素执行操作时一个或多个类型的值就有可能会造成错误。使用枚举外加match
意味着 Rust 能在编译时就保证总是会处理所有可能的情况,正如第六章讲到的那样。
如果在编写程序时不能确切无遗的知道运行时会储存进 vector 的所有类型,枚举技术就行不通了。相反,你可以使用 trait 对象,第十七章会讲到它。
现在我们了解了一些使用 vector 的最常见的方式,请一定去看看标准库中Vec
定义的很多其他实用方法的 API 文档。例如,除了push
之外还有一个pop
方法,它会移除并返回 vector 的最后一个元素。让我们继续下一个集合类型:String
!
diff --git a/docs/ch08-02-strings.html b/docs/ch08-02-strings.html
index 3717eaf..dbc8ac8 100644
--- a/docs/ch08-02-strings.html
+++ b/docs/ch08-02-strings.html
@@ -47,7 +47,7 @@
@@ -69,9 +69,9 @@
字符串
-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
参数:只能将&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
中拷贝的内容,并返回结果的所有权。换句话说,它看起来好像生成了很多拷贝不过实际上并没有:这个实现比拷贝要更高效。
如果想要级联多个字符串,+
的行为就显得笨重了:
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..02acedf 100644
--- a/docs/ch08-03-hash-maps.html
+++ b/docs/ch08-03-hash-maps.html
@@ -47,7 +47,7 @@
@@ -69,9 +69,9 @@
哈希 map
-ch08-03-hash-maps.md
-commit 0d229cc5a3da341196e15a6761735b2952281569
+commit 4f2dc564851dc04b271a2260c834643dfd86c724
最后要介绍的常用集合类型是哈希 map(hash 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..29bdedf 100644
--- a/docs/ch09-00-error-handling.html
+++ b/docs/ch09-00-error-handling.html
@@ -47,7 +47,7 @@
@@ -69,11 +69,11 @@
错误处理
-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..b5f940f 100644
--- a/docs/ch09-01-unrecoverable-errors-with-panic.html
+++ b/docs/ch09-01-unrecoverable-errors-with-panic.html
@@ -47,7 +47,7 @@
@@ -69,11 +69,11 @@
panic!
与不可恢复的错误
-ch09-01-unrecoverable-errors-with-panic.md
+
ch09-01-unrecoverable-errors-with-panic.md
-commit 380e6ee57c251f5ffa8df4c58b3949405448d914
+commit e26bb338ab14b98a850c3464e821d54940a45672
-突然有一天,糟糕的事情发生了,而你对此束手无策。对于这种情况,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,38 +121,46 @@ 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 显示了其输出:
-
+
这里有大量的输出!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..1b03969 100644
--- a/docs/ch09-02-recoverable-errors-with-result.html
+++ b/docs/ch09-02-recoverable-errors-with-result.html
@@ -47,7 +47,7 @@
@@ -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
枚举,它定义有如下连个成员,Ok
和Err
:
+大部分错误并没有严重到需要程序完全停止执行。有时,一个函数会因为一个容易理解并做出反映的原因失败。例如,如果尝试打开一个文件不过由于文件并不存在而操作就失败,这是我们可能想要创建这个文件而不是终止进程。
+回忆一下第二章“使用Result
类型来处理潜在的错误”部分中的那个Result
枚举,它定义有如下两个成员,Ok
和Err
:
enum Result<T, E> {
Ok(T),
Err(E),
@@ -82,18 +82,14 @@ commit 0c1d55ef48e5f6cf6a3b221f5b6dd4c922130bb1
T
和E
是泛型类型参数;第十章会详细介绍泛型。现在你需要知道的就是T
代表成功时返回的Ok
成员中的数据的类型,而E
代表失败时返回的Err
成员中的错误的类型。因为Result
有这些泛型类型参数,我们可以将Result
类型和标准库中为其定义的函数用于很多不同的场景,这些情况中需要返回的成功值和失败值可能会各不相同。
让我们调用一个返回Result
的函数,因为它可能会失败:如列表 9-2 所示打开一个文件:
-
+
如何知道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
表达式。
-
+
注意与Option
枚举一样,Result
枚举和其成员也被导入到了 prelude 中,所以就不需要在match
分支中的Ok
和Err
之前指定Result::
。
这里我们告诉 Rust 当结果是Ok
,返回Ok
成员中的file
值,然后将这个文件句柄赋值给变量f
。match
之后,我们可以利用这个文件句柄来进行读写。
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
增加了另一个分支:
-
+
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 的捷径:unwrap
和expect
match
能够胜任它的工作,不过它可能有点冗长并且并不总是能很好的表明意图。Result<T, E>
类型定义了很多辅助方法来处理各种情况。其中之一叫做unwrap
,它的实现就类似于列表 9-3 中的match
语句。如果Result
值是成员Ok
,unwrap
会返回Ok
中的值。如果Result
是成员Err
,unwrap
会为我们调用panic!
。
@@ -205,7 +194,6 @@ fn main() {
传播错误
当编写一个其实现会调用一些可能会失败的操作的函数时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理。这被称为传播(propagating)错误,这样能更好的控制代码调用,因为比起你代码所拥有的上下文,调用者可能拥有更多信息或逻辑来决定应该如何处理错误。
例如,列表 9-5 展示了一个从文件中读取用户名的函数。如果文件不存在或不能读取,这个函数会将这些错误返回给调用它的代码:
-
+
首先让我们看看函数的返回值:Result<String, io::Error>
。这意味着函数返回一个Result<T, E>
类型的值,其中泛型参数T
的具体类型是String
,而E
的具体类型是io::Error
。如果这个函数没有出任何错误成功返回,函数的调用者会收到一个包含String
的Ok
值————函数从文件中读取到的用户名。如果函数遇到任何错误,函数的调用者会收到一个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
并调用文件句柄f
的read_to_string
方法来将文件的内容读取到s
中。read_to_string
方法也返回一个Result
因为它也可能会失败:哪怕是File::open
已经成功了。所以我们需要另一个match
来处理这个Result
:如果read_to_string
成功了,那么这个函数就成功了,并返回文件中的用户名,它现在位于被封装进Ok
的s
中。如果read_to_string
失败了,则像之前处理File::open
的返回值的match
那样返回错误值。并不需要显式的调用return
,因为这是函数的最后一个表达式。
调用这个函数的代码最终会得到一个包含用户名的Ok
值,亦或一个包含io::Error
的Err
值。我们无从得知调用者会如何处理这些值。例如,如果他们得到了一个Err
值,他们可能会选择panic!
并使程序崩溃、使用一个默认的用户名或者从文件之外的地方寻找用户名。我们没有足够的信息知晓调用者具体会如何尝试,所以将所有的成功或失败信息向上传播,让他们选择合适处理方法。
这种传播错误的模式在 Rust 是如此的常见,以至于有一个更简便的专用语法:?
。
传播错误的捷径:?
-列表 9-6 展示了一个read_username_from_file
的实现,它实现了与列表 9-5 中的代码相同的功能,不过这个实现是使用了问号运算符:
-
+
Result
值之后的?
被定义为与列表 9-5 中定义的处理Result
值的match
表达式有着完全相同的工作方式。如果Result
的值是Ok
,这个表达式将会返回Ok
中的值而程序将继续执行。如果值是Err
,Err
中的值将作为整个函数的返回值,就好像使用了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..8a60eb1 100644
--- a/docs/ch09-03-to-panic-or-not-to-panic.html
+++ b/docs/ch09-03-to-panic-or-not-to-panic.html
@@ -47,7 +47,7 @@
@@ -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!
——在这里,有害状态是指当一些假设、保证、协议或不可变性被打破的状态,例如无效的值、自相矛盾的值或者被传递了不存在的值——外加如下几种情况:
- 有害状态并不包含预期会偶尔发生的错误
- 之后的代码的运行依赖于不再处于这种有害状态
@@ -120,7 +120,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
的实例:
-
+
首先,我们定义了一个包含u32
类型字段value
的结构体Guess
。这里是储存猜测值的地方。
接着在Guess
上实现了一个叫做new
的关联函数来创建Guess
的实例。new
定义为接收一个u32
类型的参数value
并返回一个Guess
。new
函数中代码的测试确保了其值是在 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
。
diff --git a/docs/ch10-00-generics.html b/docs/ch10-00-generics.html
index 40cf871..b715250 100644
--- a/docs/ch10-00-generics.html
+++ b/docs/ch10-00-generics.html
@@ -47,7 +47,7 @@
@@ -69,20 +69,19 @@
泛型、trait 和生命周期
-ch10-00-generics.md
-commit b335da755592f286fd97a64d98f0ca3be6a59327
+commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56
每一个编程语言都有高效的处理重复概念的工具;在 Rust 中工具之一就是泛型(generics)。泛型是具体类型或其他属性的抽象替代。我们可以表达泛型的属性,比如他们的行为或如何与其他泛型相关联,而不需要在编写和编译代码时知道他们在这里实际上代表什么。
-同理为了编写一份可以用于多种具体值的代码,函数并不知道其参数为何值,这时就可以让函数获取泛型而不是像i32
或String
这样的具体值。我们已经使用过第六章的Option<T>
,第八章的Vec<T>
和HashMap<K, V>
,以及第九章的Result<T, E>
这些泛型了。本章会探索如何使用泛型定义我们自己自己的类型、函数和方法。
+同理为了编写一份可以用于多种具体值的代码,函数并不知道其参数为何值,这时就可以让函数获取泛型而不是像i32
或String
这样的具体值。我们已经使用过第六章的Option<T>
,第八章的Vec<T>
和HashMap<K, V>
,以及第九章的Result<T, E>
这些泛型了。本章会探索如何使用泛型定义我们自己自己的类型、函数和方法!
首先,我们将回顾一下提取函数以减少代码重复的机制。接着使用一个只在参数类型上不同的泛型函数来实现相同的功能。我们也会讲到结构体和枚举定义中的泛型。
之后,我们讨论 traits,这是一个定义泛型行为的方法。trait 可以与泛型结合来将泛型限制为拥有特定行为的类型,而不是任意类型。
最后介绍生命周期(lifetimes),它是一类允许我们向编译器提供引用如何相互关联的泛型。Rust 的生命周期功能允许在很多场景下借用值同时仍然使编译器能够检查这些引用的有效性。
提取函数来减少重复
在介绍泛型语法之前,首先来回顾一个不使用泛型的处理重复的技术:提取一个函数。当熟悉了这个技术以后,我们将使用相同的机制来提取一个泛型函数!如同你识别出可以提取到函数中重复代码那样,你也会开始识别出能够使用泛型的重复代码。
考虑一下这个寻找列表中最大值的小程序,如列表 10-1 所示:
-
+
这段代码获取一个整型列表,存放在变量numbers
中。它将列表的第一项放入了变量largest
中。接着遍历了列表中的所有数字,如果当前值大于largest
中储存的值,将largest
替换为这个值。如果当前值小于目前为止的最大值,largest
保持不变。当列表中所有值都被考虑到之后,largest
将会是最大值,在这里也就是 100。
如果需要在两个不同的列表中寻找最大值,我们可以重复列表 10-1 中的代码这样程序中就会存在两段相同逻辑的代码,如列表 10-2 所示:
-
+
虽然代码能够执行,但是重复的代码是冗余且已于出错的,并且意味着当更新逻辑时需要修改多处地方的代码。
@@ -143,8 +137,7 @@ commit b335da755592f286fd97a64d98f0ca3be6a59327
为了消除重复,我们可以创建一层抽象,在这个例子中将表现为一个获取任意整型列表作为参数并对其进行处理的函数。这将增加代码的简洁性并让我们将表达和推导寻找列表中最大值的这个概念与使用这个概念的特定位置相互独。
立。
在列表 10-3 的程序中将寻找最大值的代码提取到了一个叫做largest
的函数中。这个程序可以找出两个不同数字列表的最大值,不过列表 10-1 中的代码只存在于一个位置:
-
+
这个函数有一个参数list
,它代表会传递给函数的任何具体i32
值的 slice。函数定义中的list
代表任何&[i32]
。当调用largest
函数时,其代码实际上运行于我们传递的特定值上。
从列表 10-2 到列表 10-3 中涉及的机制经历了如下几步:
diff --git a/docs/ch10-01-syntax.html b/docs/ch10-01-syntax.html
index f24de51..a02c508 100644
--- a/docs/ch10-01-syntax.html
+++ b/docs/ch10-01-syntax.html
@@ -47,7 +47,7 @@
@@ -69,7 +69,7 @@
泛型数据类型
-ch10-01-syntax.md
commit 55d9e75ffec92e922273c997026bb10613a76578
@@ -77,8 +77,7 @@ commit 55d9e75ffec92e922273c997026bb10613a76578
在函数定义中使用泛型
定义函数时可以在函数签名的参数数据类型和返回值中使用泛型。以这种方式编写的代码将更灵活并能向函数调用者提供更多功能,同时不引入重复代码。
回到largest
函数上,列表 10-4 中展示了两个提供了相同的寻找 slice 中最大值功能的函数。第一个是从列表 10-3 中提取的寻找 slice 中i32
最大值的函数。第二个函数寻找 slice 中char
的最大值:
-
-这里largest_i32
和largest_char
有着完全相同的函数体,所以能够将这两个函数变成一个来减少重复就太好了。所幸通过引入一个泛型参数就能实现。
-为了参数化要定义的函数的签名中的类型,我们需要像给函数的值参数起名那样为这类型参数起一个名字。这里选择了名称T
。任何标识符抖可以作为类型参数名,选择T
是因为 Rust 的类型命名规范是骆驼命名法(CamelCase)。另外泛型类型参数的规范也倾向于简短,经常仅仅是一个字母。T
作为“type”是大部分 Rust 程序员的首选。
+
+这里largest_i32
和largest_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
函数。注意这些代码还不能编译!
-
+
如果现在就尝试编译这些代码,会出现如下错误:
error[E0369]: binary operation `>` cannot be applied to type `T`
|
@@ -179,8 +171,7 @@ not-yet-compiling example here, which I know isn't ideal either. Let us know
what you think. /Carol -->
结构体定义中的泛型
同样也可以使用<>
语法来定义拥有一个或多个泛型参数类型字段的结构体。列表 10-6 展示了如何定义和使用一个可以存放任何类型的x
和y
坐标值的结构体Point
:
-
+
其语法类似于函数定义中的泛型应用。首先,必须在结构体名称后面的尖括号中声明泛型参数的名称。接着在结构体定义中可以指定具体数据类型的位置使用泛型类型。
注意Point
的定义中是使用了要给泛型类型,我们想要表达的是结构体Point
对于一些类型T
是泛型的,而且无论这个泛型是什么,字段x
和y
都是相同类型的。如果尝试创建一个有不同类型值的Point
的实例,像列表 10-7 中的代码就不能编译:
-
+
尝试编译会得到如下错误:
error[E0308]: mismatched types
-->
@@ -226,8 +211,7 @@ the same generic data type T
当我们将 5 赋值给x
,编译器就知道这个Point
实例的泛型类型T
是一个整型。接着我们将y
指定为 4.0,而它被定义为与x
有着相同的类型,所以出现了类型不匹配的错误。
如果想要一个x
和y
可以有不同类型且仍然是泛型的Point
结构体,我们可以使用多个泛型类型参数。在列表 10-8 中,我们修改Point
的定义为拥有两个泛型类型T
和U
。其中字段x
是T
类型的,而字段y
是U
类型的:
-
+
现在所有这些Point
实例都是被允许的了!你可以在定义中使用任意多的泛型类型参数,不过太多的话代码将难以阅读和理解。如果你处于一个需要很多泛型类型的位置,这可能是一个需要重新组织代码并分隔成一些更小部分的信号。
枚举定义中的泛型数据类型
类似于结构体,枚举也可以在其成员中存放泛型数据类型。第六章我们使用过了标准库提供的Option<T>
枚举,现在这个定义看起来就更容易理解了。让我们再看看:
@@ -263,8 +244,7 @@ values of different types
当发现代码中有多个只有存放的值的类型有所不同的结构体或枚举定义时,你就应该像之前的函数定义中那样引入泛型类型来减少重复。
方法定义中的枚举数据类型
可以像第五章介绍的那样来为其定义中带有泛型的结构体或枚举实现方法。列表 10-9 中展示了列表 10-6 中定义的结构体Point<T>
。接着我们在Point<T>
上定义了一个叫做x
的方法来返回字段x
中数据的引用:
-
+
注意必须在impl
后面声明T
,这样就可以在Point<T>
上实现的方法中使用它了。
结构体定义中的泛型类型参数并不总是与结构体方法签名中使用的泛型是同一类型。列表 10-10 中在列表 10-8 中的结构体Point<T, U>
上定义了一个方法mixup
。这个方法获取另一个Point
作为参数,而它可能与调用mixup
的self
是不同的Point
类型。这个方法用self
的Point
类型的x
值(类型T
)和参数的Point
类型的y
值(类型W
)来创建一个新Point
类型的实例:
-
+
在main
函数中,定义了一个有i32
类型的x
(其值为5
)和f64
的y
(其值为10.4
)的Point
。p2
则是一个有着字符串 slice 类型的x
(其值为"Hello"
)和char
类型的y
(其值为c
)的Point
。在p1
上以p2
调用mixup
会返回一个p3
,它会有一个i32
类型的x
,因为x
来自p1
,并拥有一个char
类型的y
,因为y
来自p2
。println!
会打印出p3.x = 5, p3.y = c
。
注意泛型参数T
和U
声明于impl
之后,因为他们于结构体定义相对应。而泛型参数V
和W
声明于fn mixup
之后,因为他们只是相对于方法本身的。
泛型代码的性能
diff --git a/docs/ch10-02-traits.html b/docs/ch10-02-traits.html
index f63db0e..219cfe8 100644
--- a/docs/ch10-02-traits.html
+++ b/docs/ch10-02-traits.html
@@ -47,7 +47,7 @@
@@ -69,35 +69,30 @@
trait:定义共享的行为
-ch10-02-traits.md
-commit 709eb1eaca48864fafd9263042f5f9d9d6ffe08d
+commit e5a987f5da3fba24e55f5c7102ec63f9dc3bc360
trait 允许我们进行另一种抽象:他们让我们可以抽象类型所通用的行为。trait 告诉 Rust 编译器某个特定类型拥有可能与其他类型共享的功能。在使用泛型类型参数的场景中,可以使用 trait bounds 在编译时指定泛型可以是任何实现了某个 trait 的类型,并由此在这个场景下拥有我们希望的功能。
注意:trait 类似于其他语言中的常被称为接口(interfaces)的功能,虽然有一些不同。
定义 trait
-一个类型的行为由其可供调用的方法构成。如果可以对不同类型调用相同的方法的话,这些类型就可以共享相同的行为了。trait 定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必须行为的集合。
+一个类型的行为由其可供调用的方法构成。如果可以对不同类型调用相同的方法的话,这些类型就可以共享相同的行为了。trait 定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必需的行为的集合。
例如,这里有多个存放了不同类型和属性文本的结构体:结构体NewsArticle
用于存放发生于世界各地的新闻故事,而结构体Tweet
最多只能存放 140 个字符的内容,以及像是否转推或是否是对推友的回复这样的元数据。
我们想要创建一个多媒体聚合库用来显示可能储存在NewsArticle
或Tweet
实例中的数据的总结。每一个结构体都需要的行为是他们是能够被总结的,这样的话就可以调用实例的summary
方法来请求总结。列表 10-11 中展示了一个表现这个概念的Summarizable
trait 的定义:
-
+
使用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 字符以内。
-
+
在类型上实现 trait 类似与实现与 trait 无关的方法。区别在于impl
关键字之后,我们提供需要实现 trait 的名称,接着是for
和需要实现 trait 的类型的名称。在impl
块中,使用 trait 定义中的方法签名,不过不再后跟分号,而是需要在大括号中编写函数体来为特定类型实现 trait 方法所拥有的行为。
一旦实现了 trait,我们就可以用与NewsArticle
和Tweet
实例的非 trait 方法一样的方式调用 trait 方法了:
let tweet = Tweet {
@@ -146,8 +138,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 和NewsArticle
与Tweet
类型,所以他们是位于同一作用域的。如果这个lib.rs
是对应aggregator
crate 的,而别人想要利用我们 crate 的功能外加为其WeatherForecast
结构体实现Summarizable
trait,在实现Summarizable
trait 之前他们首先就需要将其导入其作用域中,如列表 10-13 所示:
-
+
另外这段代码假设Summarizable
是一个公有 trait,这是因为列表 10-11 中trait
之前使用了pub
关键字。
trait 实现的一个需要注意的限制是:只能在 trait 或对应类型位于我们 crate 本地的时候为其实现 trait。换句话说,不允许对外部类型实现外部 trait。例如,不能Vec
上实现Display
trait,因为Display
和Vec
都定义于标准库中。允许在像Tweet
这样作为我们aggregator
crate 部分功能的自定义类型上实现标准库中的 trait Display
。也允许在aggregator
crate中为Vec
实现Summarizable
,因为Summarizable
定义与此。这个限制是我们称为 orphan rule 的一部分,如果你感兴趣的可以在类型理论中找到它。简单来说,它被称为 orphan rule 是因为其父类型不存在。没有这条规则的话,两个 crate 可以分别对相同类型是实现相同的 trait,因而这两个实现会相互冲突:Rust 将无从得知应该使用哪一个。因为 Rust 强制执行 orphan rule,其他人编写的代码不会破坏你代码,反之亦是如此。
默认实现
有时为 trait 中的某些或全部提供默认的行为,而不是在每个类型的每个实现中都定义自己的行为是很有用的。这样当为某个特定类型实现 trait 时,可以选择保留或重载每个方法的默认行为。
列表 10-14 中展示了如何为Summarize
trait 的summary
方法指定一个默认的字符串值,而不是像列表 10-11 中那样只是定义方法签名:
-
+
如果想要对NewsArticle
实例使用这个默认实现,而不是像列表 10-12 中那样定义一个自己的实现,则可以指定一个空的impl
块:
impl Summarizable for NewsArticle {}
@@ -283,9 +267,8 @@ error[E0507]: cannot move out of borrowed content
| cannot move out of borrowed content
错误的核心是cannot move out of type [T], a non-copy array
,对于非泛型版本的largest
函数,我们只尝试了寻找最大的i32
和char
。正如第四章讨论过的,像i32
和char
这样的类型是已知大小的并可以储存在栈上,所以他们实现了Copy
trait。当我们将largest
函数改成使用泛型后,现在list
参数的类型就有可能是没有实现Copy
trait 的,这意味着我们可能不能将list[0]
的值移动到largest
变量中。
-如果只想对实现了Copy
的类型调用这些带啊吗,可以在T
的 trait bounds 中增加Copy
!列表 10-15 中展示了一个可以编译的泛型版本的largest
函数的完整代码,只要传递给largest
的 slice 值的类型实现了PartialOrd
和Copy
这两个 trait,例如i32
和char
:
-
+
如果并不希望限制largest
函数只能用于实现了Copy
trait 的类型,我们可以在T
的 trait bounds 中指定Clone
而不是Copy
,并克隆 slice 的每一个值使得largest
函数拥有其所有权。但是使用clone
函数潜在意味着更多的堆分配,而且堆分配在涉及大量数据时可能会相当缓慢。另一种largest
的实现方式是返回 slice 中一个T
值的引用。如果我们将函数返回值从T
改为&T
并改变函数体使其能够返回一个引用,我们将不需要任何Clone
或Copy
的 trait bounds 而且也不会有任何的堆分配。尝试自己实现这种替代解决方式吧!
trait 和 trait bounds 让我们使用泛型类型参数来减少重复,并仍然能够向编译器明确指定泛型类型需要拥有哪些行为。因为我们向编译器提供了 trait bounds 信息,它就可以检查代码中所用到的具体类型是否提供了正确的行为。在动态类型语言中,如果我们尝试调用一个类型并没有实现的方法,会在运行时出现错误。Rust 将这些错误移动到了编译时,甚至在代码能够运行之前就强迫我们修复错误。另外,我们也无需编写运行时检查行为的代码,因为在编译时就已经检查过了,这样相比其他那些不愿放弃泛型灵活性的语言有更好的性能。
这里还有一种泛型,我们一直在使用它甚至都没有察觉它的存在,这就是生命周期(lifetimes)。不同于其他泛型帮助我们确保类型拥有期望的行为,生命周期则有助于确保引用在我们需要他们的时候一直有效。让我们学习生命周期是如何做到这些的。
diff --git a/docs/ch10-03-lifetime-syntax.html b/docs/ch10-03-lifetime-syntax.html
index 5c49a5b..89ec0c2 100644
--- a/docs/ch10-03-lifetime-syntax.html
+++ b/docs/ch10-03-lifetime-syntax.html
@@ -47,7 +47,7 @@
@@ -69,16 +69,15 @@
生命周期与引用有效性
-ch10-03-lifetime-syntax.md
-commit d7a4e99554da53619dd71044273535ba0186f40a
+commit c49e5ee8859f8eb8f8867cbeafbdf5b802aa5894
当在第四章讨论引用时,我们遗漏了一个重要的细节:Rust 中的每一个引用都有其生命周期,也就是引用保持有效的作用域。大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以多种不同方式向关联的情况,所以 Rust 需要我们使用泛型生命周期参数来注明他们的关系,这样就能确保运行时实际使用的引用绝对是有效的。
好吧,这有点不太寻常,而且也不同于其他语言中使用的工具。生命周期,从某种意义上说,是 Rust 最与众不同的功能。
生命周期是一个很广泛的话题,本章不可能涉及到它全部的内容,所以这里我们会讲到一些通常你可能会遇到的生命周期语法以便你熟悉这个概念。第十九章会包含生命周期所有功能的更高级的内容。
生命周期避免了悬垂引用
生命周期的主要目标是避免悬垂引用,它会导致程序引用了并非其期望引用的数据。考虑一下列表 10-16 中的程序,它有一个外部作用域和一个内部作用域,外部作用域声明了一个没有初值的变量r
,而内部作用域声明了一个初值为 5 的变量x
。在内部作用域中,我们尝试将r
的值设置为一个x
的引用。接着在内部作用域结束后,尝试打印出r
的值:
-
+
未初始化变量不能被使用
接下来的一些例子中声明了没有初始值的变量,以便这些变量存在于外部作用域。这看起来好像和 Rust 不允许存在空值相冲突。然而这是可以的,如果我们尝试在给它一个值之前使用这个变量,会出现一个编译时错误。请自行尝试!
@@ -112,7 +109,6 @@ commit d7a4e99554da53619dd71044273535ba0186f40a
变量x
并没有“存在的足够久”。为什么呢?好吧,x
在到达第 7 行的大括号的结束时就离开了作用域,这也是内部作用域的结尾。不过r
在外部作用域也是有效的;作用域越大我们就说它“存在的越久”。如果 Rust 允许这段代码工作,r
将会引用在x
离开作用域时被释放的内存,这时尝试对r
做任何操作都会不能正常工作。那么 Rust 是如何决定这段代码是不被允许的呢?
借用检查器
编译器的这一部分叫做借用检查器(borrow checker),它比较作用域来确保所有的借用都是有效的。列表 10-17 展示了与列表 10-16 相同的例子不过带有变量声明周期的注释:
-
+
@@ -140,7 +133,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 中这个并没有产生悬垂引用且可以正常编译的例子:
-
+
x
拥有生命周期 'b
,在这里它比 'a
要大。这就意味着r
可以引用x
:Rust 知道r
中的引用在x
有效的时候也会一直有效。
现在我们已经在一个具体的例子中展示了引用的声明周期位于何处,并讨论了 Rust 如何分析生命周期来保证引用总是有效的,接下来让我们聊聊在函数的上下文中参数和返回值的泛型生命周期。
函数中的泛型生命周期
让我们来编写一个返回两个字符串 slice 中最长的那一个的函数。我们希望能够通过传递两个字符串 slice 来调用这个函数,并希望返回一个字符串 slice。一旦我们实现了longest
函数,列表 10-19 中的代码应该会打印出The longest string is abcd
:
-
+
注意函数期望获取字符串 slice(如第四章所讲到的这是引用)因为我们并不希望longest
函数获取其参数的引用。我们希望函数能够接受String
的 slice(也就是变量string1
的类型)和字符串字面值(也就是变量string2
包含的值)。
参考之前第四章中的“字符串 slice 作为参数”部分中更多关于为什么上面例子中的参数正是我们想要的讨论。
如果尝试像列表 10-20 中那样实现longest
函数,它并不能编译:
-
+
将会出现如下有关生命周期的错误:
error[E0106]: missing lifetime specifier
|
@@ -230,8 +212,7 @@ longest of two string slices, but does not yet compile
生命周期注解本身没有多少意义:生命周期注解告诉 Rust 多个引用的泛型生命周期参数如何相互联系。如果函数有一个生命周期'a
的i32
的引用的参数first
,还有另一个同样是生命周期'a
的i32
的引用的参数second
,这两个生命周期注解有相同的名称意味着first
和second
必须与这相同的泛型生命周期存在得一样久。
函数签名中的生命周期注解
来看看我们编写的longest
函数的上下文中的生命周期。就像泛型类型参数,泛型生命周期参数需要声明在函数名和参数列表间的加括号中。这里我们想要告诉 Rust 关于参数中的引用和返回值之间的限制是他们都必须拥有相同的生命周期,就像列表 10-21 中在每个引用中都加上了'a
那样:
-
+
这段代码能够编译并会产生我们想要使用列表 10-19 中的main
函数得到的结果。
现在函数签名表明对于某些生命周期'a
,函数会获取两个参数,他们都是与生命周期'a
存在的一样长的字符串 slice。函数会返回一个同样也与生命周期'a
存在的一样长的字符串 slice。这就是我们告诉 Rust 需要其保证的协议。
通过在函数签名中指定生命周期参数,我们不会改变任何参数或返回值的生命周期,不过我们说过任何不坚持这个协议的类型都将被借用检查器拒绝。这个函数并不知道(或需要知道)x
和y
具体会存在多久,不过只需要知道一些可以使用'a
替代的作用域将会满足这个签名。
当在函数中使用生命周期注解时,这些注解出现在函数签名中,而不存在于函数体中的任何代码中。这是因为 Rust 能够分析函数中代码而不需要任何协助,不过当函数引用或被函数之外的代码引用时,参数或返回值的生命周期可能在每次函数被调用时都不同。这可能会产生惊人的消耗并且对于 Rust 来说经常都是不可能分析的。在这种情况下,我们需要自己标注生命周期。
当具体的引用被传递给longest
时,具体被'a
所替代的生命周期是x
的作用域与y
的作用域相重叠的那一部分。因为作用域总是嵌套的,所以换一种说法就是泛型生命周期'a
的具体生命周期等同于x
和y
的生命周期中较小的那一个。因为我们用相同的生命周期参数标注了返回的引用值,所以返回的引用值就能保证在x
和y
中较短的那个生命周期结束之前保持有效。
让我们如何通过传递拥有不同具体生命周期的引用来观察他们是如何限制longest
函数的使用的。列表 10-22 是一个应该在任何编程语言中都很直观的例子:string1
直到外部作用域结束都是有效的,string2
则在内部作用域中是有效的,而result
则引用了一些直到外部作用域结束都是有效的值。借用检查器赞同这些代码;它能够编译和运行,并打印出The longest string is long string is long
:
-
+
接下来,让我们尝试一个result
的引用的生命周期必须比两个参数的要短的例子。将result
变量的声明从内部作用域中移动出来,不过将result
和string2
变量的赋值语句一同放在内部作用域里。接下来,我们将使用result
的println!
移动到内部作用域之外,就在其结束之后。注意列表 10-23 中的代码不能编译:
-
+
如果尝试编译会出现如下错误:
error: `string2` does not live long enough
|
@@ -349,8 +312,7 @@ at 1:44...
从结果上看,生命周期语法是关于如何联系函数不同参数和返回值的生命周期的。一旦他们形成了某种联系,Rust 就有了足够的信息来允许内存安全的操作并阻止会产生悬垂指针亦或是违反内存安全的行为。
结构体定义中的生命周期注解
目前为止,我们只定义过有所有权类型的结构体。也可以定义存放引用的结构体,不过需要为结构体定义中的每一个引用添加生命周期注解。列表 10-24 中有一个存放了一个字符串 slice 的结构体ImportantExcerpt
:
-
+
这个结构体有一个字段,part
,它存放了一个字符串 slice,这是一个引用。类似于泛型参数类型,必须在结构体名称后面的尖括号中声明泛型生命周期参数,以便在结构体定义中使用生命周期参数。
这里的main
函数创建了一个ImportantExcerpt
的实例,它存放了变量novel
所拥有的String
的第一个句子的引用。
生命周期省略
在这一部分,我们知道了每一个引用都有一个生命周期,而且需要为使用了引用的函数或结构体指定生命周期。然而,第四章的“字符串 slice”部分有一个函数,我们在列表 10-25 中再次展示它,没有生命周期注解却能成功编译:
-
+
这个函数没有生命周期注解却能编译是由于一些历史原因:在早期 1.0 之前的版本的 Rust 中,这的确是不能编译的。每一个引用都必须有明确的生命周期。那时的函数签名将会写成这样:
fn first_word<'a>(s: &'a str) -> &'a str {
diff --git a/docs/ch11-00-testing.html b/docs/ch11-00-testing.html
index b86dc5b..6e46f25 100644
--- a/docs/ch11-00-testing.html
+++ b/docs/ch11-00-testing.html
@@ -47,7 +47,7 @@
@@ -69,9 +69,9 @@
测试
-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.
@@ -79,9 +79,10 @@ commit c9fd8eb1da7a79deee97020e8ad49af8ded78f9c
软件测试是证明 bug 存在的有效方法,而证明它们不存在时则显得令人绝望的不足。
Edsger W. Dijkstra,【谦卑的程序员】(1972)
-Rust 是一个非常注重正确性的编程语言,不过正确性是一个难以证明的复杂主题。Rust 在其类型系统上下了很大的功夫,来确保程序像我们希望的那样运行,不过它并不有助于所有情况。为此,Rust 也包含为语言自身编写软件测试的支持。
-例如,我们可以编写一个叫做add_two
的函数,它的签名有一个整型参数并返回一个整型值。我们可以实现并编译这个函数,而 Rust 也会进行所有的类型检查和借用检查,正如我们之前见识过的那样。Rust 所不能检查的是,我们实现的这个函数是否返回了参数值加二后的值,而不是加 10 或者减 50!这也就是测试出场的地方。例如可以编写传递3
给add_two
函数并检查我们是否得到了5
。任何时候修改了代码我们都可以运行测试来确保没有改变任何现有测试所指定的行为。
-测试是一项技能,而且我们也不能期望在一本书的一个章节中就涉及到编写好的测试的所有内容。然而我们可以讨论的是 Rust 测试功能的机制。我们会讲到编写测试时会用到的注解和宏,Rust 提供用来运行测试的默认行为和选项,以及如何将测试组织成单元测试和集成测试。
+程序的正确性意味着代码如我们期望的那样运行。Rust 是一个非常注重正确性的编程语言,不过正确性是一个难以证明的复杂主题。Rust 的类型系统在此问题上下了很大的功夫,不过它不可能捕获所有类型的错误。为此,Rust 也包含为语言自身编写软件测试的支持。
+例如,我们可以编写一个叫做add_two
的将传递给它的值加二的函数。它的签名有一个整型参数并返回一个整型值。当实现和编译这个函数时,Rust 会进行所有目前我们已经见过的的类型检查和借用检查。例如,这些检查会确保我们不会传递String
或无效的引用给这个函数。Rust 所不能检查的是这个函数是否会准确的完成我们期望的工作:返回参数加二后的值,而不是比如说参数加 10 或减 50 的值!这也就是测试出场的地方。
+我们可以编写测试断言,比如说,当传递3
给add_two
函数时,应该得到5
。当对代码进行修改时可以运行测试来确保任何现存的正确行为没有被改变。
+测试是一项复杂的技能,而且我们也不能期望在一本书的一个章节中就涉及到编写好的测试的所有内容,所以这里仅仅讨论 Rust 测试功能的机制。我们会讲到编写测试时会用到的注解和宏,Rust 提供用来运行测试的默认行为和选项,以及如何将测试组织成单元测试和集成测试。
diff --git a/docs/ch11-01-writing-tests.html b/docs/ch11-01-writing-tests.html
index 3e844ad..c5284e4 100644
--- a/docs/ch11-01-writing-tests.html
+++ b/docs/ch11-01-writing-tests.html
@@ -47,7 +47,7 @@
@@ -69,18 +69,29 @@
编写测试
-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 {
@@ -89,14 +100,18 @@ mod tests {
}
}
-现在让我们暂时忽略tests
模块和#[cfg(test)]
注解并只关注函数。注意它之前的#[test]
:这个属性表明这是一个测试函数。这个函数目前没有任何内容,所以绝对是可以通过的!使用cargo test
来运行测试:
+
+现在让我们暂时忽略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
@@ -106,133 +121,428 @@ running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
-Cargo 编译并运行了测试。这里有两部分输出:本章我们将关注第一部分。第二部分是文档测试的输出,第十四章会介绍他们。现在注意看这一行:
-test it_works ... ok
-
-it_works
文本来源于测试函数的名称。
-这里也有一行总结告诉我们所有测试的聚合结果:
-test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
-
-assert!
宏
-空的测试函数之所以能通过是因为任何没有panic!
的测试都是通过的,而任何panic!
的测试都算是失败。让我们使用`assert!宏来使测试失败:
+
+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
-#[test]
-fn it_works() {
- assert!(false);
+#[cfg(test)]
+mod tests {
+ #[test]
+ fn exploration() {
+ }
}
-assert!
宏由标准库提供,它获取一个参数,如果参数是true
,什么也不会发生。如果参数是false
,这个宏会panic!
。再次运行测试:
-$ 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
+并再次运行cargo test
。现在输出中将出现exploration
而不是it_works
:
+running 1 test
+test tests::exploration ... ok
-running 1 test
-test it_works ... FAILED
+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");
+ }
+}
+
+
+再次cargo test
运行测试。输出应该看起来像列表 11-4,它表明exploration
测试通过了而another
失败了:
+running 2 tests
+test tests::exploration ... ok
+test tests::another ... FAILED
failures:
----- it_works stdout ----
- thread 'it_works' panicked at 'assertion failed: false', src/lib.rs:5
+---- 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:
- it_works
+ tests::another
-test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
+test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
error: test failed
-Rust 表明测试失败了:
-test it_works ... FAILED
+
+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
+#[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
+ }
+}
-并展示了测试是因为src/lib.rs的第 5 行
assert!宏得到了一个
false`值而失败的:
-thread 'it_works' panicked at 'assertion failed: false', src/lib.rs: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));
+ }
+}
-失败的测试也体现在了总结行中:
-test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
+
+注意在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));
+ }
+}
+
+因为这里can_hold
函数的正确结果是false
,我们需要将这个结果取反后传递给assert!
宏。这样的话,测试就会通过而can_hold
将返回false
:
+running 2 tests
+test tests::smaller_can_hold_larger ... ok
+test tests::larger_can_hold_smaller ... ok
+
+test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured
+
+这个通过的测试!现在让我们看看如果引入一个 bug 的话测试结果会发生什么。将can_hold
方法中比较长度时本应使用大于号的地方改成小于号:
+#[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
+ }
+}
+
+现在运行测试会产生:
+running 2 tests
+test tests::smaller_can_hold_larger ... ok
+test tests::larger_can_hold_smaller ... FAILED
+
+failures:
+
+---- tests::larger_can_hold_smaller stdout ----
+ thread 'tests::larger_can_hold_smaller' panicked at 'assertion failed:
+ larger.can_hold(&smaller)', src/lib.rs:22
+note: Run with `RUST_BACKTRACE=1` for a backtrace.
+
+failures:
+ tests::larger_can_hold_smaller
+
+test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
+
+我们的测试捕获了 bug!因为larger.length
是 8 而smaller.length
是 5,can_hold
中的长度比较现在返回false
因为 8 不小于 5。
使用assert_eq!
和assert_ne!
宏来测试相等
-测试功能的一个常用方法是将需要测试代码的值与期望值做比较,并检查是否相等。可以通过向assert!
宏传递一个使用==
宏的表达式来做到。不过这个操作实在是太常见了,以至于标注库提供了一对宏来编译处理这些操作:assert_eq!
和assert_ne!
。这两个宏分别比较两个值是相等还是不相等。使用这些宏的另一个优势是当断言失败时他们会打印出这两个值具体是什么,以便于观察测试为什么失败,而assert!
只会打印出它从==
表达式中得到了false
值。
-下面是分别使用这两个宏其会测试通过的例子:
+测试功能的一个常用方法是将需要测试代码的值与期望值做比较,并检查是否相等。可以通过向assert!
宏传递一个使用==
宏的表达式来做到。不过这个操作实在是太常见了,以至于标注库提供了一对宏来方便处理这些操作:assert_eq!
和assert_ne!
。这两个宏分别比较两个值是相等还是不相等。当断言失败时他们也会打印出这两个值具体是什么,以便于观察测试为什么失败,而assert!
只会打印出它从==
表达式中得到了false
值,而不是导致false
值的原因。
+列表 11-7 中,让我们编写一个对其参数加二并返回结果的函数add_two
。接着使用assert_eq!
宏测试这个函数:
Filename: src/lib.rs
-#[test]
-fn it_works() {
- assert_eq!("Hello", "Hello");
-
- assert_ne!("Hello", "world");
-}
-
-也可以对这些宏指定可选的第三个参数,它是一个会加入错误信息的自定义文本。这两个宏展开后的逻辑看起来像这样:
-// assert_eq! - panic if the values aren't equal
-if left_val != right_val {
- panic!(
- "assertion failed: `(left == right)` (left: `{:?}`, right: `{:?}`): {}"
- left_val,
- right_val,
- optional_custom_message
- )
+pub fn add_two(a: i32) -> i32 {
+ a + 2
}
-// assert_ne! - panic if the values are equal
-if left_val == right_val {
- panic!(
- "assertion failed: `(left != right)` (left: `{:?}`, right: `{:?}`): {}"
- left_val,
- right_val,
- optional_custom_message
- )
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn it_adds_two() {
+ assert_eq!(4, add_two(2));
+ }
}
-看看这个因为hello
不等于world
而失败的测试。我们还增加了一个自定义的错误信息,greeting operation failed
:
+
+测试通过了!
+running 1 test
+test tests::it_adds_two ... ok
+
+test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
+
+传递给assert_eq!
宏的第一个参数,4,等于调用add_two(2)
的结果。我们将会看到这个测试的那一行说test tests::it_adds_two ... ok
,ok
表明测试通过了!
+在代码中引入一个 bug 来看看使用assert_eq!
的测试失败是什么样的。修改add_two
函数的实现使其加 3:
+pub fn add_two(a: i32) -> i32 {
+ a + 3
+}
+
+再次运行测试:
+running 1 test
+test tests::it_adds_two ... FAILED
+
+failures:
+
+---- tests::it_adds_two stdout ----
+ thread 'tests::it_adds_two' panicked at 'assertion failed: `(left ==
+ right)` (left: `4`, right: `5`)', src/lib.rs:11
+note: Run with `RUST_BACKTRACE=1` for a backtrace.
+
+failures:
+ tests::it_adds_two
+
+test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
+
+测试捕获到了 bug!it_adds_two
测试失败并显示信息assertion failed: `(left == right)` (left: `4`, right: `5`)
。这个信息有助于我们开始调试:它说assert_eq!
的left
参数是 4,而right
参数,也就是add_two(2)
的结果,是 5。
+注意在一些语言和测试框架中,断言两个值相等的函数的参数叫做expected
和actual
,而且指定参数的顺序是需要注意的。然而在 Rust 中,他们则叫做left
和right
,同时指定期望的值和被测试代码产生的值的顺序并不重要。这个测试中的断言也可以写成assert_eq!(add_two(2), 4)
,这时错误信息会变成assertion failed: `(left == right)` (left: `5`, right: `4`)
。
+assert_ne!
宏在传递给它的两个值不相等时通过而在相等时失败。这个宏在代码按照我们期望运行时不确定值应该是什么,不过知道他们绝对不应该是什么的时候最有用处。例如,如果一个函数确定会以某种方式改变其输出,不过这种方式由运行测试是星期几来决定,这时最好的断言可能就是函数的输出不等于其输入。
+assert_eq!
和assert_ne!
宏在底层分别使用了==
和!=
。当断言失败时,这些宏会使用调试格式打印出其参数,这意味着被比较的值必需实现了PartialEq
和Debug
trait。所有的基本类型和大部分标准库类型都实现了这些 trait。对于自定义的结构体和枚举,需要实现 PartialEq
才能断言他们的值是否相等。需要实现 Debug
才能在断言失败时打印他们的值。因为这两个 trait 都是可推导 trait,如第五章所提到的,通常可以直接在结构体或枚举上添加#[derive(PartialEq, Debug)]
注解。附录 C 中有更多关于这些和其他可推导 trait 的详细信息。
+自定义错误信息
+也可以向assert!
、assert_eq!
和assert_ne!
宏传递一个可选的参数来增加用于打印的自定义错误信息。任何在assert!
必需的一个参数和assert_eq!
和assert_ne!
必需的两个参数之后指定的参数都会传递给第八章讲到的format!
宏,所以可以传递一个包含{}
占位符的格式字符串和放入占位符的值。自定义信息有助于记录断言的意义,这样到测试失败时,就能更好的例子代码出了什么问题。
+例如,比如说有一个根据人名进行问候的函数,而我们希望测试将传递给函数的人名显示在输出中:
Filename: src/lib.rs
-#[test]
-fn a_simple_case() {
- let result = "hello"; // this value would come from running your code
- assert_eq!(result, "world", "greeting operation failed");
+pub fn greeting(name: &str) -> String {
+ format!("Hello {}!", name)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn greeting_contains_name() {
+ let result = greeting("Carol");
+ assert!(result.contains("Carol"));
+ }
}
-毫无疑问运行这个测试会失败,而错误信息解释了为什么测试失败了并且带有我们的指定的自定义错误信息:
----- a_simple_case stdout ----
- thread 'a_simple_case' panicked at 'assertion failed: `(left == right)`
- (left: `"hello"`, right: `"world"`): greeting operation failed',
- src/main.rs:4
-
-assert_eq!
的两个参数被称为 "left" 和 "right" ,而不是 "expected" 和 "actual" ;值的顺序和硬编码的值并没有什么影响。
-因为这些宏使用了==
和!=
运算符并使用调试格式打印这些值,进行比较的值必须实现PartialEq
和Debug
trait。Rust 提供的类型实现了这些 trait,不过自定义的结构体和枚举则需要自己实现PartialEq
以便能够断言这些值是否相等,和实现Debug
以便在断言失败时打印出这些值。因为第五章提到过这两个 trait 都是 derivable trait,所以通常可以直接在结构体或枚举上加上#[derive(PartialEq, Debug)]
注解。查看附录 C 来寻找更多关于这些和其他 derivable trait 的信息。
-使用should_panic
测试期望的失败
-可以使用另一个属性来反转测试中的失败:should_panic
。这在测试调用特定的函数会产生错误的函数时很有帮助。例如,让我们测试第八章中的一些我们知道会 panic 的代码:尝试使用 range 语法和并不组成完整字母的字节索引来创建一个字符串 slice。在有#[test]
属性的函数之前增加#[should_panic]
属性,如列表 11-1 所示:
-
-这个测试是成功的,因为我们表示代码应该会 panic。相反如果代码因为某种原因没有产生panic!
则测试会失败。
-使用should_panic
的测试是脆弱的,因为难以保证测试不会因为一个不同于我们期望的原因失败。为了帮助解决这个问题,should_panic
属性可以增加一个可选的expected
参数。测试工具会确保错误信息里包含我们提供的文本。一个比列表 11-1 更健壮的版本如列表 11-2 所示:
-
-请自行尝试当should_panic
的测试出现 panic 但并不符合期望的信息时会发生什么:在测试中因为不同原因造成panic!
,或者将期望的 panic 信息改为并不与字母字节边界 panic 信息相匹配。
+现在如果再次运行测试,将会看到更有价值的错误信息:
+---- tests::greeting_contains_name stdout ----
+ thread 'tests::greeting_contains_name' panicked at 'Result did not contain
+ name, value was `Hello`', src/lib.rs:12
+note: Run with `RUST_BACKTRACE=1` for a backtrace.
+
+可以在测试输出中看到所取得的确切的值,这会帮助我们理解发生了什么而不是期望发生什么。
+使用should_panic
检查 panic
+除了检查代码是否返回期望的正确的值之外,检查代码是否按照期望处理错误情况也是很重要的。例如,考虑第九章列表 9-8 创建的Guess
类型。其他使用Guess
的代码依赖于Guess
实例只会包含 1 到 100 的值的保证。可以编写一个测试来确保创建一个超出范围的值的Guess
实例会 panic。
+可以通过对函数增加另一个属性should_panic
来实现这些。这个属性在函数中的代码 panic 时会通过,而在其中的代码没有 panic 时失败。
+列表 11-8 展示了如何编写一个测试来检查Guess::new
按照我们的期望出现的错误情况:
+Filename: src/lib.rs
+struct Guess {
+ value: u32,
+}
+
+impl Guess {
+ pub fn new(value: u32) -> Guess {
+ if value < 1 || value > 100 {
+ panic!("Guess value must be between 1 and 100, got {}.", value);
+ }
+
+ Guess {
+ value: value,
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ #[should_panic]
+ fn greater_than_100() {
+ Guess::new(200);
+ }
+}
+
+
+#[should_panic]
属性位于#[test]
之后和对应的测试函数之前。让我们看看测试通过时它时什么样子:
+running 1 test
+test tests::greater_than_100 ... ok
+
+test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
+
+看起来不错!现在在代码中引入 bug,通过移除new
函数在值大于 100 时会 panic 的条件:
+# struct Guess {
+# value: u32,
+# }
+#
+impl Guess {
+ pub fn new(value: u32) -> Guess {
+ if value < 1 {
+ panic!("Guess value must be between 1 and 100, got {}.", value);
+ }
+
+ Guess {
+ value: value,
+ }
+ }
+}
+
+如果运行列表 11-8 的测试,它会失败:
+running 1 test
+test tests::greater_than_100 ... FAILED
+
+failures:
+
+failures:
+ tests::greater_than_100
+
+test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
+
+这回并没有得到非常有用的信息,不过一旦我们观察测试函数,会发现它标注了#[should_panic]
。这个错误意味着代码中函数Guess::new(200)
并没有产生 panic。
+然而should_panic
测试可能是非常含糊不清的,因为他们只是告诉我们代码并没有产生 panic。should_panic
甚至在测试因为其他不同的原因而不是我们期望发生的那个而 panic 时也会通过。为了使should_panic
测试更精确,可以给should_panic
属性增加一个可选的expected
参数。测试工具会确保错误信息中包含其提供的文本。例如,考虑列表 11-9 中修改过的Guess
,这里new
函数更具其值是过大还或者过小而提供不同的 panic 信息:
+Filename: src/lib.rs
+struct Guess {
+ value: u32,
+}
+
+impl Guess {
+ pub fn new(value: u32) -> Guess {
+ if value < 1 {
+ panic!("Guess value must be greater than or equal to 1, got {}.",
+ value);
+ } else if value > 100 {
+ panic!("Guess value must be less than or equal to 100, got {}.",
+ value);
+ }
+
+ Guess {
+ value: value,
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ #[should_panic(expected = "Guess value must be less than or equal to 100")]
+ fn greater_than_100() {
+ Guess::new(200);
+ }
+}
+
+
+这个测试会通过,因为should_panic
属性中expected
参数提供的值是Guess::new
函数 panic 信息的子字符串。我们可以指定期望的整个 panic 信息,在这个例子中是Guess value must be less than or equal to 100, got 200.
。这依赖于 panic 有多独特或动态和你希望测试有多准确。在这个例子中,错误信息的子字符串足以确保函数在else if value > 100
的情况下运行。
+为了观察带有expected
信息的should_panic
测试失败时会发生什么,让我们再次引入一个 bug 来将if value < 1
和else if value > 100
的代码块对换:
+if value < 1 {
+ panic!("Guess value must be less than or equal to 100, got {}.", value);
+} else if value > 100 {
+ panic!("Guess value must be greater than or equal to 1, got {}.", value);
+}
+
+这一次运行should_panic
测试,它会失败:
+running 1 test
+test tests::greater_than_100 ... FAILED
+
+failures:
+
+---- tests::greater_than_100 stdout ----
+ thread 'tests::greater_than_100' panicked at 'Guess value must be greater
+ than or equal to 1, got 200.', src/lib.rs:10
+note: Run with `RUST_BACKTRACE=1` for a backtrace.
+note: Panic did not include expected string 'Guess value must be less than or
+equal to 100'
+
+failures:
+ tests::greater_than_100
+
+test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
+
+错误信息表明测试确实如期望 panic 了,不过 panic 信息did not include expected string 'Guess value must be less than or equal to 100'
。可以看到我们的到的 panic 信息,在这个例子中是Guess value must be greater than or equal to 1, got 200.
。这样就可以开始寻找 bug 在哪了!
+现在我们讲完了编写测试的方法,让我们看看运行测试时会发生什么并讨论可以用于cargo test
的不同选项。
diff --git a/docs/ch11-02-running-tests.html b/docs/ch11-02-running-tests.html
index 7ec57f9..e4edde1 100644
--- a/docs/ch11-02-running-tests.html
+++ b/docs/ch11-02-running-tests.html
@@ -47,7 +47,7 @@
@@ -69,134 +69,161 @@
运行测试
-ch11-02-running-tests.md
-commit cf52d81371e24e14ce31a5582bfcb8c5b80d26cc
+commit 55b294f20fc846a13a9be623bf322d8b364cee77
-类似于cargo run
会编译代码并运行生成的二进制文件,cargo test
在测试模式下编译代码并运行生成的测试二进制文件。cargo test
生成的二进制文件默认会并行的运行所有测试并在测试过程中捕获生成的输出,这样就更容易阅读测试结果的输出。
-可以通过指定命令行选项来改变这些运行测试的默认行为。这些选项的一部分可以传递给cargo test
,而另一些则需要传递给生成的测试二进制文件。分隔这些参数的方法是--
:cargo test
之后列出了传递给cargo test
的参数,接着是分隔符--
,之后是传递给测试二进制文件的参数。
-并行运行测试
-测试使用线程来并行运行。为此,编写测试时需要注意测试之间不要相互依赖或者存在任何共享状态。共享状态也可能包含在运行环境中,比如当前工作目录或者环境变量。
-如果你不希望它这样运行,或者想要更加精确的控制使用线程的数量,可以传递--test-threads
参数和线程的数量给测试二进制文件。将线程数设置为 1 意味着没有任何并行操作:
+就像cargo run
会编译代码并运行生成的二进制文件,cargo test
在测试模式下编译代码并运行生成的测试二进制文件。这里有一些选项可以用来改变cargo test
的默认行为。例如,cargo test
生成的二进制文件的默认行为是并行的运行所有测试,并捕获测试运行过程中产生的输出避免他们被显示出来使得阅读测试结果相关的内容变得更容易。可以指定命令行参数来改变这些默认行为。
+这些选项的一部分可以传递给cargo test
,而另一些则需要传递给生成的测试二进制文件。为了分隔两种类型的参数,首先列出传递给cargo test
的参数,接着是分隔符--
,再之后是传递给测试二进制文件的参数。运行cargo test --help
会告诉你cargo test
的相关参数,而运行cargo test -- --help
则会告诉你位于分隔符--
之后的相关参数。
+并行或连续的运行测试
+
+
+当运行多个测试时,他们默认使用线程来并行的运行。这意味着测试会更快的运行完毕,所以可以更快的得到代码能否工作的反馈。因为测试是在同时运行的,你应该小心测试不能相互依赖或任何共享状态,包括类似于当前工作目录或者环境变量这样的共享环境。
+例如,每一个测试都运行一些代码在硬盘上创建一个test-output.txt
文件并写入一些数据。接着每一个测试都读取文件中的数据并断言这个文件包含特定的值,而这个值在每个测试中都是不同的。因为所有测试都是同时运行的,一个测试可能会在另一个测试读写文件过程中覆盖了文件。那么第二个测试就会失败,并不是因为代码不正确,而是因为测试并行运行时相互干涉。一个解决方案是使每一个测试读写不同的文件;另一个是一次运行一个测试。
+如果你不希望测试并行运行,或者想要更加精确的控制使用线程的数量,可以传递--test-threads
参数和希望使用线程的数量给测试二进制文件。例如:
$ cargo test -- --test-threads=1
-捕获测试输出
-Rust 的测试库默认捕获并丢弃标准输出和标准错误中的输出,除非测试失败了。例如,如果在测试中调用了println!
而测试通过了,你将不会在终端看到println!
的输出。这个行为可以通过向测试二进制文件传递--nocapture
参数来禁用:
+这里将测试线程设置为 1,告诉程序不要使用任何并行机制。这也会比并行运行花费更多时间,不过测试就不会在存在共享状态时潜在的相互干涉了。
+显示测试输出
+如果测试通过了,Rust 的测试库默认会捕获打印到标准输出的任何内容。例如,如果在测试中调用println!
而测试通过了,我们将不会在终端看到println!
的输出:只会看到说明测试通过的行。如果测试失败了,就会看到任何标准输出和其他错误信息。
+例如,列表 11-20 有一个无意义的函数它打印出其参数的值并接着返回 10。接着还有一个会通过的测试和一个会失败的测试:
+Filename: src/lib.rs
+fn prints_and_returns_10(a: i32) -> i32 {
+ println!("I got the value {}", a);
+ 10
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn this_test_will_pass() {
+ let value = prints_and_returns_10(4);
+ assert_eq!(10, value);
+ }
+
+ #[test]
+ fn this_test_will_fail() {
+ let value = prints_and_returns_10(8);
+ assert_eq!(5, value);
+ }
+}
+
+
+运行cargo test
将会看到这些测试的输出:
+running 2 tests
+test tests::this_test_will_pass ... ok
+test tests::this_test_will_fail ... FAILED
+
+failures:
+
+---- tests::this_test_will_fail stdout ----
+ I got the value 8
+thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left ==
+right)` (left: `5`, right: `10`)', src/lib.rs:19
+note: Run with `RUST_BACKTRACE=1` for a backtrace.
+
+failures:
+ tests::this_test_will_fail
+
+test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
+
+注意输出中哪里也不会出现I got the value 4
,这是当测试通过时打印的内容。这些输出被捕获。失败测试的输出,I got the value 8
,则出现在输出的测试总结部分,它也显示了测试失败的原因。
+如果你希望也能看到通过的测试中打印的值,捕获输出的行为可以通过--nocapture
参数来禁用:
$ cargo test -- --nocapture
+使用--nocapture
参数再次运行列表 11-10 中的测试会显示:
+running 2 tests
+I got the value 4
+I got the value 8
+test tests::this_test_will_pass ... ok
+thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left ==
+right)` (left: `5`, right: `10`)', src/lib.rs:19
+note: Run with `RUST_BACKTRACE=1` for a backtrace.
+test tests::this_test_will_fail ... FAILED
+
+failures:
+
+failures:
+ tests::this_test_will_fail
+
+test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
+
+注意测试的输出和测试结果的输出是相互交叉的;这是由于上一部分讲到的测试是并行运行的。尝试一同使用--test-threads=1
和--nocapture
功能来看看输出是什么样子!
通过名称来运行测试的子集
-有时运行整个测试集会耗费很多时间。如果你负责特定位置的代码,你可能会希望只与这些代码相关的测试。cargo test
有一个参数允许你通过指定名称来运行特定的测试。
-列表 11-3 中创建了三个如下名称的测试:
-
-使用不同的参数会运行不同的测试子集。没有参数的话,如你所见会运行所有的测试:
-$ cargo test
- Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
- Running target/debug/deps/adder-abcabcabc
-
-running 3 tests
-test add_three_and_two ... ok
-test one_hundred ... ok
-test add_two_and_two ... ok
-
-test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured
-
-可以传递任意测试的名称来只运行那个测试:
-$ cargo test one_hundred
- Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
- Running target/debug/deps/adder-abcabcabc
-
-running 1 test
-test one_hundred ... ok
-
-test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
-
-也可以传递名称的一部分,cargo test
会运行所有匹配的测试:
-$ cargo test add
- Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
- Running target/debug/deps/adder-abcabcabc
-
-running 2 tests
-test add_three_and_two ... ok
-test add_two_and_two ... ok
-
-test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured
-
-模块名也作为测试名的一部分,所以类似的模块名也可以用来指定测试特定模块。例如,如果将我们的代码组织成一个叫adding
的模块和一个叫subtracting
的模块并分别带有测试,如列表 11-4 所示:
-
-执行cargo test
会运行所有的测试,而模块名会出现在输出的测试名中:
-$ cargo test
- Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
- Running target/debug/deps/adder-abcabcabc
-
-running 4 tests
-test adding::add_two_and_two ... ok
-test adding::add_three_and_two ... ok
-test subtracting::subtract_three_and_two ... ok
-test adding::one_hundred ... ok
-
-运行cargo test adding
将只会运行对应模块的测试而不会运行任何 subtracting 模块中的测试:
-$ cargo test adding
- Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
- Running target/debug/deps/adder-abcabcabc
-
-running 3 tests
-test adding::add_three_and_two ... ok
-test adding::one_hundred ... ok
-test adding::add_two_and_two ... ok
+
+如果没有传递任何参数就运行测试,如你所见,所有测试都会并行运行:
+running 3 tests
+test tests::add_two_and_two ... ok
+test tests::add_three_and_two ... ok
+test tests::one_hundred ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured
+运行单个测试
+可以向cargo test
传递任意测试的名称来只运行这个测试:
+$ cargo test one_hundred
+ Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
+ Running target/debug/deps/adder-06a75b4a1f2515e9
+
+running 1 test
+test tests::one_hundred ... ok
+
+test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
+
+不能像这样指定多个测试名称,只有传递给cargo test
的第一个值才会被使用。
+过滤运行多个测试
+然而,可以指定测试的部分名称,这样任何名称匹配这个值的测试会被运行。例如,因为头两个测试的名称包含add
,可以通过cargo test add
来运行这两个测试:
+$ cargo test add
+ Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
+ Running target/debug/deps/adder-06a75b4a1f2515e9
+
+running 2 tests
+test tests::add_two_and_two ... ok
+test tests::add_three_and_two ... ok
+
+test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured
+
+这运行了所有名字中带有add
的测试。同时注意测试所在的模块作为测试名称的一部分,所以可以通过模块名来过滤运行一个模块中的所有测试。
+
+
除非指定否则忽略某些测试
-有时一些特定的测试执行起来是非常耗费时间的,所以对于大多数cargo test
命令,我们希望能排除它。无需为cargo test
创建一个用来在运行所有测试时排除特定测试的参数并每次都要记得使用它,我们可以对这些测试使用ignore
属性:
+有时一些特定的测试执行起来是非常耗费时间的,所以在运行大多数cargo test
的时候希望能排除他们。与其通过参数列举出所有希望运行的测试,也可以使用ignore
属性来标记耗时的测试来排除他们:
Filename: src/lib.rs
#[test]
fn it_works() {
@@ -209,11 +236,11 @@ fn expensive_test() {
// code that takes an hour to run
}
-现在运行测试,将会发现it_works
运行了,而expensive_test
没有:
+我们对想要排除的测试的#[test]
之后增加了#[ignore]
行。现在如果运行测试,就会发现it_works
运行了,而expensive_test
没有运行:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished debug [unoptimized + debuginfo] target(s) in 0.24 secs
- Running target/debug/deps/adder-abcabcabc
+ Running target/debug/deps/adder-ce99bcc2479f4607
running 2 tests
test expensive_test ... ignored
@@ -227,17 +254,26 @@ running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
-我们可以通过cargo test -- --ignored
来明确请求只运行那些耗时的测试:
-$ cargo test -- --ignored
+expensive_test
被列为ignored
,如果只希望运行被忽略的测试,可以使用cargo test -- --ignored
来请求运行他们:
+
+
+
+
+$ cargo test -- --ignored
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
- Running target/debug/deps/adder-abcabcabc
+ Running target/debug/deps/adder-ce99bcc2479f4607
running 1 test
test expensive_test ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
-通过这种方式,大部分时间运行cargo test
将是快速的。当需要检查ignored
测试的结果而且你也有时间等待这个结果的话,可以选择执行cargo test -- --ignored
。
+通过控制运行哪些测试,可以确保运行cargo test
的结果是快速的。当某个时刻需要检查ignored
测试的结果而且你也有时间等待这个结果的话,可以选择执行cargo test -- --ignored
。
diff --git a/docs/ch11-03-test-organization.html b/docs/ch11-03-test-organization.html
index 572be63..25265b7 100644
--- a/docs/ch11-03-test-organization.html
+++ b/docs/ch11-03-test-organization.html
@@ -47,7 +47,7 @@
@@ -69,16 +69,17 @@
测试的组织结构
-ch11-03-test-organization.md
+
ch11-03-test-organization.md
-commit cf52d81371e24e14ce31a5582bfcb8c5b80d26cc
+commit 55b294f20fc846a13a9be623bf322d8b364cee77
-正如之前提到的,测试是一个很广泛的学科,而且不同的人有时也采用不同的技术和组织。Rust 社区倾向于根据测试的两个主要分类来考虑问题:单元测试(unit tests)与集成测试(unit tests)。单元测试倾向于更小而更专注,在隔离的环境中一次测试一个模块。他们也可以测试私有接口。集成测试对于你的库来说则完全是外部的。他们与其他用户使用相同的方式使用你得代码,他们只针对共有接口而且每个测试会测试多个模块。这两类测试对于从独立和整体的角度保证你的库符合期望是非常重要的。
+正如之前提到的,测试是一个很广泛的学科,而且不同的开发者也采用不同的技术和组织。Rust 社区倾向于根据测试的两个主要分类来考虑问题:单元测试(unit tests)与集成测试(unit tests)。单元测试倾向于更小而更专注,在隔离的环境中一次测试一个模块。他们也可以测试私有接口。集成测试对于你的库来说则完全是外部的。他们与其他用户使用相同的方式使用你的代码,他们只针对共有接口而且每个测试都会测试多个模块。
+这两类测试对于从独立和整体的角度保证你的库符合期望是非常重要的。
单元测试
-单元测试的目的是在隔离与其他部分的环境中测试每一个单元的代码,以便于快速而准确的定位何处的代码是否符合预期。单元测试位于 src 目录中,与他们要测试的代码存在于相同的文件中。他们被分离进每个文件中他们自有的tests
模块中。
+单元测试的目的是在与其他部分隔离的环境中测试每一个单元的代码,以便于快速而准确的定位何处的代码是否符合预期。单元测试位于 src 目录中,与他们要测试的代码存在于相同的文件中。传统做法是在每个文件中创建包含测试函数的tests
模块,并使用cfg(test)
标注模块。
测试模块和cfg(test)
-通过将测试放进他们自己的模块并对该模块使用cfg
注解,我们可以告诉 Rust 只在执行cargo test
时才编译和运行测试代码。这在当我们只希望用cargo build
编译库代码时可以节省编译时间,并减少编译产物的大小因为并没有包含测试。
-还记得上一部分新建的adder
项目吗?Cargo 为我们生成了如下代码:
+测试模块的#[cfg(test)]
注解告诉 Rust 只在执行cargo test
时才编译和运行测试代码,而在运行cargo build
时不这么做。这在只希望构建库的时候可以节省编译时间,并能节省编译产物的空间因为他们并没有包含测试。我们将会看到因为集成测试位于另一个文件夹,他们并不需要#[cfg(test)]
注解。但是因为单元测试位于与源码相同的文件中,所以使用#[cfg(test)]
来指定他们不应该被包含进编译产物中。
+还记得本章第一部分新建的adder
项目吗?Cargo 为我们生成了如下代码:
Filename: src/lib.rs
#[cfg(test)]
mod tests {
@@ -87,48 +88,10 @@ mod tests {
}
}
-我们忽略了模块相关的信息以便更关注模块中测试代码的机制,不过现在让我们看看测试周围的代码。
-首先,这里有一个属性cfg
。cfg
属性让我们声明一些内容只在给定特定的配置(configuration)时才被包含进来。Rust 提供了test
配置用来编译和运行测试。通过这个属性,Cargo 只会在尝试运行测试时才编译测试代码。
-接下来,tests
包含了所有测试函数,而我们的代码则位于tests
模块之外。tests
模块的名称是一个惯例,除此之外这是一个遵守第七章讲到的常见可见性规则的普通模块。因为这是一个内部模块,我们需要将要测试的代码引入作用域。这对于一个大的模块来说是很烦人的,所以这里经常使用全局导入。
-从本章到现在,我们一直在为adder
项目编写并没有实际调用任何代码的测试。现在让我们做一些改变!在 src/lib.rs 中,放入add_two
函数和带有一个检验代码的测试的tests
模块,如列表 11-5 所示:
-
-注意除了测试函数之外,我们还在tests
模块中添加了use add_two;
。这将我们想要测试的代码引入到了内部的tests
模块的作用域中,正如任何内部模块需要做的那样。如果现在使用cargo test
运行测试,它会通过:
-running 1 test
-test tests::it_works ... ok
-
-test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
-
-如果我们忘记将add_two
函数引入作用域,将会得到一个 unresolved name 错误,因为tests
模块并不知道任何关于add_two
函数的信息:
-error[E0425]: unresolved name `add_two`
- --> src/lib.rs:9:23
- |
-9 | assert_eq!(4, add_two(2));
- | ^^^^^^^ unresolved name
-
-如果这个模块包含很多希望测试的代码,在测试中列出每一个use
语句将是很烦人的。相反在测试子模块中使用use super::*;
来一次将所有内容导入作用域中是很常见的。
+这里自动生成了测试模块。cfg
属性代表 configuration ,它告诉 Rust 其之后的项只被包含进特定配置中。在这个例子中,配置是test
,Rust 所提供的用于编译和运行测试的配置。通过使用这个属性,Cargo 只会在我们主动使用cargo test
运行测试时才编译测试代码。除了标注为#[test]
的函数之外,这还包括测试模块中可能存在的帮助函数。
测试私有函数
-测试社区中一直存在关于是否应该对私有函数进行单元测试的论战。不过无论你坚持哪种测试意识形态,Rust 确实允许你测试私有函数,由于私有性规则。考虑列表 11-6 中带有私有函数internal_adder
的代码:
-
-因为测试也不过是 Rust 代码而tests
也只是另一个模块,我们完全可以在一个测试中导入并调用internal_adder
。如果你并不认为私有函数应该被测试,Rust 也不会强迫你这么做。
+
+
+
+注意internal_adder
函数并没有标记为pub
,不过因为测试也不过是 Rust 代码而tests
也仅仅是另一个模块,我们完全可以在测试中导入和调用internal_adder
。如果你并不认为私有函数应该被测试,Rust 也不会强迫你这么做。
集成测试
-在 Rust 中,集成测试对于需要测试的库来说是完全独立。他们同其他代码一样使用库文件。他们的目的是测试库的个个部分结合起来能否正常工作。每个能正确运行的代码单元集成在一起也可能会出现问题,所以集成测试的覆盖率也是很重要的。
+在 Rust 中,集成测试对于需要测试的库来说是完全独立。他们同其他代码一样使用库文件,这意味着他们只能调用作为库公有 API 的一部分的函数。他们的目的是测试库的多个部分能否一起正常工作。每个能单独正确运行的代码单元集成在一起也可能会出现问题,所以集成测试的覆盖率也是很重要的。为了创建集成测试,首先需要一个 tests 目录。
tests 目录
-Cargo 支持位于 tests 目录中的集成测试。如果创建它并放入 Rust 源文件,Cargo 会将每一个文件当作单独的 crate 来编译。让我们试一试!
-首先,在项目根目录创建一个 tests 目录,挨着 src 目录。接着新建一个文件 tests/integration_test.rs,并写入列表 11-7 中的代码:
-
-在开头使用了extern crate adder
,单元测试中并不需要它。tests
目录中的每一个测试文件都是完全独立的 crate,所以需要在每个文件中导入我们的库。这也就是为何tests
是编写集成测试的绝佳场所:他们像任何其他用户那样,需要将库导入 crate 并只能使用公有 API。
-这个文件中也不需要tests
模块。除非运行测试否则整个文件夹都不会被编译,所以无需将任何部分标记为#[cfg(test)]
。另外每个测试文件都被隔离进其自己的 crate 中,无需进一步隔离测试代码。
-让我们运行集成测试,同样使用cargo test
来运行:
-$ cargo test
+
+我们在顶部增加了extern crate adder
,这在单元测试中是不需要的。这是因为每一个tests
目录中的测试文件都是完全独立的 crate,所以需要在每一个文件中导入库。集成测试就像其他使用者那样通过导入 crate 并只使用公有 API 来使用库文件。
+并不需要将 tests/integration_test.rs 中的任何代码标注为#[cfg(test)]
。Cargo 对tests
文件夹特殊处理并只会在运行cargo test
时编译这个目录中的文件。现在就试试运行cargo test
:
+cargo test
Compiling adder v0.1.0 (file:///projects/adder)
+ Finished debug [unoptimized + debuginfo] target(s) in 0.31 secs
Running target/debug/deps/adder-abcabcabc
running 1 test
-test tests::it_works ... ok
+test tests::internal ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
- Running target/debug/integration_test-952a27e0126bb565
+ Running target/debug/deps/integration_test-ce99bcc2479f4607
running 1 test
test it_adds_two ... ok
@@ -195,8 +159,14 @@ running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
-现在有了三个部分的输出:单元测试、集成测试和文档测试。注意当在任何 src 目录的文件中增加单元测试时,单元测试部分的对应输出也会增加。增加集成测试文件中的测试函数也会对应增加输出。如果在 tests 目录中增加集成测试文件,则会增加更多集成测试部分:一个文件对应一个部分。
-为cargo test
指定测试函数名称参数也会匹配集成测试文件中的函数。为了只运行某个特定集成测试文件中的所有测试,可以使用cargo test
的--test
参数:
+
+
+现在有了三个部分的输出:单元测试、集成测试和文档测试。第一部分单元测试与我们之前见过的一样:每一个单元测试一行(列表 11-12 中有一个叫做internal
的测试),接着是一个单元测试的总结行。
+集成测试部分以行Running target/debug/deps/integration-test-ce99bcc2479f4607
(输出最后的哈希值可能不同)开头。接着是每一个集成测试中的测试函数一行,以及一个就在Doc-tests adder
部分开始之前的集成测试的总结行。
+注意在任意 src 文件中增加更多单元测试函数会增加更多单元测试部分的测试结果行。在我们创建的集成测试文件中增加更多测试函数会增加更多集成测试部分的行。每一个集成测试文件有其自己的部分,所以如果在 tests 目录中增加更多文件,这里就会有更多集成测试部分。
+我们仍然可以通过指定测试函数的名称作为cargo test
的参数来运行特定集成测试。为了运行某个特定集成测试文件中的所有测试,使用cargo test
的--test
后跟文件的名称:
$ cargo test --test integration_test
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
Running target/debug/integration_test-952a27e0126bb565
@@ -206,13 +176,63 @@ test it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
+这些只是 tests 目录中我们指定的文件中的测试。
集成测试中的子模块
-随着集成测试的增加,你可能希望在 tests
目录增加更多文件,例如根据测试的功能来将测试分组。正如我们之前提到的,这是可以的,Cargo 会将每一个文件当作一个独立的 crate。
-最终,可能会有一系列在所有集成测试中通用的帮助函数,例如建立通用场景的函数。如果你将这些函数提取到 tests 目录的一个文件中,比如说 tests/common.rs,则这个文件将会像这个目录中的其他包含测试的 Rust 文件一样被编译进一个单独的 crate 中。它也会作为一个独立的部分出现在测试输出中。因为这很可能不是你所希望的,所以建议在子目录中使用 mod.rs 文件,比如 tests/common/mod.rs,来放置帮助函数。tests 的子目录不会被作为单独的 crate 编译或者作为单独的部分出现在测试输出中。
+随着集成测试的增加,你可能希望在 tests
目录增加更多文件,例如根据测试的功能来将测试分组。正如我们之前提到的,每一个 tests 目录中的文件都被编译为单独的 crate。
+将每个集成测试文件当作其自己的 crate 来对待有助于创建更类似与终端用户使用 crate 那样的单独的作用域。然而,这意味着考虑到像第七章学习的如何将代码分隔进模块和文件那样,tests 目录中的文件不能像 src 中的文件那样共享相同的行为。
+对于 tests 目录中文件的不同行为,通常在如果有一系列有助于多个集成测试文件的帮助函数,而你尝试遵循第七章的步骤将他们提取到一个通用的模块中时显得很明显。例如,如果我们创建了 tests/common.rs 并将setup
函数放入其中,这里将放入一些希望能够在多个测试文件的多个测试函数中调用的代码:
+Filename: tests/common.rs
+pub fn setup() {
+ // setup code specific to your library's tests would go here
+}
+
+如果再次运行测试,将会在测试结果中看到一个对应 common.rs 文件的新部分,即便这个文件并没有包含任何测试函数,或者没有任何地方调用了setup
函数:
+running 1 test
+test tests::internal ... ok
+
+test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
+
+ Running target/debug/deps/common-b8b07b6f1be2db70
+
+running 0 tests
+
+test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
+
+ Running target/debug/deps/integration_test-d993c68b431d39df
+
+running 1 test
+test it_adds_two ... ok
+
+test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
+
+ Doc-tests adder
+
+running 0 tests
+
+test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
+
+
+common
出现在测试结果中并显示running 0 tests
,这不是我们想要的;我们只是希望能够在其他集成测试文件中分享一些代码罢了。
+为了使common
不出现在测试输出中,需要使用第七章学习到的另一个将代码提取到文件的方式:不再创建tests/common.rs,而是创建 tests/common/mod.rs。当将setup
代码移动到 tests/common/mod.rs 并去掉 tests/common.rs 文件之后,测试输出中将不会出现这一部分。tests 目录中的子目录不会被作为单独的 crate 编译或作为一部分出现在测试输出中。
+一旦拥有了 tests/common/mod.rs,就可以将其作为模块来在任何集成测试文件中使用。这里是一个 tests/integration_test.rs 中调用setup
函数的it_adds_two
测试的例子:
+Filename: tests/integration_test.rs
+extern crate adder;
+
+mod common;
+
+#[test]
+fn it_adds_two() {
+ common::setup();
+ assert_eq!(4, adder::add_two(2));
+}
+
+注意mod common;
声明与第七章中的模块声明相同。接着在测试函数中就可以调用common::setup()
了。
二进制 crate 的集成测试
-如果项目是二进制 crate 并且只包含 src/main.rs 而没有 src/lib.rs,这样就不可能在 tests 创建集成测试并使用 extern crate
导入 src/main.rs 中的函数了。这也是 Rust 二进制项目明确采用 src/main.rs 调用 src/lib.rs 中逻辑的结构的原因之一。通过这种结构,集成测试就可以使用extern crate
测试库 crate 中的主要功能,而如果这些功能没有问题的话,src/main.rs 中的少量代码也就会正常工作且不需要测试。
+如果项目是二进制 crate 并且只包含 src/main.rs 而没有 src/lib.rs,这样就不可能在 tests 创建集成测试并使用 extern crate
导入 src/main.rs 中的函数了。只有库 crate 向其他 crate 暴露了可以调用和使用的函数;二进制 crate 只意在单独运行。
+这也是 Rust 二进制项目明确采用 src/main.rs 调用 src/lib.rs 中逻辑这样的结构的原因之一。通过这种结构,集成测试就可以使用extern crate
测试库 crate 中的主要功能,而如果这些重要的功能没有问题的话,src/main.rs 中的少量代码也就会正常工作且不需要测试。
总结
-Rust 的测试功能提供了一个确保即使改变代码函数也能继续以指定方式运行的途径。单元测试独立的验证库的每一部分并能够测试私有实现细节。集成测试则涉及多个部分结合起来时能否使用,并像其他代码那样测试库的公有 API。Rust 的类型系统和所有权规则可以帮助避免一些 bug,不过测试对于减少代码是否符合期望的逻辑 bug 是很重要的。
+Rust 的测试功能提供了一个确保即使做出改变函数也能继续以指定方式运行的途径。单元测试独立的验证库的不同部分并能够测试私有实现细节。集成测试则涉及多个部分结合起来工作时的用例,并像其他代码那样测试库的公有 API。即使 Rust 的类型系统和所有权规则可以帮助避免一些 bug,不过测试对于减少代码是否符合期望相关的逻辑 bug 是很重要的。
接下来让我们结合本章所学和其他之前章节的知识,在下一章一起编写一个项目!
diff --git a/docs/ch12-00-an-io-project.html b/docs/ch12-00-an-io-project.html
index 890c845..27045c0 100644
--- a/docs/ch12-00-an-io-project.html
+++ b/docs/ch12-00-an-io-project.html
@@ -47,7 +47,7 @@
@@ -67,22 +67,34 @@
- 一个 I/O 项目
+ 一个 I/O 项目:构建一个小巧的 grep
-ch12-00-an-io-project.md
-commit efd59dd0fe8e3658563fb5fd289af9d862e07a03
+commit 1f432fc231cfbc310433ab2a354d77058444288c
-之前几个章节我们学习了很多知识。让我们一起运用这些新知识来构建一个项目。在这个过程中,我们还将学习到更多 Rust 标准库的内容。
-那么我们应该写点什么呢?这得是一个利用 Rust 优势的项目。Rust 的一个强大的用途是命令行工具:Rust 的运行速度、安全性、“单二进制文件”输出和跨平台支持使得它称为这类工作的绝佳选择。所以我们将创建一个我们自己的经典命令行工具:grep
。grep
有着极为简单的应用场景,它完成如下工作:
-
-- 它获取一个文件和一个字符串作为参数。
+
+
+本章既是一个目前所学的很多技能的概括,也是一个更多标准库功能的探索。我们将构建一个与文件和命令行输入/输出交互的命令行工具来练习现在一些你已经掌握的 Rust 技能。
+Rust 的运行速度、安全性、“单二进制文件”输出和跨平台支持使其成为创建命令行程序的绝佳选择,所以我们的项目将创建一个我们自己版本的经典命令行工具:grep
。grep 是“Globally search a Regular Expression and Print.”的首字母缩写。grep
最简单的使用场景是使用如下步骤在特定文件中搜索指定字符串:
+
+- 获取一个文件和一个字符串作为参数。
- 读取文件
- 寻找文件中包含字符串参数的行
- 打印出这些行
-
-另外,我们还将添加一个额外的功能:一个环境变量允许我们大小写不敏感的搜索字符串参数。
-还有另一个很好的理由使用grep
作为示例项目:Rust 社区的成员,Andrew Gallant,已经使用 Rust 创建了一个功能非常完整的grep
版本。它叫做ripgrep
,并且它非常非常快。这样虽然我们的grep
将会非常简单,你也会掌握阅读现实生活中项目的基础知识。
+
+我们还会展示如何使用环境变量和打印到标准错误而不是标准输出;这些功能在命令行工具中是很常用的。
+一位 Rust 社区的成员,Andrew Gallant,已经创建了一个功能完整且非常快速的grep
版本,叫做ripgrep
。相比之下,我们的grep
将非常简单,本章将交给你一些帮助你理解像ripgrep
这样真实项目的背景知识。
这个项目将会结合之前所学的一些内容:
- 代码组织(使用第七章学习的模块)
@@ -91,13 +103,12 @@ commit efd59dd0fe8e3658563fb5fd289af9d862e07a03
- 合理的使用 trait 和生命周期(第十章)
- 测试(第十一章)
-另外,我还会简要的讲到闭包、迭代器和 trait 对象,他们分别会在第XX、YY和ZZ章详细介绍。
-让我们一如既往的使用cargo new
创建一个新项目:
-$ cargo new --bin greprs
+另外,我还会简要的讲到闭包、迭代器和 trait 对象,他们分别会在第十三章和第十七章中详细介绍。
+让我们一如既往的使用cargo new
创建一个新项目。我们称之为greprs
以便与可能已经安装在系统上的grep
工具相区别:
+$ cargo new --bin greprs
Created binary (application) `greprs` project
$ cd greprs
-我们版本的grep
的叫做“greprs”,这样就不会迷惑用户让他们以为这就是可能已经在系统上安装了功能更完整的grep
。
diff --git a/docs/ch12-01-accepting-command-line-arguments.html b/docs/ch12-01-accepting-command-line-arguments.html
index 76c02c7..373af1c 100644
--- a/docs/ch12-01-accepting-command-line-arguments.html
+++ b/docs/ch12-01-accepting-command-line-arguments.html
@@ -47,7 +47,7 @@
@@ -71,17 +71,32 @@
ch12-01-accepting-command-line-arguments.md
-commit 4f2dc564851dc04b271a2260c834643dfd86c724
+commit b8e4fcbf289b82c12121b282747ce05180afb1fb
-第一个任务是让greprs
接受两个命令行参数。crates.io 上有一些现存的库可以帮助我们,不过因为我们正在学习,我们将自己实现一个。
-我们需要调用一个 Rust 标准库提供的函数:std::env::args
。这个函数返回一个传递给程序的命令行参数的迭代器(iterator)。我们还未讨论到迭代器,第十三章会全面的介绍他们。但是对于我们的目的来说,使用他们并不需要知道多少技术细节。我们只需要明白两点:
+第一个任务是让greprs
能够接受两个命令行参数:文件名和要搜索的字符串。也就是说希望能够使用cargo run
,要搜索的字符串和被搜索的文件的路径来运行程序,像这样:
+$ cargo run searchstring example-filename.txt
+
+现在cargo new
生成的程序忽略任何传递给它的参数。crates.io 上有一些现存的可以帮助我们接受命令行参数的库,不过因为我们正在学习,让我们实现一个。
+
+
+读取参数值
+为了能够获取传递给程序的命令行参数的值,我们需要调用一个 Rust 标准库提供的函数:std::env::args
。这个函数返回一个传递给程序的命令行参数的迭代器(iterator)。我们还未讨论到迭代器,第十三章会全面的介绍他们。但是对于我们现在的目的来说只需要明白两点:
- 迭代器生成一系列的值。
- 在迭代器上调用
collect
方法可以将其生成的元素转换为一个 vector。
-让我们试试列表 12-1 中的代码:
-
+Listing 12-1: Collect the command line arguments into a vector and print them
+out
-首先使用use
语句来将std::env
模块引入作用域。当函数嵌套了多于一层模块时,比如说std::env::args
,通常使用use
将父模块引入作用域,而不是引入其本身。env::args
比单独的args
要明确一些。当然,如果使用了多余一个std::env
中的函数,我们也只需要一个use
语句。
-在main
函数的第一行,我们调用了env::args
,并立即使用collect
来创建了一个 vector。这里我们也显式的注明了args
的类型:collect
可以被用来创建很多类型的集合。Rust 并不能推断出我们需要什么类型,所以类型注解是必须的。在 Rust 中我们很少会需要注明类型,不过collect
是就一个通常需要这么做的函数。
+首先使用use
语句来将std::env
模块引入作用域以便可以使用它的args
函数。注意std::env::args
函数嵌套进了两层模块中。如第七章讲到的,当所需函数嵌套了多于一层模块时,通常将父模块引入作用域,而不是其自身。这便于我们利用std::env
中的其他函数。这比增加了use std::env::args;
后仅仅使用args
调用函数要更明确一些;这样看起来好像一个定义于当前模块的函数。
+
+
+
+注意:std::env::args
在其任何参数包含无效 Unicode 字符时会 panic。如果你需要接受包含无效 Unicode 字符的参数,使用std::env::args_os
代替。这个函数返回OsString
值而不是String
值。出于简单考虑这里使用std::env::args
,因为OsString
值每个平台都不一样而且比String
值处理起来更复杂。
+
+
+
+
+在main
函数的第一行,我们调用了env::args
,并立即使用collect
来创建了一个包含迭代器所有值的 vector。collect
可以被用来创建很多类型的集合,所以这里显式注明的args
类型来指定我们需要一个字符串 vector。虽然在 Rust 中我们很少会需要注明类型,collect
就是一个经常需要注明类型的函数,因为 Rust 不能推断出你想要什么类型的集合。
最后,我们使用调试格式:?
打印出 vector。让我们尝试不用参数运行代码,接着用两个参数:
$ cargo run
["target/debug/greprs"]
@@ -104,43 +126,47 @@ $ cargo run needle haystack
...snip...
["target/debug/greprs", "needle", "haystack"]
-你会注意一个有趣的事情:二进制文件的名字是第一个参数。其原因超出了本章介绍的范围,不过这是我们必须记住的。
-现在我们有了一个访问所有参数的方法,让我们如列表 12-2 中所示将需要的变量存放到变量中:
-
+Listing 12-2: Create variables to hold the query argument and filename argument
-记住,程序名称是是第一个参数,所以并不需要args[0]
。我们决定从第一个参数将是需要搜索的字符串,所以将第一个参数的引用放入变量search
中。第二个参数将是文件名,将其放入变量filename
中。再次尝试运行程序:
+正如我们在打印出 vector 时所看到的,程序的名称占据了 vector 的第一个值args[0]
,所以我们从索引1
开始。第一个参数greprs
是需要搜索的字符串,所以将其将第一个参数的引用存放在变量query
中。第二个参数将是文件名,所以将第二个参数的引用放入变量filename
中。
+我们将临时打印出出这些变量的值,再一次证明代码如我们期望的那样工作。让我们使用参数test
和sample.txt
再次运行这个程序:
$ cargo run test sample.txt
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
- Running `target\debug\greprs.exe test sample.txt`
+ Running `target/debug/greprs test sample.txt`
Searching for test
In file sample.txt
-很棒!不过有一个问题。让我们不带参数运行:
-$ cargo run
- Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
- Running `target\debug\greprs.exe`
-thread 'main' panicked at 'index out of bounds: the len is 1
-but the index is 1', ../src/libcollections\vec.rs:1307
-note: Run with `RUST_BACKTRACE=1` for a backtrace.
-
-因为 vector 中只有一个元素,就是程序名称,不过我们尝试访问第二元素,程序 panic 并提示越界访问。虽然这个错误信息是_准确的_,不过它对程序的用户来说就没有意义了。现在就可以修复这个问题,不过我先继续学习别的内容:在程序结束前我们会改善这个情况。
+好的,它可以工作!我们将所需的参数值保存进了对应的变量中。之后会增加一些错误处理来应对类似用户没有提供参数的情况,不过现在我们将忽略他们并开始增加读取文件功能。
diff --git a/docs/ch12-02-reading-a-file.html b/docs/ch12-02-reading-a-file.html
index 04aef1c..ac9056e 100644
--- a/docs/ch12-02-reading-a-file.html
+++ b/docs/ch12-02-reading-a-file.html
@@ -47,7 +47,7 @@
@@ -71,11 +71,11 @@
ch12-02-reading-a-file.md
-commit 4f2dc564851dc04b271a2260c834643dfd86c724
+commit b8e4fcbf289b82c12121b282747ce05180afb1fb
-现在有了一些包含我们需要的信息的变量了,让我们试着使用他们。下一步目标是打开需要搜索的文件。为此,我需要一个文件。在项目的根目录创建一个文件poem.txt
,并写入一些艾米莉·狄金森(Emily Dickinson)的诗:
+接下来我们将读取由命令行文件名参数指定的文件。首先,需要一个用来测试的示例文件——用来确保greprs
正常工作的最好的文件是拥有少量文本和多个行且有一些重复单词的文件。列表 12-3 是一首艾米莉·狄金森(Emily Dickinson)的诗,它正适合这个工作!在项目根目录创建一个文件poem.txt
,并输入诗 "I'm nobody! Who are you?":
Filename: poem.txt
-I'm nobody! Who are you?
+I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us — don't tell!
They'd banish us, you know.
@@ -85,24 +85,27 @@ How public, like a frog
To tell your name the livelong day
To an admiring bog!
+
-创建完这个文件后,让我们编辑 src/main.rs 并增加如列表 12-3 所示用来打开文件的代码:
-
+Listing 12-4: Reading the contents of the file specified by the second argument
-这里增加了一些新内容。首先,需要更多的use
语句来引入标准库中的相关部分:我们需要std::fs::File
来处理文件,而std::io::prelude::*
则包含许多对于 I/O 包括文件 I/O 有帮助的 trait。类似于 Rust 有一个通用的 prelude 来自动引入特定内容,std::io
也有其自己的 prelude 来引入处理 I/O 时需要的内容。不同于默认的 prelude,必须显式use
位于std::io
中的 prelude。
-在main
中,我们增加了三点内容:第一,我们获取了文件的句柄并使用File::open
函数与第二个参数中指定的文件名来打开这个文件。第二,我们在变量contents
中创建了一个空的可变的String
,接着对文件句柄调用read_to_string
并以contents
字符串作为参数,contents
是read_to_string
将会放置它读取到的数据地方。最后,我们打印出了整个文件的内容,这是一个确认目前为止的程序能够工作的方法。
-尝试运行这些代码,随意指定第一个参数(因为还未实现搜索功能的部分)而将 poem.txt 文件将作为第二个参数:
+首先,增加了更多的use
语句来引入标准库中的相关部分:需要std::fs::File
来处理文件,而std::io::prelude::*
则包含许多对于 I/O 包括文件 I/O 有帮助的 trait。类似于 Rust 有一个通用的 prelude 来自动引入特定内容,std::io
也有其自己的 prelude 来引入处理 I/O 时所需的通用内容。不同于默认的 prelude,必须显式use
位于std::io
中的 prelude。
+在main
中,我们增加了三点内容:第一,通过传递变量filename
的值调用File::open
函数的值来获取文件的可变句柄。创建了叫做contents
的变量并将其设置为一个可变的,空的String
。它将会存放之后读取的文件的内容。第三,对文件句柄调用read_to_string
并传递contents
的可变引用作为参数。
+在这些代码之后,我们再次增加了临时的println!
打印出读取文件后contents
的值,这样就可以检查目前为止的程序能否工作。
+尝试运行这些代码,随意指定一个字符串作为第一个命令行参数(因为还未实现搜索功能的部分)而将 poem.txt 文件将作为第二个参数:
$ cargo run the poem.txt
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
- Running `target\debug\greprs.exe the poem.txt`
+ Running `target/debug/greprs the poem.txt`
Searching for the
In file poem.txt
With text:
@@ -137,7 +138,7 @@ How public, like a frog
To tell your name the livelong day
To an admiring bog!
-好的!我们的代码可以工作了!然而,它还有一些瑕疵。因为程序还很小,这些瑕疵并不是什么大问题,不过随着程序功能的丰富,将会越来越难以用简单的方法修复他们。现在就让我们开始重构而不是等待之后处理。重构在只有少量代码时会显得容易得多。
+好的!代码读取并打印出了文件的内容。虽然它还有一些瑕疵:main
函数有着多个功能,同时也没有处理可能出现的错误。虽然我们的程序还很小,这些瑕疵并不是什么大问题。不过随着程序功能的丰富,将会越来越难以用简单的方法修复他们。在开发程序时,及早开始重构是一个最佳实践,因为重构少量代码时要容易的多,所以让我们现在就开始吧。
diff --git a/docs/ch12-03-improving-error-handling-and-modularity.html b/docs/ch12-03-improving-error-handling-and-modularity.html
index dc8ff0a..cc72436 100644
--- a/docs/ch12-03-improving-error-handling-and-modularity.html
+++ b/docs/ch12-03-improving-error-handling-and-modularity.html
@@ -47,7 +47,7 @@
@@ -67,158 +67,149 @@
- 读取文件
+ 重构改进模块性和错误处理
ch12-03-improving-error-handling-and-modularity.md
-commit 4f2dc564851dc04b271a2260c834643dfd86c724
+commit b8e4fcbf289b82c12121b282747ce05180afb1fb
-为了完善我们程序有四个问题需要修复,而他们都与潜在的错误和程序结构有关。第一个问题是在哪打开文件:我们使用了expect
来在打开文件失败时指定一个错误信息,不过这个错误信息只是说“文件不存在”。还有很多打开文件失败的方式,不过我们总是假设是由于缺少文件导致的。例如,文件存在但是没有打开它的权限:这时,我们就打印出了错误不符合事实的错误信息!
-第二,我们不停的使用expect
,这就有点类似我们之前在不传递任何命令行参数时索引会panic!
时注意到的问题:这虽然时_可以工作_的,不过这有点没有原则性,而且整个程序中都需要他们,将错误处理都置于一处则会显得好很多。
-第三个问题是main
函数现在处理两个工作:解析参数,并打开文件。对于一个小的函数来说,这不是什么大问题。然而随着程序中的main
函数不断增长,main
函数中独立的任务也会越来越多。因为一个函数拥有很多职责,它将难以理解、难以测试并难以在不破坏其他部分的情况下做出修改。
-这也关系到我们的第四个问题:search
和filename
是程序中配置性的变量,而像f
和contents
则用来执行程序逻辑。随着main
函数增长,将引入更多的变量到作用域中,而当作用域中有更多的变量,将更难以追踪哪个变量用于什么目的。如果能够将配置型变量组织进一个结构就能使他们的目的更明确了。
-让我们重新组成程序来解决这些问题。
+为了改善我们的程序这里有四个问题需要修复,而且他们都与程序的组织方式和如何处理潜在错误有关。
+第一,main
现在进行了两个任务:它解析了参数并打开了文件。对于一个这样的小函数,这并不是一个大问题。然而如果main
中的功能持续增加,main
函数处理的单独的任务也会增加。当函数承担了更多责任,它就更难以推导,更难以测试,并且更难以在不破坏其他部分的情况下做出修改。最好能分离出功能这样每个函数就负责一个任务。
+这同时也关系到第二个问题:search
和filename
是程序中的配置变量,而像f
和contents
则用来执行程序逻辑。随着main
函数的增长,就需要引入更多的变量到作用域中,而当作用域中有更多的变量时,将更难以追踪每个变量的目的。最好能将将配置变量组织进一个结构这样就能使他们的目的更明确了。
+第三个问题是如果打开文件失败我们使用expect
来打印出错误信息,不过这个错误信息只是说file not found
。除了缺少文件之外还有很多打开文件可能失败的方式:例如,文件可能存在,不过可能没有打开它的权限。如果我们现在就出于这种情况,打印出的file not found
错误信息就给了用户一个不符合事实的建议!
+第四,我们不停的使用expect
来处理不同的错误,如果用户没有指定足够的参数来运行程序,他们会从 Rust 得到 "index out of bounds" 错误而这并不能明确的解释问题。如果所有的错误处理都位于一处这样将来的维护者在需要修改错误处理逻辑时就只需要咨询一处代码。将所有的错误处理都放在一处也有助于确保我们打印的错误信息对终端用户来说是有意义的。
+让我们通过重构项目来解决这些问题。
二进制项目的关注分离
-这类项目组织上的问题在很多相似类型的项目中很常见,所以 Rust 社区开发出一种关注分离的组织模式。这种模式可以用来组织任何用 Rust 构建的二进制项目,所以可以证明应该更早的开始这项重构,以为我们的项目符合这个模式。这个模式看起来像这样:
+main
函数负责多个任务的组织问题在许多二进制项目中很常见。所以 Rust 社区开发出一个类在main
函数开始变得庞大时进行二进制程序的关注分离的指导性过程。这些过程有如下步骤:
-- 将程序拆分成 main.rs 和 lib.rs。
-- 将命令行参数解析逻辑放入 main.rs。
-- 将程序逻辑放入 lib.rs。
-main
函数的工作是:
+- 将程序拆分成 main.rs 和 lib.rs 并将程序的逻辑放入 lib.rs 中。
+- 当命令行解析逻辑比较小时,可以保留在 main.rs 中。
+- 当命令行解析开始变得复杂时,也同样将其从 main.rs 提取到 lib.rs中。
+- 经过这些过程之后保留在
main
函数中的责任是:
-- 解析参数
-- 设置所有配置性变量
+- 使用参数值调用命令行解析逻辑
+- 设置任何其他的配置
- 调用 lib.rs 中的
run
函数
-- 如果
run
返回错误则处理这个错误
+- 如果
run
返回错误,则处理这个错误
-好的!老实说这个模式好像还很复杂。这就是关注分离的所有内容:main.rs 负责实际的程序运行,而 lib.rs 处理所有真正的任务逻辑。让我们将程序重构成这种模式。首先,提取出一个目的只在于解析参数的函数。列表 12-4 中展示了一个新的开始,main
函数调用了一个新函数parse_config
,它仍然定义于 src/main.rs 中:
-
+
-这看起来好像有点复杂,不过我们将一点一点的开展重构。在做出这些改变之后,再次运行程序并验证参数解析是否仍然正常。经常验证你的进展是一个好习惯,这样在遇到问题时就能更好地理解什么修改造成了错误。
+我们仍然将命令行参数收集进一个 vector,不过不同于在main
函数中将索引 1 的参数值赋值给变量query
和将索引 2 的值赋值给变量filename
,我们将整个 vector 传递给parse_config
函数。接着parse_config
函数将包含知道哪个参数该放入哪个变量的逻辑,并将这些值返回到main
。仍然在main
中创建变量query
和filename
,不过main
不再负责处理命令行参数与变量如何对应。
+这对我们这小程序可能有点大材小用,不过我们将采用小的、增量的步骤进行重构。在做出这些改变之后,再次运行程序并验证参数解析是否仍然正常。经常验证你的进展是一个好习惯,这样在遇到问题时能帮助你定位问题的成因。
组合配置值
-现在我们有了一个函数了,让我们接着完善它。我们代码还能设计的更好一些:函数返回了一个元组,不过接着立刻就解构成了单独的部分。这些代码本身没有问题,不过有一个地方表明仍有改善的余地:我们调用了parse_config
方法。函数名中的config
部分也表明了返回的两个值应该是组合在一起的,因为他们都是某个配置值的一部分。
+我们可以采取另一个小的步骤来进一步改善这个函数。现在函数返回一个元组,不过立刻又就将元组拆成了独立的部分。这是一个我们可能没有进行正确抽象的信号。
+另一个表明还有改进空间的迹象是parse_config
的config
部分,它暗示了我们返回的两个值是相关的并都是一个配置值的一部分。目前除了将这两个值组合进元组之外并没有表达这个数据结构的意义:我们可以将这两个值放入一个结构体并给每个字段一个有意义的名字。这会让未来的维护者更容易理解不同的值如何相互关联以及他们的目的。
+
-注意:一些同学将当使用符合类型更为合适的时候使用基本类型当作一种称为基本类型偏执(primitive obsession)的反模式。
+注意:一些同学将这种当使用符合类型更为合适的时候使用基本类型的反模式称为基本类型偏执(primitive obsession)。
-让我们引入一个结构体来存放所有的配置。列表 12-5 中展示了新增的Config
结构体定义、重构后的parse_config
和main
函数中的相关更新:
-
-parse_config
的签名现在表明它返回一个Config
值。在parse_config
的函数体中,我们之前返回了args
中String
值引用的字符串 slice,不过Config
定义为拥有两个有所有权的String
值。因为parse_config
的参数是一个String
值的 slice,Config
实例不能获取String
值的所有权:这违反了 Rust 的借用规则,因为main
函数中的args
变量拥有这些String
值并只允许parse_config
函数借用他们。
-还有许多不同的方式可以处理String
的数据;现在我们使用简单但低效率的方式,在字符串 slice 上调用clone
方法。clone
调用会生成一个字符串数据的完整拷贝,而且Config
实例可以拥有它,不过这会消耗更多时间和内存来储存拷贝字符串数据的引用,不过拷贝数据让我们使我们的代码显得更加直白。
+parse_config
的签名现在表明它返回一个Config
值。在parse_config
的函数体中,之前返回了args
中String
值引用的字符串 slice,现在我们选择定义Config
来使用拥有所有权的String
值。main
中的args
变量是参数值的所有者并只允许parse_config
函数借用他们,这意味着如果Config
尝试获取args
中值的所有权将违反 Rust 的借用规则。
+还有许多不同的方式可以处理String
的数据,而最简单但有些不太高效的方式是调用这些值的clone
方法。这会生成Config
实例可以拥有的数据的完整拷贝,不过会比储存字符串数据的引用消耗更多的时间和内存。不过拷贝数据使得代码显得更加直白因为无需管理引用的生命周期,所以在这种情况下牺牲一小部分性能来换取简洁性的取舍是值得的。
使用clone
权衡取舍
-由于其运行时消耗,许多 Rustacean 之间有一个趋势是倾向于不使用clone
来解决所有权问题。在关于迭代器的第XX章中,我们将会学习如何更有效率的处理这种情况。现在,为了编写我们的程序拷贝一些字符串是没有问题。我们只进行了一次拷贝,而且文件名和要搜索的字符串都比较短。随着你对 Rust 更加熟练,将更轻松的省略这个权衡的步骤,不过现在调用clone
是完全可以接受的。
+由于其运行时消耗,许多 Rustacean 之间有一个趋势是倾向于避免使用clone
来解决所有权问题。在关于迭代器的第十三章中,我们将会学习如何更有效率的处理这种情况,不过现在,复制一些字符串来取得进展是没有问题的,因为只会进行一次这样的拷贝,而且文件名和要搜索的字符串都比较短。在第一轮编写时拥有一个可以工作但有点低效的程序要比尝试过度优化代码更好一些。随着你对 Rust 更加熟练,将能更轻松的直奔合适的方法,不过现在调用clone
是完全可以接受的。
-main
函数更新为将parse_config
返回的Config
实例放入变量config
中,并将分别使用search
和filename
变量的代码更新为使用Config
结构体的字段。
+我们更新main
将parse_config
返回的Config
实例放入变量config
中,并更新之前分别使用search
和filename
变量的代码为现在的使用Config
结构体的字段。
+现在代码更明确的表现了我们的意图,query
和filename
是相关联的并且他们的目的是配置程序如何工作的。任何使用这些值的代码就知道在config
实例中对应目的的字段名中寻找他们。
创建一个Config
构造函数
-现在让我们考虑一下parse_config
的目的:这是一个创建Config
示例的函数。我们已经见过了一个创建实例函数的规范:像String::new
这样的new
函数。列表 12-6 中展示了将parse_config
转换为一个Config
结构体关联函数new
的代码:
-
+
-我们将parse_config
的名字改为new
并将其移动到impl
块中。我们也更新了main
中的调用代码。再次尝试编译并确保程序可以运行。
-从构造函数返回Result
-这是我们对这个方法最后的重构:还记得当 vector 含有少于三个项时访问索引 1 和 2 会 panic 并给出一个糟糕的错误信息的代码吗?让我们来修改它!列表 12-7 展示了如何在访问这些位置之前检查 slice 是否足够长,并使用一个更好的 panic 信息:
-
+
-通过在new
中添加这额外的几行代码,再次尝试不带参数运行程序:
+这类似于列表 9-8 中的Guess::new
函数,那里如果value
参数超出了有效值的范围就调用panic!
。不同于检查值的范围,这里检查args
的长度至少是 3,而函数的剩余部分则可以假设这个条件成立的基础上运行。如果
+args
少于 3 个项,这个条件将为真,并调用panic!
立即终止程序。
+有了new
中这几行额外的代码,再次不带任何参数运行程序并看看现在错误看起来像什么:
$ cargo run
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
- Running `target\debug\greprs.exe`
-thread 'main' panicked at 'not enough arguments', src\main.rs:29
+ Running `target/debug/greprs`
+thread 'main' panicked at 'not enough arguments', src/main.rs:29
note: Run with `RUST_BACKTRACE=1` for a backtrace.
-这样就好多了!至少有个一个符合常理的错误信息。然而,还有一堆额外的信息我们并不希望提供给用户。可以通过改变new
的签名来完善它。现在它只返回了一个Config
,所有没有办法表示创建Config
失败的情况。相反,可以如列表 12-8 所示返回一个Result
:
-
+
-现在new
函数返回一个Result
,在成功时带有一个Config
实例而在出现错误时带有一个&'static str
。回忆一下第十章“静态声明周期”中讲到&'static str
是一个字符串字面值,他也是现在我们的错误信息。
+
+
+现在new
函数返回一个Result
,在成功时带有一个Config
实例而在出现错误时带有一个&'static str
。回忆一下第十章“静态声明周期”中讲到&'static str
是一个字符串字面值,也是目前的错误信息。
new
函数体中有两处修改:当没有足够参数时不再调用panic!
,而是返回Err
值。同时我们将Config
返回值包装进Ok
成员中。这些修改使得函数符合其新的类型签名。
-Config::new
调用和错误处理
-现在我们需要对main
做一些修改,如列表 12-9 所示:
-
+
-新增了一个use
行来从标准库中导入process
。在main
函数中我们将处理new
函数返回的Result
值,并在其返回Config::new
时以一种更加清楚的方式结束进程。
-这里使用了一个之前没有讲到的标准库中定义的Result<T, E>
的方法:unwrap_or_else
。当Result
是Ok
时其行为类似于unwrap
:它返回Ok
内部封装的值。与unwrap
不同的是,当Result
是Err
时,它调用一个闭包(closure),也就是一个我们定义的作为参数传递给unwrap_or_else
的匿名函数。第XX章会更详细的介绍闭包;这里需要理解的重要部分是unwrap_or_else
会将Err
的内部值传递给闭包中位于两道竖线间的参数err
。使用unwrap_or_else
允许我们进行一些自定义的非panic!
的错误处理。
-上述的错误处理其实只有两行:我们打印出了错误,接着调用了std::process::exit
。这个函数立刻停止程序的执行并将传递给它的数组作为返回码。依照惯例,零代表成功而任何其他数字表示失败。就结果来说这依然类似于列表 12-7 中的基于panic!
的错误处理,但是不再会有额外的输出了,让我们试一试:
-$ cargo run
+
+
+
+在上面的列表中,使用了一个之前没有涉及到的方法:unwrap_or_else
,它定义于标准库的Result<T, E>
上。使用unwrap_or_else
可以进行一些自定义的非panic!
的错误处理。当Result
是Ok
时,这个方法的行为类似于unwrap
:它返回Ok
内部封装的值。然而,当Result
是Err
时,它调用一个闭包(closure),也就是一个我们定义的作为参数传递给unwrap_or_else
的匿名函数。第十三章会更详细的介绍闭包。现在你需要理解的是unwrap_or_else
会将Err
的内部值,也就是列表 12-9 中增加的not enough arguments
静态字符串的情况,传递给闭包中位于两道竖线间的参数err
。闭包中的代码在其运行时可以使用这个err
值。
+
+
+我们新增了一个use
行来从标准库中导入process
。在错误的情况闭包中将被运行的代码只有两行:我们打印出了err
值,接着调用了std::process::exit
(在开头增加了新的use
行从标准库中导入了process
)。process::exit
会立即停止程序并将传递给它的数字作为返回状态码。这类似于列表 12-8 中使用的基于panic!
的错误处理,除了不会在得到所有的额外输出了。让我们试试:
+$ cargo run
Compiling greprs v0.1.0 (file:///projects/greprs)
Finished debug [unoptimized + debuginfo] target(s) in 0.48 secs
- Running `target\debug\greprs.exe`
+ Running `target/debug/greprs`
Problem parsing arguments: not enough arguments
-非常好!现在输出就友好多了。
-run
函数中的错误处理
-现在重构完了参数解析部分,让我们再改进一下程序的逻辑。列表 12-10 中展示了在main
函数中调用提取出函数run
之后的代码。run
函数包含之前位于main
中的部分代码:
-
+
-run
函数的内容是之前位于main
中的几行,而且run
函数获取一个Config
作为参数。现在有了一个单独的函数了,我们就可以像列表 12-8 中的Config::new
那样进行类似的改进了。列表 12-11 展示了另一个use
语句将std::error::Error
结构引入了作用域,还有使run
函数返回Result
的修改:
-
+
-这里有三个大的修改。第一个是现在run
函数的返回值是Result<(), Box<Error>>
类型的。之前,函数返回 unit 类型()
,现在它仍然是Ok
时的返回值。对于错误类型,我们将使用Box<Error>
。这是一个trait 对象(trait object),第XX章会讲到。现在可以这样理解它:Box<Error>
意味着函数返回了某个实现了Error
trait 的类型,不过并没有指定具体的返回值类型。这样就比较灵活,因为在不同的错误场景可能有不同类型的错误返回值。Box
是一个堆数据的智能指针,第YY章将会详细介绍Box
。
-第二个改变是我们去掉了expect
调用并替换为第9章讲到的?
。不同于遇到错误就panic!
,这会从函数中返回错误值并让调用者来处理它。
-第三个修改是现在成功时这个函数会返回一个Ok
值。因为run
函数签名中声明成功类型返回值是()
,所以需要将 unit 类型值包装进Ok
值中。Ok(())
一开始看起来有点奇怪,不过这样使用()
是表明我们调用run
只是为了它的副作用的惯用方式;它并没有返回什么有意义的值。
+这里做出了三个大的改变。第一,改变了run
函数的返回值为Result<(), Box<Error>>
。之前这个函数返回 unit 类型()
,现在它仍然保持作为Ok
时的返回值。
+
+
+对于错误类型,使用了trait 对象Box<Error>
(在开头使用了use
语句将std::error::Error
引入作用域)。第十七章会涉及 trait 对象。目前只需知道Box<Error>
意味着函数会返回实现了Error
trait 的类型,不过无需指定具体将会返回的值的类型。这提供了在不同的错误场景可能有不同类型的错误返回值的灵活性。
+第二个改变是去掉了expect
调用并替换为第九章讲到的?
。不同于遇到错误就panic!
,这会从函数中返回错误值并让调用者来处理它。
+第三个修改是现在成功时这个函数会返回一个Ok
值。因为run
函数签名中声明成功类型返回值是()
,这意味着需要将 unit 类型值包装进Ok
值中。Ok(())
一开始看起来有点奇怪,不过这样使用()
是表明我们调用run
只是为了它的副作用的惯用方式;它并没有返回什么有意义的值。
上述代码能够编译,不过会有一个警告:
warning: unused result which must be used, #[warn(unused_must_use)] on by default
- --> src\main.rs:39:5
+ --> src/main.rs:39:5
|
39 | run(config);
| ^^^^^^^^^^^^
-Rust 尝试告诉我们忽略Result
,它有可能是一个错误值。让我们现在来处理它。我们将采用类似于列表 12-9 中处理Config::new
错误的技巧,不过还有少许不同:
+Rust 提示我们的代码忽略了Result
值,它可能表明这里存在一个错误。虽然我们没有检查这里是否有一个错误,而编译器提醒我们这里应该有一些错误处理代码!现在就让我们修正他们。
+处理main
中run
返回的错误
+我们将检查错误并使用与列表 12-10 中处理错误类似的技术来优雅的处理他们,不过有一些细微的不同:
Filename: src/main.rs
fn main() {
// ...snip...
- println!("Searching for {}", config.search);
+ println!("Searching for {}", config.query);
println!("In file {}", config.filename);
if let Err(e) = run(config) {
@@ -569,36 +419,27 @@ fn run(config: Config) -> Result<(), Box<Error>> {
process::exit(1);
}
}
-
-fn run(config: Config) -> Result<(), Box<Error>> {
- let mut f = File::open(config.filename)?;
-
- let mut contents = String::new();
- f.read_to_string(&mut contents)?;
-
- println!("With text:\n{}", contents);
-
- Ok(())
-}
-不同于unwrap_or_else
,我们使用if let
来检查run
是否返回Err
,如果是则调用process::exit(1)
。为什么呢?这个例子和Config::new
的区别有些微妙。对于Config::new
我们关心两件事:
-
-- 检测出任何可能发生的错误
-- 如果没有出现错误创建一个
Config
-
-而在这个情况下,因为run
在成功的时候返回一个()
,唯一需要担心的就是第一件事:检测错误。如果我们使用了unwrap_or_else
,则会得到()
的返回值。它并没有什么用处。
-虽然两种情况下if let
和unwrap_or_else
的内容都是一样的:打印出错误并退出。
+我们使用if let
来检查run
是否返回一个Err
值,不同于unwrap_or_else
,并在出错时调用process::exit(1)
。run
并不返回像Config::new
返回的Config
实例那样需要unwrap
的值。因为run
在成功时返回()
,而我们只关心发现一个错误,所以并不需要unwrap_or_else
来返回未封装的值,因为它只会是()
。
+不过两个例子中if let
和unwrap_or_else
的函数体都一样:打印出错误并退出。
将代码拆分到库 crate
-现在项目看起来好多了!还有一件我们尚未开始的工作:拆分 src/main.rs 并将一些代码放入 src/lib.rs 中。让我们现在就开始吧:将 src/main.rs 中的run
函数移动到新建的 src/lib.rs 中。还需要移动相关的use
语句和Config
的定义,以及其new
方法。现在 src/lib.rs 应该如列表 12-12 所示:
-
+
-注意我们还需要使用公有的pub
:在Config
和其字段、它的new
方法和run
函数上。
-现在在 src/main.rs 中,我们需要通过extern crate greprs
来引入现在位于 src/lib.rs 的代码。接着需要增加一行use greprs::Config
来引入Config
到作用域,并对run
函数加上 crate 名称前缀,如列表 12-13 所示:
-
+
-通过这些重构,所有代码应该都能运行了。运行几次cargo run
来确保你没有破坏什么内容。好的!确实有很多的内容,不过已经为将来的成功奠定了基础。我们采用了一种更加优秀的方式来处理错误,并使得代码更模块化了一些。从现在开始几乎所有的工作都将在 src/lib.rs 中进行。
-让我们利用这新创建的模块的优势来进行一些在旧代码中难以开开展的工作,他们在新代码中却很简单:编写测试!
+通过这些重构,所有功能应该抖联系在一起并可以运行了。运行cargo run
来确保一切都正确的衔接在一起。
+
+
+哇哦!这可有很多的工作,不过我们为将来成功打下了基础。现在处理错误将更容易,同时代码也更模块化。从现在开始几乎所有的工作都将在 src/lib.rs 中进行。
+让我们利用这些新创建的模块的优势来进行一些在旧代码中难以展开的工作,他们在新代码中却很简单:编写测试!
diff --git a/docs/ch12-04-testing-the-librarys-functionality.html b/docs/ch12-04-testing-the-librarys-functionality.html
index d9b6c46..965c3ac 100644
--- a/docs/ch12-04-testing-the-librarys-functionality.html
+++ b/docs/ch12-04-testing-the-librarys-functionality.html
@@ -47,7 +47,7 @@
@@ -71,12 +71,11 @@
ch12-04-testing-the-librarys-functionality.md
-commit 4f2dc564851dc04b271a2260c834643dfd86c724
+commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56
现在为项目的核心功能编写测试将更加容易,因为我们将逻辑提取到了 src/lib.rs 中并将参数解析和错误处理都留在了 src/main.rs 里。现在我们可以直接使用多种参数调用代码并检查返回值而不用从命令行运行二进制文件了。
我们将要编写的是一个叫做grep
的函数,它获取要搜索的项以及文本并产生一个搜索结果列表。让我们从run
中去掉那行println!
(也去掉 src/main.rs 中的,因为再也不需要他们了),并使用之前收集的选项来调用新的grep
函数。眼下我们只增加一个空的实现,和指定grep
期望行为的测试。当然,这个测试对于空的实现来说是会失败的,不过可以确保代码是可以编译的并得到期望的错误信息。列表 12-14 展示了这些修改:
-
+
注意需要在grep
的签名中显式声明声明周期'a
并用于contents
参数和返回值。记住,生命周期参数用于指定函数参数于返回值的生命周期的关系。在这个例子中,我们表明返回的 vector 将包含引用参数contents
的字符串 slice,而不是引用参数search
的字符串 slice。换一种说法就是我们告诉 Rust 函数grep
返回的数据将和传递给它的参数contents
的数据存活的同样久。这是非常重要的!考虑为了使引用有效则 slice 引用的数据也需要保持有效,如果编译器认为我们是在创建search
而不是contents
的 slice,那么安全检查将是不正确的。如果尝试不用生命周期编译的话,我们将得到如下错误:
error[E0106]: missing lifetime specifier
@@ -197,8 +193,7 @@ error: test failed
最终,我们需要一个方法来存储包含要搜索字符串的行。为此可以在for
循环之前创建一个可变的 vector 并调用push
方法来存放一个line
。在for
循环之后,返回这个 vector。列表 12-15 中为完整的实现:
-
+
尝试运行一下:
$ cargo test
diff --git a/docs/ch12-05-working-with-environment-variables.html b/docs/ch12-05-working-with-environment-variables.html
index 26e1d03..9b60371 100644
--- a/docs/ch12-05-working-with-environment-variables.html
+++ b/docs/ch12-05-working-with-environment-variables.html
@@ -47,7 +47,7 @@
@@ -71,7 +71,7 @@
ch12-05-working-with-environment-variables.md
-commit 4f2dc564851dc04b271a2260c834643dfd86c724
+commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56
让我们再增加一个功能:大小写不敏感搜索。另外,这个设定将不是一个命令行参数:相反它将是一个环境变量。当然可以选择创建一个大小写不敏感的命令行参数,不过用户要求提供一个环境变量这样设置一次之后在整个终端会话中所有的搜索都将是大小写不敏感的了。
实现并测试一个大小写不敏感grep
函数
@@ -113,8 +113,7 @@ Trust me.";
我们将定义一个叫做grep_case_insensitive
的新函数。它的实现与grep
函数大体上相似,不过列表 12-16 展示了一些小的区别:
-
+
首先,将search
字符串转换为小写,并存放于一个同名的覆盖变量中。注意现在search
是一个String
而不是字符串 slice,所以在将search
传递给contains
时需要加上 &,因为contains
获取一个字符串 slice。
接着在检查每个line
是否包含search
之前增加了一个to_lowercase
调用。因为将line
和search
都转换为小写,我们就可以无视大小写的匹配文件和命令行参数了。看看测试是否通过了:
diff --git a/docs/ch12-06-writing-to-stderr-instead-of-stdout.html b/docs/ch12-06-writing-to-stderr-instead-of-stdout.html
index 570fbd6..26722a2 100644
--- a/docs/ch12-06-writing-to-stderr-instead-of-stdout.html
+++ b/docs/ch12-06-writing-to-stderr-instead-of-stdout.html
@@ -47,7 +47,7 @@
@@ -71,7 +71,7 @@
ch12-06-writing-to-stderr-instead-of-stdout.md
-commit 4f2dc564851dc04b271a2260c834643dfd86c724
+commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56
目前为止,我们将所有的输出都println!
到了终端。这是可以的,不过大部分终端都提供了两种输出:“标准输出”对应大部分信息,而“标准错误”则用于错误信息。这使得处理类似于“将错误打印到终端而将其他信息输出到文件”的情况变得更容易。
可以通过在命令行使用>
来将输出重定向到文件中,同时不使用任何参数运行来造成一个错误,就会发现我们的程序只能打印到stdout
:
@@ -81,8 +81,7 @@ commit 4f2dc564851dc04b271a2260c834643dfd86c724
Problem parsing arguments: not enough arguments
我们希望这个信息被打印到屏幕上,而只有成功运行产生的输出写入到文件中。让我们如列表 12-17 中所示改变如何打印错误信息的方法:
-
+
Rust 并没有类似println!
这样的方便写入标准错误的函数。相反,我们使用writeln!
宏,它有点像println!
,不过它获取一个额外的参数。第一个参数是希望写入内容的位置。可以通过std::io::stderr
函数获取一个标准错误的句柄。我们将一个stderr
的可变引用传递给writeln!
;它需要是可变的因为这样才能写入信息!第二个和第三个参数就像println!
的第一个和第二参数:一个格式化字符串和任何需要插入的变量。
让我们再次用相同方式运行程序,不带任何参数并用 >
重定向stdout
:
@@ -137,7 +134,7 @@ How dreary to be somebody!
总结
在这一章,我们涉及了如果在 Rust 中进行常规的 I/O 操作。通过使用命令行参数、文件、环境变量和写入stderr
的功能。现在你已经准备好编写命令行程序了。结合前几章的知识,你的代码将会是组织良好的,并能有效的将数据存储到合适的数据结构中、更好的处理错误,并且还是经过良好测试的。我们也接触了一个真实情况下需要生命周期注解来保证引用一直有效的场景。
-接下来,让我们探索如何利用一些 Rust 中受函数式编程语言影响的功能”闭包和迭代器。
+接下来,让我们探索如何利用一些 Rust 中受函数式编程语言影响的功能:闭包和迭代器。
diff --git a/docs/ch13-00-functional-features.html b/docs/ch13-00-functional-features.html
index 6796374..e241f67 100644
--- a/docs/ch13-00-functional-features.html
+++ b/docs/ch13-00-functional-features.html
@@ -47,7 +47,7 @@
diff --git a/docs/ch13-01-closures.html b/docs/ch13-01-closures.html
index 258f109..3d9d013 100644
--- a/docs/ch13-01-closures.html
+++ b/docs/ch13-01-closures.html
@@ -47,7 +47,7 @@
diff --git a/docs/ch13-02-iterators.html b/docs/ch13-02-iterators.html
index e6e18ae..42b8c3e 100644
--- a/docs/ch13-02-iterators.html
+++ b/docs/ch13-02-iterators.html
@@ -47,7 +47,7 @@
diff --git a/docs/ch13-03-improving-our-io-project.html b/docs/ch13-03-improving-our-io-project.html
index 87ce6eb..15d806e 100644
--- a/docs/ch13-03-improving-our-io-project.html
+++ b/docs/ch13-03-improving-our-io-project.html
@@ -47,7 +47,7 @@
diff --git a/docs/ch13-04-performance.html b/docs/ch13-04-performance.html
index 35d082e..93c30f6 100644
--- a/docs/ch13-04-performance.html
+++ b/docs/ch13-04-performance.html
@@ -47,7 +47,7 @@
diff --git a/docs/ch14-00-more-about-cargo.html b/docs/ch14-00-more-about-cargo.html
index ce255d2..2738007 100644
--- a/docs/ch14-00-more-about-cargo.html
+++ b/docs/ch14-00-more-about-cargo.html
@@ -47,7 +47,7 @@
diff --git a/docs/ch14-01-release-profiles.html b/docs/ch14-01-release-profiles.html
index 6849119..10b4943 100644
--- a/docs/ch14-01-release-profiles.html
+++ b/docs/ch14-01-release-profiles.html
@@ -47,7 +47,7 @@
diff --git a/docs/ch14-02-publishing-to-crates-io.html b/docs/ch14-02-publishing-to-crates-io.html
index 9dc5540..c548572 100644
--- a/docs/ch14-02-publishing-to-crates-io.html
+++ b/docs/ch14-02-publishing-to-crates-io.html
@@ -47,7 +47,7 @@
@@ -71,7 +71,7 @@
ch14-02-publishing-to-crates-io.md
-commit c49e5ee8859f8eb8f8867cbeafbdf5b802aa5894
+commit f2eef19b3a39ee68dd363db2fcba173491ba9dc4
我们曾经在项目中增加 crates.io 上的 crate 作为依赖。也可以选择将代码分享给其他人。Crates.io 用来分发包的源代码,所以它主要用于分发开源代码。
Rust 和 Cargo 有一些帮助人们找到和使用你发布的包的功能。我们将介绍这些功能,接着讲到如何发布一个包。
@@ -86,10 +86,7 @@ commit c49e5ee8859f8eb8f8867cbeafbdf5b802aa5894
/// ```
/// let five = 5;
///
-/// assert_eq!(6, add_one(5));
-/// # fn add_one(x: i32) -> i32 {
-/// # x + 1
-/// # }
+/// assert_eq!(6, add_one(five));
/// ```
pub fn add_one(x: i32) -> i32 {
x + 1
@@ -115,7 +112,7 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
使用pub use
来导出合适的公有 API
第七章介绍了如何使用mod
关键字来将代码组织进模块中,如何使用pub
关键字将项变为公有,和如何使用use
关键字将项引入作用域。当发布 crate 给并不熟悉其使用的库的实现的人时,就值得花时间考虑 crate 的结构对于开发和对于依赖 crate 的人来说是否同样有用。如果结构对于供其他库使用来说并不方便,也无需重新安排内部组织:可以选择使用pub use
来重新导出一个不同的公有结构。
-例如列表 14-2中,我们创建了一个库art
,其包含一个kinds
模块,模块中包含枚举Color
和包含函数mix
的模块utils
:
+例如列表 14-2 中,我们创建了一个库art
,其包含一个kinds
模块,模块中包含枚举Color
和包含函数mix
的模块utils
:
Filename: src/lib.rs
//! # Art
//!
diff --git a/docs/ch14-03-cargo-workspaces.html b/docs/ch14-03-cargo-workspaces.html
index 9a3c96f..fad2cb4 100644
--- a/docs/ch14-03-cargo-workspaces.html
+++ b/docs/ch14-03-cargo-workspaces.html
@@ -47,7 +47,7 @@
diff --git a/docs/ch14-04-installing-binaries.html b/docs/ch14-04-installing-binaries.html
index 4b84881..ab563a5 100644
--- a/docs/ch14-04-installing-binaries.html
+++ b/docs/ch14-04-installing-binaries.html
@@ -47,7 +47,7 @@
diff --git a/docs/ch14-05-extending-cargo.html b/docs/ch14-05-extending-cargo.html
index 28cbd78..090f726 100644
--- a/docs/ch14-05-extending-cargo.html
+++ b/docs/ch14-05-extending-cargo.html
@@ -47,7 +47,7 @@
diff --git a/docs/ch15-00-smart-pointers.html b/docs/ch15-00-smart-pointers.html
index 0487ae8..88a2133 100644
--- a/docs/ch15-00-smart-pointers.html
+++ b/docs/ch15-00-smart-pointers.html
@@ -47,7 +47,7 @@
diff --git a/docs/ch15-01-box.html b/docs/ch15-01-box.html
index dd5fb6b..e9e2bd6 100644
--- a/docs/ch15-01-box.html
+++ b/docs/ch15-01-box.html
@@ -47,7 +47,7 @@
diff --git a/docs/ch15-02-deref.html b/docs/ch15-02-deref.html
index a57a909..942b27b 100644
--- a/docs/ch15-02-deref.html
+++ b/docs/ch15-02-deref.html
@@ -47,7 +47,7 @@
@@ -71,9 +71,9 @@
ch15-02-deref.md
-commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56
+commit ecc3adfe0cfa0a4a15a178dc002702fd0ea74b3f
-第一个智能指针相关的重要 trait 是Deref
,它允许我们重载*
,解引用运算符(不同于乘法运算符和全局引用运算符)。重载智能指针的*
方便访问其后的数据,在这个部分的稍后介绍解引用强制多态时我们会讨论方便的意义。
+第一个智能指针相关的重要 trait 是Deref
,它允许我们重载*
,解引用运算符(不同于乘法运算符和全局引用运算符)。重载智能指针的*
能使访问其后的数据更为方便,在这个部分的稍后介绍解引用强制多态时我们会讨论方便的意义。
第八章的哈希 map 的“根据旧值更新一个值”部分简要的提到了解引用运算符。当时有一个可变引用,而我们希望改变这个引用所指向的值。为此,首先我们必须解引用。这是另一个使用i32
值引用的例子:
let mut x = 5;
{
@@ -119,14 +119,14 @@ fn main() {
struct that holds mp3 file data and metadata
大部分代码看起来都比较熟悉:一个结构体、一个 trait 实现、和一个创建了结构体示例的 main 函数。其中有一部分我们还未全面的讲解:类似于第十三章学习迭代器 trait 时出现的type Item
,type Target = T;
语法用于定义关联类型,第十九章会更详细的介绍。不必过分担心例子中的这一部分;它只是一个稍显不同的定义泛型参数的方式。
在assert_eq!
中,我们验证vec![1, 2, 3]
是否为Mp3
实例*my_favorite_song
解引用的值,结果正是如此因为我们实现了deref
方法来返回音频数据。如果没有为Mp3
实现Deref
trait,Rust 将不会编译*my_favorite_song
:会出现错误说Mp3
类型不能被解引用。
-代码能够工作的原因在于调用*my_favorite_song
时*
在背后所做的操作:
+没有Deref
trait 的话,编译器只能解引用&
引用,而my_favorite_song
并不是(它是一个Mp3
结构体)。通过Deref
trait,编译器知道实现了Deref
trait 的类型有一个返回引用的deref
方法(在这个例子中,是&self.audio
因为列表 15-7 中的deref
的定义)。所以为了得到一个*
可以解引用的&
引用,编译器将*my_favorite_song
展开为如下:
*(my_favorite_song.deref())
-这对my_favorite_song
调用了deref
方法,它借用了my_favorite_song
并返回指向my_favorite_song.audio
的引用,这正是列表 15-5 中deref
所定义的。引用的*
被定义为仅仅从引用中返回其数据,所以上面*
的展开形式对于外部*
来说并不是递归的。最终的数据类型是Vec<u8>
,它与列表 15-5 中assert_eq!
的vec![1, 2, 3]
相匹配。
-deref
方法的返回值类型仍然是引用和为何必须解引用方法的结果的原因是如果deref
方法就返回值,使用*
总是会获取其所有权。
+这个就是self.audio
中的结果值。deref
返回一个引用并接下来必需解引用而不是直接返回值的原因是所有权:如果deref
方法直接返回值而不是引用,其值将被移动出self
。这里和大部分使用解引用运算符的地方并不想获取my_favorite_song.audio
的所有权。
+注意将*
替换为deref
调用和*
调用的过程在每次使用*
的时候都会发生一次。*
的替换并不会无限递归进行。最终的数据类型是Vec<u8>
,它与列表 15-7 中assert_eq!
的vec![1, 2, 3]
相匹配。
函数和方法的隐式解引用强制多态
-Rust 倾向于偏爱明确而不是隐晦,不过一个情况下这并不成立,就是函数和方法的参数的解引用强制多态(deref coercions)。解引用强制多态会自动的将指针或智能指针的引用转换为指针内容的引用。解引用强制多态发生于一个值被传递给函数或方法,并只发生于需要将传递的值类型与签名中参数类型相匹配的时候。解引用强制多态的加入使得 Rust 调用函数或方法时无需很多使用&
和*
的引用和解引用。
-使用列表 15-5 中的Mp3
结构体,如下是一个获取u8
slice 并压缩 mp3 音频数据的函数签名:
+Rust 倾向于偏爱明确而不是隐晦,不过一个情况下这并不成立,就是函数和方法的参数的解引用强制多态(deref coercions)。解引用强制多态会自动的将指针或智能指针的引用转换为指针内容的引用。解引用强制多态发生于当传递给函数的参数类型不同于函数签名中定义参数类型的时候。解引用强制多态的加入使得 Rust 调用函数或方法时无需很多使用&
和*
的引用和解引用。
+使用列表 15-7 中的Mp3
结构体,如下是一个获取u8
slice 并压缩 mp3 音频数据的函数签名:
fn compress_mp3(audio: &[u8]) -> Vec<u8> {
// the actual implementation would go here
}
@@ -138,8 +138,8 @@ struct that holds mp3 file data and metadata
然而,因为解引用强制多态和Mp3
的Deref
trait 实现,我们可以使用如下代码使用my_favorite_song
中的数据调用这个函数:
let result = compress_mp3(&my_favorite_song);
-只有&
和实例,好的!我们可以把智能指针当成普通的引用。也就是说解引用强制多态意味着 Rust 利用了Deref
实现的优势:Rust 知道Mp3
实现了Deref
trait 并从deref
方法返回&Vec<u8>
。它也知道标准库实现了Vec<T>
的Deref
trait,其deref
方法返回&[T]
(我们也可以通过查阅Vec<T>
的 API 文档来发现这一点)。所以,在编译时,Rust 会发现它可以调用两次Deref::deref
来将&Mp3
变成&Vec<u8>
再变成&[T]
来满足compress_mp3
的签名。这意味着我们可以少写一些代码!Rust 会多次分析Deref::deref
的返回值类型直到它满足参数的类型,只要相关类型实现了Deref
trait。这些间接转换在编译时进行,所以利用解引用强制多态并没有运行时惩罚。
-这里还有一个重载了&mut T
的*
的DerefMut
trait,它以与Deref
重载&T
的*
相同的方式用于参数中。
+只有&
和实例,好的!我们可以把智能指针当成普通的引用。也就是说解引用强制多态意味着 Rust 利用了Deref
实现的优势:Rust 知道Mp3
实现了Deref
trait 并从deref
方法返回&Vec<u8>
。它也知道标准库实现了Vec<T>
的Deref
trait,其deref
方法返回&[T]
(我们也可以通过查阅Vec<T>
的 API 文档来发现这一点)。所以,在编译时,Rust 会发现它可以调用两次Deref::deref
来将&Mp3
变成&Vec<u8>
再变成&[T]
来满足compress_mp3
的签名。这意味着我们可以少写一些代码!Rust 会多次分析Deref::deref
的返回值类型直到它满足参数的类型,只要相关类型实现了Deref
trait。这些间接转换在编译时进行,所以利用解引用强制多态并没有运行时惩罚!
+类似于如何使用Deref
trait 重载&T
的*
运算符,DerefMut
trait用于重载&mut T
的*
运算符。
Rust 在发现类型和 trait 实现满足三种情况时会进行解引用强制多态:
- 从
&T
到&U
当T: Deref<Target=U>
。
diff --git a/docs/ch15-03-drop.html b/docs/ch15-03-drop.html
index 0d5e648..54975e3 100644
--- a/docs/ch15-03-drop.html
+++ b/docs/ch15-03-drop.html
@@ -47,7 +47,7 @@
diff --git a/docs/ch15-04-rc.html b/docs/ch15-04-rc.html
index 2b500c1..b1de0ee 100644
--- a/docs/ch15-04-rc.html
+++ b/docs/ch15-04-rc.html
@@ -47,7 +47,7 @@
diff --git a/docs/ch15-05-interior-mutability.html b/docs/ch15-05-interior-mutability.html
index e2e0edf..cdb5c64 100644
--- a/docs/ch15-05-interior-mutability.html
+++ b/docs/ch15-05-interior-mutability.html
@@ -47,7 +47,7 @@
diff --git a/docs/ch15-06-reference-cycles.html b/docs/ch15-06-reference-cycles.html
index 8ebe28e..97ffebf 100644
--- a/docs/ch15-06-reference-cycles.html
+++ b/docs/ch15-06-reference-cycles.html
@@ -47,7 +47,7 @@
diff --git a/docs/ch16-00-concurrency.html b/docs/ch16-00-concurrency.html
index 10c247b..8d6c268 100644
--- a/docs/ch16-00-concurrency.html
+++ b/docs/ch16-00-concurrency.html
@@ -47,7 +47,7 @@
diff --git a/docs/ch16-01-threads.html b/docs/ch16-01-threads.html
index ddb3896..9cf22aa 100644
--- a/docs/ch16-01-threads.html
+++ b/docs/ch16-01-threads.html
@@ -47,7 +47,7 @@
diff --git a/docs/ch16-02-message-passing.html b/docs/ch16-02-message-passing.html
index 8be316a..a05e6b7 100644
--- a/docs/ch16-02-message-passing.html
+++ b/docs/ch16-02-message-passing.html
@@ -47,7 +47,7 @@
diff --git a/docs/ch16-03-shared-state.html b/docs/ch16-03-shared-state.html
index 95fa581..b720818 100644
--- a/docs/ch16-03-shared-state.html
+++ b/docs/ch16-03-shared-state.html
@@ -47,7 +47,7 @@
@@ -67,7 +67,255 @@
-
+ 共享状态并发
+
+ch16-03-shared-state.md
+
+commit 9df612e93e038b05fc959db393c15a5402033f47
+
+虽然消息传递是一个很好的处理并发的方式,但并不是唯一的一个。再次考虑一下它的口号:
+
+Do not communicate by sharing memory; instead, share memory by
+communicating.
+不要共享内存来通讯;而是要通讯来共享内存。
+
+那么“共享内存来通讯”看起来是怎样的呢?共享内存并发有点像多所有权:多个线程可以同时访问相同的内存位置。正如第十五章中智能指针使得多所有权成为可能时我们所看到的,这会增加额外的复杂性,因为需要以某种方式管理这些不同的所有者。
+但是 Rust 的类型系统和所有权可以很好的帮助我们正确的进行管理。例如,让我们看看一个共享内存中更常见的并发原语:互斥器(mutexes)。
+互斥器一次只允许一个线程访问数据
+互斥器(mutex)是一个用于共享内存的并发原语。它是“mutual exclusion”的缩写,也就是说,任何给定时间它只允许一个线程访问某些数据。互斥器以难以使用著称,因为你不得不记住:
+
+- 必须记住在使用数据之前尝试获取锁。
+- 一旦处理完被互斥器所保护的数据之后,必须记得解锁数据这样其他线程才能够获取锁。
+
+对于一个现实中的互斥器的例子,想象一下在一个会议中的专门小组讨论会上,不过只有一个麦克风。在一个小组成员可能发言之前,他们必须请求或示意他们需要使用麦克风。一旦得到了麦克风,他们可以发言任意长的时间,接着将麦克风交给系一个希望讲话的小组成员。如果小组成员在没有麦克风的时候就开始叫喊或者在其他成员发言结束之前就取得麦克风将是很无理的。如果对这个共享的麦克风的管理因为任何这些原因出现问题,讨论会将无法如期进行。
+正确的管理互斥器是异常复杂的,这也就是为什么这么多人都热衷于通道。然而,在 Rust 中,得益于类型系统和所有权,我们不可能会在锁和解锁上出错。
+Mutex<T>
的 API
+让我们看看列表 16-12 中使用互斥器的例子,现在并不涉及到多线程:
+Filename: src/main.rs
+use std::sync::Mutex;
+
+fn main() {
+ let m = Mutex::new(5);
+
+ {
+ let mut num = m.lock().unwrap();
+ *num = 6;
+ }
+
+ println!("m = {:?}", m);
+}
+
+
+与很多类型一样,我们通过叫做new
的关联函数来创建一个Mutex<T>
。为了访问互斥器中的数据,使用lock
方法来获取锁。这个调用会阻塞到直到轮到我们拥有锁为止。如果另一个线程拥有锁接着那个线程 panic 了则这个调用会失败。类似于上一部分列表 16-6 那样,我们暂时使用unwrap()
而不是更好的错误处理。请查看第九章中提供的更好的工具。
+一旦获取了锁,就可以将返回值(在这里是num
)作为一个数据的可变引用使用了。类型系统是 Rust 如何保证使用值之前必须获取锁的:Mutex<i32>
并不是一个i32
,所以必须获取锁才能使用这个i32
值。我们是不会忘记这么做的;类否则型系统是不会允许的。
+与你可能怀疑的一样,Mutex<T>
是一个智能指针。好吧,更准确的说,lock
调用返回一个叫做MutexGuard
的智能指针。类似我们在第十五章见过的智能指针,它实现了Deref
来指向其内部数据。另外MutexGuard
有一个用来释放锁的Drop
实现。这样就不会忘记释放锁了。这在MutexGuard
离开作用域时会自动发生,例如它发生于列表 16-12 中内部作用域的结尾。接着可以打印出互斥器的值并发现能够将其内部的i32
改为 6。
+在线程间共享Mutex<T>
+现在让我们尝试使用Mutex<T>
在多个线程间共享值。我们将启动十个线程,并在每一个线程中对一个计数器值加一,这样计数器将从 0 变为 10。注意接下来的几个例子会有编译错误,而我们将利用这些错误来学习如何使用
+Mutex<T>
以及 Rust 又是怎样帮助我们正确使用它的。列表 16-13 是最开始的例子:
+Filename: src/main.rs
+use std::sync::Mutex;
+use std::thread;
+
+fn main() {
+ let counter = Mutex::new(0);
+ let mut handles = vec![];
+
+ for _ in 0..10 {
+ let handle = thread::spawn(|| {
+ let mut num = counter.lock().unwrap();
+
+ *num += 1;
+ });
+ handles.push(handle);
+ }
+
+ for handle in handles {
+ handle.join().unwrap();
+ }
+
+ println!("Result: {}", *counter.lock().unwrap());
+}
+
+
+这里创建了一个counter
变量来存放内含i32
的Mutex<T>
,类似列表 16-12 那样。接下来使用 range 创建了 10 个线程。这里使用了thread::spawn
并对所有线程使用了相同的闭包:他们每一个都将调用lock
方法来获取Mutex<T>
上的锁并对接着互斥器中的值加一。当一个线程结束执行其闭包,num
会离开作用域并释放锁这样另一个线程就可以获取它了。
+在主线程中,我们像列表 16-2 那样收集了所有的 join 句柄,并接着每一个的join
方法来确保所有线程都会结束。那时,主线程会获取锁并打印出程序的结果。
+之前提示过这个例子不能编译,让我们看看为什么!
+error[E0373]: closure may outlive the current function, but it borrows
+`counter`, which is owned by the current function
+ -->
+ |
+9 | let handle = thread::spawn(|| {
+ | ^^ may outlive borrowed value `counter`
+10 | let mut num = counter.lock().unwrap();
+ | ------- `counter` is borrowed here
+ |
+help: to force the closure to take ownership of `counter` (and any other
+referenced variables), use the `move` keyword, as shown:
+ | let handle = thread::spawn(move || {
+
+这类似于列表 16-5 中解决了的问题。考虑到启动了多个线程,Rust 无法知道这些线程会运行多久而counter
是否在每一个线程尝试借用它时仍然保持有效。帮助信息提醒了我们如何解决它:可以使用move
来给予每个线程其所有权。试试将这个修改用到闭包上:
+thread::spawn(move || {
+
+再次尝试编译。这会出现了一个不同的错误!
+error[E0382]: capture of moved value: `counter`
+ -->
+ |
+9 | let handle = thread::spawn(move || {
+ | ------- value moved (into closure) here
+10 | let mut num = counter.lock().unwrap();
+ | ^^^^^^^ value captured here after move
+ |
+ = note: move occurs because `counter` has type `std::sync::Mutex<i32>`,
+ which does not implement the `Copy` trait
+
+error[E0382]: use of moved value: `counter`
+ -->
+ |
+9 | let handle = thread::spawn(move || {
+ | ------- value moved (into closure) here
+...
+21 | println!("Result: {}", *counter.lock().unwrap());
+ | ^^^^^^^ value used here after move
+ |
+ = note: move occurs because `counter` has type `std::sync::Mutex<i32>`,
+ which does not implement the `Copy` trait
+
+error: aborting due to 2 previous errors
+
+move
并没有像列表 16-5 中那样解决这个程序中的问题。为什么没有呢?这个错误信息有些难以理解,因为它表明counter
被移动进了闭包,接着它在调用lock
时被捕获。这听起来像是我们希望的,不过这是不允许的。
+让我们推理一下。现在不再使用for
循环创建 10 个线程,让我们不用循环而只创建两个线程来看看会发生什么。将列表 16-13 中第一个for
循环替换为如下代码:
+let handle = thread::spawn(move || {
+ let mut num = counter.lock().unwrap();
+
+ *num += 1;
+});
+handles.push(handle);
+
+let handle2 = thread::spawn(move || {
+ let mut num2 = counter.lock().unwrap();
+
+ *num2 += 1;
+});
+handles.push(handle2);
+
+这里创建了两个线程,并将用于第二个线程的变量名改为handle2
和num2
。现在我们简化了例子来看看是否能够理解错误信息。这一次编译给出如下信息:
+error[E0382]: capture of moved value: `counter`
+ -->
+ |
+8 | let handle = thread::spawn(move || {
+ | ------- value moved (into closure) here
+...
+16 | let mut num2 = counter.lock().unwrap();
+ | ^^^^^^^ value captured here after move
+ |
+ = note: move occurs because `counter` has type `std::sync::Mutex<i32>`,
+ which does not implement the `Copy` trait
+
+error[E0382]: use of moved value: `counter`
+ -->
+ |
+8 | let handle = thread::spawn(move || {
+ | ------- value moved (into closure) here
+...
+26 | println!("Result: {}", *counter.lock().unwrap());
+ | ^^^^^^^ value used here after move
+ |
+ = note: move occurs because `counter` has type `std::sync::Mutex<i32>`,
+ which does not implement the `Copy` trait
+
+error: aborting due to 2 previous errors
+
+啊哈!在第一个错误信息中,Rust 表明了counter
被移动进了handle
所代表线程的闭包中。这个移动阻止我们在对其调用lock
并将结果储存在num2
中时捕获counter
,这是已经在第二个线程中了!所以 Rust 告诉我们不能将counter
的所有权移动到多个线程中。这在之前很难看出是因为我们在循环中创建多个线程,而 Rust 无法在循环的迭代中指明不同的线程(没有临时变量)。
+多线程和多所有权
+在第十五章中,我们可以通过使用智能指针Rc<T>
来创建引用计数的值来拥有多所有权。同时第十五章提到了Rc<T>
只能用于单线程上下文,不过还是让我们在这里试用Rc<T>
来观察会发生什么。列表 16-14 将Mutex<T>
封装进了Rc<T>
中,并在移动到线程中之前克隆了Rc<T>
。切换回循环来创建线程,并保留闭包中的move
关键字:
+Filename: src/main.rs
+use std::rc::Rc;
+use std::sync::Mutex;
+use std::thread;
+
+fn main() {
+ let counter = Rc::new(Mutex::new(0));
+ let mut handles = vec![];
+
+ for _ in 0..10 {
+ let counter = counter.clone();
+ let handle = thread::spawn(move || {
+ let mut num = counter.lock().unwrap();
+
+ *num += 1;
+ });
+ handles.push(handle);
+ }
+
+ for handle in handles {
+ handle.join().unwrap();
+ }
+
+ println!("Result: {}", *counter.lock().unwrap());
+}
+
+
+又一次,编译并...出现了不同的错误!编译器真是教会了我们很多东西!
+error[E0277]: the trait bound `std::rc::Rc<std::sync::Mutex<i32>>:
+std::marker::Send` is not satisfied
+ -->
+ |
+11 | let handle = thread::spawn(move || {
+ | ^^^^^^^^^^^^^ the trait `std::marker::Send` is not
+ implemented for `std::rc::Rc<std::sync::Mutex<i32>>`
+ |
+ = note: `std::rc::Rc<std::sync::Mutex<i32>>` cannot be sent between threads
+ safely
+ = note: required because it appears within the type
+ `[closure@src/main.rs:11:36: 15:10
+ counter:std::rc::Rc<std::sync::Mutex<i32>>]`
+ = note: required by `std::thread::spawn`
+
+哇哦,太长不看!需要指出一些重要的部分:第一个提示表明Rc<Mutex<i32>>
不能安全的在线程间传递。理由也在错误信息中,经过提取之后,表明“不满足Send
trait bound”(the trait bound Send is not satisfied
)。下一部分将会讨论Send
,它是一个确保确保用于线程的类型是适合并发环境的 trait。
+不幸的是,Rc<T>
并不能安全的在线程间共享。当Rc<T>
管理引用计数时,它必须在每一个clone
调用时增加计数并在每一个克隆被丢弃时减少计数。Rc<T>
并没有使用任何并发原语来确保改变计数的操作不会被其他线程打断。在计数出错时这可能会导致诡异的 bug,比如可能会造成内存泄漏或在使用结束之前就丢弃一个值。那么如果有一个正好与Rc<T>
类似,不过以一种线程安全的方式改变引用计数的类型会怎么样呢?
+原子引用计数Arc<T>
+如果你思考过像之前那样的问题的话,你就是正确的。确实有一个类似Rc<T>
并可以安全的用于并发环境的类型:Arc<T>
。字母“a”代表原子性(atomic),所以这是一个原子引用计数(atomically reference counted)类型。原子性是另一类这里还未涉及到的并发原语;请查看标准库中std::sync::atomic
的文档来获取更多细节。其中的要点就是:原子性类型工作起来类似原始类型,不过可以安全的在线程间共享。
+那为什么不是所有的原始类型都是原子性的呢,然后为什么不是所有标准库中的类型都默认使用Arc<T>
实现呢?线程安全伴随一些性能惩罚,我们只希望在需要时才为此付出代价。如果只是在单线程中会值进行操作,因为并不需要原子性提供的保证代码可以运行的更快。
+回到之前的例子:Arc<T>
和Rc<T>
除了Arc<T>
内部的原子性之外他们是等价的。其 API 也是一样的,所以可以修改use
行和new
调用。列表 16-15 中的代码最终可以编译和运行:
+Filename: src/main.rs
+use std::sync::{Mutex, Arc};
+use std::thread;
+
+fn main() {
+ let counter = Arc::new(Mutex::new(0));
+ let mut handles = vec![];
+
+ for _ in 0..10 {
+ let counter = counter.clone();
+ let handle = thread::spawn(move || {
+ let mut num = counter.lock().unwrap();
+
+ *num += 1;
+ });
+ handles.push(handle);
+ }
+
+ for handle in handles {
+ handle.join().unwrap();
+ }
+
+ println!("Result: {}", *counter.lock().unwrap());
+}
+
+
+这会打印出:
+Result: 10
+
+成功了!我们从 0 数到了 10,这可能并不是很显眼,不过一路上我们学习了很多关于Mutex<T>
和线程安全的内容!这个例子中构建的结构可以用于比增加计数更为复杂的操作。可以被分解为独立部分的计算可以像这样被分散到多个线程中,并可以使用Mutex<T>
来允许每个线程在他们自己的部分更新最终的结果。
+你可能注意到了,因为counter
是不可变的,不过可以获取其内部值的可变引用,这意味着Mutex<T>
提供了内部可变性,就像Cell
系列类型那样。正如第十五章中使用RefCell<T>
可以改变Rc<T>
中的内容那样,同样的可以使用Mutex<T>
来改变Arc<T>
中的内容。
+回忆一下Rc<T>
并没有避免所有可能的问题:我们也讨论了当两个Rc<T>
相互引用时的引用循环的可能性,这可能造成内存泄露。Mutex<T>
有一个类似的 Rust 同样也不能避免的问题:死锁。死锁(deadlock)是一个场景中操作需要锁定两个资源,而两个线程分别拥有一个锁并永远相互等待的问题。如果你对这个主题感兴趣,尝试编写一个带有死锁的 Rust 程序,接着研究任何其他语言中使用互斥器的死锁规避策略并尝试在 Rust 中实现他们。标准库中Mutex<T>
和MutexGuard
的 API 文档会提供拥有的信息。
+Rust 的类型系统和所有权确保了线程在更新共享值时拥有独占的访问权限,所以线程不会以一种不可预测的方式覆盖彼此的操作。为了和编译器一起使一切正确运行花了一些时间,不过我们节省了未来可能需要重现只在线程以特定顺序执行才会出现的诡异错误场景的时间。
+接下来,为了丰富本章的内容,让我们讨论一下Send
和Sync
trait 以及如何对自定义类型使用他们。
+
diff --git a/docs/ch16-04-extensible-concurrency-sync-and-send.html b/docs/ch16-04-extensible-concurrency-sync-and-send.html
index 27877a8..dd90e16 100644
--- a/docs/ch16-04-extensible-concurrency-sync-and-send.html
+++ b/docs/ch16-04-extensible-concurrency-sync-and-send.html
@@ -47,7 +47,7 @@
@@ -67,7 +67,30 @@
-
+ 使用Sync
和Send
trait 的可扩展并发
+
+ch16-04-extensible-concurrency-sync-and-send.md
+
+commit 55b294f20fc846a13a9be623bf322d8b364cee77
+
+Rust 的并发模型中一个有趣的方面是语言本身对并发知道的很少。我们讨论过的几乎所有内容都是标准库的一部分,而不是语言本身的内容。因为并不需要语言提供任何用于并发上下文中的内容,并发选择也不仅限于标准库或语言所提供的:我们可以编写自己的或使用别人编写的内容。
+我们说了几乎所有内容都不在语言本身,那么位于语言本身的是什么呢?这是两个 trait,都位于std::marker
:Sync
和Send
。
+Send
用于表明所有权可能被传送给其他线程
+Send
标记 trait 表明类型的所有权可能被在线程间传递。几乎所有的 Rust 类型都是Send
的,不过有一些例外。标准库中提供的一个不是Send
的类型是Rc<T>
:如果克隆Rc<T>
值并尝试将克隆的所有权传递给另一个线程,这两个线程可能会同时更新引用计数。正如上一部分提到的,Rc<T>
被实现为用于单线程场景,这时不需要为拥有线程安全的引用计数而付出性能代价。
+因为Rc<T>
没有标记为Send
,Rust 的类型系统和 trait bound 会确保我们永远也不会忘记或错误的把一个Rc<T>
值不安全的在线程间传递。列表 16-14 曾尝试这么做,不过得到了一个错误说the trait Send is not implemented for Rc<Mutex<i32>>
。当切换为标记为Send
的Arc<T>
时,代码就可以编译了。
+任何完全由Send
的类型组成的类型也会自动被标记为Send
。几乎所有基本类型都是Send
的,除了第十九章将会讨论的裸指针(raw pointer)之外。大部分标准库类型是Send
的,除了Rc<T>
之外。
+Sync
表明多线程访问是安全的
+Sync
标记 trait 表明一个类型可以安全的在多个线程中拥有其值的引用。换一种方式来说就是对于任意类型T
,如果&T
(T
的引用)是Send
的话T
就是Sync
的,这样其引用就可以安全的发送到另一个线程。类似于Send
的情况,基本类型是Sync
的,完全由Sync
的类型组成的类型也是Sync
的。
+Rc<T>
也不是Sync
的,出于其不是Send
的相同的原因。RefCell<T>
(第十五章讨论过)和Cell<T>
系列类型不是Sync
的。RefCell<T>
在运行时所进行的借用检查也不是线程安全的。Mutex<T>
是Sync
的,正如上一部分所讲的它可以被用来在多线程中共享访问。
+手动实现Send
和Sync
是不安全的
+通常并不需要实现Send
和Sync
trait,因为由是Send
和Sync
的类型组成的类型也自动就是Send
和Sync
的了。因为他们是标记 trait,甚至都不需要实现任何方法。他们只是用来加强并发相关的不可变行性的。
+实现这些标记 trait 涉及到实现不安全的 Rust 代码。第十九章将会讲到如何使用不安全 Rust 代码;现在,重要的是在创建新的由不是Send
和Sync
的部分构成的并发类型时需要多加小心,以确保维持其安全保证。The Nomicon 中由更多关于这些保证和如何维持他们的信息。
+总结
+这不会是本书最后一个出现并发的章节;第二十章的项目会在更现实的场景中使用这些概念,而不像本章中讨论的这些小例子。
+正如我们提到的,因为 Rust 本身很少有处理并发的部分内容,有很多的并发方案都由 crate 实现。他们比标准库要发展的更快;请在网上搜索当前最新的用于多线程场景的 crate。
+Rust 提供了用于消息传递的通道,和像Mutex<T>
和Arc<T>
这样可以安全的用于并发上下文的智能指针。类型系统和借用检查器会确保这些场景中的代码不会出现数据竞争和无效的引用。一旦代码可以编译了,我们就可以坚信这些代码可以正确的运行于多线程环境,而不会出现其他语言中经常出现的那些难以追踪的 bug。并发编程不再是什么可怕的概念:无所畏惧的使你的程序使用并发吧!
+接下来,让我们讨论一下当 Rust 程序变得更大时那些符合习惯的模拟问题和结构的解决方案,以及 Rust 风格如何与面向对象编程(Object Oriented Programming)中那些你所熟悉的概念相联系。
+
@@ -78,6 +101,10 @@
+
+
+
+
@@ -88,6 +115,10 @@
+
+
+
+
diff --git a/docs/ch17-00-oop.html b/docs/ch17-00-oop.html
new file mode 100644
index 0000000..b629dbf
--- /dev/null
+++ b/docs/ch17-00-oop.html
@@ -0,0 +1,123 @@
+
+
+
+
+
面向对象 - Rust 程序设计语言 简体中文版
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Rust 是一个面向对象的编程语言吗?
+
+ch17-00-oop.md
+
+commit 759801361bde74b47e81755fff545c66020e6e63
+
+面向对象编程是一种起源于20世纪60年代Simula的模式化编程的方式,然后在90年代在C++语言开始流行。为了描述OOP有很多种复杂的定义:在一些定义下,Rust是面向对象的;在其他定义下,Rust不是。在本章节中,我们会探索一些被普遍认为是面向对象的特性和这些特性是如何转换为Rust的方言的。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/ch17-01-what-is-oo.html b/docs/ch17-01-what-is-oo.html
new file mode 100644
index 0000000..7efdbff
--- /dev/null
+++ b/docs/ch17-01-what-is-oo.html
@@ -0,0 +1,180 @@
+
+
+
+
+ 什么是面向对象 - Rust 程序设计语言 简体中文版
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 什么是面向对象?
+
+ch17-01-what-is-oo.md
+
+commit 46334522e22d6217b392451cff8b4feca2d69d79
+
+关于一门语言是否需要是面向对象,在编程社区内并达成一致意见。Rust被很多不同的编程模式影响,我们探索了13章提到的函数式编程的特性。面向对象编程语言的一些特性往往是对象、封装和继承。我们看一下每个的含义和Rust是否支持它们。
+对象包含数据和行为
+Design Patterns: Elements of Reusable Object-Oriented Software
这本书被俗称为The Gang of Four book
,是面向对象编程模式的目录。它这样定义面向对象编程:
+
+面向对象的程序是由对象组成的。一个对象包数据和操作这些数据的程序。程序通常被称为方法或操作。
+
+在这个定一下,Rust是面向对象的:结构体和枚举包含数据和impl块提供了在结构体和枚举上的方法。虽然带有方法的结构体和枚举不称为对象,但是他们提供了和对象相同的功能,使用了Gang of Four
定义的对象。
+隐藏了实现细节的封装
+通常与面向对象编程相关的另一个方面是封装的思想:对象的实现细节不能被使用对象的代码获取到。唯一与对象交互的方式是通过对象提供的public API,使用对象的代码无法深入到对象内部并直接改变数据或者行为。封装使得改变和重构对象的内部,无需改变使用对象的代码。
+就像我们在第7张讨论的那样,我们可以使用pub关键字来决定模块、类型函数和方法是public的(默认情况下一切都是private)。比如,我们可以定义一个结构体AveragedCollection
包含一个i32
类型的vector。结构体也可以有一个字段,该字段保存了vector中所有值的平均值。这样,希望知道结构体中的vector的平均值的人可以随着获取到,而无需自己计算。AveragedCollection
会为我们缓存平均值结果。 Listing 17-1有AveragedCollection
结构体的定义。
+Filename: src/lib.rs
+pub struct AveragedCollection {
+ list: Vec<i32>,
+ average: f64,
+}
+
+AveragedCollection
结构体维护了一个Integer列表和集合中所有元素的平均值。
+注意,结构体本身被标记为pub,这样其他代码可以使用这个结构体,但是在结构体内部的字段仍然是private。这是非常重要的,因为我们希望保证变量被增加到列表或者被从列表删除时,也会同时更新平均值。我们通过在结构体上实现add、remove和average方法来做到这一点( Listing 17-2:):
+Filename: src/lib.rs
+# pub struct AveragedCollection {
+# list: Vec<i32>,
+# average: f64,
+# }
+impl AveragedCollection {
+ pub fn add(&mut self, value: i32) {
+ self.list.push(value);
+ self.update_average();
+ }
+
+ pub fn remove(&mut self) -> Option<i32> {
+ let result = self.list.pop();
+ match result {
+ Some(value) => {
+ self.update_average();
+ Some(value)
+ },
+ None => None,
+ }
+ }
+
+ pub fn average(&self) -> f64 {
+ self.average
+ }
+
+ fn update_average(&mut self) {
+ let total: i32 = self.list.iter().sum();
+ self.average = total as f64 / self.list.len() as f64;
+ }
+}
+
+Listing 17-2:在AveragedCollection
结构体上实现了add、remove和average public方法
+public方法add
、remove
和average
是修改AveragedCollection
实例的唯一方式。当使用add方法把一个元素加入到list
或者使用remove
方法来删除它,这些方法的实现同时会调用私有的update_average
方法来更新average
成员变量。因为list
和average
是私有的,没有其他方式来使得外部的代码直接向list
增加或者删除元素,直接操作list
可能会引发average
字段不同步。average
方法返回average
字段的值,这指的外部的代码只能读取average
而不能修改它。
+因为我们已经封装好了AveragedCollection
的实现细节,所以我们也可以像使用list
一样使用的一个不同的数据结构,比如用HashSet
代替Vec
。只要签名add
、remove
和average
公有函数保持相同,使用AveragedCollection
的代码无需改变。如果我们暴露List
给外部代码时,未必都是这样,因为HashSet
和Vec
使用不同的函数增加元素,所以如果要想直接修改list
的话,外部的代码可能还得修改。
+如果封装是一个语言被认为是面向对象语言必要的方面的话,那么Rust满足要求。在代码中不同的部分使用或者不使用pub
决定了实现细节的封装。
+作为类型系统的继承和作为代码共享的继承
+继承是一个很多编程语言都提供的机制,一个对象可以从另外一个对象的定义继承,这使得可以获得父对象的数据和行为,而不用重新定义。很多人定义面向对象语言时,认为继承是一个特色。
+如果一个语言必须有继承才能被称为面向对象的语言,那么Rust就不是面向对象的。没有办法定义一个结构体继承自另外一个结构体,从而获得父结构体的成员和方法。然而,如果你过去常常在你的编程工具箱使用继承,依赖于你要使用继承的原因,在Rust中有其他的方式。
+使用继承有两个主要的原因。第一个是为了重用代码:一旦一个特殊的行为从一个类型继承,继承可以在另外一个类型实现代码重用。Rust代码可以被共享通过使用默认的trait方法实现,可以在Listing 10-14看到,我们增加一个summary
方法到Summarizable
trait。任何继承了Summarizable
trait的类型上会有summary
方法,而无需任何的父代码。这类似于父类有一个继承的方法,一个从父类继承的子类也因为继承有了继承的方法。当实现Summarizable
trait时,我们也可以选择覆写默认的summary
方法,这类似于子类覆写了从父类继承的实现方法。
+第二个使用继承的原因是,使用类型系统:子类型可以在父类型被使用的地方使用。这也称为多态,意味着如果多种对象有一个相同的shape,它们可以被其他替代。
+
+虽然很多人使用多态来描述继承,但是它实际上是一种特殊的多态,称为子类型多态。也有很多种其他形式,在Rust中带有通用的ttait绑定的一个参数
+也是多态——更特殊的类型多态。在多种类型的多态间的细节不是关键的,所以不要过于担心细节,只需要知道Rust有多种多态相关的特色就好,不像很多其他OOP语言。
+
+为了支持这种样式,Rust有trait对象,这样我们可以指定给任何类型的值,只要值实现了一种特定的trait。
+继承最近在很多编程语言的设计方案中失宠了。使用继承类实现代码重用需要共享比你需要共享的代码。子类不应该经常共享它们的父类的所有特色,但是继承意味着子类得到了它的父类的数据和行为。这使得一个程序的设计不灵活,创建了无意义的子类的方法被调用的可能性或者由于方法不适用于子类但是必须从父类继承,从而触发错误。另外,很多语言只允许从一个类继承,更加限制了程序设计的灵活性。
+因为这些原因,Rust选择了一个另外的途径,使用trait替代继承。让我们看一下在Rust中trait对象是如何实现多态的。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/index.html b/docs/index.html
index c7abfb0..3ee5a0f 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -46,7 +46,7 @@
@@ -70,14 +70,14 @@
ch01-00-introduction.md
-commit 4f2dc564851dc04b271a2260c834643dfd86c724
+commit 62f78bb3f7c222b574ff547d0161c2533691f9b4
欢迎阅读“Rust 程序设计语言”,一本关于 Rust 的介绍性书籍。Rust 是一个着用于安全、速度和并发的编程语言。它的设计不仅可以使程序获得性能和对底层语言的控制,并且能够享受高级语言强大的抽象能力。这些特性使得 Rust 适合那些有类似 C 语言经验并正在寻找一个更安全的替代者的程序员,同时也适合那些来自类似 Python 语言背景,正在探索在不牺牲表现力的情况下编写更好性能代码的开发者。
Rust 在编译时进行其绝大多数的安全检查和内存管理决策,因此程序的运行时性能没有受到影响。这让其在许多其他语言不擅长的应用场景中得以大显身手:存在可预测空间和时间要求的程序,嵌入到其他语言中,以及编写底层代码,如设备驱动和操作系统。Rust 也很擅长 web 程序:它驱动着 Rust 包注册网站(package
registry site),crates.io!我们期待看到你使用 Rust 进行创作。
本书是为已经至少了解一门编程语言的读者而写的。读完本书之后,你应该能自如的编写 Rust 程序。我们将通过小而集中并且相互依赖的例子来学习 Rust,并向你展示如何使用 Rust 多样的功能,同时了解它们在幕后是如何执行的。
为本书做出贡献
-本书是开源的。如果你发现任何错误,请不要犹豫,在 GitHub 上发起 issue 或提交 pull request。
+本书是开源的。如果你发现任何错误,请不要犹豫,在 GitHub 上发起 issue 或提交 pull request。请查看CONTRIBUTING.md获取更多信息。
译者注:这是本译本的 GitHub 仓库,同样欢迎 Issue 和 PR :)
diff --git a/docs/print.html b/docs/print.html
index d591de3..2275a73 100644
--- a/docs/print.html
+++ b/docs/print.html
@@ -2,7 +2,7 @@
- 可扩展的并发:`Sync`和`Send` - Rust 程序设计语言 简体中文版
+ 什么是面向对象 - Rust 程序设计语言 简体中文版
@@ -47,7 +47,7 @@
@@ -71,14 +71,14 @@
ch01-00-introduction.md
-commit 4f2dc564851dc04b271a2260c834643dfd86c724
+commit 62f78bb3f7c222b574ff547d0161c2533691f9b4
欢迎阅读“Rust 程序设计语言”,一本关于 Rust 的介绍性书籍。Rust 是一个着用于安全、速度和并发的编程语言。它的设计不仅可以使程序获得性能和对底层语言的控制,并且能够享受高级语言强大的抽象能力。这些特性使得 Rust 适合那些有类似 C 语言经验并正在寻找一个更安全的替代者的程序员,同时也适合那些来自类似 Python 语言背景,正在探索在不牺牲表现力的情况下编写更好性能代码的开发者。
Rust 在编译时进行其绝大多数的安全检查和内存管理决策,因此程序的运行时性能没有受到影响。这让其在许多其他语言不擅长的应用场景中得以大显身手:存在可预测空间和时间要求的程序,嵌入到其他语言中,以及编写底层代码,如设备驱动和操作系统。Rust 也很擅长 web 程序:它驱动着 Rust 包注册网站(package
registry site),crates.io!我们期待看到你使用 Rust 进行创作。
本书是为已经至少了解一门编程语言的读者而写的。读完本书之后,你应该能自如的编写 Rust 程序。我们将通过小而集中并且相互依赖的例子来学习 Rust,并向你展示如何使用 Rust 多样的功能,同时了解它们在幕后是如何执行的。
为本书做出贡献
-本书是开源的。如果你发现任何错误,请不要犹豫,在 GitHub 上发起 issue 或提交 pull request。
+本书是开源的。如果你发现任何错误,请不要犹豫,在 GitHub 上发起 issue 或提交 pull request。请查看CONTRIBUTING.md获取更多信息。
译者注:这是本译本的 GitHub 仓库,同样欢迎 Issue 和 PR :)
@@ -86,7 +86,7 @@ registry site),crates.io!我们期待看
ch01-01-installation.md
-commit 4f2dc564851dc04b271a2260c834643dfd86c724
+commit c1b95a18dbcbb06aadf07c03759f27d88ccf62cf
使用 Rust 的第一步是安装。你需要网络连接来执行本章的命令,因为我们要从网上下载 Rust。
我们将会展示很多使用终端的命令,并且这些代码都以$
开头。并不需要真正输入$
,它们在这里代表每行指令的开头。在网上会看到很多使用这个惯例的教程和例子:$
代表以常规用户运行命令,#
代表需要用管理员运行的命令。没有以$
(或#
)的行通常是之前命令的输出。
@@ -103,6 +103,10 @@ commit 4f2dc564851dc04b271a2260c834643dfd86c724
本书其余 Windows 相关的命令假设你使用cmd
作为你的 shell。如果你使用不同的 shell,可能能够执行 Linux 和 Mac 用户相同的命令。如果都不行,请查看所使用的 shell 的文档。
自定义安装
如果有理由倾向于不使用 rustup.rs,请查看Rust 安装页面获取其他选择。
+更新
+一旦安装完 Rust,更新到最新版本是简单的。在 shell 中运行更新脚本:
+$ rustup update
+
卸载
卸载 Rust 同安装一样简单。在 shell 中运行卸载脚本
$ rustup self uninstall
@@ -1294,7 +1298,7 @@ help: consider removing this semicolon:
8 | x + 1;
| ^
-主要的错误信息,“mismatched types,”(类型不匹配),揭示了代码的核心问题。函数plus_one
的定义说明它要返回一个i32
,不过语句并不返回一个值,这由那个空元组()
表明。因此,这个函数没有返回任何值,这与函数定义相矛盾并导致一个错误。在输出中,Rust 提供了一个可能会对修正问题有帮助的信息:它建议去掉分号,这会修复这个错误。
+主要的错误信息,“mismatched types,”(类型不匹配),揭示了代码的核心问题。函数plus_one
的定义说明它要返回一个i32
,不过语句并不返回一个值,这由那个空元组()
表明。因此,这个函数返回了空元组()
(译者注:原文说此函数没有返回任何值,可能有误),这与函数定义相矛盾并导致一个错误。在输出中,Rust 提供了一个可能会对修正问题有帮助的信息:它建议去掉分号,这会修复这个错误。
注释
ch03-04-comments.md
@@ -1541,8 +1545,7 @@ the value is: 50
所有数组中的五个元素都如期被打印出来。尽管index
在某一时刻会到达值5
,不过循环在其尝试从数组获取第六个值(会越界)之前就停止了。
不过这个过程是容易出错的;如果索引长度不正确会导致程序 panic。这也使程序更慢,因为编译器增加了运行时代码来对每次循环的每个元素进行条件检查。
可以使用for
循环来对一个集合的每个元素执行一些代码,来作为一个更有效率替代。for
循环看起来像这样:
-
+
当运行这段代码,将看到与列表 3-5 一样的输出。更为重要的是,我们增强了代码安全性并消除了出现可能会导致超出数组的结尾或遍历长度不够而缺少一些元素这类 bug 机会。
例如,在列表 3-5 的代码中,如果从数组a
中移除一个元素但忘记更新条件为while index < 4
,代码将会 panic。使用for
循环的话,就不需要惦记着在更新数组元素数量时修改其他的代码了。
for
循环的安全性和简洁性使得它在成为 Rust 中使用最多的循环结构。即使是在想要循环执行代码特定次数时,例如列表 3-5 中使用while
循环的倒计时例子,大部分 Rustacean 也会使用for
循环。这么做的方式是使用Range
,它是标准库提供的用来生成从一个数字开始到另一个数字结束的所有数字序列的类型。
@@ -1634,7 +1635,7 @@ valid
- 当
s
进入作用域,它就是有效的。
- 这一直持续到它离开作用域为止。
-目前为止,变量是否有效与作用域的关系跟其他变成语言是类似的。现在我们在此基础上介绍String
类型。
+目前为止,变量是否有效与作用域的关系跟其他编程语言是类似的。现在我们在此基础上介绍String
类型。
String
类型
为了演示所有权的规则,我们需要一个比第三章讲到的任何一个都要复杂的数据类型。之前出现的数据类型都是储存在栈上的并且当离开作用域时被移出栈,不过我们需要寻找一个储存在堆上的数据来探索 Rust 如何知道该在何时清理数据的。
这里使用String
作为例子并专注于String
与所有权相关的部分。这些方面也同样适用于其他标准库提供的或你自己创建的复杂数据类型。在第八章会更深入地讲解String
。
@@ -1717,7 +1718,7 @@ println!("{}", s1);
= note: move occurs because `s1` has type `std::string::String`,
which does not implement the `Copy` trait
-如果你在其他语言中听说过术语“浅拷贝”(“shallow copy”)和“深拷贝”(“deep copy”),那么拷贝指针、长度和容量而不拷贝数据可能听起来像浅拷贝。不过因为 Rust 同时使第一个变量无效化了,这个操作被成为移动(move),而不是浅拷贝。上面的例子可以解读为s1
被移动到了s2
中。那么具体发生了什么如图 4-6 所示。
+如果你在其他语言中听说过术语“浅拷贝”(“shallow copy”)和“深拷贝”(“deep copy”),那么拷贝指针、长度和容量而不拷贝数据可能听起来像浅拷贝。不过因为 Rust 同时使第一个变量无效化了,这个操作被称为移动(move),而不是浅拷贝。上面的例子可以解读为s1
被移动到了s2
中。那么具体发生了什么如图 4-6 所示。
@@ -1812,7 +1813,7 @@ fn takes_and_gives_back(a_string: String) -> String { // a_string comes into
a_string // a_string is returned and moves out to the calling function.
}
-变量的所有权总是遵循相同的模式:将值赋值给另一个变量时移动它,并且当变量值的堆书卷离开作用域时,如果数据的所有权没有被移动到另外一个变量时,其值将通过drop
被清理掉。
+变量的所有权总是遵循相同的模式:将值赋值给另一个变量时移动它,并且当持有堆中数据值的变量离开作用域时,如果数据的所有权没有被移动到另外一个变量时,其值将通过drop
被清理掉。
在每一个函数中都获取并接着返回所有权是冗余乏味的。如果我们想要函数使用一个值但不获取所有权改怎么办呢?如果我们还要接着使用它的话,每次都传递出去再传回来就有点烦人了,另外我们也可能想要返回函数体产生的任何(不止一个)数据。
使用元组来返回多个值是可能的,像这样:
Filename: src/main.rs
@@ -1835,7 +1836,7 @@ fn calculate_length(s: String) -> (String, usize) {
ch04-02-references-and-borrowing.md
-commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56
+commit 5e0546f53cce14b126527d9ba6d1b8eb212b4f3d
在上一部分的结尾处的使用元组的代码是有问题的,我们需要将String
返回给调用者函数这样就可以在调用calculate_length
后仍然可以使用String
了,因为String
先被移动到了calculate_length
。
下面是如何定义并使用一个(新的)calculate_length
函数,它以一个对象的引用作为参数而不是获取值的所有权:
@@ -1967,7 +1968,7 @@ immutable
哇哦!我们也不能在拥有不可变引用的同时拥有可变引用。不可变引用的用户可不希望在它的眼皮底下值突然就被改变了!然而,多个不可变引用是没有问题的因为没有哪个读取数据的人有能力影响其他人读取到的数据。
即使这些错误有时是使人沮丧的。记住这是 Rust 编译器在提早指出一个潜在的 bug(在编译时而不是运行时)并明确告诉你问题在哪而不是任由你去追踪为何有时数据并不是你想象中的那样。
悬垂引用
-在存在指针的语言中,容易错误地生成一个悬垂指针(dangling pointer),一个引用某个内存位置的指针,这个内存可能已经因为被分配给别人,因为释放内存时指向内存的指针被保留了下来。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当我们拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。
+在存在指针的语言中,容易通过释放内存时保留指向它的指针而错误地生成一个悬垂指针(dangling pointer),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者,。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当我们拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。
让我们尝试创建一个悬垂引用:
Filename: src/main.rs
fn main() {
@@ -2007,7 +2008,7 @@ for it to be borrowed from.
// Danger!
因为s
是在dangle
创建的,当dangle
的代码执行完毕后,s
将被释放。不过我们尝试返回一个它的引用。这意味着这个引用会指向一个无效的String
!这可不好。Rust 不会允许我们这么做的。
-正确的代码是直接返回String
:
+这里的解决方法是直接返回String
:
fn no_dangle() -> String {
let s = String::from("hello");
@@ -2070,7 +2071,7 @@ byte index value into the String
parameter
}
s.len()
-现在有了一个找到字符串中第一个单词结尾索引的方法了,不过这有一个问题。我们返回了单单一个usize
,不过它只在&String
的上下文中才是一个有意义的数字。换句话说,因为它是一个与String
像分离的值,无法保证将来它仍然有效。考虑一下列表 4-11 中使用了列表 4-10 first_word
函数的程序:
+现在有了一个找到字符串中第一个单词结尾索引的方法了,不过这有一个问题。我们返回了单单一个usize
,不过它只在&String
的上下文中才是一个有意义的数字。换句话说,因为它是一个与String
相分离的值,无法保证将来它仍然有效。考虑一下列表 4-11 中使用了列表 4-10 first_word
函数的程序:
Filename: src/main.rs
# fn first_word(s: &String) -> usize {
# let bytes = s.as_bytes();
@@ -2231,7 +2232,7 @@ let slice = &a[1..3];
所有权系统影响了 Rust 中其他很多部分如何工作,所以我们会继续讲到这些概念,贯穿本书的余下内容。让我们开始下一个章节,来看看如何将多份数据组合进一个struct
中。
结构体
-ch05-00-structs.md
commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56
@@ -2374,7 +2375,7 @@ fn area(rectangle: &Rectangle) -> u32 {
这里我们定义了一个结构体并称其为Rectangle
。在{}
中定义了字段length
和width
,都是u32
类型的。接着在main
中,我们创建了一个长度为 50 和宽度为 30 的Rectangle
的具体实例。
函数area
现在被定义为接收一个名叫rectangle
的参数,它的类型是一个结构体Rectangle
实例的不可变借用。第四章讲到过,我们希望借用结构体而不是获取它的所有权这样main
函数就可以保持rect1
的所有权并继续使用它,所以这就是为什么在函数签名和调用的地方会有&
。
-area
函数访问Rectangle
的length
和width
字段。area
的签名现在明确的表明了我们的意图:通过其length
和width
字段,计算一个Rectangle
的面积,。这表明了长度和宽度是相互联系的,并为这些值提供了描述性的名称而不是使用元组的索引值0
和1
。这是明确性的胜利。
+area
函数访问Rectangle
的length
和width
字段。area
的签名现在明确的表明了我们的意图:通过其length
和width
字段,计算一个Rectangle
的面积,。这表明了长度和宽度是相互联系的,并为这些值提供了描述性的名称而不是使用元组的索引值0
和1
。结构体胜在更清晰明了。
通过衍生 trait 增加实用功能
如果能够在调试程序时打印出Rectangle
实例来查看其所有字段的值就更好了。列表 5-5 尝试像往常一样使用println!
宏:
Filename: src/main.rs
@@ -2435,7 +2436,7 @@ trait and printing the Rectangle
instance using debug formatting我们的area
函数是非常明确的————它只是计算了长方形的面积。如果这个行为与Rectangle
结构体再结合得更紧密一些就更好了,因为这明显就是Rectangle
类型的行为。现在让我们看看如何继续重构这些代码,来将area
函数协调进Rectangle
类型定义的area
方法中。
方法语法
-ch05-01-method-syntax.md
commit 8c1c1a55d5c0f9bc3c866ee79b267df9dc5c04e2
@@ -2469,7 +2470,7 @@ struct
为了使函数定义于Rectangle
的上下文中,我们开始了一个impl
块(impl
是 implementation 的缩写)。接着将函数移动到impl
大括号中,并将签名中的第一个(在这里也是唯一一个)参数和函数体中其他地方的对应参数改成self
。然后在main
中将我们调用area
方法并传递rect1
作为参数的地方,改成使用方法语法在Rectangle
实例上调用area
方法。方法语法获取一个实例并加上一个点号后跟方法名、括号以及任何参数。
在area
的签名中,开始使用&self
来替代rectangle: &Rectangle
,因为该方法位于impl Rectangle
上下文中所以 Rust 知道self
的类型是Rectangle
。注意仍然需要在self
前面加上&
,就像&Rectangle
一样。方法可以选择获取self
的所有权,像我们这里一样不可变的借用self
,或者可变的借用self
,就跟其他别的参数一样。
-这里选择&self
跟在函数版本中使用&Rectangle
出于同样的理由:我们并不想获取所有权,只希望能够读取结构体中的数据,而不是写入。如果想要能够在方法中改变调用方法的实例的话,需要将抵押给参数改为&mut self
。通过仅仅使用self
作为第一个参数来使方法获取实例的所有权,不过这是很少见的;这通常用在当方法将self
转换成别的实例的时候,同时我们想要防止调用者在转换之后使用原始的实例。
+这里选择&self
跟在函数版本中使用&Rectangle
出于同样的理由:我们并不想获取所有权,只希望能够读取结构体中的数据,而不是写入。如果想要能够在方法中改变调用方法的实例的话,需要将第一个参数改为&mut self
。通过仅仅使用self
作为第一个参数来使方法获取实例的所有权,不过这是很少见的;这通常用在当方法将self
转换成别的实例的时候,同时我们想要防止调用者在转换之后使用原始的实例。
使用方法而不是函数,除了使用了方法语法和不需要在每个函数签名中重复self
类型外,其主要好处在于组织性。我将某个类型实例能做的所有事情都一起放入impl
块中,而不是让将来的用户在我们的代码中到处寻找Rectangle
的功能。
@@ -2496,7 +2497,7 @@ struct
p1.distance(&p2);
(&p1).distance(&p2);
-第一行看起来简洁的多。这种自动引用的行为之所以能行得通是因为方法有一个明确的接收者————self
的类型。在给出接收者和方法名的前提下,Rust 可以明确的计算出方法是仅仅读取(所以需要&self
),做出修改(所以是&mut self
)或者是获取所有权(所以是self
)。Rust 这种使得借用对方法接收者来说是隐式的做法是其所有权系统人体工程学实践的一大部分。
+第一行看起来简洁的多。这种自动引用的行为之所以能行得通是因为方法有一个明确的接收者————self
的类型。在给出接收者和方法名的前提下,Rust 可以明确的计算出方法是仅仅读取(所以需要&self
),做出修改(所以是&mut self
)或者是获取所有权(所以是self
)。Rust 这种使得借用对方法接收者来说是隐式的做法是其所有权系统程序员友好性实现的一大部分。
带有更多参数的方法
@@ -2561,7 +2562,7 @@ impl Rectangle {
结构体并不是创建自定义类型的唯一方法;让我们转向 Rust 的enum
功能并为自己的工具箱再填一个工具。
枚举和模式匹配
-ch06-00-enums.md
commit 4f2dc564851dc04b271a2260c834643dfd86c724
@@ -2569,12 +2570,12 @@ commit 4f2dc564851dc04b271a2260c834643dfd86c724
枚举是一个很多语言都有的功能,不过不同语言中的功能各不相同。Rust 的枚举与像F#、OCaml 和 Haskell这样的函数式编程语言中的代数数据类型(algebraic data types)最为相似。
定义枚举
-ch06-01-defining-an-enum.md
commit e6d6caab41471f7115a621029bd428a812c5260e
-让我们通过一用代码来表现的场景,来看看为什么这里枚举是有用的而且比结构体更合适。比如我们要处理 IP 地。目前被广泛使用的两个主要 IP 标准:IPv4(version four)和 IPv6(version six)。这是我们的程序只可能会遇到两种 IP 地址:我们可以枚举出所有可能的值,这也正是它名字的由来。
-任何一个 IP 地址要么是 IPv4 的要么是 IPv6 的而不能两者都是。IP 地址的这个特性使得枚举数据结构非常适合这个场景,因为枚举值尽可能是其一个成员。IPv4 和 IPv6 从根本上讲都是 IP 地址,所以当代码在处理申请任何类型的 IP 地址的场景时应该把他们当作相同的类型。
+让我们通过一用代码来表现的场景,来看看为什么这里枚举是有用的而且比结构体更合适。比如我们要处理 IP 地。目前被广泛使用的两个主要 IP 标准:IPv4(version four)和 IPv6(version six)。这是我们的程序只可能会遇到两种 IP 地址:所以可以枚举出所有可能的值,这也正是它名字的由来。
+任何一个 IP 地址要么是 IPv4 的要么是 IPv6 的而不能两者都是。IP 地址的这个特性使得枚举数据结构非常适合这个场景,因为枚举值只可能是其中一个成员。IPv4 和 IPv6 从根本上讲仍是 IP 地址,所以当代码在处理申请任何类型的 IP 地址的场景时应该把他们当作相同的类型。
可以通过在代码中定义一个IpAddrKind
枚举来表现这个概念并列出可能的 IP 地址类型,V4
和V6
。这被称为枚举的成员(variants):
enum IpAddrKind {
V4,
@@ -2611,8 +2612,7 @@ fn route(ip_type: IpAddrKind) { }
route(IpAddrKind::V4);
route(IpAddrKind::V6);
-使用枚举甚至还有更多优势。进一步考虑一下我们的 IP 地址类型,目前没有一个储存实际 IP 地址数据的方法;只知道它是什么类型的。考虑到已经在第五章学习过结构体了,你可以想如列表 6-1 那样修改这个问题:
-
+
这里我们定义了一个有两个字段的结构体IpAddr
:kind
字段是IpAddrKind
(之前定义的枚举)类型的而address
字段是String
类型的。这里有两个结构体的实例。第一个,home
,它的kind
的值是IpAddrKind::V4
与之相关联的地址数据是127.0.0.1
。第二个实例,loopback
,kind
的值是IpAddrKind
的另一个成员,V6
,关联的地址是::1
。我们使用了要给结构体来将kind
和address
打包在一起,现在枚举成员就与值相关联了。
我们可以使用一种更简洁的方式来表达相同的概念,仅仅使用枚举并将数据直接放进每一个枚举成员而不是将枚举作为结构体的一部分。IpAddr
枚举的新定义表明了V4
和V6
成员都关联了String
值:
enum IpAddr {
@@ -2660,7 +2657,7 @@ let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
-这些代码展示了使用枚举来储存两种不同 IP 地址的几种可能的选择。然而,事实证明储存和编码 IP 地址实在是太常见了以致标准库提供了一个可供使用的定义!让我们看看标准库如何定义IpAddr
的:它正有着跟我们定义和使用的一样的枚举和成员,不过它将成员种的地址数据嵌入到了两个不同形式的结构体中,他们对不同的成员的定义是不同的:
+这些代码展示了使用枚举来储存两种不同 IP 地址的几种可能的选择。然而,事实证明储存和编码 IP 地址实在是太常见了以致标准库提供了一个可供使用的定义!让我们看看标准库如何定义IpAddr
的:它正有着跟我们定义和使用的一样的枚举和成员,不过它将成员中的地址数据嵌入到了两个不同形式的结构体中,他们对不同的成员的定义是不同的:
struct Ipv4Addr {
// details elided
}
@@ -2674,10 +2671,9 @@ enum IpAddr {
V6(Ipv6Addr),
}
-这些代码展示了可以将任意类型的数据放入枚举成员中:例如字符串、数字类型或者结构体。甚至可以包含另一个枚举!另外,标准库中的类型通常并不比你可能设想出来的要复杂多少。
+这些代码展示了可以将任意类型的数据放入枚举成员中:例如字符串、数字类型或者结构体。甚至可以包含另一个枚举!另外,标准库中的类型通常并不比你设想出来的要复杂多少。
注意虽然标准库中包含一个IpAddr
的定义,仍然可以创建和使用我们自己的定义而不会有冲突,因为我们并没有将标准库中的定义引入作用域。第七章会讲到如何导入类型。
来看看列表 6-2 中的另一个枚举的例子:它的成员中内嵌了多种多样的类型:
-
+
这个枚举有四个含有不同类型的成员:
Quit
没有关联任何数据。
@@ -2697,7 +2690,7 @@ types of values
Write
包含单独一个String
。
ChangeColor
包含三个i32
。
-定义一个像列表 6-2 中的枚举类似于定义不同类型的结构体,除了枚举不使用struct
关键字而且所有成员都被组合在一起位于Message
下。如下这些结构体可以包含与之前枚举成员中相同的数据:
+定义一个像列表 6-2 中的枚举类似于定义不同类型的结构体,除了枚举不使用struct
关键字而且所有成员都被组合在一起位于Message
下之外。如下这些结构体可以包含与之前枚举成员中相同的数据:
struct QuitMessage; // unit struct
struct MoveMessage {
x: i32,
@@ -2728,14 +2721,22 @@ m.call();
让我们看看标准库中的另一个非常常见和实用的枚举:Option
。
Option
枚举和其相对空值的优势
在之前的部分,我们看到了IpAddr
枚举如何利用 Rust 的类型系统编码更多信息而不单单是程序中的数据。这一部分探索一个Option
的案例分析,它是标准库定义的另一个枚举。Option
类型应用广泛因为它编码了一个非常普遍的场景,就是一个值可能是某个值或者什么都不是。从类型系统的角度来表达这个概念就意味着编译器需要检查是否处理了所有应该处理的情况,这样就可以避免在其他编程语言中非常常见的 bug。
-编程语言的设计经常从其包含功能的角度考虑问题,但是从不包含的功能的角度思考也很重要。Rust 并没有很多其他语言中有的空值功能。空值(Null )是一个值它代表没有值。在有空值的语言中,变量总是这两种状态之一:空值和非空值。
+编程语言的设计经常从其包含功能的角度考虑问题,但是从其所没有的功能的角度思考也很重要。Rust 并没有很多其他语言中有的空值功能。空值(Null )是一个值,它代表没有值。在有空值的语言中,变量总是这两种状态之一:空值和非空值。
在“Null References: The Billion Dollar Mistake”中,Tony Hoare,null 的发明者,曾经说到:
+I call it my billion-dollar mistake. At that time, I was designing the first
+comprehensive type system for references in an object-oriented language. My
+goal was to ensure that all use of references should be absolutely safe, with
+checking performed automatically by the compiler. But I couldn't resist the
+temptation to put in a null reference, simply because it was so easy to
+implement. This has led to innumerable errors, vulnerabilities, and system
+crashes, which have probably caused a billion dollars of pain and damage in
+the last forty years.
我称之为我万亿美元的错误。当时,我在在一个面向对象语言设计第一个综合性的面向引用的类型系统。我的目标是通过编译器的自动检查来保证所有引用的应有都应该是绝对安全的。不过我未能抗拒引入一个空引用的诱惑,仅仅是因为它是这么的容易实现。这引发了无数错误、漏洞和系统崩溃,在之后的四十多年中造成了数以万计美元的苦痛和伤害。
空值的为题在于当你尝试像一个非空值那样使用一个空值,会出现某种形式的错误。因为空和非空的属性是无处不在的,非常容易出现这类错误。
然而,空值尝试表达的概念仍然是有意义的:空值是一个因为某种原因目前无效或缺失的值。
-问题不在于实际的概念而在于具体的实现。为此,Rust 并没有空值,不过它确实拥有一个可以编码存在或不存在概念的枚举。这个枚举是Option<T>
,而且它定义于标准库中,如下:
+问题不在于具体的概念而在于特定的实现。为此,Rust 并没有空值,不过它确实拥有一个可以编码存在或不存在概念的枚举。这个枚举是Option<T>
,而且它定义于标准库中,如下:
enum Option<T> {
Some(T),
None,
@@ -2766,20 +2767,19 @@ not satisfied
|
哇哦!事实上,错误信息意味着 Rust 不知道该如何将Option<i8>
与i8
相加。当在 Rust 中拥有一个像i8
这样类型的值时,编译器确保它总是有一个有效的值。我们可以自信使用而无需判空。只有当使用Option<i8>
(或者任何用到的类型)是需要担心可能没有一个值,而编译器会确保我们在使用值之前处理为空的情况。
-换句话说,在对Option<T>
进行T
的运算之前必须转为T
。通常这能帮助我们捕获空值最常见的问题之一:假设某值不为空但实际上为空。
-无需担心错过非空值的假设(和处理)让我们对代码更加有信心,为了拥有一个可能为空的值,必须显式的将其放入对应类型的Option<T>
中。接着,当使用这个值时,必须明确的处理值为空的情况。任何地方一个值不是Option<T>
类型的话,可以安全的假设它的值不为空。这是 Rust 的一个有意为之的设计选择,来限制空值的泛滥和增加 Rust 代码的安全性。
-那么当有一个Option<T>
的值时,如何从Some
成员中取出T
的值来使用它呢?Option<T>
枚举拥有大量用于各种情况的方法:你可以查看相关代码。熟悉Option<T>
的方法将对你的 Rust 之旅提供巨大的帮助。
+换句话说,在对Option<T>
进行T
的运算之前必须转为T
。通常这能帮助我们捕获空值最常见的问题之一:假设某值不为空但实际上为空的情况。
+无需担心错过存在非空值的假设让我们对代码更加有信心,为了拥有一个可能为空的值,必须显式的将其放入对应类型的Option<T>
中。接着,当使用这个值时,必须明确的处理值为空的情况。任何地方一个值不是Option<T>
类型的话,可以安全的假设它的值不为空。这是 Rust 的一个有意为之的设计选择,来限制空值的泛滥和增加 Rust 代码的安全性。
+那么当有一个Option<T>
的值时,如何从Some
成员中取出T
的值来使用它呢?Option<T>
枚举拥有大量用于各种情况的方法:你可以查看相关代码。熟悉Option<T>
的方法将对你的 Rust 之旅提供巨大的帮助。
总的来说,为了使用Option<T>
值,需要编写处理每个成员的代码。我们想要一些代码只当拥有Some(T)
值时运行,这些代码允许使用其中的T
。也希望一些代码当在None
值时运行,这些代码并没有一个可用的T
值。match
表达式就是这么一个处理枚举的控制流结构:它会根据枚举的成员运行不同的代码,这些代码可以使用匹配到的值中的数据。
match
控制流运算符
-ch06-02-match.md
-commit 396e2db4f7de2e5e7869b1f8bc905c45c631ad7d
+commit 64090418c23d615facfe49a8d548ad9baea6b097
-Rust 有一个叫做match
的极为强大的控制流运算符,它允许我们将一个值与一系列的模式相比较并根据匹配的模式执行代码。模式可由字面值、变量、通配符和许多其他内容构成;第十八章会讲到所有不同种类的模式以及他们的作用。match
的力量来源于模式的表现力以及编译器检查,它确保了所有可能的情况都得到处理。
-把match
表达式想象成某种硬币分啦机:硬币滑入有着不同大小孔洞的轨道,每一个硬币都会掉入符合它大小的孔洞。同样地,值也会检查match
的每一个模式,并且在遇到第一个“符合”的模式时,值会进入相关联的代码块并在执行中被使用。
+Rust 有一个叫做match
的极为强大的控制流运算符,它允许我们将一个值与一系列的模式相比较并根据匹配的模式执行代码。模式可由字面值、变量、通配符和许多其他内容构成;第十八章会涉及到所有不同种类的模式以及他们的作用。match
的力量来源于模式的表现力以及编译器检查,它确保了所有可能的情况都得到处理。
+把match
表达式想象成某种硬币分类器:硬币滑入有着不同大小孔洞的轨道,每一个硬币都会掉入符合它大小的孔洞。同样地,值也会检查match
的每一个模式,并且在遇到第一个“符合”的模式时,值会进入相关联的代码块并在执行中被使用。
因为刚刚提到了硬币,让我们用他们来作为一个使用match
的例子!我们可以编写一个函数来获取一个未知的(美国)硬币,并以一种类似验钞机的方式,确定它是何种硬币并返回它的美分值,如列表 6-3 中所示:
-
+
拆开value_in_cents
函数中的match
来看。首先,我们列出match
关键字后跟一个表达式,在这个例子中是coin
的值。这看起来非常像if
使用的表达式,不过这里有一个非常大的区别:对于if
,表达式必须返回一个布尔值。而这里它可以是任何类型的。例子中的coin
的类型是列表 6-3 中定义的Coin
枚举。
接下来是match
的分支。一个分支有两个部分:一个模式和一些代码。第一个分支的模式是值Coin::Penny
而之后的=>
运算符将模式和将要运行的代码分开。这里的代码就仅仅是值1
。每一个分支之间使用逗号分隔。
+当match
表达式执行时,它将结果值按顺序与每一个分支的模式相比较,如果模式匹配了这个值,这个模式相关联的代码将被执行。如果模式并不匹配这个值,将继续执行下一个分支,非常像一个硬币分类器。可以拥有任意多的分支:列表 6-3 中的match
有四个分支。
每个分支相关联的代码是一个表达式,而表达式的结果值将作为整个match
表达式的返回值。
-如果分支代码较短的话可以不适用大括号,正如列表 6-3 中的每个分支都只是返回一个值。如果想要在分支中运行多行代码,可以使用大括号。例如,如下代码在每次使用Coin::Penny
调用时都会打印出“Lucky penny!”,同时仍然返回代码块最后的值,1
:
+如果分支代码较短的话通常不使用大括号,正如列表 6-3 中的每个分支都只是返回一个值。如果想要在分支中运行多行代码,可以使用大括号。例如,如下代码在每次使用Coin::Penny
调用时都会打印出“Lucky penny!”,同时仍然返回代码块最后的值,1
:
# enum Coin {
# Penny,
# Nickel,
@@ -2826,8 +2824,7 @@ fn value_in_cents(coin: Coin) -> i32 {
绑定值的模式
匹配分支的另一个有用的功能是可以绑定匹配的模式的部分值。这也就是如何从枚举成员中提取值。
-作为一个例子,让我们修改枚举的一个成员来存放数据。1999 年到 2008 年间,美帝在 25 美分的硬币的一侧为 50 个州每一个都印刷了不同的设计。其他的硬币都没有这种区分州的设计,所以只有这些 25 美分硬币有特殊的价值。可以将这些信息加入我们的enum
,通过改变Quarter
成员来包含一个State
值,列表 6-4 中完成了这些修改:
-
-想象一下我们的一个朋友尝试收集所有 50 个州的 25 美分硬币。在根据硬币类型分类零钱的同时,也可以报告出每个 25 美分硬币所对应的州名称,这样如何我们的朋友没有的话,他可以把它加入收藏。
-在这些代码的匹配表达式中,我们在匹配Coin::Quarter
成员的分支的模式中增加了一个叫做state
的变量。当匹配到Coin::Quarter
时,变量state
将会绑定 25 美分硬币所对应州的值。接着在代码那个分支中使用state
,如下:
+
+想象一下我们的一个朋友尝试收集所有 50 个州的 25 美分硬币。在根据硬币类型分类零钱的同时,也可以报告出每个 25 美分硬币所对应的州名称,这样如果我们的朋友没有的话,他可以把它加入收藏。
+在这些代码的匹配表达式中,我们在匹配Coin::Quarter
成员的分支的模式中增加了一个叫做state
的变量。当匹配到Coin::Quarter
时,变量state
将会绑定 25 美分硬币所对应州的值。接着在那个分支的代码中使用state
,如下:
# #[derive(Debug)]
# enum UsState {
# Alabama,
@@ -2877,9 +2871,8 @@ fn value_in_cents(coin: Coin) -> i32 {
如果调用value_in_cents(Coin::Quarter(UsState::Alaska))
,coin
将是Coin::Quarter(UsState::Alaska)
。当将值与每个分支相比较时,没有分支会匹配知道遇到Coin::Quarter(state)
。这时,state
绑定的将会是值UsState::Alaska
。接着就可以在println!
表达式中使用这个绑定了,像这样就可以获取Coin
枚举的Quarter
成员中内部的州的值。
匹配Option<T>
在之前的部分在使用Option<T>
时我们想要从Some
中取出其内部的T
值;也可以像处理Coin
枚举那样使用match
处理Option<T>
!与其直接比较硬币,我们将比较Option<T>
的成员,不过match
表达式的工作方式保持不变。
-比如想要编写一个函数,它获取一个Option<i32>
并且如果其中有一个值,将其加一。如果其中没有值,函数应该返回None
值并不尝试执行任何操作。
+比如我们想要编写一个函数,它获取一个Option<i32>
并且如果其中有一个值,将其加一。如果其中没有值,函数应该返回None
值并不尝试执行任何操作。
编写这个函数非常简单,得益于match
,它将看起来像列表 6-5 中这样:
-
+
匹配Some(T)
更仔细的检查plus_one
的第一行操作。当调用plus_one(five)
时,plus_one
函数体中的x
将会是值Some(5)
。接着将其与每个分支比较。
None => None,
@@ -2908,7 +2899,7 @@ let none = plus_one(None);
None => None,
匹配上了!这里没有值来加一,所以程序结束并返回=>
右侧的值None
,因为第一个分支就匹配到了,其他的分支将不再比较。
-将match
与枚举相结合在很多场景中都是有用的。你会在 Rust 代码中看到很多这样的模式:match
一个枚举,绑定其中的值到一个变量,接着根据其值执行代码。这在一开有点复杂,不过一旦习惯了,你将希望所有语言都拥有它!这一直是用户的最爱。
+将match
与枚举相结合在很多场景中都是有用的。你会在 Rust 代码中看到很多这样的模式:match
一个枚举,绑定其中的值到一个变量,接着根据其值执行代码。这在一开有点复杂,不过一旦习惯了,你会希望所有语言都拥有它!这一直都是用户的最爱。
匹配是穷尽的
match
还有另一方面需要讨论。考虑一下plus_one
函数的这个版本:
fn plus_one(x: Option<i32>) -> Option<i32> {
@@ -2940,23 +2931,19 @@ match some_u8_value {
然而,match
在只关心一个情况的场景中可能就有点啰嗦了。为此 Rust 提供了if let
。
if let
简单控制流
-ch06-03-if-let.md
-commit 396e2db4f7de2e5e7869b1f8bc905c45c631ad7d
+commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56
if let
语法让我们以一种不那么冗长的方式结合if
和let
,来处理匹配一个模式的值而忽略其他的值。考虑列表 6-6 中的程序,它匹配一个Option<u8>
值并只希望当值是三时执行代码:
-
+
我们想要对Some(3)
匹配进行操作不过不想处理任何其他Some<u8>
值或None
值。为了满足match
表达式(穷尽性)的要求,必须在处理完这唯一的成员后加上_ => ()
,这样也要增加很多样板代码。
不过我们可以使用if let
这种更短的方式编写。如下代码与列表 6-6 中的match
行为一致:
# let some_u8_value = Some(0u8);
@@ -3012,12 +2999,12 @@ if let Coin::Quarter(state) = coin {
总结
现在我们涉及到了如何使用枚举来创建有一系列可列举值的自定义类型。我们也展示了标准库的Option<T>
类型是如何帮助你利用类型系统来避免出错。当枚举值包含数据时,你可以根据需要处理多少情况来选择使用match
或if let
来获取并使用这些值。
你的 Rust 程序现在能够使用结构体和枚举在自己的作用域内表现其内容了。在你的 API 中使用自定义类型保证了类型安全:编译器会确保你的函数只会得到它期望的类型的值。
-为了向你的用户提供一个组织良好的 API,它使用直观且只向用户暴露他们确实需要的部分,那么让我们转向 Rust 的模块系统吧。
+为了向你的用户提供一个组织良好的 API,它使用起来很直观并且只向用户暴露他们确实需要的部分,那么现在就让我们转向 Rust 的模块系统吧。
模块
-ch07-00-modules.md
-commit e2a129961ae346f726f8b342455ec2255cdfed68
+commit 4f2dc564851dc04b271a2260c834643dfd86c724
在你刚开始编写 Rust 程序时,代码可能仅仅位于main
函数里。随着代码数量的增长,最终你会将功能移动到其他函数中,为了复用也为了更好的组织。通过将代码分隔成更小的块,每一个块代码自身就更易于理解。不过当你发现自己有太多的函数了该怎么办呢?Rust 有一个模块系统来处理编写可复用代码同时保持代码组织度的问题。
就跟你将代码行提取到一个函数中一样,也可以将函数(和其他类似结构体和枚举的代码)提取到不同模块中。模块(module)是一个包含函数或类型定义的命名空间,你可以选择这些定义是能(公有)还是不能(私有)在其模块外可见。这是一个模块如何工作的概括:
@@ -3029,11 +3016,11 @@ commit e2a129961ae346f726f8b342455ec2255cdfed68
我们会逐一了解这每一部分并学习如何将他们结合在一起。
mod
和文件系统
-ch07-01-mod-and-the-filesystem.md
+
ch07-01-mod-and-the-filesystem.md
-commit e2a129961ae346f726f8b342455ec2255cdfed68
+commit 6fc32eabcd09f7a130094767abadb691dfcdddf7
-我们将通过使用 Cargo 创建一个新项目来开始我们的模块之旅,不过我们不再创建一个二进制 crate,而是创建一个库 crate:一个其他人可以作为依赖导入的项目。第二章我们见过的rand
就是这样的 crate。
+我们将通过使用 Cargo 创建一个新项目来开始我们的模块之旅,不过不再创建一个二进制 crate,而是创建一个库 crate:一个其他人可以作为依赖导入的项目。第二章我们见过的rand
就是这样的 crate。
我们将创建一个提供一些通用网络功能的项目的骨架结构;我们将专注于模块和函数的组织,而不担心函数体中的具体代码。这个项目叫做communicator
。Cargo 默认会创建一个库 crate 除非指定其他项目类型,所以如果不像一直以来那样加入--bin
参数则项目将会是一个库:
$ cargo new communicator
$ cd communicator
@@ -3051,7 +3038,7 @@ mod tests {
因为没有 src/main.rs 文件,所以没有可供 Cargo 的cargo run
执行的东西。因此,我们将使用cargo build
命令只是编译库 crate 的代码。
我们将学习根据编写代码的意图来选择不同的织库项目代码组织来适应多种场景。
模块定义
-对于communicator
网络库,首先我们要定义一个叫做network
的模块,它包含一个叫做connect
的函数定义。Rust 中所有模块的定义以关键字mod
开始。在 src/lib.rs 文件的开头在测试代码的上面增加这些代码:
+对于communicator
网络库,首先要定义一个叫做network
的模块,它包含一个叫做connect
的函数定义。Rust 中所有模块的定义以关键字mod
开始。在 src/lib.rs 文件的开头在测试代码的上面增加这些代码:
Filename: src/lib.rs
mod network {
fn connect() {
@@ -3060,8 +3047,7 @@ mod tests {
mod
关键字的后面是模块的名字,network
,接着是位于大括号中的代码块。代码块中的一切都位于network
命名空间中。在这个例子中,只有一个函数,connect
。如果想要在network
模块外面的代码中调用这个函数,需要指定模块名并使用命名空间语法::
,像这样:network::connect()
,而不是只是connect()
。
也可以在 src/lib.rs 文件中同时存在多个模块。例如,再拥有一个client
模块,它也有一个叫做connect
的函数,如列表 7-1 中所示那样增加这个模块:
-
-现在我们有了network::connect
函数和client::connect
函数。他们可能有着完全不同的功能,同时他们也不会彼此冲突因为他们位于不同的模块。
+
+现在我们有了network::connect
函数和client::connect
函数。他们可能有着完全不同的功能,同时他们也不会彼此冲突,因为他们位于不同的模块。
虽然在这个例子中,我们构建了一个库,但是 src/lib.rs 并没有什么特殊意义。也可以在 src/main.rs 中使用子模块。事实上,也可以将模块放入其他模块中。这有助于随着模块的增长,将相关的功能组织在一起并又保持各自独立。如何选择组织代码依赖于如何考虑代码不同部分之间的关系。例如,对于库的用户来说,client
模块和它的函数connect
可能放在network
命名空间里显得更有道理,如列表 7-2 所示:
-
+
在 src/lib.rs 文件中,将现有的mod network
和mod client
的定义替换为client
模块作为network
的一个内部模块。现在我们有了network::connect
和network::client::connect
函数:又一次,这两个connect
函数也不相冲突,因为他们在不同的命名空间中。
这样,模块之间形成了一个层次结构。src/lib.rs 的内容位于最顶层,而其子模块位于较低的层次。这是列表 7-1 中的例子以这种方式考虑的组织结构:
communicator
@@ -3109,8 +3089,7 @@ in src/lib.rs
可以看到列表 7-2 中,client
是network
的子模块,而不是它的同级模块。更为负责的项目可以有很多的模块,所以他们需要符合逻辑地组合在一起以便记录他们。在项目中“符合逻辑”的意义全凭你得理解和库的用户对你项目领域的认识。利用我们这里讲到的技术来创建同级模块和嵌套的模块将是你会喜欢的结构。
将模块移动到其他文件
位于层级结构中的模块,非常类似计算机领域的另一个我们非常熟悉的结构:文件系统!我们可以利用 Rust 的模块系统连同多个文件一起分解 Rust 项目,这样就不是所有的内容都落到 src/lib.rs 中了。作为例子,我们将从列表 7-3 中的代码开始:
-
+
这是模块层次结构:
communicator
├── client
└── network
└── server
+如果这些模块有很多函数,而这些函数又很长,将难以在文件中寻找我们需要的代码。因为这些函数被嵌套进一个或多个模块中,同时函数中的代码也会开始变长。这就有充分的理由将client
、network
和server
每一个模块从 src/lib.rs 抽出并放入他们自己的文件中。
+让我们开始把client
模块提取到另一个文件中。首先,将 src/lib.rs 中的client
模块代码替换为如下:
+Filename: src/lib.rs
+mod client;
+
+mod network {
+ fn connect() {
+ }
+
+ mod server {
+ fn connect() {
+ }
+ }
+}
+
这里我们仍然定义了client
模块,不过去掉了大括号和client
模块中的定义并替换为一个分号,这使得 Rust 知道去其他地方寻找模块中定义的代码。
那么现在需要创建对应模块名的外部文件。在 src/ 目录创建一个 client.rs 文件,接着打开它并输入如下内容,它是上一步client
模块中被去掉的connect
函数:
Filename: src/client.rs
fn connect() {
}
-注意这个文件中并不需要一个mod
声明;因为已经在 src/lib.rs 中已经使用mod
声明了client
模块。这个文件仅仅提供client
模块的内容。如果在这里加上一个mod client
,那么就等于给client
模块增加了一个叫做client
的子模块!
+注意这个文件中并不需要一个mod
声明;因为已经在 src/lib.rs 中已经使用mod
声明了client
模块。这个文件仅仅提供client
模块的内容。如果在这里加上一个mod client
,那么就等于给client
模块增加了一个叫做client
的子模块了!
Rust 默认只知道 src/lib.rs 中的内容。如果想要对项目加入更多文件,我们需要在 src/lib.rs 中告诉 Rust 去寻找其他文件;这就是为什么mod client
需要被定义在 src/lib.rs 而不是在 src/client.rs。
现在,一切应该能成功编译,虽然会有一些警告。记住使用cargo build
而不是cargo run
因为这是一个库 crate 而不是二进制 crate:
$ cargo build
@@ -3185,7 +3176,7 @@ mod server {
}
注意这个模块文件中我们也使用了一个mod
声明;这是因为我们希望server
成为network
的一个子模块。
-现在再次运行cargo build
。成功!不过我们还需要再提取出另一个模块:server
。因为这是一个子模块————也就是模块中的模块————目前的将模块提取到对应名字的文件中的策略就不管用了。如果我们仍这么尝试则会出现错误。对 src/network.rs 的第一个修改是用mod server;
替换server
模块的内容:
+现在再次运行cargo build
。成功!不过我们还需要再提取出另一个模块:server
。因为这是一个子模块——也就是模块中的模块——目前的将模块提取到对应名字的文件中的策略就不管用了。如果我们仍这么尝试则会出现错误。对 src/network.rs 的第一个修改是用mod server;
替换server
模块的内容:
Filename: src/network.rs
fn connect() {
}
@@ -3198,7 +3189,6 @@ mod server;
}
当尝试运行cargo build
时,会出现如列表 7-4 中所示的错误:
-
+
这个错误说明“不能在这个位置新声明一个模块”并指出 src/network.rs 中的mod server;
这一行。看来 src/network.rs 与 src/lib.rs 在某些方面是不同的;让我们继续阅读以理解这是为什么。
列表 7-4 中间的记录事实上是非常有帮助的,因为它指出了一些我们还未讲到的操作:
-note: maybe move this module `network` to its own directory via `network/mod.rs`
+note: maybe move this module `network` to its own directory via `network/mod.rs`
我们可以按照记录所建议的去操作,而不是继续使用之前的与模块同名的文件的模式:
@@ -3275,9 +3262,9 @@ $ mv src/server.rs src/network
接下来,我们讨论一下pub
关键字,并除掉那些警告!
使用pub
控制可见性
-ch07-02-controlling-visibility-with-pub.md
+
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
函数没有被使用:
warning: function is never used: `connect`, #[warn(dead_code)] on by default
@@ -3298,7 +3285,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;
@@ -3318,7 +3305,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
@@ -3396,8 +3383,7 @@ pub mod network;
私有性示例
让我们看看更多例子作为练习。创建一个新的库项目并在新项目的 src/lib.rs 输入列表 7-5 中的代码:
-
+
在尝试编译这些代码之前,猜测一下try_me
函数的哪一行会出错。接着编译项目来看看是否猜对了,然后继续阅读后面关于错误的讨论!
检查错误
try_me
函数位于项目的根模块。叫做outermost
的模块是私有的,不过第二条私有性规则说明try_me
函数允许访问outermost
模块,因为outermost
位于当前(根)模块,try_me
也是。
@@ -3439,13 +3422,12 @@ incorrect
接下来,让我们讨论一下使用use
关键字来将项引入作用域。
导入命名
-ch07-03-importing-names-with-use.md
+
ch07-03-importing-names-with-use.md
-commit e2a129961ae346f726f8b342455ec2255cdfed68
+commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56
我们已经讲到了如何使用模块名称作为调用的一部分,来调用模块中的函数,如列表 7-6 中所示的nested_modules
函数调用。
-
+
如你所见,指定函数的完全限定名称可能会非常冗长。所幸 Rust 有一个关键字使得这些调用显得更简洁。
使用use
的简单导入
Rust 的use
关键字的工作是缩短冗长的函数调用,通过将想要调用的函数所在的模块引入到作用域中。这是一个将a::series::of
模块导入一个二进制 crate 的根作用域的例子:
@@ -3545,7 +3524,7 @@ mod tests {
}
}
-第十二章会更详细的解释测试,不过其部分内容现在应该可以理解了:有一个叫做tests
的模块紧邻其他模块,同时包含一个叫做it_works
的函数。即便存在一些特殊注解,tests
也不过是另外一个模块!所以我们的模块层次结构看起来像这样:
+第十一章会更详细的解释测试,不过其部分内容现在应该可以理解了:有一个叫做tests
的模块紧邻其他模块,同时包含一个叫做it_works
的函数。即便存在一些特殊注解,tests
也不过是另外一个模块!所以我们的模块层次结构看起来像这样:
communicator
├── client
├── network
@@ -3613,9 +3592,9 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
接下来,让我们看看一些标准库提供的集合数据类型,你可以利用他们编写出漂亮整洁的代码。
通用集合类型
-ch08-00-common-collections.md
+
ch08-00-common-collections.md
-commit 0d229cc5a3da341196e15a6761735b2952281569
+commit e6d6caab41471f7115a621029bd428a812c5260e
Rust 标准库中包含一系列被称为集合(collections)的非常有用的数据结构。大部分其他数据类型都代表一个特定的值,不过集合可以包含多个值。不同于内建的数组和元组类型,这些集合指向的数据是储存在堆上的,这意味着数据的数量不必在编译时就可知并且可以随着程序的运行增长或缩小。每种集合都有着不同能力和代价,而为所处的场景选择合适的集合则是你将要始终发展的技能。在这一章里,我们将详细的了解三个在 Rust 程序中被广泛使用的集合:
@@ -3623,15 +3602,15 @@ commit 0d229cc5a3da341196e15a6761735b2952281569
- 字符串(string)是一个字符的集合。我们之前见过
String
类型,现在将详细介绍它。
- 哈希 map(hash map)允许我们将值与一个特定的键(key)相关联。这是一个叫做 map 的更通用的数据结构的特定实现。
-对于标准库提供的其他类型的集合,请查看文档。
-我们将讨论如何创建和更新 vector、字符串和哈希 map,以及他们何以如此特殊。
+对于标准库提供的其他类型的集合,请查看文档。
+我们将讨论如何创建和更新 vector、字符串和哈希 map,以及他们有什么不同。
vector
-ch08-01-vectors.md
-commit 0d229cc5a3da341196e15a6761735b2952281569
+commit 6c24544ba718bce0755bdaf03423af86280051d5
-我们要讲到的第一个类型是Vec<T>
,也被称为 vector。vector 允许我们在一个单独的数据结构中储存多于一个值,它在内存中彼此相邻的排列所有的值。vector 只能储存相同类型的值。他们在拥有一系列的场景下非常实用,例如文件中的文本行或是购物车中商品的价格。
+我们要讲到的第一个类型是Vec<T>
,也被称为 vector。vector 允许我们在一个单独的数据结构中储存多于一个值,它在内存中彼此相邻的排列所有的值。vector 只能储存相同类型的值。他们在拥有一系列项的场景下非常实用,例如文件中的文本行或是购物车中商品的价格。
新建 vector
为了创建一个新的,空的 vector,可以调用Vec::new
函数:
let v: Vec<i32> = Vec::new();
@@ -3717,14 +3696,16 @@ let row = vec![
SpreadsheetCell::Float(10.12),
];
-Rust 在编译时就必须准确的知道 vector 中类型的原因是它需要知道储存每个元素到底需要多少内存。第二个优点是可以准确的知道这个 vector 中允许什么类型。如果 Rust 允许 vector 存放任意类型,那么当对 vector 元素执行操作时一个或多个类型的值就有可能会造成错误。使用枚举外加match
意味着 Rust 能在编译时就保证总是会处理所有可能的情况,正如第六章讲到的那样。
+
+Rust 在编译时就必须准确的知道 vector 中类型的原因是它需要知道储存每个元素到底需要多少内存。第二个好处是可以准确的知道这个 vector 中允许什么类型。如果 Rust 允许 vector 存放任意类型,那么当对 vector 元素执行操作时一个或多个类型的值就有可能会造成错误。使用枚举外加match
意味着 Rust 能在编译时就保证总是会处理所有可能的情况,正如第六章讲到的那样。
如果在编写程序时不能确切无遗的知道运行时会储存进 vector 的所有类型,枚举技术就行不通了。相反,你可以使用 trait 对象,第十七章会讲到它。
现在我们了解了一些使用 vector 的最常见的方式,请一定去看看标准库中Vec
定义的很多其他实用方法的 API 文档。例如,除了push
之外还有一个pop
方法,它会移除并返回 vector 的最后一个元素。让我们继续下一个集合类型:String
!
字符串
-ch08-02-strings.md
-commit 65f52921e21ad2e1c79d620fcfd01bde3ee30571
+commit d362dadae60a7cc3212b107b9e9562769b0f20e3
第四章已经讲过一些字符串的内容,不过现在让我们更深入地了解一下它。字符串是新晋 Rustacean 们通常会被困住的领域。这是由于三方面内容的结合:Rust 倾向于确保暴露出可能的错误,字符串是比很多程序员所想象的要更为复杂的数据结构,以及 UTF-8。所有这些结合起来对于来自其他语言背景的程序员就可能显得很困难了。
字符串出现在集合章节的原因是,字符串是作为字节的集合外加一些方法实现的,当这些字节被解释为文本时,这些方法提供了实用的功能。在这一部分,我们会讲到String
那些任何集合类型都有的操作,比如创建、更新和读取。也会讨论String
于其他集合不一样的地方,例如索引String
是很复杂的,由于人和计算机理解String
数据的不同方式。
@@ -3765,7 +3746,7 @@ let hello = "Hola";
更新字符串
String
的大小可以增长其内容也可以改变,就像可以放入更多数据来改变Vec
的内容一样。另外,String
实现了+
运算符作为级联运算符以便于使用。
-附加字符串
+使用 push 附加字符串
可以通过push_str
方法来附加字符串 slice,从而使String
变长:
let mut s = String::from("foo");
s.push_str("bar");
@@ -3790,7 +3771,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
参数:只能将&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
中拷贝的内容,并返回结果的所有权。换句话说,它看起来好像生成了很多拷贝不过实际上并没有:这个实现比拷贝要更高效。
如果想要级联多个字符串,+
的行为就显得笨重了:
let s1 = String::from("tic");
@@ -3820,7 +3801,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();
@@ -3894,9 +3875,9 @@ character boundary', ../src/libcore/str/mod.rs:1694
现在让我们转向一些不太复杂的集合:哈希 map!
哈希 map
-ch08-03-hash-maps.md
-commit 0d229cc5a3da341196e15a6761735b2952281569
+commit 4f2dc564851dc04b271a2260c834643dfd86c724
最后要介绍的常用集合类型是哈希 map(hash map)。HashMap<K, V>
类型储存了一个键类型K
对应一个值类型V
的映射。它通过一个哈希函数(hashing function)来实现映射,它决定了如何将键和值放入内存中。很多编程语言支持这种数据结构,不过通常有不同的名字:哈希、map、对象、哈希表或者关联数组,仅举几例。
哈希 map 可以用于需要任何类型作为键来寻找数据的情况,而不是像 vector 那样通过索引。例如,在一个游戏中,你可以将每个团队的分数记录到哈希 map 中,其中键是队伍的名字而值是每个队伍的分数。给出一个队名,就能得到他们的得分。
@@ -4017,23 +3998,23 @@ println!("{:?}", map);
- 使用哈希 map 和 vector,创建一个文本接口来允许用户向公司的部门中增加员工的名字。例如,“Add Sally to Engineering”或“Add Amir to Sales”。接着让用户获取一个部门的所有员工的列表,或者公司每个部门的所有员工按照字母顺排序的列表。
标准库 API 文档中描述的这些类型的方法将有助于你进行这些练习!
-我们已经开始解除可能会有失败操作的复杂程序了,这也意味着接下来是一个了解错误处理的绝佳时机!
+我们已经开始接触可能会有失败操作的复杂程序了,这也意味着接下来是一个了解错误处理的绝佳时机!
错误处理
-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 e26bb338ab14b98a850c3464e821d54940a45672
-突然有一天,糟糕的事情发生了,而你对此束手无策。对于这种情况,Rust 有`panic!宏。当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据,并接着退出。出现这种情况的场景通常是检测到一些类型的 bug 而且程序员并不清楚该如何处理它。
+突然有一天,糟糕的事情发生了,而你对此束手无策。对于这种情况,Rust 有panic!
宏。当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据,然后接着退出。出现这种情况的场景通常是检测到一些类型的 bug 而且程序员并不清楚该如何处理它。
Panic 中的栈展开与终止
当出现panic!
时,程序默认会开始展开(unwinding),这意味着 Rust 会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的过程有很多工作。另一种选择是直接终止(abort),这会不清理数据就退出程序。那么程序所使用的内存需要由操作系统来清理。如果你需要项目的最终二进制文件越小越好,可以由 panic 时展开切换为终止,通过在 Cargo.toml 的[profile]
部分增加panic = 'abort'
。例如,如果你想要在发布模式中 panic 时直接终止:
@@ -4058,7 +4039,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() {
@@ -4081,49 +4062,57 @@ 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 显示了其输出:
-
+
这里有大量的输出!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
枚举,它定义有如下连个成员,Ok
和Err
:
+大部分错误并没有严重到需要程序完全停止执行。有时,一个函数会因为一个容易理解并做出反映的原因失败。例如,如果尝试打开一个文件不过由于文件并不存在而操作就失败,这是我们可能想要创建这个文件而不是终止进程。
+回忆一下第二章“使用Result
类型来处理潜在的错误”部分中的那个Result
枚举,它定义有如下两个成员,Ok
和Err
:
enum Result<T, E> {
Ok(T),
Err(E),
@@ -4131,18 +4120,14 @@ commit 0c1d55ef48e5f6cf6a3b221f5b6dd4c922130bb1
T
和E
是泛型类型参数;第十章会详细介绍泛型。现在你需要知道的就是T
代表成功时返回的Ok
成员中的数据的类型,而E
代表失败时返回的Err
成员中的错误的类型。因为Result
有这些泛型类型参数,我们可以将Result
类型和标准库中为其定义的函数用于很多不同的场景,这些情况中需要返回的成功值和失败值可能会各不相同。
让我们调用一个返回Result
的函数,因为它可能会失败:如列表 9-2 所示打开一个文件:
-
+
如何知道File::open
返回一个Result
呢?我们可以查看标准库 API 文档,或者可以直接问编译器!如果给f
某个我们知道不是函数返回值类型的类型注解,接着尝试编译代码,编译器会告诉我们类型不匹配。然后错误信息会告诉我们f
的类型应该是什么,为此我们将let f
语句改为:
let f: u32 = File::open("hello.txt");
@@ -4161,8 +4146,7 @@ fn main() {
这个返回值类型说明File::open
调用可能会成功并返回一个可以进行读写的文件句柄。这个函数也可能会失败:例如,文件可能并不存在,或者可能没有访问文件的权限。File::open
需要一个方式告诉我们是成功还是失败,并同时提供给我们文件句柄或错误信息。而这些信息正是Result
枚举可以提供的。
当File::open
成功的情况下,变量f
的值将会是一个包含文件句柄的Ok
实例。在失败的情况下,f
会是一个包含更多关于出现了何种错误信息的Err
实例。
我们需要在列表 9-2 的代码中增加根据File::open
返回值进行不同处理的逻辑。列表 9-3 展示了一个处理Result
的基本工具:第六章学习过的match
表达式。
-
+
注意与Option
枚举一样,Result
枚举和其成员也被导入到了 prelude 中,所以就不需要在match
分支中的Ok
和Err
之前指定Result::
。
这里我们告诉 Rust 当结果是Ok
,返回Ok
成员中的file
值,然后将这个文件句柄赋值给变量f
。match
之后,我们可以利用这个文件句柄来进行读写。
match
的另一个分支处理从File::open
得到Err
值的情况。在这种情况下,我们选择调用panic!
宏。如果当前目录没有一个叫做 hello.txt 的文件,当运行这段代码时会看到如下来自panic!
宏的输出:
@@ -4189,8 +4170,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
增加了另一个分支:
-
+
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 的捷径:unwrap
和expect
match
能够胜任它的工作,不过它可能有点冗长并且并不总是能很好的表明意图。Result<T, E>
类型定义了很多辅助方法来处理各种情况。其中之一叫做unwrap
,它的实现就类似于列表 9-3 中的match
语句。如果Result
值是成员Ok
,unwrap
会返回Ok
中的值。如果Result
是成员Err
,unwrap
会为我们调用panic!
。
@@ -4254,7 +4232,6 @@ fn main() {
传播错误
当编写一个其实现会调用一些可能会失败的操作的函数时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理。这被称为传播(propagating)错误,这样能更好的控制代码调用,因为比起你代码所拥有的上下文,调用者可能拥有更多信息或逻辑来决定应该如何处理错误。
例如,列表 9-5 展示了一个从文件中读取用户名的函数。如果文件不存在或不能读取,这个函数会将这些错误返回给调用它的代码:
-
+
首先让我们看看函数的返回值:Result<String, io::Error>
。这意味着函数返回一个Result<T, E>
类型的值,其中泛型参数T
的具体类型是String
,而E
的具体类型是io::Error
。如果这个函数没有出任何错误成功返回,函数的调用者会收到一个包含String
的Ok
值————函数从文件中读取到的用户名。如果函数遇到任何错误,函数的调用者会收到一个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
并调用文件句柄f
的read_to_string
方法来将文件的内容读取到s
中。read_to_string
方法也返回一个Result
因为它也可能会失败:哪怕是File::open
已经成功了。所以我们需要另一个match
来处理这个Result
:如果read_to_string
成功了,那么这个函数就成功了,并返回文件中的用户名,它现在位于被封装进Ok
的s
中。如果read_to_string
失败了,则像之前处理File::open
的返回值的match
那样返回错误值。并不需要显式的调用return
,因为这是函数的最后一个表达式。
调用这个函数的代码最终会得到一个包含用户名的Ok
值,亦或一个包含io::Error
的Err
值。我们无从得知调用者会如何处理这些值。例如,如果他们得到了一个Err
值,他们可能会选择panic!
并使程序崩溃、使用一个默认的用户名或者从文件之外的地方寻找用户名。我们没有足够的信息知晓调用者具体会如何尝试,所以将所有的成功或失败信息向上传播,让他们选择合适处理方法。
这种传播错误的模式在 Rust 是如此的常见,以至于有一个更简便的专用语法:?
。
传播错误的捷径:?
-列表 9-6 展示了一个read_username_from_file
的实现,它实现了与列表 9-5 中的代码相同的功能,不过这个实现是使用了问号运算符:
-
+
Result
值之后的?
被定义为与列表 9-5 中定义的处理Result
值的match
表达式有着完全相同的工作方式。如果Result
的值是Ok
,这个表达式将会返回Ok
中的值而程序将继续执行。如果值是Err
,Err
中的值将作为整个函数的返回值,就好像使用了return
关键字一样,这样错误值就被传播给了调用者。
在列表 9-6 的上下文中,File::open
调用结尾的?
将会把Ok
中的值返回给变量f
。如果出现了错误,?
会提早返回整个函数并将任何Err
值传播给调用者。同理也适用于read_to_string
调用结尾的?
。
?
消除了大量样板代码并使得函数的实现更简单。我们甚至可以在?
之后直接使用链式方法调用来进一步缩短代码:
@@ -4349,11 +4322,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!
的方法可以被理解为一个你实际希望程序处理错误方式的占位符,它根据其余代码运行方式可能会各不相同。
@@ -4367,7 +4340,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!
——在这里,有害状态是指当一些假设、保证、协议或不可变性被打破的状态,例如无效的值、自相矛盾的值或者被传递了不存在的值——外加如下几种情况:
- 有害状态并不包含预期会偶尔发生的错误
- 之后的代码的运行依赖于不再处于这种有害状态
@@ -4400,7 +4373,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
的实例:
-
+
首先,我们定义了一个包含u32
类型字段value
的结构体Guess
。这里是储存猜测值的地方。
接着在Guess
上实现了一个叫做new
的关联函数来创建Guess
的实例。new
定义为接收一个u32
类型的参数value
并返回一个Guess
。new
函数中代码的测试确保了其值是在 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
。
@@ -4435,20 +4404,19 @@ impl Guess {
现在我们已经见识过了标准库中Option
和Result
泛型枚举的能力了,让我们聊聊泛型是如何工作的,以及如果在你的代码中利用他们。
泛型、trait 和生命周期
-ch10-00-generics.md
-commit b335da755592f286fd97a64d98f0ca3be6a59327
+commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56
每一个编程语言都有高效的处理重复概念的工具;在 Rust 中工具之一就是泛型(generics)。泛型是具体类型或其他属性的抽象替代。我们可以表达泛型的属性,比如他们的行为或如何与其他泛型相关联,而不需要在编写和编译代码时知道他们在这里实际上代表什么。
-同理为了编写一份可以用于多种具体值的代码,函数并不知道其参数为何值,这时就可以让函数获取泛型而不是像i32
或String
这样的具体值。我们已经使用过第六章的Option<T>
,第八章的Vec<T>
和HashMap<K, V>
,以及第九章的Result<T, E>
这些泛型了。本章会探索如何使用泛型定义我们自己自己的类型、函数和方法。
+同理为了编写一份可以用于多种具体值的代码,函数并不知道其参数为何值,这时就可以让函数获取泛型而不是像i32
或String
这样的具体值。我们已经使用过第六章的Option<T>
,第八章的Vec<T>
和HashMap<K, V>
,以及第九章的Result<T, E>
这些泛型了。本章会探索如何使用泛型定义我们自己自己的类型、函数和方法!
首先,我们将回顾一下提取函数以减少代码重复的机制。接着使用一个只在参数类型上不同的泛型函数来实现相同的功能。我们也会讲到结构体和枚举定义中的泛型。
之后,我们讨论 traits,这是一个定义泛型行为的方法。trait 可以与泛型结合来将泛型限制为拥有特定行为的类型,而不是任意类型。
最后介绍生命周期(lifetimes),它是一类允许我们向编译器提供引用如何相互关联的泛型。Rust 的生命周期功能允许在很多场景下借用值同时仍然使编译器能够检查这些引用的有效性。
提取函数来减少重复
在介绍泛型语法之前,首先来回顾一个不使用泛型的处理重复的技术:提取一个函数。当熟悉了这个技术以后,我们将使用相同的机制来提取一个泛型函数!如同你识别出可以提取到函数中重复代码那样,你也会开始识别出能够使用泛型的重复代码。
考虑一下这个寻找列表中最大值的小程序,如列表 10-1 所示:
-
+
这段代码获取一个整型列表,存放在变量numbers
中。它将列表的第一项放入了变量largest
中。接着遍历了列表中的所有数字,如果当前值大于largest
中储存的值,将largest
替换为这个值。如果当前值小于目前为止的最大值,largest
保持不变。当列表中所有值都被考虑到之后,largest
将会是最大值,在这里也就是 100。
如果需要在两个不同的列表中寻找最大值,我们可以重复列表 10-1 中的代码这样程序中就会存在两段相同逻辑的代码,如列表 10-2 所示:
-
+
虽然代码能够执行,但是重复的代码是冗余且已于出错的,并且意味着当更新逻辑时需要修改多处地方的代码。
@@ -4509,8 +4472,7 @@ commit b335da755592f286fd97a64d98f0ca3be6a59327
为了消除重复,我们可以创建一层抽象,在这个例子中将表现为一个获取任意整型列表作为参数并对其进行处理的函数。这将增加代码的简洁性并让我们将表达和推导寻找列表中最大值的这个概念与使用这个概念的特定位置相互独。
立。
在列表 10-3 的程序中将寻找最大值的代码提取到了一个叫做largest
的函数中。这个程序可以找出两个不同数字列表的最大值,不过列表 10-1 中的代码只存在于一个位置:
-
+
这个函数有一个参数list
,它代表会传递给函数的任何具体i32
值的 slice。函数定义中的list
代表任何&[i32]
。当调用largest
函数时,其代码实际上运行于我们传递的特定值上。
从列表 10-2 到列表 10-3 中涉及的机制经历了如下几步:
@@ -4552,7 +4512,7 @@ fn main() {
如果我们有两个函数,一个寻找一个i32
值的 slice 中的最大项而另一个寻找char
值的 slice 中的最大项该怎么办?该如何消除重复呢?让我们拭目以待!
泛型数据类型
-ch10-01-syntax.md
commit 55d9e75ffec92e922273c997026bb10613a76578
@@ -4560,8 +4520,7 @@ commit 55d9e75ffec92e922273c997026bb10613a76578
在函数定义中使用泛型
定义函数时可以在函数签名的参数数据类型和返回值中使用泛型。以这种方式编写的代码将更灵活并能向函数调用者提供更多功能,同时不引入重复代码。
回到largest
函数上,列表 10-4 中展示了两个提供了相同的寻找 slice 中最大值功能的函数。第一个是从列表 10-3 中提取的寻找 slice 中i32
最大值的函数。第二个函数寻找 slice 中char
的最大值:
-
-这里largest_i32
和largest_char
有着完全相同的函数体,所以能够将这两个函数变成一个来减少重复就太好了。所幸通过引入一个泛型参数就能实现。
-为了参数化要定义的函数的签名中的类型,我们需要像给函数的值参数起名那样为这类型参数起一个名字。这里选择了名称T
。任何标识符抖可以作为类型参数名,选择T
是因为 Rust 的类型命名规范是骆驼命名法(CamelCase)。另外泛型类型参数的规范也倾向于简短,经常仅仅是一个字母。T
作为“type”是大部分 Rust 程序员的首选。
+
+这里largest_i32
和largest_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
函数。注意这些代码还不能编译!
-
+
如果现在就尝试编译这些代码,会出现如下错误:
error[E0369]: binary operation `>` cannot be applied to type `T`
|
@@ -4662,8 +4614,7 @@ not-yet-compiling example here, which I know isn't ideal either. Let us know
what you think. /Carol -->
结构体定义中的泛型
同样也可以使用<>
语法来定义拥有一个或多个泛型参数类型字段的结构体。列表 10-6 展示了如何定义和使用一个可以存放任何类型的x
和y
坐标值的结构体Point
:
-
+
其语法类似于函数定义中的泛型应用。首先,必须在结构体名称后面的尖括号中声明泛型参数的名称。接着在结构体定义中可以指定具体数据类型的位置使用泛型类型。
注意Point
的定义中是使用了要给泛型类型,我们想要表达的是结构体Point
对于一些类型T
是泛型的,而且无论这个泛型是什么,字段x
和y
都是相同类型的。如果尝试创建一个有不同类型值的Point
的实例,像列表 10-7 中的代码就不能编译:
-
+
尝试编译会得到如下错误:
error[E0308]: mismatched types
-->
@@ -4709,8 +4654,7 @@ the same generic data type T
当我们将 5 赋值给x
,编译器就知道这个Point
实例的泛型类型T
是一个整型。接着我们将y
指定为 4.0,而它被定义为与x
有着相同的类型,所以出现了类型不匹配的错误。
如果想要一个x
和y
可以有不同类型且仍然是泛型的Point
结构体,我们可以使用多个泛型类型参数。在列表 10-8 中,我们修改Point
的定义为拥有两个泛型类型T
和U
。其中字段x
是T
类型的,而字段y
是U
类型的:
-
+
现在所有这些Point
实例都是被允许的了!你可以在定义中使用任意多的泛型类型参数,不过太多的话代码将难以阅读和理解。如果你处于一个需要很多泛型类型的位置,这可能是一个需要重新组织代码并分隔成一些更小部分的信号。
枚举定义中的泛型数据类型
类似于结构体,枚举也可以在其成员中存放泛型数据类型。第六章我们使用过了标准库提供的Option<T>
枚举,现在这个定义看起来就更容易理解了。让我们再看看:
@@ -4746,8 +4687,7 @@ values of different types
当发现代码中有多个只有存放的值的类型有所不同的结构体或枚举定义时,你就应该像之前的函数定义中那样引入泛型类型来减少重复。
方法定义中的枚举数据类型
可以像第五章介绍的那样来为其定义中带有泛型的结构体或枚举实现方法。列表 10-9 中展示了列表 10-6 中定义的结构体Point<T>
。接着我们在Point<T>
上定义了一个叫做x
的方法来返回字段x
中数据的引用:
-
+
注意必须在impl
后面声明T
,这样就可以在Point<T>
上实现的方法中使用它了。
结构体定义中的泛型类型参数并不总是与结构体方法签名中使用的泛型是同一类型。列表 10-10 中在列表 10-8 中的结构体Point<T, U>
上定义了一个方法mixup
。这个方法获取另一个Point
作为参数,而它可能与调用mixup
的self
是不同的Point
类型。这个方法用self
的Point
类型的x
值(类型T
)和参数的Point
类型的y
值(类型W
)来创建一个新Point
类型的实例:
-
+
在main
函数中,定义了一个有i32
类型的x
(其值为5
)和f64
的y
(其值为10.4
)的Point
。p2
则是一个有着字符串 slice 类型的x
(其值为"Hello"
)和char
类型的y
(其值为c
)的Point
。在p1
上以p2
调用mixup
会返回一个p3
,它会有一个i32
类型的x
,因为x
来自p1
,并拥有一个char
类型的y
,因为y
来自p2
。println!
会打印出p3.x = 5, p3.y = c
。
注意泛型参数T
和U
声明于impl
之后,因为他们于结构体定义相对应。而泛型参数V
和W
声明于fn mixup
之后,因为他们只是相对于方法本身的。
泛型代码的性能
@@ -4833,35 +4767,30 @@ fn main() {
我们可以使用泛型来编写不重复的代码,而 Rust 会将会为每一个实例编译其特定类型的代码。这意味着在使用泛型时没有运行时开销;当代码运行,它的执行效率就跟好像手写每个具体定义的重复代码一样。这个单态化过程正是 Rust 泛型在运行时极其高效的原因。
trait:定义共享的行为
-ch10-02-traits.md
-commit 709eb1eaca48864fafd9263042f5f9d9d6ffe08d
+commit e5a987f5da3fba24e55f5c7102ec63f9dc3bc360
trait 允许我们进行另一种抽象:他们让我们可以抽象类型所通用的行为。trait 告诉 Rust 编译器某个特定类型拥有可能与其他类型共享的功能。在使用泛型类型参数的场景中,可以使用 trait bounds 在编译时指定泛型可以是任何实现了某个 trait 的类型,并由此在这个场景下拥有我们希望的功能。
注意:trait 类似于其他语言中的常被称为接口(interfaces)的功能,虽然有一些不同。
定义 trait
-一个类型的行为由其可供调用的方法构成。如果可以对不同类型调用相同的方法的话,这些类型就可以共享相同的行为了。trait 定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必须行为的集合。
+一个类型的行为由其可供调用的方法构成。如果可以对不同类型调用相同的方法的话,这些类型就可以共享相同的行为了。trait 定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必需的行为的集合。
例如,这里有多个存放了不同类型和属性文本的结构体:结构体NewsArticle
用于存放发生于世界各地的新闻故事,而结构体Tweet
最多只能存放 140 个字符的内容,以及像是否转推或是否是对推友的回复这样的元数据。
我们想要创建一个多媒体聚合库用来显示可能储存在NewsArticle
或Tweet
实例中的数据的总结。每一个结构体都需要的行为是他们是能够被总结的,这样的话就可以调用实例的summary
方法来请求总结。列表 10-11 中展示了一个表现这个概念的Summarizable
trait 的定义:
-
+
使用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 字符以内。
-
+
在类型上实现 trait 类似与实现与 trait 无关的方法。区别在于impl
关键字之后,我们提供需要实现 trait 的名称,接着是for
和需要实现 trait 的类型的名称。在impl
块中,使用 trait 定义中的方法签名,不过不再后跟分号,而是需要在大括号中编写函数体来为特定类型实现 trait 方法所拥有的行为。
一旦实现了 trait,我们就可以用与NewsArticle
和Tweet
实例的非 trait 方法一样的方式调用 trait 方法了:
let tweet = Tweet {
@@ -4910,8 +4836,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 和NewsArticle
与Tweet
类型,所以他们是位于同一作用域的。如果这个lib.rs
是对应aggregator
crate 的,而别人想要利用我们 crate 的功能外加为其WeatherForecast
结构体实现Summarizable
trait,在实现Summarizable
trait 之前他们首先就需要将其导入其作用域中,如列表 10-13 所示:
-
+
另外这段代码假设Summarizable
是一个公有 trait,这是因为列表 10-11 中trait
之前使用了pub
关键字。
trait 实现的一个需要注意的限制是:只能在 trait 或对应类型位于我们 crate 本地的时候为其实现 trait。换句话说,不允许对外部类型实现外部 trait。例如,不能Vec
上实现Display
trait,因为Display
和Vec
都定义于标准库中。允许在像Tweet
这样作为我们aggregator
crate 部分功能的自定义类型上实现标准库中的 trait Display
。也允许在aggregator
crate中为Vec
实现Summarizable
,因为Summarizable
定义与此。这个限制是我们称为 orphan rule 的一部分,如果你感兴趣的可以在类型理论中找到它。简单来说,它被称为 orphan rule 是因为其父类型不存在。没有这条规则的话,两个 crate 可以分别对相同类型是实现相同的 trait,因而这两个实现会相互冲突:Rust 将无从得知应该使用哪一个。因为 Rust 强制执行 orphan rule,其他人编写的代码不会破坏你代码,反之亦是如此。
默认实现
有时为 trait 中的某些或全部提供默认的行为,而不是在每个类型的每个实现中都定义自己的行为是很有用的。这样当为某个特定类型实现 trait 时,可以选择保留或重载每个方法的默认行为。
列表 10-14 中展示了如何为Summarize
trait 的summary
方法指定一个默认的字符串值,而不是像列表 10-11 中那样只是定义方法签名:
-
+
如果想要对NewsArticle
实例使用这个默认实现,而不是像列表 10-12 中那样定义一个自己的实现,则可以指定一个空的impl
块:
impl Summarizable for NewsArticle {}
@@ -5047,9 +4965,8 @@ error[E0507]: cannot move out of borrowed content
| cannot move out of borrowed content
错误的核心是cannot move out of type [T], a non-copy array
,对于非泛型版本的largest
函数,我们只尝试了寻找最大的i32
和char
。正如第四章讨论过的,像i32
和char
这样的类型是已知大小的并可以储存在栈上,所以他们实现了Copy
trait。当我们将largest
函数改成使用泛型后,现在list
参数的类型就有可能是没有实现Copy
trait 的,这意味着我们可能不能将list[0]
的值移动到largest
变量中。
-如果只想对实现了Copy
的类型调用这些带啊吗,可以在T
的 trait bounds 中增加Copy
!列表 10-15 中展示了一个可以编译的泛型版本的largest
函数的完整代码,只要传递给largest
的 slice 值的类型实现了PartialOrd
和Copy
这两个 trait,例如i32
和char
:
-
+
如果并不希望限制largest
函数只能用于实现了Copy
trait 的类型,我们可以在T
的 trait bounds 中指定Clone
而不是Copy
,并克隆 slice 的每一个值使得largest
函数拥有其所有权。但是使用clone
函数潜在意味着更多的堆分配,而且堆分配在涉及大量数据时可能会相当缓慢。另一种largest
的实现方式是返回 slice 中一个T
值的引用。如果我们将函数返回值从T
改为&T
并改变函数体使其能够返回一个引用,我们将不需要任何Clone
或Copy
的 trait bounds 而且也不会有任何的堆分配。尝试自己实现这种替代解决方式吧!
trait 和 trait bounds 让我们使用泛型类型参数来减少重复,并仍然能够向编译器明确指定泛型类型需要拥有哪些行为。因为我们向编译器提供了 trait bounds 信息,它就可以检查代码中所用到的具体类型是否提供了正确的行为。在动态类型语言中,如果我们尝试调用一个类型并没有实现的方法,会在运行时出现错误。Rust 将这些错误移动到了编译时,甚至在代码能够运行之前就强迫我们修复错误。另外,我们也无需编写运行时检查行为的代码,因为在编译时就已经检查过了,这样相比其他那些不愿放弃泛型灵活性的语言有更好的性能。
这里还有一种泛型,我们一直在使用它甚至都没有察觉它的存在,这就是生命周期(lifetimes)。不同于其他泛型帮助我们确保类型拥有期望的行为,生命周期则有助于确保引用在我们需要他们的时候一直有效。让我们学习生命周期是如何做到这些的。
生命周期与引用有效性
-ch10-03-lifetime-syntax.md
-commit d7a4e99554da53619dd71044273535ba0186f40a
+commit c49e5ee8859f8eb8f8867cbeafbdf5b802aa5894
当在第四章讨论引用时,我们遗漏了一个重要的细节:Rust 中的每一个引用都有其生命周期,也就是引用保持有效的作用域。大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以多种不同方式向关联的情况,所以 Rust 需要我们使用泛型生命周期参数来注明他们的关系,这样就能确保运行时实际使用的引用绝对是有效的。
好吧,这有点不太寻常,而且也不同于其他语言中使用的工具。生命周期,从某种意义上说,是 Rust 最与众不同的功能。
生命周期是一个很广泛的话题,本章不可能涉及到它全部的内容,所以这里我们会讲到一些通常你可能会遇到的生命周期语法以便你熟悉这个概念。第十九章会包含生命周期所有功能的更高级的内容。
生命周期避免了悬垂引用
生命周期的主要目标是避免悬垂引用,它会导致程序引用了并非其期望引用的数据。考虑一下列表 10-16 中的程序,它有一个外部作用域和一个内部作用域,外部作用域声明了一个没有初值的变量r
,而内部作用域声明了一个初值为 5 的变量x
。在内部作用域中,我们尝试将r
的值设置为一个x
的引用。接着在内部作用域结束后,尝试打印出r
的值:
-
+
未初始化变量不能被使用
接下来的一些例子中声明了没有初始值的变量,以便这些变量存在于外部作用域。这看起来好像和 Rust 不允许存在空值相冲突。然而这是可以的,如果我们尝试在给它一个值之前使用这个变量,会出现一个编译时错误。请自行尝试!
@@ -5129,7 +5041,6 @@ commit d7a4e99554da53619dd71044273535ba0186f40a
变量x
并没有“存在的足够久”。为什么呢?好吧,x
在到达第 7 行的大括号的结束时就离开了作用域,这也是内部作用域的结尾。不过r
在外部作用域也是有效的;作用域越大我们就说它“存在的越久”。如果 Rust 允许这段代码工作,r
将会引用在x
离开作用域时被释放的内存,这时尝试对r
做任何操作都会不能正常工作。那么 Rust 是如何决定这段代码是不被允许的呢?
借用检查器
编译器的这一部分叫做借用检查器(borrow checker),它比较作用域来确保所有的借用都是有效的。列表 10-17 展示了与列表 10-16 相同的例子不过带有变量声明周期的注释:
-
+
@@ -5157,7 +5065,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 中这个并没有产生悬垂引用且可以正常编译的例子:
-
+
x
拥有生命周期 'b
,在这里它比 'a
要大。这就意味着r
可以引用x
:Rust 知道r
中的引用在x
有效的时候也会一直有效。
现在我们已经在一个具体的例子中展示了引用的声明周期位于何处,并讨论了 Rust 如何分析生命周期来保证引用总是有效的,接下来让我们聊聊在函数的上下文中参数和返回值的泛型生命周期。
函数中的泛型生命周期
让我们来编写一个返回两个字符串 slice 中最长的那一个的函数。我们希望能够通过传递两个字符串 slice 来调用这个函数,并希望返回一个字符串 slice。一旦我们实现了longest
函数,列表 10-19 中的代码应该会打印出The longest string is abcd
:
-
+
注意函数期望获取字符串 slice(如第四章所讲到的这是引用)因为我们并不希望longest
函数获取其参数的引用。我们希望函数能够接受String
的 slice(也就是变量string1
的类型)和字符串字面值(也就是变量string2
包含的值)。
参考之前第四章中的“字符串 slice 作为参数”部分中更多关于为什么上面例子中的参数正是我们想要的讨论。
如果尝试像列表 10-20 中那样实现longest
函数,它并不能编译:
-
+
将会出现如下有关生命周期的错误:
error[E0106]: missing lifetime specifier
|
@@ -5247,8 +5144,7 @@ longest of two string slices, but does not yet compile
生命周期注解本身没有多少意义:生命周期注解告诉 Rust 多个引用的泛型生命周期参数如何相互联系。如果函数有一个生命周期'a
的i32
的引用的参数first
,还有另一个同样是生命周期'a
的i32
的引用的参数second
,这两个生命周期注解有相同的名称意味着first
和second
必须与这相同的泛型生命周期存在得一样久。
函数签名中的生命周期注解
来看看我们编写的longest
函数的上下文中的生命周期。就像泛型类型参数,泛型生命周期参数需要声明在函数名和参数列表间的加括号中。这里我们想要告诉 Rust 关于参数中的引用和返回值之间的限制是他们都必须拥有相同的生命周期,就像列表 10-21 中在每个引用中都加上了'a
那样:
-
+
这段代码能够编译并会产生我们想要使用列表 10-19 中的main
函数得到的结果。
现在函数签名表明对于某些生命周期'a
,函数会获取两个参数,他们都是与生命周期'a
存在的一样长的字符串 slice。函数会返回一个同样也与生命周期'a
存在的一样长的字符串 slice。这就是我们告诉 Rust 需要其保证的协议。
通过在函数签名中指定生命周期参数,我们不会改变任何参数或返回值的生命周期,不过我们说过任何不坚持这个协议的类型都将被借用检查器拒绝。这个函数并不知道(或需要知道)x
和y
具体会存在多久,不过只需要知道一些可以使用'a
替代的作用域将会满足这个签名。
当在函数中使用生命周期注解时,这些注解出现在函数签名中,而不存在于函数体中的任何代码中。这是因为 Rust 能够分析函数中代码而不需要任何协助,不过当函数引用或被函数之外的代码引用时,参数或返回值的生命周期可能在每次函数被调用时都不同。这可能会产生惊人的消耗并且对于 Rust 来说经常都是不可能分析的。在这种情况下,我们需要自己标注生命周期。
当具体的引用被传递给longest
时,具体被'a
所替代的生命周期是x
的作用域与y
的作用域相重叠的那一部分。因为作用域总是嵌套的,所以换一种说法就是泛型生命周期'a
的具体生命周期等同于x
和y
的生命周期中较小的那一个。因为我们用相同的生命周期参数标注了返回的引用值,所以返回的引用值就能保证在x
和y
中较短的那个生命周期结束之前保持有效。
让我们如何通过传递拥有不同具体生命周期的引用来观察他们是如何限制longest
函数的使用的。列表 10-22 是一个应该在任何编程语言中都很直观的例子:string1
直到外部作用域结束都是有效的,string2
则在内部作用域中是有效的,而result
则引用了一些直到外部作用域结束都是有效的值。借用检查器赞同这些代码;它能够编译和运行,并打印出The longest string is long string is long
:
-
+
接下来,让我们尝试一个result
的引用的生命周期必须比两个参数的要短的例子。将result
变量的声明从内部作用域中移动出来,不过将result
和string2
变量的赋值语句一同放在内部作用域里。接下来,我们将使用result
的println!
移动到内部作用域之外,就在其结束之后。注意列表 10-23 中的代码不能编译:
-
+
如果尝试编译会出现如下错误:
error: `string2` does not live long enough
|
@@ -5366,8 +5244,7 @@ at 1:44...
从结果上看,生命周期语法是关于如何联系函数不同参数和返回值的生命周期的。一旦他们形成了某种联系,Rust 就有了足够的信息来允许内存安全的操作并阻止会产生悬垂指针亦或是违反内存安全的行为。
结构体定义中的生命周期注解
目前为止,我们只定义过有所有权类型的结构体。也可以定义存放引用的结构体,不过需要为结构体定义中的每一个引用添加生命周期注解。列表 10-24 中有一个存放了一个字符串 slice 的结构体ImportantExcerpt
:
-
+
这个结构体有一个字段,part
,它存放了一个字符串 slice,这是一个引用。类似于泛型参数类型,必须在结构体名称后面的尖括号中声明泛型生命周期参数,以便在结构体定义中使用生命周期参数。
这里的main
函数创建了一个ImportantExcerpt
的实例,它存放了变量novel
所拥有的String
的第一个句子的引用。
生命周期省略
在这一部分,我们知道了每一个引用都有一个生命周期,而且需要为使用了引用的函数或结构体指定生命周期。然而,第四章的“字符串 slice”部分有一个函数,我们在列表 10-25 中再次展示它,没有生命周期注解却能成功编译:
-
+
这个函数没有生命周期注解却能编译是由于一些历史原因:在早期 1.0 之前的版本的 Rust 中,这的确是不能编译的。每一个引用都必须有明确的生命周期。那时的函数签名将会写成这样:
fn first_word<'a>(s: &'a str) -> &'a str {
@@ -5511,9 +5382,9 @@ fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann
你可能不会相信,这个领域还有更多需要学习的内容:第十七章会讨论 trait 对象,这是另一种使用 trait 的方式。第十九章会涉及到生命周期注解更复杂的场景。第二十章讲解一些高级的类型系统功能。不过接下来,让我们聊聊如何在 Rust 中编写测试,来确保代码的所有功能能像我们希望的那样工作!
测试
-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.
@@ -5521,23 +5392,35 @@ commit c9fd8eb1da7a79deee97020e8ad49af8ded78f9c
软件测试是证明 bug 存在的有效方法,而证明它们不存在时则显得令人绝望的不足。
Edsger W. Dijkstra,【谦卑的程序员】(1972)
-Rust 是一个非常注重正确性的编程语言,不过正确性是一个难以证明的复杂主题。Rust 在其类型系统上下了很大的功夫,来确保程序像我们希望的那样运行,不过它并不有助于所有情况。为此,Rust 也包含为语言自身编写软件测试的支持。
-例如,我们可以编写一个叫做add_two
的函数,它的签名有一个整型参数并返回一个整型值。我们可以实现并编译这个函数,而 Rust 也会进行所有的类型检查和借用检查,正如我们之前见识过的那样。Rust 所不能检查的是,我们实现的这个函数是否返回了参数值加二后的值,而不是加 10 或者减 50!这也就是测试出场的地方。例如可以编写传递3
给add_two
函数并检查我们是否得到了5
。任何时候修改了代码我们都可以运行测试来确保没有改变任何现有测试所指定的行为。
-测试是一项技能,而且我们也不能期望在一本书的一个章节中就涉及到编写好的测试的所有内容。然而我们可以讨论的是 Rust 测试功能的机制。我们会讲到编写测试时会用到的注解和宏,Rust 提供用来运行测试的默认行为和选项,以及如何将测试组织成单元测试和集成测试。
+程序的正确性意味着代码如我们期望的那样运行。Rust 是一个非常注重正确性的编程语言,不过正确性是一个难以证明的复杂主题。Rust 的类型系统在此问题上下了很大的功夫,不过它不可能捕获所有类型的错误。为此,Rust 也包含为语言自身编写软件测试的支持。
+例如,我们可以编写一个叫做add_two
的将传递给它的值加二的函数。它的签名有一个整型参数并返回一个整型值。当实现和编译这个函数时,Rust 会进行所有目前我们已经见过的的类型检查和借用检查。例如,这些检查会确保我们不会传递String
或无效的引用给这个函数。Rust 所不能检查的是这个函数是否会准确的完成我们期望的工作:返回参数加二后的值,而不是比如说参数加 10 或减 50 的值!这也就是测试出场的地方。
+我们可以编写测试断言,比如说,当传递3
给add_two
函数时,应该得到5
。当对代码进行修改时可以运行测试来确保任何现存的正确行为没有被改变。
+测试是一项复杂的技能,而且我们也不能期望在一本书的一个章节中就涉及到编写好的测试的所有内容,所以这里仅仅讨论 Rust 测试功能的机制。我们会讲到编写测试时会用到的注解和宏,Rust 提供用来运行测试的默认行为和选项,以及如何将测试组织成单元测试和集成测试。
编写测试
-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 {
@@ -5546,14 +5429,18 @@ mod tests {
}
}
-现在让我们暂时忽略tests
模块和#[cfg(test)]
注解并只关注函数。注意它之前的#[test]
:这个属性表明这是一个测试函数。这个函数目前没有任何内容,所以绝对是可以通过的!使用cargo test
来运行测试:
+
+现在让我们暂时忽略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
@@ -5563,263 +5450,585 @@ running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
-Cargo 编译并运行了测试。这里有两部分输出:本章我们将关注第一部分。第二部分是文档测试的输出,第十四章会介绍他们。现在注意看这一行:
-test it_works ... ok
-
-it_works
文本来源于测试函数的名称。
-这里也有一行总结告诉我们所有测试的聚合结果:
-test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
-
-assert!
宏
-空的测试函数之所以能通过是因为任何没有panic!
的测试都是通过的,而任何panic!
的测试都算是失败。让我们使用`assert!宏来使测试失败:
+
+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
-#[test]
-fn it_works() {
- assert!(false);
+#[cfg(test)]
+mod tests {
+ #[test]
+ fn exploration() {
+ }
}
-assert!
宏由标准库提供,它获取一个参数,如果参数是true
,什么也不会发生。如果参数是false
,这个宏会panic!
。再次运行测试:
-$ 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 1 test
-test it_works ... FAILED
-
-failures:
-
----- it_works stdout ----
- thread 'it_works' panicked at 'assertion failed: false', src/lib.rs:5
-note: Run with `RUST_BACKTRACE=1` for a backtrace.
-
-
-failures:
- it_works
-
-test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
-
-error: test failed
-
-Rust 表明测试失败了:
-test it_works ... FAILED
-
-并展示了测试是因为src/lib.rs的第 5 行
assert!宏得到了一个
false`值而失败的:
-thread 'it_works' panicked at 'assertion failed: false', src/lib.rs:5
-
-失败的测试也体现在了总结行中:
-test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
-
-使用assert_eq!
和assert_ne!
宏来测试相等
-测试功能的一个常用方法是将需要测试代码的值与期望值做比较,并检查是否相等。可以通过向assert!
宏传递一个使用==
宏的表达式来做到。不过这个操作实在是太常见了,以至于标注库提供了一对宏来编译处理这些操作:assert_eq!
和assert_ne!
。这两个宏分别比较两个值是相等还是不相等。使用这些宏的另一个优势是当断言失败时他们会打印出这两个值具体是什么,以便于观察测试为什么失败,而assert!
只会打印出它从==
表达式中得到了false
值。
-下面是分别使用这两个宏其会测试通过的例子:
-Filename: src/lib.rs
-#[test]
-fn it_works() {
- assert_eq!("Hello", "Hello");
-
- assert_ne!("Hello", "world");
-}
-
-也可以对这些宏指定可选的第三个参数,它是一个会加入错误信息的自定义文本。这两个宏展开后的逻辑看起来像这样:
-// assert_eq! - panic if the values aren't equal
-if left_val != right_val {
- panic!(
- "assertion failed: `(left == right)` (left: `{:?}`, right: `{:?}`): {}"
- left_val,
- right_val,
- optional_custom_message
- )
-}
-
-// assert_ne! - panic if the values are equal
-if left_val == right_val {
- panic!(
- "assertion failed: `(left != right)` (left: `{:?}`, right: `{:?}`): {}"
- left_val,
- right_val,
- optional_custom_message
- )
-}
-
-看看这个因为hello
不等于world
而失败的测试。我们还增加了一个自定义的错误信息,greeting operation failed
:
-Filename: src/lib.rs
-#[test]
-fn a_simple_case() {
- let result = "hello"; // this value would come from running your code
- assert_eq!(result, "world", "greeting operation failed");
-}
-
-毫无疑问运行这个测试会失败,而错误信息解释了为什么测试失败了并且带有我们的指定的自定义错误信息:
----- a_simple_case stdout ----
- thread 'a_simple_case' panicked at 'assertion failed: `(left == right)`
- (left: `"hello"`, right: `"world"`): greeting operation failed',
- src/main.rs:4
-
-assert_eq!
的两个参数被称为 "left" 和 "right" ,而不是 "expected" 和 "actual" ;值的顺序和硬编码的值并没有什么影响。
-因为这些宏使用了==
和!=
运算符并使用调试格式打印这些值,进行比较的值必须实现PartialEq
和Debug
trait。Rust 提供的类型实现了这些 trait,不过自定义的结构体和枚举则需要自己实现PartialEq
以便能够断言这些值是否相等,和实现Debug
以便在断言失败时打印出这些值。因为第五章提到过这两个 trait 都是 derivable trait,所以通常可以直接在结构体或枚举上加上#[derive(PartialEq, Debug)]
注解。查看附录 C 来寻找更多关于这些和其他 derivable trait 的信息。
-使用should_panic
测试期望的失败
-可以使用另一个属性来反转测试中的失败:should_panic
。这在测试调用特定的函数会产生错误的函数时很有帮助。例如,让我们测试第八章中的一些我们知道会 panic 的代码:尝试使用 range 语法和并不组成完整字母的字节索引来创建一个字符串 slice。在有#[test]
属性的函数之前增加#[should_panic]
属性,如列表 11-1 所示:
-
-这个测试是成功的,因为我们表示代码应该会 panic。相反如果代码因为某种原因没有产生panic!
则测试会失败。
-使用should_panic
的测试是脆弱的,因为难以保证测试不会因为一个不同于我们期望的原因失败。为了帮助解决这个问题,should_panic
属性可以增加一个可选的expected
参数。测试工具会确保错误信息里包含我们提供的文本。一个比列表 11-1 更健壮的版本如列表 11-2 所示:
-
-请自行尝试当should_panic
的测试出现 panic 但并不符合期望的信息时会发生什么:在测试中因为不同原因造成panic!
,或者将期望的 panic 信息改为并不与字母字节边界 panic 信息相匹配。
-运行测试
-
-ch11-02-running-tests.md
-
-commit cf52d81371e24e14ce31a5582bfcb8c5b80d26cc
-
-类似于cargo run
会编译代码并运行生成的二进制文件,cargo test
在测试模式下编译代码并运行生成的测试二进制文件。cargo test
生成的二进制文件默认会并行的运行所有测试并在测试过程中捕获生成的输出,这样就更容易阅读测试结果的输出。
-可以通过指定命令行选项来改变这些运行测试的默认行为。这些选项的一部分可以传递给cargo test
,而另一些则需要传递给生成的测试二进制文件。分隔这些参数的方法是--
:cargo test
之后列出了传递给cargo test
的参数,接着是分隔符--
,之后是传递给测试二进制文件的参数。
-并行运行测试
-测试使用线程来并行运行。为此,编写测试时需要注意测试之间不要相互依赖或者存在任何共享状态。共享状态也可能包含在运行环境中,比如当前工作目录或者环境变量。
-如果你不希望它这样运行,或者想要更加精确的控制使用线程的数量,可以传递--test-threads
参数和线程的数量给测试二进制文件。将线程数设置为 1 意味着没有任何并行操作:
-$ cargo test -- --test-threads=1
-
-捕获测试输出
-Rust 的测试库默认捕获并丢弃标准输出和标准错误中的输出,除非测试失败了。例如,如果在测试中调用了println!
而测试通过了,你将不会在终端看到println!
的输出。这个行为可以通过向测试二进制文件传递--nocapture
参数来禁用:
-$ cargo test -- --nocapture
-
-通过名称来运行测试的子集
-有时运行整个测试集会耗费很多时间。如果你负责特定位置的代码,你可能会希望只与这些代码相关的测试。cargo test
有一个参数允许你通过指定名称来运行特定的测试。
-列表 11-3 中创建了三个如下名称的测试:
-
-使用不同的参数会运行不同的测试子集。没有参数的话,如你所见会运行所有的测试:
-$ cargo test
- Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
- Running target/debug/deps/adder-abcabcabc
-
-running 3 tests
-test add_three_and_two ... ok
-test one_hundred ... ok
-test add_two_and_two ... ok
-
-test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured
-
-可以传递任意测试的名称来只运行那个测试:
-$ cargo test one_hundred
- Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
- Running target/debug/deps/adder-abcabcabc
-
-running 1 test
-test one_hundred ... ok
+并再次运行cargo test
。现在输出中将出现exploration
而不是it_works
:
+running 1 test
+test tests::exploration ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
-也可以传递名称的一部分,cargo test
会运行所有匹配的测试:
-$ cargo test add
- Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
- Running target/debug/deps/adder-abcabcabc
+让我们增加另一个测试,不过这一次是一个会失败的测试!当测试函数中出现 panic 时测试就失败了。第九章讲到了最简单的造成 panic 的方法:调用panic!
宏!写入新函数后 src/lib.rs
现在看起来如列表 11-3 所示:
+Filename: src/lib.rs
+#[cfg(test)]
+mod tests {
+ #[test]
+ fn exploration() {
+ }
-running 2 tests
-test add_three_and_two ... ok
-test add_two_and_two ... ok
+ #[test]
+ fn another() {
+ panic!("Make this test fail");
+ }
+}
+
+
+再次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
+
+
+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
+#[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
+ }
+}
+
+
+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));
+ }
+}
+
+
+注意在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));
+ }
+}
+
+因为这里can_hold
函数的正确结果是false
,我们需要将这个结果取反后传递给assert!
宏。这样的话,测试就会通过而can_hold
将返回false
:
+running 2 tests
+test tests::smaller_can_hold_larger ... ok
+test tests::larger_can_hold_smaller ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured
-模块名也作为测试名的一部分,所以类似的模块名也可以用来指定测试特定模块。例如,如果将我们的代码组织成一个叫adding
的模块和一个叫subtracting
的模块并分别带有测试,如列表 11-4 所示:
-
-执行cargo test
会运行所有的测试,而模块名会出现在输出的测试名中:
-$ cargo test
- Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
- Running target/debug/deps/adder-abcabcabc
-
-running 4 tests
-test adding::add_two_and_two ... ok
-test adding::add_three_and_two ... ok
-test subtracting::subtract_three_and_two ... ok
-test adding::one_hundred ... ok
-
-运行cargo test adding
将只会运行对应模块的测试而不会运行任何 subtracting 模块中的测试:
-$ cargo test adding
- Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
- Running target/debug/deps/adder-abcabcabc
-
-running 3 tests
-test adding::add_three_and_two ... ok
-test adding::one_hundred ... ok
-test adding::add_two_and_two ... ok
+
+如果没有传递任何参数就运行测试,如你所见,所有测试都会并行运行:
+running 3 tests
+test tests::add_two_and_two ... ok
+test tests::add_three_and_two ... ok
+test tests::one_hundred ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured
+运行单个测试
+可以向cargo test
传递任意测试的名称来只运行这个测试:
+$ cargo test one_hundred
+ Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
+ Running target/debug/deps/adder-06a75b4a1f2515e9
+
+running 1 test
+test tests::one_hundred ... ok
+
+test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
+
+不能像这样指定多个测试名称,只有传递给cargo test
的第一个值才会被使用。
+过滤运行多个测试
+然而,可以指定测试的部分名称,这样任何名称匹配这个值的测试会被运行。例如,因为头两个测试的名称包含add
,可以通过cargo test add
来运行这两个测试:
+$ cargo test add
+ Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
+ Running target/debug/deps/adder-06a75b4a1f2515e9
+
+running 2 tests
+test tests::add_two_and_two ... ok
+test tests::add_three_and_two ... ok
+
+test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured
+
+这运行了所有名字中带有add
的测试。同时注意测试所在的模块作为测试名称的一部分,所以可以通过模块名来过滤运行一个模块中的所有测试。
+
+
除非指定否则忽略某些测试
-有时一些特定的测试执行起来是非常耗费时间的,所以对于大多数cargo test
命令,我们希望能排除它。无需为cargo test
创建一个用来在运行所有测试时排除特定测试的参数并每次都要记得使用它,我们可以对这些测试使用ignore
属性:
+有时一些特定的测试执行起来是非常耗费时间的,所以在运行大多数cargo test
的时候希望能排除他们。与其通过参数列举出所有希望运行的测试,也可以使用ignore
属性来标记耗时的测试来排除他们:
Filename: src/lib.rs
#[test]
fn it_works() {
@@ -5832,11 +6041,11 @@ fn expensive_test() {
// code that takes an hour to run
}
-现在运行测试,将会发现it_works
运行了,而expensive_test
没有:
+我们对想要排除的测试的#[test]
之后增加了#[ignore]
行。现在如果运行测试,就会发现it_works
运行了,而expensive_test
没有运行:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished debug [unoptimized + debuginfo] target(s) in 0.24 secs
- Running target/debug/deps/adder-abcabcabc
+ Running target/debug/deps/adder-ce99bcc2479f4607
running 2 tests
test expensive_test ... ignored
@@ -5850,29 +6059,39 @@ running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
-我们可以通过cargo test -- --ignored
来明确请求只运行那些耗时的测试:
-$ cargo test -- --ignored
+expensive_test
被列为ignored
,如果只希望运行被忽略的测试,可以使用cargo test -- --ignored
来请求运行他们:
+
+
+
+
+$ cargo test -- --ignored
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
- Running target/debug/deps/adder-abcabcabc
+ Running target/debug/deps/adder-ce99bcc2479f4607
running 1 test
test expensive_test ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
-通过这种方式,大部分时间运行cargo test
将是快速的。当需要检查ignored
测试的结果而且你也有时间等待这个结果的话,可以选择执行cargo test -- --ignored
。
+通过控制运行哪些测试,可以确保运行cargo test
的结果是快速的。当某个时刻需要检查ignored
测试的结果而且你也有时间等待这个结果的话,可以选择执行cargo test -- --ignored
。
测试的组织结构
-ch11-03-test-organization.md
+
ch11-03-test-organization.md
-commit cf52d81371e24e14ce31a5582bfcb8c5b80d26cc
+commit 55b294f20fc846a13a9be623bf322d8b364cee77
-正如之前提到的,测试是一个很广泛的学科,而且不同的人有时也采用不同的技术和组织。Rust 社区倾向于根据测试的两个主要分类来考虑问题:单元测试(unit tests)与集成测试(unit tests)。单元测试倾向于更小而更专注,在隔离的环境中一次测试一个模块。他们也可以测试私有接口。集成测试对于你的库来说则完全是外部的。他们与其他用户使用相同的方式使用你得代码,他们只针对共有接口而且每个测试会测试多个模块。这两类测试对于从独立和整体的角度保证你的库符合期望是非常重要的。
+正如之前提到的,测试是一个很广泛的学科,而且不同的开发者也采用不同的技术和组织。Rust 社区倾向于根据测试的两个主要分类来考虑问题:单元测试(unit tests)与集成测试(unit tests)。单元测试倾向于更小而更专注,在隔离的环境中一次测试一个模块。他们也可以测试私有接口。集成测试对于你的库来说则完全是外部的。他们与其他用户使用相同的方式使用你的代码,他们只针对共有接口而且每个测试都会测试多个模块。
+这两类测试对于从独立和整体的角度保证你的库符合期望是非常重要的。
单元测试
-单元测试的目的是在隔离与其他部分的环境中测试每一个单元的代码,以便于快速而准确的定位何处的代码是否符合预期。单元测试位于 src 目录中,与他们要测试的代码存在于相同的文件中。他们被分离进每个文件中他们自有的tests
模块中。
+单元测试的目的是在与其他部分隔离的环境中测试每一个单元的代码,以便于快速而准确的定位何处的代码是否符合预期。单元测试位于 src 目录中,与他们要测试的代码存在于相同的文件中。传统做法是在每个文件中创建包含测试函数的tests
模块,并使用cfg(test)
标注模块。
测试模块和cfg(test)
-通过将测试放进他们自己的模块并对该模块使用cfg
注解,我们可以告诉 Rust 只在执行cargo test
时才编译和运行测试代码。这在当我们只希望用cargo build
编译库代码时可以节省编译时间,并减少编译产物的大小因为并没有包含测试。
-还记得上一部分新建的adder
项目吗?Cargo 为我们生成了如下代码:
+测试模块的#[cfg(test)]
注解告诉 Rust 只在执行cargo test
时才编译和运行测试代码,而在运行cargo build
时不这么做。这在只希望构建库的时候可以节省编译时间,并能节省编译产物的空间因为他们并没有包含测试。我们将会看到因为集成测试位于另一个文件夹,他们并不需要#[cfg(test)]
注解。但是因为单元测试位于与源码相同的文件中,所以使用#[cfg(test)]
来指定他们不应该被包含进编译产物中。
+还记得本章第一部分新建的adder
项目吗?Cargo 为我们生成了如下代码:
Filename: src/lib.rs
#[cfg(test)]
mod tests {
@@ -5881,48 +6100,10 @@ mod tests {
}
}
-我们忽略了模块相关的信息以便更关注模块中测试代码的机制,不过现在让我们看看测试周围的代码。
-首先,这里有一个属性cfg
。cfg
属性让我们声明一些内容只在给定特定的配置(configuration)时才被包含进来。Rust 提供了test
配置用来编译和运行测试。通过这个属性,Cargo 只会在尝试运行测试时才编译测试代码。
-接下来,tests
包含了所有测试函数,而我们的代码则位于tests
模块之外。tests
模块的名称是一个惯例,除此之外这是一个遵守第七章讲到的常见可见性规则的普通模块。因为这是一个内部模块,我们需要将要测试的代码引入作用域。这对于一个大的模块来说是很烦人的,所以这里经常使用全局导入。
-从本章到现在,我们一直在为adder
项目编写并没有实际调用任何代码的测试。现在让我们做一些改变!在 src/lib.rs 中,放入add_two
函数和带有一个检验代码的测试的tests
模块,如列表 11-5 所示:
-
-注意除了测试函数之外,我们还在tests
模块中添加了use add_two;
。这将我们想要测试的代码引入到了内部的tests
模块的作用域中,正如任何内部模块需要做的那样。如果现在使用cargo test
运行测试,它会通过:
-running 1 test
-test tests::it_works ... ok
-
-test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
-
-如果我们忘记将add_two
函数引入作用域,将会得到一个 unresolved name 错误,因为tests
模块并不知道任何关于add_two
函数的信息:
-error[E0425]: unresolved name `add_two`
- --> src/lib.rs:9:23
- |
-9 | assert_eq!(4, add_two(2));
- | ^^^^^^^ unresolved name
-
-如果这个模块包含很多希望测试的代码,在测试中列出每一个use
语句将是很烦人的。相反在测试子模块中使用use super::*;
来一次将所有内容导入作用域中是很常见的。
+这里自动生成了测试模块。cfg
属性代表 configuration ,它告诉 Rust 其之后的项只被包含进特定配置中。在这个例子中,配置是test
,Rust 所提供的用于编译和运行测试的配置。通过使用这个属性,Cargo 只会在我们主动使用cargo test
运行测试时才编译测试代码。除了标注为#[test]
的函数之外,这还包括测试模块中可能存在的帮助函数。
测试私有函数
-测试社区中一直存在关于是否应该对私有函数进行单元测试的论战。不过无论你坚持哪种测试意识形态,Rust 确实允许你测试私有函数,由于私有性规则。考虑列表 11-6 中带有私有函数internal_adder
的代码:
-
-因为测试也不过是 Rust 代码而tests
也只是另一个模块,我们完全可以在一个测试中导入并调用internal_adder
。如果你并不认为私有函数应该被测试,Rust 也不会强迫你这么做。
+
+
+
+注意internal_adder
函数并没有标记为pub
,不过因为测试也不过是 Rust 代码而tests
也仅仅是另一个模块,我们完全可以在测试中导入和调用internal_adder
。如果你并不认为私有函数应该被测试,Rust 也不会强迫你这么做。
集成测试
-在 Rust 中,集成测试对于需要测试的库来说是完全独立。他们同其他代码一样使用库文件。他们的目的是测试库的个个部分结合起来能否正常工作。每个能正确运行的代码单元集成在一起也可能会出现问题,所以集成测试的覆盖率也是很重要的。
+在 Rust 中,集成测试对于需要测试的库来说是完全独立。他们同其他代码一样使用库文件,这意味着他们只能调用作为库公有 API 的一部分的函数。他们的目的是测试库的多个部分能否一起正常工作。每个能单独正确运行的代码单元集成在一起也可能会出现问题,所以集成测试的覆盖率也是很重要的。为了创建集成测试,首先需要一个 tests 目录。
tests 目录
-Cargo 支持位于 tests 目录中的集成测试。如果创建它并放入 Rust 源文件,Cargo 会将每一个文件当作单独的 crate 来编译。让我们试一试!
-首先,在项目根目录创建一个 tests 目录,挨着 src 目录。接着新建一个文件 tests/integration_test.rs,并写入列表 11-7 中的代码:
-
-在开头使用了extern crate adder
,单元测试中并不需要它。tests
目录中的每一个测试文件都是完全独立的 crate,所以需要在每个文件中导入我们的库。这也就是为何tests
是编写集成测试的绝佳场所:他们像任何其他用户那样,需要将库导入 crate 并只能使用公有 API。
-这个文件中也不需要tests
模块。除非运行测试否则整个文件夹都不会被编译,所以无需将任何部分标记为#[cfg(test)]
。另外每个测试文件都被隔离进其自己的 crate 中,无需进一步隔离测试代码。
-让我们运行集成测试,同样使用cargo test
来运行:
-$ cargo test
+
+我们在顶部增加了extern crate adder
,这在单元测试中是不需要的。这是因为每一个tests
目录中的测试文件都是完全独立的 crate,所以需要在每一个文件中导入库。集成测试就像其他使用者那样通过导入 crate 并只使用公有 API 来使用库文件。
+并不需要将 tests/integration_test.rs 中的任何代码标注为#[cfg(test)]
。Cargo 对tests
文件夹特殊处理并只会在运行cargo test
时编译这个目录中的文件。现在就试试运行cargo test
:
+cargo test
Compiling adder v0.1.0 (file:///projects/adder)
+ Finished debug [unoptimized + debuginfo] target(s) in 0.31 secs
Running target/debug/deps/adder-abcabcabc
running 1 test
-test tests::it_works ... ok
+test tests::internal ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
- Running target/debug/integration_test-952a27e0126bb565
+ Running target/debug/deps/integration_test-ce99bcc2479f4607
running 1 test
test it_adds_two ... ok
@@ -5989,8 +6171,14 @@ running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
-现在有了三个部分的输出:单元测试、集成测试和文档测试。注意当在任何 src 目录的文件中增加单元测试时,单元测试部分的对应输出也会增加。增加集成测试文件中的测试函数也会对应增加输出。如果在 tests 目录中增加集成测试文件,则会增加更多集成测试部分:一个文件对应一个部分。
-为cargo test
指定测试函数名称参数也会匹配集成测试文件中的函数。为了只运行某个特定集成测试文件中的所有测试,可以使用cargo test
的--test
参数:
+
+
+现在有了三个部分的输出:单元测试、集成测试和文档测试。第一部分单元测试与我们之前见过的一样:每一个单元测试一行(列表 11-12 中有一个叫做internal
的测试),接着是一个单元测试的总结行。
+集成测试部分以行Running target/debug/deps/integration-test-ce99bcc2479f4607
(输出最后的哈希值可能不同)开头。接着是每一个集成测试中的测试函数一行,以及一个就在Doc-tests adder
部分开始之前的集成测试的总结行。
+注意在任意 src 文件中增加更多单元测试函数会增加更多单元测试部分的测试结果行。在我们创建的集成测试文件中增加更多测试函数会增加更多集成测试部分的行。每一个集成测试文件有其自己的部分,所以如果在 tests 目录中增加更多文件,这里就会有更多集成测试部分。
+我们仍然可以通过指定测试函数的名称作为cargo test
的参数来运行特定集成测试。为了运行某个特定集成测试文件中的所有测试,使用cargo test
的--test
后跟文件的名称:
$ cargo test --test integration_test
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
Running target/debug/integration_test-952a27e0126bb565
@@ -6000,30 +6188,92 @@ test it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
+这些只是 tests 目录中我们指定的文件中的测试。
集成测试中的子模块
-随着集成测试的增加,你可能希望在 tests
目录增加更多文件,例如根据测试的功能来将测试分组。正如我们之前提到的,这是可以的,Cargo 会将每一个文件当作一个独立的 crate。
-最终,可能会有一系列在所有集成测试中通用的帮助函数,例如建立通用场景的函数。如果你将这些函数提取到 tests 目录的一个文件中,比如说 tests/common.rs,则这个文件将会像这个目录中的其他包含测试的 Rust 文件一样被编译进一个单独的 crate 中。它也会作为一个独立的部分出现在测试输出中。因为这很可能不是你所希望的,所以建议在子目录中使用 mod.rs 文件,比如 tests/common/mod.rs,来放置帮助函数。tests 的子目录不会被作为单独的 crate 编译或者作为单独的部分出现在测试输出中。
+随着集成测试的增加,你可能希望在 tests
目录增加更多文件,例如根据测试的功能来将测试分组。正如我们之前提到的,每一个 tests 目录中的文件都被编译为单独的 crate。
+将每个集成测试文件当作其自己的 crate 来对待有助于创建更类似与终端用户使用 crate 那样的单独的作用域。然而,这意味着考虑到像第七章学习的如何将代码分隔进模块和文件那样,tests 目录中的文件不能像 src 中的文件那样共享相同的行为。
+对于 tests 目录中文件的不同行为,通常在如果有一系列有助于多个集成测试文件的帮助函数,而你尝试遵循第七章的步骤将他们提取到一个通用的模块中时显得很明显。例如,如果我们创建了 tests/common.rs 并将setup
函数放入其中,这里将放入一些希望能够在多个测试文件的多个测试函数中调用的代码:
+Filename: tests/common.rs
+pub fn setup() {
+ // setup code specific to your library's tests would go here
+}
+
+如果再次运行测试,将会在测试结果中看到一个对应 common.rs 文件的新部分,即便这个文件并没有包含任何测试函数,或者没有任何地方调用了setup
函数:
+running 1 test
+test tests::internal ... ok
+
+test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
+
+ Running target/debug/deps/common-b8b07b6f1be2db70
+
+running 0 tests
+
+test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
+
+ Running target/debug/deps/integration_test-d993c68b431d39df
+
+running 1 test
+test it_adds_two ... ok
+
+test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
+
+ Doc-tests adder
+
+running 0 tests
+
+test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
+
+
+common
出现在测试结果中并显示running 0 tests
,这不是我们想要的;我们只是希望能够在其他集成测试文件中分享一些代码罢了。
+为了使common
不出现在测试输出中,需要使用第七章学习到的另一个将代码提取到文件的方式:不再创建tests/common.rs,而是创建 tests/common/mod.rs。当将setup
代码移动到 tests/common/mod.rs 并去掉 tests/common.rs 文件之后,测试输出中将不会出现这一部分。tests 目录中的子目录不会被作为单独的 crate 编译或作为一部分出现在测试输出中。
+一旦拥有了 tests/common/mod.rs,就可以将其作为模块来在任何集成测试文件中使用。这里是一个 tests/integration_test.rs 中调用setup
函数的it_adds_two
测试的例子:
+Filename: tests/integration_test.rs
+extern crate adder;
+
+mod common;
+
+#[test]
+fn it_adds_two() {
+ common::setup();
+ assert_eq!(4, adder::add_two(2));
+}
+
+注意mod common;
声明与第七章中的模块声明相同。接着在测试函数中就可以调用common::setup()
了。
二进制 crate 的集成测试
-如果项目是二进制 crate 并且只包含 src/main.rs 而没有 src/lib.rs,这样就不可能在 tests 创建集成测试并使用 extern crate
导入 src/main.rs 中的函数了。这也是 Rust 二进制项目明确采用 src/main.rs 调用 src/lib.rs 中逻辑的结构的原因之一。通过这种结构,集成测试就可以使用extern crate
测试库 crate 中的主要功能,而如果这些功能没有问题的话,src/main.rs 中的少量代码也就会正常工作且不需要测试。
+如果项目是二进制 crate 并且只包含 src/main.rs 而没有 src/lib.rs,这样就不可能在 tests 创建集成测试并使用 extern crate
导入 src/main.rs 中的函数了。只有库 crate 向其他 crate 暴露了可以调用和使用的函数;二进制 crate 只意在单独运行。
+这也是 Rust 二进制项目明确采用 src/main.rs 调用 src/lib.rs 中逻辑这样的结构的原因之一。通过这种结构,集成测试就可以使用extern crate
测试库 crate 中的主要功能,而如果这些重要的功能没有问题的话,src/main.rs 中的少量代码也就会正常工作且不需要测试。
总结
-Rust 的测试功能提供了一个确保即使改变代码函数也能继续以指定方式运行的途径。单元测试独立的验证库的每一部分并能够测试私有实现细节。集成测试则涉及多个部分结合起来时能否使用,并像其他代码那样测试库的公有 API。Rust 的类型系统和所有权规则可以帮助避免一些 bug,不过测试对于减少代码是否符合期望的逻辑 bug 是很重要的。
+Rust 的测试功能提供了一个确保即使做出改变函数也能继续以指定方式运行的途径。单元测试独立的验证库的不同部分并能够测试私有实现细节。集成测试则涉及多个部分结合起来工作时的用例,并像其他代码那样测试库的公有 API。即使 Rust 的类型系统和所有权规则可以帮助避免一些 bug,不过测试对于减少代码是否符合期望相关的逻辑 bug 是很重要的。
接下来让我们结合本章所学和其他之前章节的知识,在下一章一起编写一个项目!
-一个 I/O 项目
+一个 I/O 项目:构建一个小巧的 grep
-ch12-00-an-io-project.md
-commit efd59dd0fe8e3658563fb5fd289af9d862e07a03
+commit 1f432fc231cfbc310433ab2a354d77058444288c
-之前几个章节我们学习了很多知识。让我们一起运用这些新知识来构建一个项目。在这个过程中,我们还将学习到更多 Rust 标准库的内容。
-那么我们应该写点什么呢?这得是一个利用 Rust 优势的项目。Rust 的一个强大的用途是命令行工具:Rust 的运行速度、安全性、“单二进制文件”输出和跨平台支持使得它称为这类工作的绝佳选择。所以我们将创建一个我们自己的经典命令行工具:grep
。grep
有着极为简单的应用场景,它完成如下工作:
-
-- 它获取一个文件和一个字符串作为参数。
+
+
+本章既是一个目前所学的很多技能的概括,也是一个更多标准库功能的探索。我们将构建一个与文件和命令行输入/输出交互的命令行工具来练习现在一些你已经掌握的 Rust 技能。
+Rust 的运行速度、安全性、“单二进制文件”输出和跨平台支持使其成为创建命令行程序的绝佳选择,所以我们的项目将创建一个我们自己版本的经典命令行工具:grep
。grep 是“Globally search a Regular Expression and Print.”的首字母缩写。grep
最简单的使用场景是使用如下步骤在特定文件中搜索指定字符串:
+
+- 获取一个文件和一个字符串作为参数。
- 读取文件
- 寻找文件中包含字符串参数的行
- 打印出这些行
-
-另外,我们还将添加一个额外的功能:一个环境变量允许我们大小写不敏感的搜索字符串参数。
-还有另一个很好的理由使用grep
作为示例项目:Rust 社区的成员,Andrew Gallant,已经使用 Rust 创建了一个功能非常完整的grep
版本。它叫做ripgrep
,并且它非常非常快。这样虽然我们的grep
将会非常简单,你也会掌握阅读现实生活中项目的基础知识。
+
+我们还会展示如何使用环境变量和打印到标准错误而不是标准输出;这些功能在命令行工具中是很常用的。
+一位 Rust 社区的成员,Andrew Gallant,已经创建了一个功能完整且非常快速的grep
版本,叫做ripgrep
。相比之下,我们的grep
将非常简单,本章将交给你一些帮助你理解像ripgrep
这样真实项目的背景知识。
这个项目将会结合之前所学的一些内容:
- 代码组织(使用第七章学习的模块)
@@ -6032,28 +6282,42 @@ commit efd59dd0fe8e3658563fb5fd289af9d862e07a03
- 合理的使用 trait 和生命周期(第十章)
- 测试(第十一章)
-另外,我还会简要的讲到闭包、迭代器和 trait 对象,他们分别会在第XX、YY和ZZ章详细介绍。
-让我们一如既往的使用cargo new
创建一个新项目:
-$ cargo new --bin greprs
+另外,我还会简要的讲到闭包、迭代器和 trait 对象,他们分别会在第十三章和第十七章中详细介绍。
+让我们一如既往的使用cargo new
创建一个新项目。我们称之为greprs
以便与可能已经安装在系统上的grep
工具相区别:
+$ cargo new --bin greprs
Created binary (application) `greprs` project
$ cd greprs
-我们版本的grep
的叫做“greprs”,这样就不会迷惑用户让他们以为这就是可能已经在系统上安装了功能更完整的grep
。
接受命令行参数
ch12-01-accepting-command-line-arguments.md
-commit 4f2dc564851dc04b271a2260c834643dfd86c724
+commit b8e4fcbf289b82c12121b282747ce05180afb1fb
-第一个任务是让greprs
接受两个命令行参数。crates.io 上有一些现存的库可以帮助我们,不过因为我们正在学习,我们将自己实现一个。
-我们需要调用一个 Rust 标准库提供的函数:std::env::args
。这个函数返回一个传递给程序的命令行参数的迭代器(iterator)。我们还未讨论到迭代器,第十三章会全面的介绍他们。但是对于我们的目的来说,使用他们并不需要知道多少技术细节。我们只需要明白两点:
+第一个任务是让greprs
能够接受两个命令行参数:文件名和要搜索的字符串。也就是说希望能够使用cargo run
,要搜索的字符串和被搜索的文件的路径来运行程序,像这样:
+$ cargo run searchstring example-filename.txt
+
+现在cargo new
生成的程序忽略任何传递给它的参数。crates.io 上有一些现存的可以帮助我们接受命令行参数的库,不过因为我们正在学习,让我们实现一个。
+
+
+读取参数值
+为了能够获取传递给程序的命令行参数的值,我们需要调用一个 Rust 标准库提供的函数:std::env::args
。这个函数返回一个传递给程序的命令行参数的迭代器(iterator)。我们还未讨论到迭代器,第十三章会全面的介绍他们。但是对于我们现在的目的来说只需要明白两点:
- 迭代器生成一系列的值。
- 在迭代器上调用
collect
方法可以将其生成的元素转换为一个 vector。
-让我们试试列表 12-1 中的代码:
-
+Listing 12-1: Collect the command line arguments into a vector and print them
+out
-首先使用use
语句来将std::env
模块引入作用域。当函数嵌套了多于一层模块时,比如说std::env::args
,通常使用use
将父模块引入作用域,而不是引入其本身。env::args
比单独的args
要明确一些。当然,如果使用了多余一个std::env
中的函数,我们也只需要一个use
语句。
-在main
函数的第一行,我们调用了env::args
,并立即使用collect
来创建了一个 vector。这里我们也显式的注明了args
的类型:collect
可以被用来创建很多类型的集合。Rust 并不能推断出我们需要什么类型,所以类型注解是必须的。在 Rust 中我们很少会需要注明类型,不过collect
是就一个通常需要这么做的函数。
+首先使用use
语句来将std::env
模块引入作用域以便可以使用它的args
函数。注意std::env::args
函数嵌套进了两层模块中。如第七章讲到的,当所需函数嵌套了多于一层模块时,通常将父模块引入作用域,而不是其自身。这便于我们利用std::env
中的其他函数。这比增加了use std::env::args;
后仅仅使用args
调用函数要更明确一些;这样看起来好像一个定义于当前模块的函数。
+
+
+
+注意:std::env::args
在其任何参数包含无效 Unicode 字符时会 panic。如果你需要接受包含无效 Unicode 字符的参数,使用std::env::args_os
代替。这个函数返回OsString
值而不是String
值。出于简单考虑这里使用std::env::args
,因为OsString
值每个平台都不一样而且比String
值处理起来更复杂。
+
+
+
+
+在main
函数的第一行,我们调用了env::args
,并立即使用collect
来创建了一个包含迭代器所有值的 vector。collect
可以被用来创建很多类型的集合,所以这里显式注明的args
类型来指定我们需要一个字符串 vector。虽然在 Rust 中我们很少会需要注明类型,collect
就是一个经常需要注明类型的函数,因为 Rust 不能推断出你想要什么类型的集合。
最后,我们使用调试格式:?
打印出 vector。让我们尝试不用参数运行代码,接着用两个参数:
$ cargo run
["target/debug/greprs"]
@@ -6076,52 +6347,56 @@ $ cargo run needle haystack
...snip...
["target/debug/greprs", "needle", "haystack"]
-你会注意一个有趣的事情:二进制文件的名字是第一个参数。其原因超出了本章介绍的范围,不过这是我们必须记住的。
-现在我们有了一个访问所有参数的方法,让我们如列表 12-2 中所示将需要的变量存放到变量中:
-
+Listing 12-2: Create variables to hold the query argument and filename argument
-记住,程序名称是是第一个参数,所以并不需要args[0]
。我们决定从第一个参数将是需要搜索的字符串,所以将第一个参数的引用放入变量search
中。第二个参数将是文件名,将其放入变量filename
中。再次尝试运行程序:
+正如我们在打印出 vector 时所看到的,程序的名称占据了 vector 的第一个值args[0]
,所以我们从索引1
开始。第一个参数greprs
是需要搜索的字符串,所以将其将第一个参数的引用存放在变量query
中。第二个参数将是文件名,所以将第二个参数的引用放入变量filename
中。
+我们将临时打印出出这些变量的值,再一次证明代码如我们期望的那样工作。让我们使用参数test
和sample.txt
再次运行这个程序:
$ cargo run test sample.txt
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
- Running `target\debug\greprs.exe test sample.txt`
+ Running `target/debug/greprs test sample.txt`
Searching for test
In file sample.txt
-很棒!不过有一个问题。让我们不带参数运行:
-$ cargo run
- Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
- Running `target\debug\greprs.exe`
-thread 'main' panicked at 'index out of bounds: the len is 1
-but the index is 1', ../src/libcollections\vec.rs:1307
-note: Run with `RUST_BACKTRACE=1` for a backtrace.
-
-因为 vector 中只有一个元素,就是程序名称,不过我们尝试访问第二元素,程序 panic 并提示越界访问。虽然这个错误信息是_准确的_,不过它对程序的用户来说就没有意义了。现在就可以修复这个问题,不过我先继续学习别的内容:在程序结束前我们会改善这个情况。
+好的,它可以工作!我们将所需的参数值保存进了对应的变量中。之后会增加一些错误处理来应对类似用户没有提供参数的情况,不过现在我们将忽略他们并开始增加读取文件功能。
读取文件
ch12-02-reading-a-file.md
-commit 4f2dc564851dc04b271a2260c834643dfd86c724
+commit b8e4fcbf289b82c12121b282747ce05180afb1fb
-现在有了一些包含我们需要的信息的变量了,让我们试着使用他们。下一步目标是打开需要搜索的文件。为此,我需要一个文件。在项目的根目录创建一个文件poem.txt
,并写入一些艾米莉·狄金森(Emily Dickinson)的诗:
+接下来我们将读取由命令行文件名参数指定的文件。首先,需要一个用来测试的示例文件——用来确保greprs
正常工作的最好的文件是拥有少量文本和多个行且有一些重复单词的文件。列表 12-3 是一首艾米莉·狄金森(Emily Dickinson)的诗,它正适合这个工作!在项目根目录创建一个文件poem.txt
,并输入诗 "I'm nobody! Who are you?":
Filename: poem.txt
-I'm nobody! Who are you?
+I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us — don't tell!
They'd banish us, you know.
@@ -6131,24 +6406,27 @@ How public, like a frog
To tell your name the livelong day
To an admiring bog!
+
-创建完这个文件后,让我们编辑 src/main.rs 并增加如列表 12-3 所示用来打开文件的代码:
-
+Listing 12-4: Reading the contents of the file specified by the second argument
-这里增加了一些新内容。首先,需要更多的use
语句来引入标准库中的相关部分:我们需要std::fs::File
来处理文件,而std::io::prelude::*
则包含许多对于 I/O 包括文件 I/O 有帮助的 trait。类似于 Rust 有一个通用的 prelude 来自动引入特定内容,std::io
也有其自己的 prelude 来引入处理 I/O 时需要的内容。不同于默认的 prelude,必须显式use
位于std::io
中的 prelude。
-在main
中,我们增加了三点内容:第一,我们获取了文件的句柄并使用File::open
函数与第二个参数中指定的文件名来打开这个文件。第二,我们在变量contents
中创建了一个空的可变的String
,接着对文件句柄调用read_to_string
并以contents
字符串作为参数,contents
是read_to_string
将会放置它读取到的数据地方。最后,我们打印出了整个文件的内容,这是一个确认目前为止的程序能够工作的方法。
-尝试运行这些代码,随意指定第一个参数(因为还未实现搜索功能的部分)而将 poem.txt 文件将作为第二个参数:
+首先,增加了更多的use
语句来引入标准库中的相关部分:需要std::fs::File
来处理文件,而std::io::prelude::*
则包含许多对于 I/O 包括文件 I/O 有帮助的 trait。类似于 Rust 有一个通用的 prelude 来自动引入特定内容,std::io
也有其自己的 prelude 来引入处理 I/O 时所需的通用内容。不同于默认的 prelude,必须显式use
位于std::io
中的 prelude。
+在main
中,我们增加了三点内容:第一,通过传递变量filename
的值调用File::open
函数的值来获取文件的可变句柄。创建了叫做contents
的变量并将其设置为一个可变的,空的String
。它将会存放之后读取的文件的内容。第三,对文件句柄调用read_to_string
并传递contents
的可变引用作为参数。
+在这些代码之后,我们再次增加了临时的println!
打印出读取文件后contents
的值,这样就可以检查目前为止的程序能否工作。
+尝试运行这些代码,随意指定一个字符串作为第一个命令行参数(因为还未实现搜索功能的部分)而将 poem.txt 文件将作为第二个参数:
$ cargo run the poem.txt
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
- Running `target\debug\greprs.exe the poem.txt`
+ Running `target/debug/greprs the poem.txt`
Searching for the
In file poem.txt
With text:
@@ -6183,159 +6459,150 @@ How public, like a frog
To tell your name the livelong day
To an admiring bog!
-好的!我们的代码可以工作了!然而,它还有一些瑕疵。因为程序还很小,这些瑕疵并不是什么大问题,不过随着程序功能的丰富,将会越来越难以用简单的方法修复他们。现在就让我们开始重构而不是等待之后处理。重构在只有少量代码时会显得容易得多。
-读取文件
+好的!代码读取并打印出了文件的内容。虽然它还有一些瑕疵:main
函数有着多个功能,同时也没有处理可能出现的错误。虽然我们的程序还很小,这些瑕疵并不是什么大问题。不过随着程序功能的丰富,将会越来越难以用简单的方法修复他们。在开发程序时,及早开始重构是一个最佳实践,因为重构少量代码时要容易的多,所以让我们现在就开始吧。
+重构改进模块性和错误处理
ch12-03-improving-error-handling-and-modularity.md
-commit 4f2dc564851dc04b271a2260c834643dfd86c724
+commit b8e4fcbf289b82c12121b282747ce05180afb1fb
-为了完善我们程序有四个问题需要修复,而他们都与潜在的错误和程序结构有关。第一个问题是在哪打开文件:我们使用了expect
来在打开文件失败时指定一个错误信息,不过这个错误信息只是说“文件不存在”。还有很多打开文件失败的方式,不过我们总是假设是由于缺少文件导致的。例如,文件存在但是没有打开它的权限:这时,我们就打印出了错误不符合事实的错误信息!
-第二,我们不停的使用expect
,这就有点类似我们之前在不传递任何命令行参数时索引会panic!
时注意到的问题:这虽然时_可以工作_的,不过这有点没有原则性,而且整个程序中都需要他们,将错误处理都置于一处则会显得好很多。
-第三个问题是main
函数现在处理两个工作:解析参数,并打开文件。对于一个小的函数来说,这不是什么大问题。然而随着程序中的main
函数不断增长,main
函数中独立的任务也会越来越多。因为一个函数拥有很多职责,它将难以理解、难以测试并难以在不破坏其他部分的情况下做出修改。
-这也关系到我们的第四个问题:search
和filename
是程序中配置性的变量,而像f
和contents
则用来执行程序逻辑。随着main
函数增长,将引入更多的变量到作用域中,而当作用域中有更多的变量,将更难以追踪哪个变量用于什么目的。如果能够将配置型变量组织进一个结构就能使他们的目的更明确了。
-让我们重新组成程序来解决这些问题。
+为了改善我们的程序这里有四个问题需要修复,而且他们都与程序的组织方式和如何处理潜在错误有关。
+第一,main
现在进行了两个任务:它解析了参数并打开了文件。对于一个这样的小函数,这并不是一个大问题。然而如果main
中的功能持续增加,main
函数处理的单独的任务也会增加。当函数承担了更多责任,它就更难以推导,更难以测试,并且更难以在不破坏其他部分的情况下做出修改。最好能分离出功能这样每个函数就负责一个任务。
+这同时也关系到第二个问题:search
和filename
是程序中的配置变量,而像f
和contents
则用来执行程序逻辑。随着main
函数的增长,就需要引入更多的变量到作用域中,而当作用域中有更多的变量时,将更难以追踪每个变量的目的。最好能将将配置变量组织进一个结构这样就能使他们的目的更明确了。
+第三个问题是如果打开文件失败我们使用expect
来打印出错误信息,不过这个错误信息只是说file not found
。除了缺少文件之外还有很多打开文件可能失败的方式:例如,文件可能存在,不过可能没有打开它的权限。如果我们现在就出于这种情况,打印出的file not found
错误信息就给了用户一个不符合事实的建议!
+第四,我们不停的使用expect
来处理不同的错误,如果用户没有指定足够的参数来运行程序,他们会从 Rust 得到 "index out of bounds" 错误而这并不能明确的解释问题。如果所有的错误处理都位于一处这样将来的维护者在需要修改错误处理逻辑时就只需要咨询一处代码。将所有的错误处理都放在一处也有助于确保我们打印的错误信息对终端用户来说是有意义的。
+让我们通过重构项目来解决这些问题。
二进制项目的关注分离
-这类项目组织上的问题在很多相似类型的项目中很常见,所以 Rust 社区开发出一种关注分离的组织模式。这种模式可以用来组织任何用 Rust 构建的二进制项目,所以可以证明应该更早的开始这项重构,以为我们的项目符合这个模式。这个模式看起来像这样:
+main
函数负责多个任务的组织问题在许多二进制项目中很常见。所以 Rust 社区开发出一个类在main
函数开始变得庞大时进行二进制程序的关注分离的指导性过程。这些过程有如下步骤:
-- 将程序拆分成 main.rs 和 lib.rs。
-- 将命令行参数解析逻辑放入 main.rs。
-- 将程序逻辑放入 lib.rs。
-main
函数的工作是:
+- 将程序拆分成 main.rs 和 lib.rs 并将程序的逻辑放入 lib.rs 中。
+- 当命令行解析逻辑比较小时,可以保留在 main.rs 中。
+- 当命令行解析开始变得复杂时,也同样将其从 main.rs 提取到 lib.rs中。
+- 经过这些过程之后保留在
main
函数中的责任是:
-- 解析参数
-- 设置所有配置性变量
+- 使用参数值调用命令行解析逻辑
+- 设置任何其他的配置
- 调用 lib.rs 中的
run
函数
-- 如果
run
返回错误则处理这个错误
+- 如果
run
返回错误,则处理这个错误
-好的!老实说这个模式好像还很复杂。这就是关注分离的所有内容:main.rs 负责实际的程序运行,而 lib.rs 处理所有真正的任务逻辑。让我们将程序重构成这种模式。首先,提取出一个目的只在于解析参数的函数。列表 12-4 中展示了一个新的开始,main
函数调用了一个新函数parse_config
,它仍然定义于 src/main.rs 中:
-
+
-这看起来好像有点复杂,不过我们将一点一点的开展重构。在做出这些改变之后,再次运行程序并验证参数解析是否仍然正常。经常验证你的进展是一个好习惯,这样在遇到问题时就能更好地理解什么修改造成了错误。
+我们仍然将命令行参数收集进一个 vector,不过不同于在main
函数中将索引 1 的参数值赋值给变量query
和将索引 2 的值赋值给变量filename
,我们将整个 vector 传递给parse_config
函数。接着parse_config
函数将包含知道哪个参数该放入哪个变量的逻辑,并将这些值返回到main
。仍然在main
中创建变量query
和filename
,不过main
不再负责处理命令行参数与变量如何对应。
+这对我们这小程序可能有点大材小用,不过我们将采用小的、增量的步骤进行重构。在做出这些改变之后,再次运行程序并验证参数解析是否仍然正常。经常验证你的进展是一个好习惯,这样在遇到问题时能帮助你定位问题的成因。
组合配置值
-现在我们有了一个函数了,让我们接着完善它。我们代码还能设计的更好一些:函数返回了一个元组,不过接着立刻就解构成了单独的部分。这些代码本身没有问题,不过有一个地方表明仍有改善的余地:我们调用了parse_config
方法。函数名中的config
部分也表明了返回的两个值应该是组合在一起的,因为他们都是某个配置值的一部分。
+我们可以采取另一个小的步骤来进一步改善这个函数。现在函数返回一个元组,不过立刻又就将元组拆成了独立的部分。这是一个我们可能没有进行正确抽象的信号。
+另一个表明还有改进空间的迹象是parse_config
的config
部分,它暗示了我们返回的两个值是相关的并都是一个配置值的一部分。目前除了将这两个值组合进元组之外并没有表达这个数据结构的意义:我们可以将这两个值放入一个结构体并给每个字段一个有意义的名字。这会让未来的维护者更容易理解不同的值如何相互关联以及他们的目的。
+
-注意:一些同学将当使用符合类型更为合适的时候使用基本类型当作一种称为基本类型偏执(primitive obsession)的反模式。
+注意:一些同学将这种当使用符合类型更为合适的时候使用基本类型的反模式称为基本类型偏执(primitive obsession)。
-让我们引入一个结构体来存放所有的配置。列表 12-5 中展示了新增的Config
结构体定义、重构后的parse_config
和main
函数中的相关更新:
-
-parse_config
的签名现在表明它返回一个Config
值。在parse_config
的函数体中,我们之前返回了args
中String
值引用的字符串 slice,不过Config
定义为拥有两个有所有权的String
值。因为parse_config
的参数是一个String
值的 slice,Config
实例不能获取String
值的所有权:这违反了 Rust 的借用规则,因为main
函数中的args
变量拥有这些String
值并只允许parse_config
函数借用他们。
-还有许多不同的方式可以处理String
的数据;现在我们使用简单但低效率的方式,在字符串 slice 上调用clone
方法。clone
调用会生成一个字符串数据的完整拷贝,而且Config
实例可以拥有它,不过这会消耗更多时间和内存来储存拷贝字符串数据的引用,不过拷贝数据让我们使我们的代码显得更加直白。
+parse_config
的签名现在表明它返回一个Config
值。在parse_config
的函数体中,之前返回了args
中String
值引用的字符串 slice,现在我们选择定义Config
来使用拥有所有权的String
值。main
中的args
变量是参数值的所有者并只允许parse_config
函数借用他们,这意味着如果Config
尝试获取args
中值的所有权将违反 Rust 的借用规则。
+还有许多不同的方式可以处理String
的数据,而最简单但有些不太高效的方式是调用这些值的clone
方法。这会生成Config
实例可以拥有的数据的完整拷贝,不过会比储存字符串数据的引用消耗更多的时间和内存。不过拷贝数据使得代码显得更加直白因为无需管理引用的生命周期,所以在这种情况下牺牲一小部分性能来换取简洁性的取舍是值得的。
使用clone
权衡取舍
-由于其运行时消耗,许多 Rustacean 之间有一个趋势是倾向于不使用clone
来解决所有权问题。在关于迭代器的第XX章中,我们将会学习如何更有效率的处理这种情况。现在,为了编写我们的程序拷贝一些字符串是没有问题。我们只进行了一次拷贝,而且文件名和要搜索的字符串都比较短。随着你对 Rust 更加熟练,将更轻松的省略这个权衡的步骤,不过现在调用clone
是完全可以接受的。
+由于其运行时消耗,许多 Rustacean 之间有一个趋势是倾向于避免使用clone
来解决所有权问题。在关于迭代器的第十三章中,我们将会学习如何更有效率的处理这种情况,不过现在,复制一些字符串来取得进展是没有问题的,因为只会进行一次这样的拷贝,而且文件名和要搜索的字符串都比较短。在第一轮编写时拥有一个可以工作但有点低效的程序要比尝试过度优化代码更好一些。随着你对 Rust 更加熟练,将能更轻松的直奔合适的方法,不过现在调用clone
是完全可以接受的。
-main
函数更新为将parse_config
返回的Config
实例放入变量config
中,并将分别使用search
和filename
变量的代码更新为使用Config
结构体的字段。
+我们更新main
将parse_config
返回的Config
实例放入变量config
中,并更新之前分别使用search
和filename
变量的代码为现在的使用Config
结构体的字段。
+现在代码更明确的表现了我们的意图,query
和filename
是相关联的并且他们的目的是配置程序如何工作的。任何使用这些值的代码就知道在config
实例中对应目的的字段名中寻找他们。
创建一个Config
构造函数
-现在让我们考虑一下parse_config
的目的:这是一个创建Config
示例的函数。我们已经见过了一个创建实例函数的规范:像String::new
这样的new
函数。列表 12-6 中展示了将parse_config
转换为一个Config
结构体关联函数new
的代码:
-
+
-我们将parse_config
的名字改为new
并将其移动到impl
块中。我们也更新了main
中的调用代码。再次尝试编译并确保程序可以运行。
-从构造函数返回Result
-这是我们对这个方法最后的重构:还记得当 vector 含有少于三个项时访问索引 1 和 2 会 panic 并给出一个糟糕的错误信息的代码吗?让我们来修改它!列表 12-7 展示了如何在访问这些位置之前检查 slice 是否足够长,并使用一个更好的 panic 信息:
-
+
-通过在new
中添加这额外的几行代码,再次尝试不带参数运行程序:
+这类似于列表 9-8 中的Guess::new
函数,那里如果value
参数超出了有效值的范围就调用panic!
。不同于检查值的范围,这里检查args
的长度至少是 3,而函数的剩余部分则可以假设这个条件成立的基础上运行。如果
+args
少于 3 个项,这个条件将为真,并调用panic!
立即终止程序。
+有了new
中这几行额外的代码,再次不带任何参数运行程序并看看现在错误看起来像什么:
$ cargo run
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
- Running `target\debug\greprs.exe`
-thread 'main' panicked at 'not enough arguments', src\main.rs:29
+ Running `target/debug/greprs`
+thread 'main' panicked at 'not enough arguments', src/main.rs:29
note: Run with `RUST_BACKTRACE=1` for a backtrace.
-这样就好多了!至少有个一个符合常理的错误信息。然而,还有一堆额外的信息我们并不希望提供给用户。可以通过改变new
的签名来完善它。现在它只返回了一个Config
,所有没有办法表示创建Config
失败的情况。相反,可以如列表 12-8 所示返回一个Result
:
-
+
-现在new
函数返回一个Result
,在成功时带有一个Config
实例而在出现错误时带有一个&'static str
。回忆一下第十章“静态声明周期”中讲到&'static str
是一个字符串字面值,他也是现在我们的错误信息。
+
+
+现在new
函数返回一个Result
,在成功时带有一个Config
实例而在出现错误时带有一个&'static str
。回忆一下第十章“静态声明周期”中讲到&'static str
是一个字符串字面值,也是目前的错误信息。
new
函数体中有两处修改:当没有足够参数时不再调用panic!
,而是返回Err
值。同时我们将Config
返回值包装进Ok
成员中。这些修改使得函数符合其新的类型签名。
-Config::new
调用和错误处理
-现在我们需要对main
做一些修改,如列表 12-9 所示:
-
+
-新增了一个use
行来从标准库中导入process
。在main
函数中我们将处理new
函数返回的Result
值,并在其返回Config::new
时以一种更加清楚的方式结束进程。
-这里使用了一个之前没有讲到的标准库中定义的Result<T, E>
的方法:unwrap_or_else
。当Result
是Ok
时其行为类似于unwrap
:它返回Ok
内部封装的值。与unwrap
不同的是,当Result
是Err
时,它调用一个闭包(closure),也就是一个我们定义的作为参数传递给unwrap_or_else
的匿名函数。第XX章会更详细的介绍闭包;这里需要理解的重要部分是unwrap_or_else
会将Err
的内部值传递给闭包中位于两道竖线间的参数err
。使用unwrap_or_else
允许我们进行一些自定义的非panic!
的错误处理。
-上述的错误处理其实只有两行:我们打印出了错误,接着调用了std::process::exit
。这个函数立刻停止程序的执行并将传递给它的数组作为返回码。依照惯例,零代表成功而任何其他数字表示失败。就结果来说这依然类似于列表 12-7 中的基于panic!
的错误处理,但是不再会有额外的输出了,让我们试一试:
-$ cargo run
+
+
+
+在上面的列表中,使用了一个之前没有涉及到的方法:unwrap_or_else
,它定义于标准库的Result<T, E>
上。使用unwrap_or_else
可以进行一些自定义的非panic!
的错误处理。当Result
是Ok
时,这个方法的行为类似于unwrap
:它返回Ok
内部封装的值。然而,当Result
是Err
时,它调用一个闭包(closure),也就是一个我们定义的作为参数传递给unwrap_or_else
的匿名函数。第十三章会更详细的介绍闭包。现在你需要理解的是unwrap_or_else
会将Err
的内部值,也就是列表 12-9 中增加的not enough arguments
静态字符串的情况,传递给闭包中位于两道竖线间的参数err
。闭包中的代码在其运行时可以使用这个err
值。
+
+
+我们新增了一个use
行来从标准库中导入process
。在错误的情况闭包中将被运行的代码只有两行:我们打印出了err
值,接着调用了std::process::exit
(在开头增加了新的use
行从标准库中导入了process
)。process::exit
会立即停止程序并将传递给它的数字作为返回状态码。这类似于列表 12-8 中使用的基于panic!
的错误处理,除了不会在得到所有的额外输出了。让我们试试:
+$ cargo run
Compiling greprs v0.1.0 (file:///projects/greprs)
Finished debug [unoptimized + debuginfo] target(s) in 0.48 secs
- Running `target\debug\greprs.exe`
+ Running `target/debug/greprs`
Problem parsing arguments: not enough arguments
-非常好!现在输出就友好多了。
-run
函数中的错误处理
-现在重构完了参数解析部分,让我们再改进一下程序的逻辑。列表 12-10 中展示了在main
函数中调用提取出函数run
之后的代码。run
函数包含之前位于main
中的部分代码:
-
+
-run
函数的内容是之前位于main
中的几行,而且run
函数获取一个Config
作为参数。现在有了一个单独的函数了,我们就可以像列表 12-8 中的Config::new
那样进行类似的改进了。列表 12-11 展示了另一个use
语句将std::error::Error
结构引入了作用域,还有使run
函数返回Result
的修改:
-
+
-这里有三个大的修改。第一个是现在run
函数的返回值是Result<(), Box<Error>>
类型的。之前,函数返回 unit 类型()
,现在它仍然是Ok
时的返回值。对于错误类型,我们将使用Box<Error>
。这是一个trait 对象(trait object),第XX章会讲到。现在可以这样理解它:Box<Error>
意味着函数返回了某个实现了Error
trait 的类型,不过并没有指定具体的返回值类型。这样就比较灵活,因为在不同的错误场景可能有不同类型的错误返回值。Box
是一个堆数据的智能指针,第YY章将会详细介绍Box
。
-第二个改变是我们去掉了expect
调用并替换为第9章讲到的?
。不同于遇到错误就panic!
,这会从函数中返回错误值并让调用者来处理它。
-第三个修改是现在成功时这个函数会返回一个Ok
值。因为run
函数签名中声明成功类型返回值是()
,所以需要将 unit 类型值包装进Ok
值中。Ok(())
一开始看起来有点奇怪,不过这样使用()
是表明我们调用run
只是为了它的副作用的惯用方式;它并没有返回什么有意义的值。
+这里做出了三个大的改变。第一,改变了run
函数的返回值为Result<(), Box<Error>>
。之前这个函数返回 unit 类型()
,现在它仍然保持作为Ok
时的返回值。
+
+
+对于错误类型,使用了trait 对象Box<Error>
(在开头使用了use
语句将std::error::Error
引入作用域)。第十七章会涉及 trait 对象。目前只需知道Box<Error>
意味着函数会返回实现了Error
trait 的类型,不过无需指定具体将会返回的值的类型。这提供了在不同的错误场景可能有不同类型的错误返回值的灵活性。
+第二个改变是去掉了expect
调用并替换为第九章讲到的?
。不同于遇到错误就panic!
,这会从函数中返回错误值并让调用者来处理它。
+第三个修改是现在成功时这个函数会返回一个Ok
值。因为run
函数签名中声明成功类型返回值是()
,这意味着需要将 unit 类型值包装进Ok
值中。Ok(())
一开始看起来有点奇怪,不过这样使用()
是表明我们调用run
只是为了它的副作用的惯用方式;它并没有返回什么有意义的值。
上述代码能够编译,不过会有一个警告:
warning: unused result which must be used, #[warn(unused_must_use)] on by default
- --> src\main.rs:39:5
+ --> src/main.rs:39:5
|
39 | run(config);
| ^^^^^^^^^^^^
-Rust 尝试告诉我们忽略Result
,它有可能是一个错误值。让我们现在来处理它。我们将采用类似于列表 12-9 中处理Config::new
错误的技巧,不过还有少许不同:
+Rust 提示我们的代码忽略了Result
值,它可能表明这里存在一个错误。虽然我们没有检查这里是否有一个错误,而编译器提醒我们这里应该有一些错误处理代码!现在就让我们修正他们。
+处理main
中run
返回的错误
+我们将检查错误并使用与列表 12-10 中处理错误类似的技术来优雅的处理他们,不过有一些细微的不同:
Filename: src/main.rs
fn main() {
// ...snip...
- println!("Searching for {}", config.search);
+ println!("Searching for {}", config.query);
println!("In file {}", config.filename);
if let Err(e) = run(config) {
@@ -6686,36 +6812,27 @@ fn run(config: Config) -> Result<(), Box<Error>> {
process::exit(1);
}
}
-
-fn run(config: Config) -> Result<(), Box<Error>> {
- let mut f = File::open(config.filename)?;
-
- let mut contents = String::new();
- f.read_to_string(&mut contents)?;
-
- println!("With text:\n{}", contents);
-
- Ok(())
-}
-不同于unwrap_or_else
,我们使用if let
来检查run
是否返回Err
,如果是则调用process::exit(1)
。为什么呢?这个例子和Config::new
的区别有些微妙。对于Config::new
我们关心两件事:
-
-- 检测出任何可能发生的错误
-- 如果没有出现错误创建一个
Config
-
-而在这个情况下,因为run
在成功的时候返回一个()
,唯一需要担心的就是第一件事:检测错误。如果我们使用了unwrap_or_else
,则会得到()
的返回值。它并没有什么用处。
-虽然两种情况下if let
和unwrap_or_else
的内容都是一样的:打印出错误并退出。
+我们使用if let
来检查run
是否返回一个Err
值,不同于unwrap_or_else
,并在出错时调用process::exit(1)
。run
并不返回像Config::new
返回的Config
实例那样需要unwrap
的值。因为run
在成功时返回()
,而我们只关心发现一个错误,所以并不需要unwrap_or_else
来返回未封装的值,因为它只会是()
。
+不过两个例子中if let
和unwrap_or_else
的函数体都一样:打印出错误并退出。
将代码拆分到库 crate
-现在项目看起来好多了!还有一件我们尚未开始的工作:拆分 src/main.rs 并将一些代码放入 src/lib.rs 中。让我们现在就开始吧:将 src/main.rs 中的run
函数移动到新建的 src/lib.rs 中。还需要移动相关的use
语句和Config
的定义,以及其new
方法。现在 src/lib.rs 应该如列表 12-12 所示:
-
+
-注意我们还需要使用公有的pub
:在Config
和其字段、它的new
方法和run
函数上。
-现在在 src/main.rs 中,我们需要通过extern crate greprs
来引入现在位于 src/lib.rs 的代码。接着需要增加一行use greprs::Config
来引入Config
到作用域,并对run
函数加上 crate 名称前缀,如列表 12-13 所示:
-
+
-通过这些重构,所有代码应该都能运行了。运行几次cargo run
来确保你没有破坏什么内容。好的!确实有很多的内容,不过已经为将来的成功奠定了基础。我们采用了一种更加优秀的方式来处理错误,并使得代码更模块化了一些。从现在开始几乎所有的工作都将在 src/lib.rs 中进行。
-让我们利用这新创建的模块的优势来进行一些在旧代码中难以开开展的工作,他们在新代码中却很简单:编写测试!
+通过这些重构,所有功能应该抖联系在一起并可以运行了。运行cargo run
来确保一切都正确的衔接在一起。
+
+
+哇哦!这可有很多的工作,不过我们为将来成功打下了基础。现在处理错误将更容易,同时代码也更模块化。从现在开始几乎所有的工作都将在 src/lib.rs 中进行。
+让我们利用这些新创建的模块的优势来进行一些在旧代码中难以展开的工作,他们在新代码中却很简单:编写测试!
测试库的功能
ch12-04-testing-the-librarys-functionality.md
-commit 4f2dc564851dc04b271a2260c834643dfd86c724
+commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56
现在为项目的核心功能编写测试将更加容易,因为我们将逻辑提取到了 src/lib.rs 中并将参数解析和错误处理都留在了 src/main.rs 里。现在我们可以直接使用多种参数调用代码并检查返回值而不用从命令行运行二进制文件了。
我们将要编写的是一个叫做grep
的函数,它获取要搜索的项以及文本并产生一个搜索结果列表。让我们从run
中去掉那行println!
(也去掉 src/main.rs 中的,因为再也不需要他们了),并使用之前收集的选项来调用新的grep
函数。眼下我们只增加一个空的实现,和指定grep
期望行为的测试。当然,这个测试对于空的实现来说是会失败的,不过可以确保代码是可以编译的并得到期望的错误信息。列表 12-14 展示了这些修改:
-
+
注意需要在grep
的签名中显式声明声明周期'a
并用于contents
参数和返回值。记住,生命周期参数用于指定函数参数于返回值的生命周期的关系。在这个例子中,我们表明返回的 vector 将包含引用参数contents
的字符串 slice,而不是引用参数search
的字符串 slice。换一种说法就是我们告诉 Rust 函数grep
返回的数据将和传递给它的参数contents
的数据存活的同样久。这是非常重要的!考虑为了使引用有效则 slice 引用的数据也需要保持有效,如果编译器认为我们是在创建search
而不是contents
的 slice,那么安全检查将是不正确的。如果尝试不用生命周期编译的话,我们将得到如下错误:
error[E0106]: missing lifetime specifier
@@ -6917,8 +7032,7 @@ error: test failed
最终,我们需要一个方法来存储包含要搜索字符串的行。为此可以在for
循环之前创建一个可变的 vector 并调用push
方法来存放一个line
。在for
循环之后,返回这个 vector。列表 12-15 中为完整的实现:
-
+
尝试运行一下:
$ cargo test
@@ -6996,7 +7108,7 @@ To an admiring bog!
ch12-05-working-with-environment-variables.md
-commit 4f2dc564851dc04b271a2260c834643dfd86c724
+commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56
让我们再增加一个功能:大小写不敏感搜索。另外,这个设定将不是一个命令行参数:相反它将是一个环境变量。当然可以选择创建一个大小写不敏感的命令行参数,不过用户要求提供一个环境变量这样设置一次之后在整个终端会话中所有的搜索都将是大小写不敏感的了。
实现并测试一个大小写不敏感grep
函数
@@ -7038,8 +7150,7 @@ Trust me.";
我们将定义一个叫做grep_case_insensitive
的新函数。它的实现与grep
函数大体上相似,不过列表 12-16 展示了一些小的区别:
-
+
首先,将search
字符串转换为小写,并存放于一个同名的覆盖变量中。注意现在search
是一个String
而不是字符串 slice,所以在将search
传递给contains
时需要加上 &,因为contains
获取一个字符串 slice。
接着在检查每个line
是否包含search
之前增加了一个to_lowercase
调用。因为将line
和search
都转换为小写,我们就可以无视大小写的匹配文件和命令行参数了。看看测试是否通过了:
@@ -7176,7 +7285,7 @@ To an admiring bog!
ch12-06-writing-to-stderr-instead-of-stdout.md
-commit 4f2dc564851dc04b271a2260c834643dfd86c724
+commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56
目前为止,我们将所有的输出都println!
到了终端。这是可以的,不过大部分终端都提供了两种输出:“标准输出”对应大部分信息,而“标准错误”则用于错误信息。这使得处理类似于“将错误打印到终端而将其他信息输出到文件”的情况变得更容易。
可以通过在命令行使用>
来将输出重定向到文件中,同时不使用任何参数运行来造成一个错误,就会发现我们的程序只能打印到stdout
:
@@ -7186,8 +7295,7 @@ commit 4f2dc564851dc04b271a2260c834643dfd86c724
Problem parsing arguments: not enough arguments
我们希望这个信息被打印到屏幕上,而只有成功运行产生的输出写入到文件中。让我们如列表 12-17 中所示改变如何打印错误信息的方法:
-
+
Rust 并没有类似println!
这样的方便写入标准错误的函数。相反,我们使用writeln!
宏,它有点像println!
,不过它获取一个额外的参数。第一个参数是希望写入内容的位置。可以通过std::io::stderr
函数获取一个标准错误的句柄。我们将一个stderr
的可变引用传递给writeln!
;它需要是可变的因为这样才能写入信息!第二个和第三个参数就像println!
的第一个和第二参数:一个格式化字符串和任何需要插入的变量。
让我们再次用相同方式运行程序,不带任何参数并用 >
重定向stdout
:
@@ -7242,7 +7348,7 @@ How dreary to be somebody!
总结
在这一章,我们涉及了如果在 Rust 中进行常规的 I/O 操作。通过使用命令行参数、文件、环境变量和写入stderr
的功能。现在你已经准备好编写命令行程序了。结合前几章的知识,你的代码将会是组织良好的,并能有效的将数据存储到合适的数据结构中、更好的处理错误,并且还是经过良好测试的。我们也接触了一个真实情况下需要生命周期注解来保证引用一直有效的场景。
-接下来,让我们探索如何利用一些 Rust 中受函数式编程语言影响的功能”闭包和迭代器。
+接下来,让我们探索如何利用一些 Rust 中受函数式编程语言影响的功能:闭包和迭代器。
Rust 中的函数式语言功能 —— 迭代器和闭包
ch13-00-functional-features.md
@@ -7733,7 +7839,7 @@ opt-level = 1
ch14-02-publishing-to-crates-io.md
-commit c49e5ee8859f8eb8f8867cbeafbdf5b802aa5894
+commit f2eef19b3a39ee68dd363db2fcba173491ba9dc4
我们曾经在项目中增加 crates.io 上的 crate 作为依赖。也可以选择将代码分享给其他人。Crates.io 用来分发包的源代码,所以它主要用于分发开源代码。
Rust 和 Cargo 有一些帮助人们找到和使用你发布的包的功能。我们将介绍这些功能,接着讲到如何发布一个包。
@@ -7748,10 +7854,7 @@ commit c49e5ee8859f8eb8f8867cbeafbdf5b802aa5894
/// ```
/// let five = 5;
///
-/// assert_eq!(6, add_one(5));
-/// # fn add_one(x: i32) -> i32 {
-/// # x + 1
-/// # }
+/// assert_eq!(6, add_one(five));
/// ```
pub fn add_one(x: i32) -> i32 {
x + 1
@@ -7777,7 +7880,7 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
使用pub use
来导出合适的公有 API
第七章介绍了如何使用mod
关键字来将代码组织进模块中,如何使用pub
关键字将项变为公有,和如何使用use
关键字将项引入作用域。当发布 crate 给并不熟悉其使用的库的实现的人时,就值得花时间考虑 crate 的结构对于开发和对于依赖 crate 的人来说是否同样有用。如果结构对于供其他库使用来说并不方便,也无需重新安排内部组织:可以选择使用pub use
来重新导出一个不同的公有结构。
-例如列表 14-2中,我们创建了一个库art
,其包含一个kinds
模块,模块中包含枚举Color
和包含函数mix
的模块utils
:
+例如列表 14-2 中,我们创建了一个库art
,其包含一个kinds
模块,模块中包含枚举Color
和包含函数mix
的模块utils
:
Filename: src/lib.rs
//! # Art
//!
@@ -8187,9 +8290,9 @@ order to have a known size
ch15-02-deref.md
-commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56
+commit ecc3adfe0cfa0a4a15a178dc002702fd0ea74b3f
-第一个智能指针相关的重要 trait 是Deref
,它允许我们重载*
,解引用运算符(不同于乘法运算符和全局引用运算符)。重载智能指针的*
方便访问其后的数据,在这个部分的稍后介绍解引用强制多态时我们会讨论方便的意义。
+第一个智能指针相关的重要 trait 是Deref
,它允许我们重载*
,解引用运算符(不同于乘法运算符和全局引用运算符)。重载智能指针的*
能使访问其后的数据更为方便,在这个部分的稍后介绍解引用强制多态时我们会讨论方便的意义。
第八章的哈希 map 的“根据旧值更新一个值”部分简要的提到了解引用运算符。当时有一个可变引用,而我们希望改变这个引用所指向的值。为此,首先我们必须解引用。这是另一个使用i32
值引用的例子:
let mut x = 5;
{
@@ -8235,14 +8338,14 @@ fn main() {
struct that holds mp3 file data and metadata
大部分代码看起来都比较熟悉:一个结构体、一个 trait 实现、和一个创建了结构体示例的 main 函数。其中有一部分我们还未全面的讲解:类似于第十三章学习迭代器 trait 时出现的type Item
,type Target = T;
语法用于定义关联类型,第十九章会更详细的介绍。不必过分担心例子中的这一部分;它只是一个稍显不同的定义泛型参数的方式。
在assert_eq!
中,我们验证vec![1, 2, 3]
是否为Mp3
实例*my_favorite_song
解引用的值,结果正是如此因为我们实现了deref
方法来返回音频数据。如果没有为Mp3
实现Deref
trait,Rust 将不会编译*my_favorite_song
:会出现错误说Mp3
类型不能被解引用。
-代码能够工作的原因在于调用*my_favorite_song
时*
在背后所做的操作:
+没有Deref
trait 的话,编译器只能解引用&
引用,而my_favorite_song
并不是(它是一个Mp3
结构体)。通过Deref
trait,编译器知道实现了Deref
trait 的类型有一个返回引用的deref
方法(在这个例子中,是&self.audio
因为列表 15-7 中的deref
的定义)。所以为了得到一个*
可以解引用的&
引用,编译器将*my_favorite_song
展开为如下:
*(my_favorite_song.deref())
-这对my_favorite_song
调用了deref
方法,它借用了my_favorite_song
并返回指向my_favorite_song.audio
的引用,这正是列表 15-5 中deref
所定义的。引用的*
被定义为仅仅从引用中返回其数据,所以上面*
的展开形式对于外部*
来说并不是递归的。最终的数据类型是Vec<u8>
,它与列表 15-5 中assert_eq!
的vec![1, 2, 3]
相匹配。
-deref
方法的返回值类型仍然是引用和为何必须解引用方法的结果的原因是如果deref
方法就返回值,使用*
总是会获取其所有权。
+这个就是self.audio
中的结果值。deref
返回一个引用并接下来必需解引用而不是直接返回值的原因是所有权:如果deref
方法直接返回值而不是引用,其值将被移动出self
。这里和大部分使用解引用运算符的地方并不想获取my_favorite_song.audio
的所有权。
+注意将*
替换为deref
调用和*
调用的过程在每次使用*
的时候都会发生一次。*
的替换并不会无限递归进行。最终的数据类型是Vec<u8>
,它与列表 15-7 中assert_eq!
的vec![1, 2, 3]
相匹配。
函数和方法的隐式解引用强制多态
-Rust 倾向于偏爱明确而不是隐晦,不过一个情况下这并不成立,就是函数和方法的参数的解引用强制多态(deref coercions)。解引用强制多态会自动的将指针或智能指针的引用转换为指针内容的引用。解引用强制多态发生于一个值被传递给函数或方法,并只发生于需要将传递的值类型与签名中参数类型相匹配的时候。解引用强制多态的加入使得 Rust 调用函数或方法时无需很多使用&
和*
的引用和解引用。
-使用列表 15-5 中的Mp3
结构体,如下是一个获取u8
slice 并压缩 mp3 音频数据的函数签名:
+Rust 倾向于偏爱明确而不是隐晦,不过一个情况下这并不成立,就是函数和方法的参数的解引用强制多态(deref coercions)。解引用强制多态会自动的将指针或智能指针的引用转换为指针内容的引用。解引用强制多态发生于当传递给函数的参数类型不同于函数签名中定义参数类型的时候。解引用强制多态的加入使得 Rust 调用函数或方法时无需很多使用&
和*
的引用和解引用。
+使用列表 15-7 中的Mp3
结构体,如下是一个获取u8
slice 并压缩 mp3 音频数据的函数签名:
fn compress_mp3(audio: &[u8]) -> Vec<u8> {
// the actual implementation would go here
}
@@ -8254,8 +8357,8 @@ struct that holds mp3 file data and metadata
然而,因为解引用强制多态和Mp3
的Deref
trait 实现,我们可以使用如下代码使用my_favorite_song
中的数据调用这个函数:
let result = compress_mp3(&my_favorite_song);
-只有&
和实例,好的!我们可以把智能指针当成普通的引用。也就是说解引用强制多态意味着 Rust 利用了Deref
实现的优势:Rust 知道Mp3
实现了Deref
trait 并从deref
方法返回&Vec<u8>
。它也知道标准库实现了Vec<T>
的Deref
trait,其deref
方法返回&[T]
(我们也可以通过查阅Vec<T>
的 API 文档来发现这一点)。所以,在编译时,Rust 会发现它可以调用两次Deref::deref
来将&Mp3
变成&Vec<u8>
再变成&[T]
来满足compress_mp3
的签名。这意味着我们可以少写一些代码!Rust 会多次分析Deref::deref
的返回值类型直到它满足参数的类型,只要相关类型实现了Deref
trait。这些间接转换在编译时进行,所以利用解引用强制多态并没有运行时惩罚。
-这里还有一个重载了&mut T
的*
的DerefMut
trait,它以与Deref
重载&T
的*
相同的方式用于参数中。
+只有&
和实例,好的!我们可以把智能指针当成普通的引用。也就是说解引用强制多态意味着 Rust 利用了Deref
实现的优势:Rust 知道Mp3
实现了Deref
trait 并从deref
方法返回&Vec<u8>
。它也知道标准库实现了Vec<T>
的Deref
trait,其deref
方法返回&[T]
(我们也可以通过查阅Vec<T>
的 API 文档来发现这一点)。所以,在编译时,Rust 会发现它可以调用两次Deref::deref
来将&Mp3
变成&Vec<u8>
再变成&[T]
来满足compress_mp3
的签名。这意味着我们可以少写一些代码!Rust 会多次分析Deref::deref
的返回值类型直到它满足参数的类型,只要相关类型实现了Deref
trait。这些间接转换在编译时进行,所以利用解引用强制多态并没有运行时惩罚!
+类似于如何使用Deref
trait 重载&T
的*
运算符,DerefMut
trait用于重载&mut T
的*
运算符。
Rust 在发现类型和 trait 实现满足三种情况时会进行解引用强制多态:
- 从
&T
到&U
当T: Deref<Target=U>
。
@@ -9207,6 +9310,356 @@ Got: you
虽然你可能会看到这些以不同的顺序出现。这依赖于你的系统!这也就是并发既有趣又困难的原因。如果你拿thread::sleep
做实验,在不同的线程中提供不同的值,就会发现他们的运行更加不确定并每次都会产生不同的输出。
现在我们见识过了通道如何工作,再看看共享内存并发吧。
+共享状态并发
+
+ch16-03-shared-state.md
+
+commit 9df612e93e038b05fc959db393c15a5402033f47
+
+虽然消息传递是一个很好的处理并发的方式,但并不是唯一的一个。再次考虑一下它的口号:
+
+Do not communicate by sharing memory; instead, share memory by
+communicating.
+不要共享内存来通讯;而是要通讯来共享内存。
+
+那么“共享内存来通讯”看起来是怎样的呢?共享内存并发有点像多所有权:多个线程可以同时访问相同的内存位置。正如第十五章中智能指针使得多所有权成为可能时我们所看到的,这会增加额外的复杂性,因为需要以某种方式管理这些不同的所有者。
+但是 Rust 的类型系统和所有权可以很好的帮助我们正确的进行管理。例如,让我们看看一个共享内存中更常见的并发原语:互斥器(mutexes)。
+互斥器一次只允许一个线程访问数据
+互斥器(mutex)是一个用于共享内存的并发原语。它是“mutual exclusion”的缩写,也就是说,任何给定时间它只允许一个线程访问某些数据。互斥器以难以使用著称,因为你不得不记住:
+
+- 必须记住在使用数据之前尝试获取锁。
+- 一旦处理完被互斥器所保护的数据之后,必须记得解锁数据这样其他线程才能够获取锁。
+
+对于一个现实中的互斥器的例子,想象一下在一个会议中的专门小组讨论会上,不过只有一个麦克风。在一个小组成员可能发言之前,他们必须请求或示意他们需要使用麦克风。一旦得到了麦克风,他们可以发言任意长的时间,接着将麦克风交给系一个希望讲话的小组成员。如果小组成员在没有麦克风的时候就开始叫喊或者在其他成员发言结束之前就取得麦克风将是很无理的。如果对这个共享的麦克风的管理因为任何这些原因出现问题,讨论会将无法如期进行。
+正确的管理互斥器是异常复杂的,这也就是为什么这么多人都热衷于通道。然而,在 Rust 中,得益于类型系统和所有权,我们不可能会在锁和解锁上出错。
+Mutex<T>
的 API
+让我们看看列表 16-12 中使用互斥器的例子,现在并不涉及到多线程:
+Filename: src/main.rs
+use std::sync::Mutex;
+
+fn main() {
+ let m = Mutex::new(5);
+
+ {
+ let mut num = m.lock().unwrap();
+ *num = 6;
+ }
+
+ println!("m = {:?}", m);
+}
+
+
+与很多类型一样,我们通过叫做new
的关联函数来创建一个Mutex<T>
。为了访问互斥器中的数据,使用lock
方法来获取锁。这个调用会阻塞到直到轮到我们拥有锁为止。如果另一个线程拥有锁接着那个线程 panic 了则这个调用会失败。类似于上一部分列表 16-6 那样,我们暂时使用unwrap()
而不是更好的错误处理。请查看第九章中提供的更好的工具。
+一旦获取了锁,就可以将返回值(在这里是num
)作为一个数据的可变引用使用了。类型系统是 Rust 如何保证使用值之前必须获取锁的:Mutex<i32>
并不是一个i32
,所以必须获取锁才能使用这个i32
值。我们是不会忘记这么做的;类否则型系统是不会允许的。
+与你可能怀疑的一样,Mutex<T>
是一个智能指针。好吧,更准确的说,lock
调用返回一个叫做MutexGuard
的智能指针。类似我们在第十五章见过的智能指针,它实现了Deref
来指向其内部数据。另外MutexGuard
有一个用来释放锁的Drop
实现。这样就不会忘记释放锁了。这在MutexGuard
离开作用域时会自动发生,例如它发生于列表 16-12 中内部作用域的结尾。接着可以打印出互斥器的值并发现能够将其内部的i32
改为 6。
+在线程间共享Mutex<T>
+现在让我们尝试使用Mutex<T>
在多个线程间共享值。我们将启动十个线程,并在每一个线程中对一个计数器值加一,这样计数器将从 0 变为 10。注意接下来的几个例子会有编译错误,而我们将利用这些错误来学习如何使用
+Mutex<T>
以及 Rust 又是怎样帮助我们正确使用它的。列表 16-13 是最开始的例子:
+Filename: src/main.rs
+use std::sync::Mutex;
+use std::thread;
+
+fn main() {
+ let counter = Mutex::new(0);
+ let mut handles = vec![];
+
+ for _ in 0..10 {
+ let handle = thread::spawn(|| {
+ let mut num = counter.lock().unwrap();
+
+ *num += 1;
+ });
+ handles.push(handle);
+ }
+
+ for handle in handles {
+ handle.join().unwrap();
+ }
+
+ println!("Result: {}", *counter.lock().unwrap());
+}
+
+
+这里创建了一个counter
变量来存放内含i32
的Mutex<T>
,类似列表 16-12 那样。接下来使用 range 创建了 10 个线程。这里使用了thread::spawn
并对所有线程使用了相同的闭包:他们每一个都将调用lock
方法来获取Mutex<T>
上的锁并对接着互斥器中的值加一。当一个线程结束执行其闭包,num
会离开作用域并释放锁这样另一个线程就可以获取它了。
+在主线程中,我们像列表 16-2 那样收集了所有的 join 句柄,并接着每一个的join
方法来确保所有线程都会结束。那时,主线程会获取锁并打印出程序的结果。
+之前提示过这个例子不能编译,让我们看看为什么!
+error[E0373]: closure may outlive the current function, but it borrows
+`counter`, which is owned by the current function
+ -->
+ |
+9 | let handle = thread::spawn(|| {
+ | ^^ may outlive borrowed value `counter`
+10 | let mut num = counter.lock().unwrap();
+ | ------- `counter` is borrowed here
+ |
+help: to force the closure to take ownership of `counter` (and any other
+referenced variables), use the `move` keyword, as shown:
+ | let handle = thread::spawn(move || {
+
+这类似于列表 16-5 中解决了的问题。考虑到启动了多个线程,Rust 无法知道这些线程会运行多久而counter
是否在每一个线程尝试借用它时仍然保持有效。帮助信息提醒了我们如何解决它:可以使用move
来给予每个线程其所有权。试试将这个修改用到闭包上:
+thread::spawn(move || {
+
+再次尝试编译。这会出现了一个不同的错误!
+error[E0382]: capture of moved value: `counter`
+ -->
+ |
+9 | let handle = thread::spawn(move || {
+ | ------- value moved (into closure) here
+10 | let mut num = counter.lock().unwrap();
+ | ^^^^^^^ value captured here after move
+ |
+ = note: move occurs because `counter` has type `std::sync::Mutex<i32>`,
+ which does not implement the `Copy` trait
+
+error[E0382]: use of moved value: `counter`
+ -->
+ |
+9 | let handle = thread::spawn(move || {
+ | ------- value moved (into closure) here
+...
+21 | println!("Result: {}", *counter.lock().unwrap());
+ | ^^^^^^^ value used here after move
+ |
+ = note: move occurs because `counter` has type `std::sync::Mutex<i32>`,
+ which does not implement the `Copy` trait
+
+error: aborting due to 2 previous errors
+
+move
并没有像列表 16-5 中那样解决这个程序中的问题。为什么没有呢?这个错误信息有些难以理解,因为它表明counter
被移动进了闭包,接着它在调用lock
时被捕获。这听起来像是我们希望的,不过这是不允许的。
+让我们推理一下。现在不再使用for
循环创建 10 个线程,让我们不用循环而只创建两个线程来看看会发生什么。将列表 16-13 中第一个for
循环替换为如下代码:
+let handle = thread::spawn(move || {
+ let mut num = counter.lock().unwrap();
+
+ *num += 1;
+});
+handles.push(handle);
+
+let handle2 = thread::spawn(move || {
+ let mut num2 = counter.lock().unwrap();
+
+ *num2 += 1;
+});
+handles.push(handle2);
+
+这里创建了两个线程,并将用于第二个线程的变量名改为handle2
和num2
。现在我们简化了例子来看看是否能够理解错误信息。这一次编译给出如下信息:
+error[E0382]: capture of moved value: `counter`
+ -->
+ |
+8 | let handle = thread::spawn(move || {
+ | ------- value moved (into closure) here
+...
+16 | let mut num2 = counter.lock().unwrap();
+ | ^^^^^^^ value captured here after move
+ |
+ = note: move occurs because `counter` has type `std::sync::Mutex<i32>`,
+ which does not implement the `Copy` trait
+
+error[E0382]: use of moved value: `counter`
+ -->
+ |
+8 | let handle = thread::spawn(move || {
+ | ------- value moved (into closure) here
+...
+26 | println!("Result: {}", *counter.lock().unwrap());
+ | ^^^^^^^ value used here after move
+ |
+ = note: move occurs because `counter` has type `std::sync::Mutex<i32>`,
+ which does not implement the `Copy` trait
+
+error: aborting due to 2 previous errors
+
+啊哈!在第一个错误信息中,Rust 表明了counter
被移动进了handle
所代表线程的闭包中。这个移动阻止我们在对其调用lock
并将结果储存在num2
中时捕获counter
,这是已经在第二个线程中了!所以 Rust 告诉我们不能将counter
的所有权移动到多个线程中。这在之前很难看出是因为我们在循环中创建多个线程,而 Rust 无法在循环的迭代中指明不同的线程(没有临时变量)。
+多线程和多所有权
+在第十五章中,我们可以通过使用智能指针Rc<T>
来创建引用计数的值来拥有多所有权。同时第十五章提到了Rc<T>
只能用于单线程上下文,不过还是让我们在这里试用Rc<T>
来观察会发生什么。列表 16-14 将Mutex<T>
封装进了Rc<T>
中,并在移动到线程中之前克隆了Rc<T>
。切换回循环来创建线程,并保留闭包中的move
关键字:
+Filename: src/main.rs
+use std::rc::Rc;
+use std::sync::Mutex;
+use std::thread;
+
+fn main() {
+ let counter = Rc::new(Mutex::new(0));
+ let mut handles = vec![];
+
+ for _ in 0..10 {
+ let counter = counter.clone();
+ let handle = thread::spawn(move || {
+ let mut num = counter.lock().unwrap();
+
+ *num += 1;
+ });
+ handles.push(handle);
+ }
+
+ for handle in handles {
+ handle.join().unwrap();
+ }
+
+ println!("Result: {}", *counter.lock().unwrap());
+}
+
+
+又一次,编译并...出现了不同的错误!编译器真是教会了我们很多东西!
+error[E0277]: the trait bound `std::rc::Rc<std::sync::Mutex<i32>>:
+std::marker::Send` is not satisfied
+ -->
+ |
+11 | let handle = thread::spawn(move || {
+ | ^^^^^^^^^^^^^ the trait `std::marker::Send` is not
+ implemented for `std::rc::Rc<std::sync::Mutex<i32>>`
+ |
+ = note: `std::rc::Rc<std::sync::Mutex<i32>>` cannot be sent between threads
+ safely
+ = note: required because it appears within the type
+ `[closure@src/main.rs:11:36: 15:10
+ counter:std::rc::Rc<std::sync::Mutex<i32>>]`
+ = note: required by `std::thread::spawn`
+
+哇哦,太长不看!需要指出一些重要的部分:第一个提示表明Rc<Mutex<i32>>
不能安全的在线程间传递。理由也在错误信息中,经过提取之后,表明“不满足Send
trait bound”(the trait bound Send is not satisfied
)。下一部分将会讨论Send
,它是一个确保确保用于线程的类型是适合并发环境的 trait。
+不幸的是,Rc<T>
并不能安全的在线程间共享。当Rc<T>
管理引用计数时,它必须在每一个clone
调用时增加计数并在每一个克隆被丢弃时减少计数。Rc<T>
并没有使用任何并发原语来确保改变计数的操作不会被其他线程打断。在计数出错时这可能会导致诡异的 bug,比如可能会造成内存泄漏或在使用结束之前就丢弃一个值。那么如果有一个正好与Rc<T>
类似,不过以一种线程安全的方式改变引用计数的类型会怎么样呢?
+原子引用计数Arc<T>
+如果你思考过像之前那样的问题的话,你就是正确的。确实有一个类似Rc<T>
并可以安全的用于并发环境的类型:Arc<T>
。字母“a”代表原子性(atomic),所以这是一个原子引用计数(atomically reference counted)类型。原子性是另一类这里还未涉及到的并发原语;请查看标准库中std::sync::atomic
的文档来获取更多细节。其中的要点就是:原子性类型工作起来类似原始类型,不过可以安全的在线程间共享。
+那为什么不是所有的原始类型都是原子性的呢,然后为什么不是所有标准库中的类型都默认使用Arc<T>
实现呢?线程安全伴随一些性能惩罚,我们只希望在需要时才为此付出代价。如果只是在单线程中会值进行操作,因为并不需要原子性提供的保证代码可以运行的更快。
+回到之前的例子:Arc<T>
和Rc<T>
除了Arc<T>
内部的原子性之外他们是等价的。其 API 也是一样的,所以可以修改use
行和new
调用。列表 16-15 中的代码最终可以编译和运行:
+Filename: src/main.rs
+use std::sync::{Mutex, Arc};
+use std::thread;
+
+fn main() {
+ let counter = Arc::new(Mutex::new(0));
+ let mut handles = vec![];
+
+ for _ in 0..10 {
+ let counter = counter.clone();
+ let handle = thread::spawn(move || {
+ let mut num = counter.lock().unwrap();
+
+ *num += 1;
+ });
+ handles.push(handle);
+ }
+
+ for handle in handles {
+ handle.join().unwrap();
+ }
+
+ println!("Result: {}", *counter.lock().unwrap());
+}
+
+
+这会打印出:
+Result: 10
+
+成功了!我们从 0 数到了 10,这可能并不是很显眼,不过一路上我们学习了很多关于Mutex<T>
和线程安全的内容!这个例子中构建的结构可以用于比增加计数更为复杂的操作。可以被分解为独立部分的计算可以像这样被分散到多个线程中,并可以使用Mutex<T>
来允许每个线程在他们自己的部分更新最终的结果。
+你可能注意到了,因为counter
是不可变的,不过可以获取其内部值的可变引用,这意味着Mutex<T>
提供了内部可变性,就像Cell
系列类型那样。正如第十五章中使用RefCell<T>
可以改变Rc<T>
中的内容那样,同样的可以使用Mutex<T>
来改变Arc<T>
中的内容。
+回忆一下Rc<T>
并没有避免所有可能的问题:我们也讨论了当两个Rc<T>
相互引用时的引用循环的可能性,这可能造成内存泄露。Mutex<T>
有一个类似的 Rust 同样也不能避免的问题:死锁。死锁(deadlock)是一个场景中操作需要锁定两个资源,而两个线程分别拥有一个锁并永远相互等待的问题。如果你对这个主题感兴趣,尝试编写一个带有死锁的 Rust 程序,接着研究任何其他语言中使用互斥器的死锁规避策略并尝试在 Rust 中实现他们。标准库中Mutex<T>
和MutexGuard
的 API 文档会提供拥有的信息。
+Rust 的类型系统和所有权确保了线程在更新共享值时拥有独占的访问权限,所以线程不会以一种不可预测的方式覆盖彼此的操作。为了和编译器一起使一切正确运行花了一些时间,不过我们节省了未来可能需要重现只在线程以特定顺序执行才会出现的诡异错误场景的时间。
+接下来,为了丰富本章的内容,让我们讨论一下Send
和Sync
trait 以及如何对自定义类型使用他们。
+使用Sync
和Send
trait 的可扩展并发
+
+ch16-04-extensible-concurrency-sync-and-send.md
+
+commit 55b294f20fc846a13a9be623bf322d8b364cee77
+
+Rust 的并发模型中一个有趣的方面是语言本身对并发知道的很少。我们讨论过的几乎所有内容都是标准库的一部分,而不是语言本身的内容。因为并不需要语言提供任何用于并发上下文中的内容,并发选择也不仅限于标准库或语言所提供的:我们可以编写自己的或使用别人编写的内容。
+我们说了几乎所有内容都不在语言本身,那么位于语言本身的是什么呢?这是两个 trait,都位于std::marker
:Sync
和Send
。
+Send
用于表明所有权可能被传送给其他线程
+Send
标记 trait 表明类型的所有权可能被在线程间传递。几乎所有的 Rust 类型都是Send
的,不过有一些例外。标准库中提供的一个不是Send
的类型是Rc<T>
:如果克隆Rc<T>
值并尝试将克隆的所有权传递给另一个线程,这两个线程可能会同时更新引用计数。正如上一部分提到的,Rc<T>
被实现为用于单线程场景,这时不需要为拥有线程安全的引用计数而付出性能代价。
+因为Rc<T>
没有标记为Send
,Rust 的类型系统和 trait bound 会确保我们永远也不会忘记或错误的把一个Rc<T>
值不安全的在线程间传递。列表 16-14 曾尝试这么做,不过得到了一个错误说the trait Send is not implemented for Rc<Mutex<i32>>
。当切换为标记为Send
的Arc<T>
时,代码就可以编译了。
+任何完全由Send
的类型组成的类型也会自动被标记为Send
。几乎所有基本类型都是Send
的,除了第十九章将会讨论的裸指针(raw pointer)之外。大部分标准库类型是Send
的,除了Rc<T>
之外。
+Sync
表明多线程访问是安全的
+Sync
标记 trait 表明一个类型可以安全的在多个线程中拥有其值的引用。换一种方式来说就是对于任意类型T
,如果&T
(T
的引用)是Send
的话T
就是Sync
的,这样其引用就可以安全的发送到另一个线程。类似于Send
的情况,基本类型是Sync
的,完全由Sync
的类型组成的类型也是Sync
的。
+Rc<T>
也不是Sync
的,出于其不是Send
的相同的原因。RefCell<T>
(第十五章讨论过)和Cell<T>
系列类型不是Sync
的。RefCell<T>
在运行时所进行的借用检查也不是线程安全的。Mutex<T>
是Sync
的,正如上一部分所讲的它可以被用来在多线程中共享访问。
+手动实现Send
和Sync
是不安全的
+通常并不需要实现Send
和Sync
trait,因为由是Send
和Sync
的类型组成的类型也自动就是Send
和Sync
的了。因为他们是标记 trait,甚至都不需要实现任何方法。他们只是用来加强并发相关的不可变行性的。
+实现这些标记 trait 涉及到实现不安全的 Rust 代码。第十九章将会讲到如何使用不安全 Rust 代码;现在,重要的是在创建新的由不是Send
和Sync
的部分构成的并发类型时需要多加小心,以确保维持其安全保证。The Nomicon 中由更多关于这些保证和如何维持他们的信息。
+总结
+这不会是本书最后一个出现并发的章节;第二十章的项目会在更现实的场景中使用这些概念,而不像本章中讨论的这些小例子。
+正如我们提到的,因为 Rust 本身很少有处理并发的部分内容,有很多的并发方案都由 crate 实现。他们比标准库要发展的更快;请在网上搜索当前最新的用于多线程场景的 crate。
+Rust 提供了用于消息传递的通道,和像Mutex<T>
和Arc<T>
这样可以安全的用于并发上下文的智能指针。类型系统和借用检查器会确保这些场景中的代码不会出现数据竞争和无效的引用。一旦代码可以编译了,我们就可以坚信这些代码可以正确的运行于多线程环境,而不会出现其他语言中经常出现的那些难以追踪的 bug。并发编程不再是什么可怕的概念:无所畏惧的使你的程序使用并发吧!
+接下来,让我们讨论一下当 Rust 程序变得更大时那些符合习惯的模拟问题和结构的解决方案,以及 Rust 风格如何与面向对象编程(Object Oriented Programming)中那些你所熟悉的概念相联系。
+Rust 是一个面向对象的编程语言吗?
+
+ch17-00-oop.md
+
+commit 759801361bde74b47e81755fff545c66020e6e63
+
+面向对象编程是一种起源于20世纪60年代Simula的模式化编程的方式,然后在90年代在C++语言开始流行。为了描述OOP有很多种复杂的定义:在一些定义下,Rust是面向对象的;在其他定义下,Rust不是。在本章节中,我们会探索一些被普遍认为是面向对象的特性和这些特性是如何转换为Rust的方言的。
+什么是面向对象?
+
+ch17-01-what-is-oo.md
+
+commit 46334522e22d6217b392451cff8b4feca2d69d79
+
+关于一门语言是否需要是面向对象,在编程社区内并达成一致意见。Rust被很多不同的编程模式影响,我们探索了13章提到的函数式编程的特性。面向对象编程语言的一些特性往往是对象、封装和继承。我们看一下每个的含义和Rust是否支持它们。
+对象包含数据和行为
+Design Patterns: Elements of Reusable Object-Oriented Software
这本书被俗称为The Gang of Four book
,是面向对象编程模式的目录。它这样定义面向对象编程:
+
+面向对象的程序是由对象组成的。一个对象包数据和操作这些数据的程序。程序通常被称为方法或操作。
+
+在这个定一下,Rust是面向对象的:结构体和枚举包含数据和impl块提供了在结构体和枚举上的方法。虽然带有方法的结构体和枚举不称为对象,但是他们提供了和对象相同的功能,使用了Gang of Four
定义的对象。
+隐藏了实现细节的封装
+通常与面向对象编程相关的另一个方面是封装的思想:对象的实现细节不能被使用对象的代码获取到。唯一与对象交互的方式是通过对象提供的public API,使用对象的代码无法深入到对象内部并直接改变数据或者行为。封装使得改变和重构对象的内部,无需改变使用对象的代码。
+就像我们在第7张讨论的那样,我们可以使用pub关键字来决定模块、类型函数和方法是public的(默认情况下一切都是private)。比如,我们可以定义一个结构体AveragedCollection
包含一个i32
类型的vector。结构体也可以有一个字段,该字段保存了vector中所有值的平均值。这样,希望知道结构体中的vector的平均值的人可以随着获取到,而无需自己计算。AveragedCollection
会为我们缓存平均值结果。 Listing 17-1有AveragedCollection
结构体的定义。
+Filename: src/lib.rs
+pub struct AveragedCollection {
+ list: Vec<i32>,
+ average: f64,
+}
+
+AveragedCollection
结构体维护了一个Integer列表和集合中所有元素的平均值。
+注意,结构体本身被标记为pub,这样其他代码可以使用这个结构体,但是在结构体内部的字段仍然是private。这是非常重要的,因为我们希望保证变量被增加到列表或者被从列表删除时,也会同时更新平均值。我们通过在结构体上实现add、remove和average方法来做到这一点( Listing 17-2:):
+Filename: src/lib.rs
+# pub struct AveragedCollection {
+# list: Vec<i32>,
+# average: f64,
+# }
+impl AveragedCollection {
+ pub fn add(&mut self, value: i32) {
+ self.list.push(value);
+ self.update_average();
+ }
+
+ pub fn remove(&mut self) -> Option<i32> {
+ let result = self.list.pop();
+ match result {
+ Some(value) => {
+ self.update_average();
+ Some(value)
+ },
+ None => None,
+ }
+ }
+
+ pub fn average(&self) -> f64 {
+ self.average
+ }
+
+ fn update_average(&mut self) {
+ let total: i32 = self.list.iter().sum();
+ self.average = total as f64 / self.list.len() as f64;
+ }
+}
+
+Listing 17-2:在AveragedCollection
结构体上实现了add、remove和average public方法
+public方法add
、remove
和average
是修改AveragedCollection
实例的唯一方式。当使用add方法把一个元素加入到list
或者使用remove
方法来删除它,这些方法的实现同时会调用私有的update_average
方法来更新average
成员变量。因为list
和average
是私有的,没有其他方式来使得外部的代码直接向list
增加或者删除元素,直接操作list
可能会引发average
字段不同步。average
方法返回average
字段的值,这指的外部的代码只能读取average
而不能修改它。
+因为我们已经封装好了AveragedCollection
的实现细节,所以我们也可以像使用list
一样使用的一个不同的数据结构,比如用HashSet
代替Vec
。只要签名add
、remove
和average
公有函数保持相同,使用AveragedCollection
的代码无需改变。如果我们暴露List
给外部代码时,未必都是这样,因为HashSet
和Vec
使用不同的函数增加元素,所以如果要想直接修改list
的话,外部的代码可能还得修改。
+如果封装是一个语言被认为是面向对象语言必要的方面的话,那么Rust满足要求。在代码中不同的部分使用或者不使用pub
决定了实现细节的封装。
+作为类型系统的继承和作为代码共享的继承
+继承是一个很多编程语言都提供的机制,一个对象可以从另外一个对象的定义继承,这使得可以获得父对象的数据和行为,而不用重新定义。很多人定义面向对象语言时,认为继承是一个特色。
+如果一个语言必须有继承才能被称为面向对象的语言,那么Rust就不是面向对象的。没有办法定义一个结构体继承自另外一个结构体,从而获得父结构体的成员和方法。然而,如果你过去常常在你的编程工具箱使用继承,依赖于你要使用继承的原因,在Rust中有其他的方式。
+使用继承有两个主要的原因。第一个是为了重用代码:一旦一个特殊的行为从一个类型继承,继承可以在另外一个类型实现代码重用。Rust代码可以被共享通过使用默认的trait方法实现,可以在Listing 10-14看到,我们增加一个summary
方法到Summarizable
trait。任何继承了Summarizable
trait的类型上会有summary
方法,而无需任何的父代码。这类似于父类有一个继承的方法,一个从父类继承的子类也因为继承有了继承的方法。当实现Summarizable
trait时,我们也可以选择覆写默认的summary
方法,这类似于子类覆写了从父类继承的实现方法。
+第二个使用继承的原因是,使用类型系统:子类型可以在父类型被使用的地方使用。这也称为多态,意味着如果多种对象有一个相同的shape,它们可以被其他替代。
+
+虽然很多人使用多态来描述继承,但是它实际上是一种特殊的多态,称为子类型多态。也有很多种其他形式,在Rust中带有通用的ttait绑定的一个参数
+也是多态——更特殊的类型多态。在多种类型的多态间的细节不是关键的,所以不要过于担心细节,只需要知道Rust有多种多态相关的特色就好,不像很多其他OOP语言。
+
+为了支持这种样式,Rust有trait对象,这样我们可以指定给任何类型的值,只要值实现了一种特定的trait。
+继承最近在很多编程语言的设计方案中失宠了。使用继承类实现代码重用需要共享比你需要共享的代码。子类不应该经常共享它们的父类的所有特色,但是继承意味着子类得到了它的父类的数据和行为。这使得一个程序的设计不灵活,创建了无意义的子类的方法被调用的可能性或者由于方法不适用于子类但是必须从父类继承,从而触发错误。另外,很多语言只允许从一个类继承,更加限制了程序设计的灵活性。
+因为这些原因,Rust选择了一个另外的途径,使用trait替代继承。让我们看一下在Rust中trait对象是如何实现多态的。
diff --git a/src/PREFACE.md b/src/PREFACE.md
index 4631943..16d2525 100644
--- a/src/PREFACE.md
+++ b/src/PREFACE.md
@@ -1,5 +1,7 @@
# Rust 程序设计语言(第二版) 简体中文版
-还在施工中:正在翻译第十六章第二部分
+还在施工中:目前翻译到第十六章,正在更新第十二章
-目前正在解决代码排版问题:已检查到第五章
\ No newline at end of file
+目前官方进度:[第十七章](https://github.com/rust-lang/book/projects/1)(18~20 章还在编写当中)
+
+GitBook 代码排版已大体解决,已不影响阅读
\ No newline at end of file
diff --git a/src/SUMMARY.md b/src/SUMMARY.md
index 841e239..db3d81a 100644
--- a/src/SUMMARY.md
+++ b/src/SUMMARY.md
@@ -90,4 +90,9 @@
- [线程](ch16-01-threads.md)
- [消息传递](ch16-02-message-passing.md)
- [共享状态](ch16-03-shared-state.md)
- - [可扩展的并发:`Sync`和`Send`](ch16-04-extensible-concurrency-sync-and-send.md)
\ No newline at end of file
+ - [可扩展的并发:`Sync`和`Send`](ch16-04-extensible-concurrency-sync-and-send.md)
+
+- [面向对象](ch17-00-oop.md)
+ - [什么是面向对象](ch17-01-what-is-oo.md)
+
+
\ No newline at end of file
diff --git a/src/ch01-00-introduction.md b/src/ch01-00-introduction.md
index 31a1d15..ce17d46 100644
--- a/src/ch01-00-introduction.md
+++ b/src/ch01-00-introduction.md
@@ -2,7 +2,7 @@
> [ch01-00-introduction.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch01-00-introduction.md)
>
-> commit 4f2dc564851dc04b271a2260c834643dfd86c724
+> commit 62f78bb3f7c222b574ff547d0161c2533691f9b4
欢迎阅读“Rust 程序设计语言”,一本关于 Rust 的介绍性书籍。Rust 是一个着用于安全、速度和并发的编程语言。它的设计不仅可以使程序获得性能和对底层语言的控制,并且能够享受高级语言强大的抽象能力。这些特性使得 Rust 适合那些有类似 C 语言经验并正在寻找一个更安全的替代者的程序员,同时也适合那些来自类似 Python 语言背景,正在探索在不牺牲表现力的情况下编写更好性能代码的开发者。
@@ -15,9 +15,10 @@ registry site),[crates.io]!我们期待看到**你**使用 Rust 进行创
## 为本书做出贡献
-本书是开源的。如果你发现任何错误,请不要犹豫,[在 GitHub 上][on GitHub]发起 issue 或提交 pull request。
+本书是开源的。如果你发现任何错误,请不要犹豫,[在 GitHub 上][on GitHub]发起 issue 或提交 pull request。请查看[CONTRIBUTING.md]获取更多信息。
[on GitHub]: https://github.com/rust-lang/book
+[CONTRIBUTING.md]: https://github.com/rust-lang/book/blob/master/CONTRIBUTING.md
> 译者注:这是本译本的 [GitHub 仓库][trpl-zh-cn],同样欢迎 Issue 和 PR :)
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/ch03-03-how-functions-work.md b/src/ch03-03-how-functions-work.md
index 8995bc3..de54692 100644
--- a/src/ch03-03-how-functions-work.md
+++ b/src/ch03-03-how-functions-work.md
@@ -261,4 +261,4 @@ help: consider removing this semicolon:
| ^
```
-主要的错误信息,“mismatched types,”(类型不匹配),揭示了代码的核心问题。函数`plus_one`的定义说明它要返回一个`i32`,不过语句并不返回一个值,这由那个空元组`()`表明。因此,这个函数返回了空元组(),这与函数定义相矛盾并导致一个错误。在输出中,Rust 提供了一个可能会对修正问题有帮助的信息:它建议去掉分号,这会修复这个错误。
\ No newline at end of file
+主要的错误信息,“mismatched types,”(类型不匹配),揭示了代码的核心问题。函数`plus_one`的定义说明它要返回一个`i32`,不过语句并不返回一个值,这由那个空元组`()`表明。因此,这个函数返回了空元组`()`(译者注:原文说此函数没有返回任何值,可能有误),这与函数定义相矛盾并导致一个错误。在输出中,Rust 提供了一个可能会对修正问题有帮助的信息:它建议去掉分号,这会修复这个错误。
\ No newline at end of file
diff --git a/src/ch03-05-control-flow.md b/src/ch03-05-control-flow.md
index 0c17184..6fb1f6d 100644
--- a/src/ch03-05-control-flow.md
+++ b/src/ch03-05-control-flow.md
@@ -305,7 +305,6 @@ the value is: 50
可以使用`for`循环来对一个集合的每个元素执行一些代码,来作为一个更有效率替代。`for`循环看起来像这样:
-
+
当运行这段代码,将看到与列表 3-5 一样的输出。更为重要的是,我们增强了代码安全性并消除了出现可能会导致超出数组的结尾或遍历长度不够而缺少一些元素这类 bug 机会。
diff --git a/src/ch04-02-references-and-borrowing.md b/src/ch04-02-references-and-borrowing.md
index d07764d..a8bf6ad 100644
--- a/src/ch04-02-references-and-borrowing.md
+++ b/src/ch04-02-references-and-borrowing.md
@@ -2,7 +2,7 @@
> [ch04-02-references-and-borrowing.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch04-02-references-and-borrowing.md)
>
-> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56
+> commit 5e0546f53cce14b126527d9ba6d1b8eb212b4f3d
在上一部分的结尾处的使用元组的代码是有问题的,我们需要将`String`返回给调用者函数这样就可以在调用`calculate_length`后仍然可以使用`String`了,因为`String`先被移动到了`calculate_length`。
@@ -243,7 +243,7 @@ fn dangle() -> &String { // dangle returns a reference to a String
因为`s`是在`dangle`创建的,当`dangle`的代码执行完毕后,`s`将被释放。不过我们尝试返回一个它的引用。这意味着这个引用会指向一个无效的`String`!这可不好。Rust 不会允许我们这么做的。
-正确的代码是直接返回`String`:
+这里的解决方法是直接返回`String`:
```rust
fn no_dangle() -> String {
diff --git a/src/ch05-00-structs.md b/src/ch05-00-structs.md
index 7494c35..211838b 100644
--- a/src/ch05-00-structs.md
+++ b/src/ch05-00-structs.md
@@ -1,6 +1,6 @@
# 结构体
-> [ch05-00-structs.md](https://github.com/rust-lang/book/blob/master/src/ch05-00-structs.md)
+> [ch05-00-structs.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch05-00-structs.md)
>
> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56
diff --git a/src/ch05-01-method-syntax.md b/src/ch05-01-method-syntax.md
index 3cfc72d..bf887da 100644
--- a/src/ch05-01-method-syntax.md
+++ b/src/ch05-01-method-syntax.md
@@ -1,6 +1,6 @@
## 方法语法
-> [ch05-01-method-syntax.md](https://github.com/rust-lang/book/blob/master/src/ch05-01-method-syntax.md)
+> [ch05-01-method-syntax.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch05-01-method-syntax.md)
>
> commit 8c1c1a55d5c0f9bc3c866ee79b267df9dc5c04e2
diff --git a/src/ch06-00-enums.md b/src/ch06-00-enums.md
index 09a11ca..9f949a2 100644
--- a/src/ch06-00-enums.md
+++ b/src/ch06-00-enums.md
@@ -1,6 +1,6 @@
# 枚举和模式匹配
-> [ch06-00-enums.md](https://github.com/rust-lang/book/blob/master/src/ch06-00-enums.md)
+> [ch06-00-enums.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch06-00-enums.md)
>
> commit 4f2dc564851dc04b271a2260c834643dfd86c724
diff --git a/src/ch06-01-defining-an-enum.md b/src/ch06-01-defining-an-enum.md
index 8058f90..f5d334e 100644
--- a/src/ch06-01-defining-an-enum.md
+++ b/src/ch06-01-defining-an-enum.md
@@ -1,12 +1,12 @@
# 定义枚举
-> [ch06-01-defining-an-enum.md](https://github.com/rust-lang/book/blob/master/src/ch06-01-defining-an-enum.md)
+> [ch06-01-defining-an-enum.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch06-01-defining-an-enum.md)
>
> commit e6d6caab41471f7115a621029bd428a812c5260e
-让我们通过一用代码来表现的场景,来看看为什么这里枚举是有用的而且比结构体更合适。比如我们要处理 IP 地。目前被广泛使用的两个主要 IP 标准:IPv4(version four)和 IPv6(version six)。这是我们的程序只可能会遇到两种 IP 地址:我们可以**枚举**出所有可能的值,这也正是它名字的由来。
+让我们通过一用代码来表现的场景,来看看为什么这里枚举是有用的而且比结构体更合适。比如我们要处理 IP 地。目前被广泛使用的两个主要 IP 标准:IPv4(version four)和 IPv6(version six)。这是我们的程序只可能会遇到两种 IP 地址:所以可以**枚举**出所有可能的值,这也正是它名字的由来。
-任何一个 IP 地址要么是 IPv4 的要么是 IPv6 的而不能两者都是。IP 地址的这个特性使得枚举数据结构非常适合这个场景,因为枚举值尽可能是其一个成员。IPv4 和 IPv6 从根本上讲都是 IP 地址,所以当代码在处理申请任何类型的 IP 地址的场景时应该把他们当作相同的类型。
+任何一个 IP 地址要么是 IPv4 的要么是 IPv6 的而不能两者都是。IP 地址的这个特性使得枚举数据结构非常适合这个场景,因为枚举值只可能是其中一个成员。IPv4 和 IPv6 从根本上讲仍是 IP 地址,所以当代码在处理申请任何类型的 IP 地址的场景时应该把他们当作相同的类型。
可以通过在代码中定义一个`IpAddrKind`枚举来表现这个概念并列出可能的 IP 地址类型,`V4`和`V6`。这被称为枚举的**成员**(*variants*):
@@ -58,9 +58,7 @@ route(IpAddrKind::V4);
route(IpAddrKind::V6);
```
-使用枚举甚至还有更多优势。进一步考虑一下我们的 IP 地址类型,目前没有一个储存实际 IP 地址**数据**的方法;只知道它是什么**类型**的。考虑到已经在第五章学习过结构体了,你可以想如列表 6-1 那样修改这个问题:
-
-
+
这里我们定义了一个有两个字段的结构体`IpAddr`:`kind`字段是`IpAddrKind`(之前定义的枚举)类型的而`address`字段是`String`类型的。这里有两个结构体的实例。第一个,`home`,它的`kind`的值是`IpAddrKind::V4`与之相关联的地址数据是`127.0.0.1`。第二个实例,`loopback`,`kind`的值是`IpAddrKind`的另一个成员,`V6`,关联的地址是`::1`。我们使用了要给结构体来将`kind`和`address`打包在一起,现在枚举成员就与值相关联了。
@@ -124,7 +117,7 @@ let loopback = IpAddr::V6(String::from("::1"));
这些代码展示了使用枚举来储存两种不同 IP 地址的几种可能的选择。然而,事实证明储存和编码 IP 地址实在是太常见了[以致标准库提供了一个可供使用的定义!][IpAddr]让我们看看标准库如何定义`IpAddr`的:它正有着跟我们定义和使用的一样的枚举和成员,不过它将成员中的地址数据嵌入到了两个不同形式的结构体中,他们对不同的成员的定义是不同的:
-[IpAddr]: ../std/net/enum.IpAddr.html
+[IpAddr]: https://doc.rust-lang.org/std/net/enum.IpAddr.html
```rust
struct Ipv4Addr {
@@ -141,14 +134,12 @@ enum IpAddr {
}
```
-这些代码展示了可以将任意类型的数据放入枚举成员中:例如字符串、数字类型或者结构体。甚至可以包含另一个枚举!另外,标准库中的类型通常并不比你可能设想出来的要复杂多少。
+这些代码展示了可以将任意类型的数据放入枚举成员中:例如字符串、数字类型或者结构体。甚至可以包含另一个枚举!另外,标准库中的类型通常并不比你设想出来的要复杂多少。
注意虽然标准库中包含一个`IpAddr`的定义,仍然可以创建和使用我们自己的定义而不会有冲突,因为我们并没有将标准库中的定义引入作用域。第七章会讲到如何导入类型。
来看看列表 6-2 中的另一个枚举的例子:它的成员中内嵌了多种多样的类型:
-
+
这个枚举有四个含有不同类型的成员:
@@ -173,7 +159,7 @@ types of values
* `Write`包含单独一个`String`。
* `ChangeColor`包含三个`i32`。
-定义一个像列表 6-2 中的枚举类似于定义不同类型的结构体,除了枚举不使用`struct`关键字而且所有成员都被组合在一起位于`Message`下。如下这些结构体可以包含与之前枚举成员中相同的数据:
+定义一个像列表 6-2 中的枚举类似于定义不同类型的结构体,除了枚举不使用`struct`关键字而且所有成员都被组合在一起位于`Message`下之外。如下这些结构体可以包含与之前枚举成员中相同的数据:
```rust
@@ -216,19 +202,28 @@ m.call();
在之前的部分,我们看到了`IpAddr`枚举如何利用 Rust 的类型系统编码更多信息而不单单是程序中的数据。这一部分探索一个`Option`的案例分析,它是标准库定义的另一个枚举。`Option`类型应用广泛因为它编码了一个非常普遍的场景,就是一个值可能是某个值或者什么都不是。从类型系统的角度来表达这个概念就意味着编译器需要检查是否处理了所有应该处理的情况,这样就可以避免在其他编程语言中非常常见的 bug。
-编程语言的设计经常从其包含功能的角度考虑问题,但是从不包含的功能的角度思考也很重要。Rust 并没有很多其他语言中有的空值功能。**空值**(*Null* )是一个值它代表没有值。在有空值的语言中,变量总是这两种状态之一:空值和非空值。
+编程语言的设计经常从其包含功能的角度考虑问题,但是从其所没有的功能的角度思考也很重要。Rust 并没有很多其他语言中有的空值功能。**空值**(*Null* )是一个值,它代表没有值。在有空值的语言中,变量总是这两种状态之一:空值和非空值。
在“Null References: The Billion Dollar Mistake”中,Tony Hoare,null 的发明者,曾经说到:
+> I call it my billion-dollar mistake. At that time, I was designing the first
+> comprehensive type system for references in an object-oriented language. My
+> goal was to ensure that all use of references should be absolutely safe, with
+> checking performed automatically by the compiler. But I couldn't resist the
+> temptation to put in a null reference, simply because it was so easy to
+> implement. This has led to innumerable errors, vulnerabilities, and system
+> crashes, which have probably caused a billion dollars of pain and damage in
+> the last forty years.
+>
> 我称之为我万亿美元的错误。当时,我在在一个面向对象语言设计第一个综合性的面向引用的类型系统。我的目标是通过编译器的自动检查来保证所有引用的应有都应该是绝对安全的。不过我未能抗拒引入一个空引用的诱惑,仅仅是因为它是这么的容易实现。这引发了无数错误、漏洞和系统崩溃,在之后的四十多年中造成了数以万计美元的苦痛和伤害。
空值的问题在于当你尝试像一个非空值那样使用一个空值,会出现某种形式的错误。因为空和非空的属性是无处不在的,非常容易出现这类错误。
然而,空值尝试表达的概念仍然是有意义的:空值是一个因为某种原因目前无效或缺失的值。
-问题不在于实际的概念而在于具体的实现。为此,Rust 并没有空值,不过它确实拥有一个可以编码存在或不存在概念的枚举。这个枚举是`Option`,而且它[定义于标准库中][option],如下:
+问题不在于具体的概念而在于特定的实现。为此,Rust 并没有空值,不过它确实拥有一个可以编码存在或不存在概念的枚举。这个枚举是`Option`,而且它[定义于标准库中][option],如下:
-[option]: ../std/option/enum.Option.html
+[option]: https://doc.rust-lang.org/std/option/enum.Option.html
```rust
enum Option {
@@ -276,12 +271,12 @@ not satisfied
哇哦!事实上,错误信息意味着 Rust 不知道该如何将`Option`与`i8`相加。当在 Rust 中拥有一个像`i8`这样类型的值时,编译器确保它总是有一个有效的值。我们可以自信使用而无需判空。只有当使用`Option`(或者任何用到的类型)是需要担心可能没有一个值,而编译器会确保我们在使用值之前处理为空的情况。
-换句话说,在对`Option`进行`T`的运算之前必须转为`T`。通常这能帮助我们捕获空值最常见的问题之一:假设某值不为空但实际上为空。
+换句话说,在对`Option`进行`T`的运算之前必须转为`T`。通常这能帮助我们捕获空值最常见的问题之一:假设某值不为空但实际上为空的情况。
-无需担心错过非空值的假设(和处理)让我们对代码更加有信心,为了拥有一个可能为空的值,必须显式的将其放入对应类型的`Option`中。接着,当使用这个值时,必须明确的处理值为空的情况。任何地方一个值不是`Option`类型的话,**可以**安全的假设它的值不为空。这是 Rust 的一个有意为之的设计选择,来限制空值的泛滥和增加 Rust 代码的安全性。
+无需担心错过存在非空值的假设让我们对代码更加有信心,为了拥有一个可能为空的值,必须显式的将其放入对应类型的`Option`中。接着,当使用这个值时,必须明确的处理值为空的情况。任何地方一个值不是`Option`类型的话,**可以**安全的假设它的值不为空。这是 Rust 的一个有意为之的设计选择,来限制空值的泛滥和增加 Rust 代码的安全性。
那么当有一个`Option`的值时,如何从`Some`成员中取出`T`的值来使用它呢?`Option`枚举拥有大量用于各种情况的方法:你可以查看[相关代码][docs]。熟悉`Option`的方法将对你的 Rust 之旅提供巨大的帮助。
-[docs]: ../std/option/enum.Option.html
+[docs]: https://doc.rust-lang.org/std/option/enum.Option.html
总的来说,为了使用`Option`值,需要编写处理每个成员的代码。我们想要一些代码只当拥有`Some(T)`值时运行,这些代码允许使用其中的`T`。也希望一些代码当在`None`值时运行,这些代码并没有一个可用的`T`值。`match`表达式就是这么一个处理枚举的控制流结构:它会根据枚举的成员运行不同的代码,这些代码可以使用匹配到的值中的数据。
\ No newline at end of file
diff --git a/src/ch06-02-match.md b/src/ch06-02-match.md
index aa51e19..7e482c0 100644
--- a/src/ch06-02-match.md
+++ b/src/ch06-02-match.md
@@ -1,17 +1,15 @@
## `match`控制流运算符
-> [ch06-02-match.md](https://github.com/rust-lang/book/blob/master/src/ch06-02-match.md)
+> [ch06-02-match.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch06-02-match.md)
>
-> commit 396e2db4f7de2e5e7869b1f8bc905c45c631ad7d
+> commit 64090418c23d615facfe49a8d548ad9baea6b097
-Rust 有一个叫做`match`的极为强大的控制流运算符,它允许我们将一个值与一系列的模式相比较并根据匹配的模式执行代码。模式可由字面值、变量、通配符和许多其他内容构成;第十八章会讲到所有不同种类的模式以及他们的作用。`match`的力量来源于模式的表现力以及编译器检查,它确保了所有可能的情况都得到处理。
+Rust 有一个叫做`match`的极为强大的控制流运算符,它允许我们将一个值与一系列的模式相比较并根据匹配的模式执行代码。模式可由字面值、变量、通配符和许多其他内容构成;第十八章会涉及到所有不同种类的模式以及他们的作用。`match`的力量来源于模式的表现力以及编译器检查,它确保了所有可能的情况都得到处理。
-把`match`表达式想象成某种硬币筛选机器:硬币滑入有着不同大小孔洞的轨道,每一个硬币都会掉入符合它大小的孔洞。同样地,值也会检查`match`的每一个模式,并且在遇到第一个“符合”的模式时,值会进入相关联的代码块并在执行中被使用。
+把`match`表达式想象成某种硬币分类器:硬币滑入有着不同大小孔洞的轨道,每一个硬币都会掉入符合它大小的孔洞。同样地,值也会检查`match`的每一个模式,并且在遇到第一个“符合”的模式时,值会进入相关联的代码块并在执行中被使用。
因为刚刚提到了硬币,让我们用他们来作为一个使用`match`的例子!我们可以编写一个函数来获取一个未知的(美国)硬币,并以一种类似验钞机的方式,确定它是何种硬币并返回它的美分值,如列表 6-3 中所示:
-
+
拆开`value_in_cents`函数中的`match`来看。首先,我们列出`match`关键字后跟一个表达式,在这个例子中是`coin`的值。这看起来非常像`if`使用的表达式,不过这里有一个非常大的区别:对于`if`,表达式必须返回一个布尔值。而这里它可以是任何类型的。例子中的`coin`的类型是列表 6-3 中定义的`Coin`枚举。
接下来是`match`的分支。一个分支有两个部分:一个模式和一些代码。第一个分支的模式是值`Coin::Penny`而之后的`=>`运算符将模式和将要运行的代码分开。这里的代码就仅仅是值`1`。每一个分支之间使用逗号分隔。
+当`match`表达式执行时,它将结果值按顺序与每一个分支的模式相比较,如果模式匹配了这个值,这个模式相关联的代码将被执行。如果模式并不匹配这个值,将继续执行下一个分支,非常像一个硬币分类器。可以拥有任意多的分支:列表 6-3 中的`match`有四个分支。
+
每个分支相关联的代码是一个表达式,而表达式的结果值将作为整个`match`表达式的返回值。
-如果分支代码较短的话可以不适用大括号,正如列表 6-3 中的每个分支都只是返回一个值。如果想要在分支中运行多行代码,可以使用大括号。例如,如下代码在每次使用`Coin::Penny`调用时都会打印出“Lucky penny!”,同时仍然返回代码块最后的值,`1`:
+如果分支代码较短的话通常不使用大括号,正如列表 6-3 中的每个分支都只是返回一个值。如果想要在分支中运行多行代码,可以使用大括号。例如,如下代码在每次使用`Coin::Penny`调用时都会打印出“Lucky penny!”,同时仍然返回代码块最后的值,`1`:
```rust
# enum Coin {
@@ -71,9 +66,7 @@ fn value_in_cents(coin: Coin) -> i32 {
匹配分支的另一个有用的功能是可以绑定匹配的模式的部分值。这也就是如何从枚举成员中提取值。
-作为一个例子,让我们修改枚举的一个成员来存放数据。1999 年到 2008 年间,美帝在 25 美分的硬币的一侧为 50 个州每一个都印刷了不同的设计。其他的硬币都没有这种区分州的设计,所以只有这些 25 美分硬币有特殊的价值。可以将这些信息加入我们的`enum`,通过改变`Quarter`成员来包含一个`State`值,列表 6-4 中完成了这些修改:
-
-
-
-想象一下我们的一个朋友尝试收集所有 50 个州的 25 美分硬币。在根据硬币类型分类零钱的同时,也可以报告出每个 25 美分硬币所对应的州名称,这样如何我们的朋友没有的话,他可以把它加入收藏。
-
-在这些代码的匹配表达式中,我们在匹配`Coin::Quarter`成员的分支的模式中增加了一个叫做`state`的变量。当匹配到`Coin::Quarter`时,变量`state`将会绑定 25 美分硬币所对应州的值。接着在代码那个分支中使用`state`,如下:
+在这些代码的匹配表达式中,我们在匹配`Coin::Quarter`成员的分支的模式中增加了一个叫做`state`的变量。当匹配到`Coin::Quarter`时,变量`state`将会绑定 25 美分硬币所对应州的值。接着在那个分支的代码中使用`state`,如下:
```rust
# #[derive(Debug)]
@@ -136,12 +124,10 @@ fn value_in_cents(coin: Coin) -> i32 {
在之前的部分在使用`Option`时我们想要从`Some`中取出其内部的`T`值;也可以像处理`Coin`枚举那样使用`match`处理`Option`!与其直接比较硬币,我们将比较`Option`的成员,不过`match`表达式的工作方式保持不变。
-比如想要编写一个函数,它获取一个`Option`并且如果其中有一个值,将其加一。如果其中没有值,函数应该返回`None`值并不尝试执行任何操作。
+比如我们想要编写一个函数,它获取一个`Option`并且如果其中有一个值,将其加一。如果其中没有值,函数应该返回`None`值并不尝试执行任何操作。
编写这个函数非常简单,得益于`match`,它将看起来像列表 6-5 中这样:
-
+
-> commit 396e2db4f7de2e5e7869b1f8bc905c45c631ad7d
+> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56
`if let`语法让我们以一种不那么冗长的方式结合`if`和`let`,来处理匹配一个模式的值而忽略其他的值。考虑列表 6-6 中的程序,它匹配一个`Option`值并只希望当值是三时执行代码:
-
+`值或`None`值。为了满足`match`表达式(穷尽性)的要求,必须在处理完这唯一的成员后加上`_ => ()`,这样也要增加很多样板代码。
@@ -96,4 +89,4 @@ if let Coin::Quarter(state) = coin {
你的 Rust 程序现在能够使用结构体和枚举在自己的作用域内表现其内容了。在你的 API 中使用自定义类型保证了类型安全:编译器会确保你的函数只会得到它期望的类型的值。
-为了向你的用户提供一个组织良好的 API,它使用直观且只向用户暴露他们确实需要的部分,那么让我们转向 Rust 的模块系统吧。
\ No newline at end of file
+为了向你的用户提供一个组织良好的 API,它使用起来很直观并且只向用户暴露他们确实需要的部分,那么现在就让我们转向 Rust 的模块系统吧。
\ No newline at end of file
diff --git a/src/ch07-00-modules.md b/src/ch07-00-modules.md
index 792bf2c..cdaa95c 100644
--- a/src/ch07-00-modules.md
+++ b/src/ch07-00-modules.md
@@ -1,8 +1,8 @@
# 模块
-> [ch07-00-modules.md](https://github.com/rust-lang/book/blob/master/src/ch07-00-modules.md)
+> [ch07-00-modules.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch07-00-modules.md)
>
-> commit e2a129961ae346f726f8b342455ec2255cdfed68
+> commit 4f2dc564851dc04b271a2260c834643dfd86c724
在你刚开始编写 Rust 程序时,代码可能仅仅位于`main`函数里。随着代码数量的增长,最终你会将功能移动到其他函数中,为了复用也为了更好的组织。通过将代码分隔成更小的块,每一个块代码自身就更易于理解。不过当你发现自己有太多的函数了该怎么办呢?Rust 有一个模块系统来处理编写可复用代码同时保持代码组织度的问题。
diff --git a/src/ch07-01-mod-and-the-filesystem.md b/src/ch07-01-mod-and-the-filesystem.md
index 7fe7253..1b7a958 100644
--- a/src/ch07-01-mod-and-the-filesystem.md
+++ b/src/ch07-01-mod-and-the-filesystem.md
@@ -1,10 +1,10 @@
## `mod`和文件系统
-> [ch07-01-mod-and-the-filesystem.md](https://github.com/rust-lang/book/blob/master/src/ch07-01-mod-and-the-filesystem.md)
+> [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 e2a129961ae346f726f8b342455ec2255cdfed68
+> commit 6fc32eabcd09f7a130094767abadb691dfcdddf7
-我们将通过使用 Cargo 创建一个新项目来开始我们的模块之旅,不过我们不再创建一个二进制 crate,而是创建一个库 crate:一个其他人可以作为依赖导入的项目。第二章我们见过的`rand`就是这样的 crate。
+我们将通过使用 Cargo 创建一个新项目来开始我们的模块之旅,不过不再创建一个二进制 crate,而是创建一个库 crate:一个其他人可以作为依赖导入的项目。第二章我们见过的`rand`就是这样的 crate。
我们将创建一个提供一些通用网络功能的项目的骨架结构;我们将专注于模块和函数的组织,而不担心函数体中的具体代码。这个项目叫做`communicator`。Cargo 默认会创建一个库 crate 除非指定其他项目类型,所以如果不像一直以来那样加入`--bin`参数则项目将会是一个库:
@@ -34,7 +34,7 @@ Cargo 创建了一个空的测试来帮助我们开始库项目,不像使用`-
### 模块定义
-对于`communicator`网络库,首先我们要定义一个叫做`network`的模块,它包含一个叫做`connect`的函数定义。Rust 中所有模块的定义以关键字`mod`开始。在 *src/lib.rs* 文件的开头在测试代码的上面增加这些代码:
+对于`communicator`网络库,首先要定义一个叫做`network`的模块,它包含一个叫做`connect`的函数定义。Rust 中所有模块的定义以关键字`mod`开始。在 *src/lib.rs* 文件的开头在测试代码的上面增加这些代码:
Filename: src/lib.rs
@@ -49,8 +49,6 @@ mod network {
也可以在 *src/lib.rs* 文件中同时存在多个模块。例如,再拥有一个`client`模块,它也有一个叫做`connect`的函数,如列表 7-1 中所示那样增加这个模块:
-
-
-
-现在我们有了`network::connect`函数和`client::connect`函数。他们可能有着完全不同的功能,同时他们也不会彼此冲突因为他们位于不同的模块。
+现在我们有了`network::connect`函数和`client::connect`函数。他们可能有着完全不同的功能,同时他们也不会彼此冲突,因为他们位于不同的模块。
虽然在这个例子中,我们构建了一个库,但是 *src/lib.rs* 并没有什么特殊意义。也可以在 *src/main.rs* 中使用子模块。事实上,也可以将模块放入其他模块中。这有助于随着模块的增长,将相关的功能组织在一起并又保持各自独立。如何选择组织代码依赖于如何考虑代码不同部分之间的关系。例如,对于库的用户来说,`client`模块和它的函数`connect`可能放在`network`命名空间里显得更有道理,如列表 7-2 所示:
-
+
在 *src/lib.rs* 文件中,将现有的`mod network`和`mod client`的定义替换为`client`模块作为`network`的一个内部模块。现在我们有了`network::connect`和`network::client::connect`函数:又一次,这两个`connect`函数也不相冲突,因为他们在不同的命名空间中。
@@ -123,7 +111,6 @@ communicator
位于层级结构中的模块,非常类似计算机领域的另一个我们非常熟悉的结构:文件系统!我们可以利用 Rust 的模块系统连同多个文件一起分解 Rust 项目,这样就不是所有的内容都落到 *src/lib.rs* 中了。作为例子,我们将从列表 7-3 中的代码开始:
-
+
这是模块层次结构:
@@ -160,6 +142,26 @@ communicator
└── server
```
+如果这些模块有很多函数,而这些函数又很长,将难以在文件中寻找我们需要的代码。因为这些函数被嵌套进一个或多个模块中,同时函数中的代码也会开始变长。这就有充分的理由将`client`、`network`和`server`每一个模块从 *src/lib.rs* 抽出并放入他们自己的文件中。
+
+让我们开始把`client`模块提取到另一个文件中。首先,将 *src/lib.rs* 中的`client`模块代码替换为如下:
+
+Filename: src/lib.rs
+
+```rust,ignore
+mod client;
+
+mod network {
+ fn connect() {
+ }
+
+ mod server {
+ fn connect() {
+ }
+ }
+}
+```
+
这里我们仍然**定义**了`client`模块,不过去掉了大括号和`client`模块中的定义并替换为一个分号,这使得 Rust 知道去其他地方寻找模块中定义的代码。
那么现在需要创建对应模块名的外部文件。在 *src/* 目录创建一个 *client.rs* 文件,接着打开它并输入如下内容,它是上一步`client`模块中被去掉的`connect`函数:
@@ -171,7 +173,7 @@ fn connect() {
}
```
-注意这个文件中并不需要一个`mod`声明;因为已经在 *src/lib.rs* 中已经使用`mod`声明了`client`模块。这个文件仅仅提供`client`模块的内容。如果在这里加上一个`mod client`,那么就等于给`client`模块增加了一个叫做`client`的子模块!
+注意这个文件中并不需要一个`mod`声明;因为已经在 *src/lib.rs* 中已经使用`mod`声明了`client`模块。这个文件仅仅提供`client`模块的**内容**。如果在这里加上一个`mod client`,那么就等于给`client`模块增加了一个叫做`client`的子模块了!
Rust 默认只知道 *src/lib.rs* 中的内容。如果想要对项目加入更多文件,我们需要在 *src/lib.rs* 中告诉 Rust 去寻找其他文件;这就是为什么`mod client`需要被定义在 *src/lib.rs* 而不是在 *src/client.rs*。
@@ -228,7 +230,7 @@ mod server {
注意这个模块文件中我们也使用了一个`mod`声明;这是因为我们希望`server`成为`network`的一个子模块。
-现在再次运行`cargo build`。成功!不过我们还需要再提取出另一个模块:`server`。因为这是一个子模块————也就是模块中的模块————目前的将模块提取到对应名字的文件中的策略就不管用了。如果我们仍这么尝试则会出现错误。对 *src/network.rs* 的第一个修改是用`mod server;`替换`server`模块的内容:
+现在再次运行`cargo build`。成功!不过我们还需要再提取出另一个模块:`server`。因为这是一个子模块——也就是模块中的模块——目前的将模块提取到对应名字的文件中的策略就不管用了。如果我们仍这么尝试则会出现错误。对 *src/network.rs* 的第一个修改是用`mod server;`替换`server`模块的内容:
Filename: src/network.rs
@@ -250,8 +252,6 @@ fn connect() {
当尝试运行`cargo build`时,会出现如列表 7-4 中所示的错误:
-
+
这个错误说明“不能在这个位置新声明一个模块”并指出 *src/network.rs* 中的`mod server;`这一行。看来 *src/network.rs* 与 *src/lib.rs* 在某些方面是不同的;让我们继续阅读以理解这是为什么。
列表 7-4 中间的记录事实上是非常有帮助的,因为它指出了一些我们还未讲到的操作:
-```text
+```
note: maybe move this module `network` to its own directory via `network/mod.rs`
```
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 中的代码:
-
+
在尝试编译这些代码之前,猜测一下`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`函数调用。
-
+
如你所见,指定函数的完全限定名称可能会非常冗长。所幸 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..7c6b1f8 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 6c24544ba718bce0755bdaf03423af86280051d5
-我们要讲到的第一个类型是`Vec`,也被称为 *vector*。vector 允许我们在一个单独的数据结构中储存多于一个值,它在内存中彼此相邻的排列所有的值。vector 只能储存相同类型的值。他们在拥有一系列的场景下非常实用,例如文件中的文本行或是购物车中商品的价格。
+我们要讲到的第一个类型是`Vec`,也被称为 *vector*。vector 允许我们在一个单独的数据结构中储存多于一个值,它在内存中彼此相邻的排列所有的值。vector 只能储存相同类型的值。他们在拥有一系列项的场景下非常实用,例如文件中的文本行或是购物车中商品的价格。
### 新建 vector
@@ -133,7 +133,10 @@ let row = vec![
];
```
-Rust 在编译时就必须准确的知道 vector 中类型的原因是它需要知道储存每个元素到底需要多少内存。第二个优点是可以准确的知道这个 vector 中允许什么类型。如果 Rust 允许 vector 存放任意类型,那么当对 vector 元素执行操作时一个或多个类型的值就有可能会造成错误。使用枚举外加`match`意味着 Rust 能在编译时就保证总是会处理所有可能的情况,正如第六章讲到的那样。
+
-> 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..5d0e6c9 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 e26bb338ab14b98a850c3464e821d54940a45672
-突然有一天,糟糕的事情发生了,而你对此束手无策。对于这种情况,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,43 +76,48 @@ error: Process didn't exit successfully: `target/debug/panic` (exit code: 101)
接下来的几行提醒我们可以设置`RUST_BACKTRACE`环境变量来得到一个 backtrace 来调查究竟是什么导致了错误。让我们来试试看。列表 9-1 显示了其输出:
-
+
这里有大量的输出!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 所示打开一个文件:
-
+`。这意味着函数返回一个`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 中的代码相同的功能,不过这个实现是使用了问号运算符:
-
-
+
-> 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`的实例:
-
+
-> commit b335da755592f286fd97a64d98f0ca3be6a59327
+> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56
每一个编程语言都有高效的处理重复概念的工具;在 Rust 中工具之一就是**泛型**(*generics*)。泛型是具体类型或其他属性的抽象替代。我们可以表达泛型的属性,比如他们的行为或如何与其他泛型相关联,而不需要在编写和编译代码时知道他们在这里实际上代表什么。
-同理为了编写一份可以用于多种具体值的代码,函数并不知道其参数为何值,这时就可以让函数获取泛型而不是像`i32`或`String`这样的具体值。我们已经使用过第六章的`Option`,第八章的`Vec`和`HashMap`,以及第九章的`Result`这些泛型了。本章会探索如何使用泛型定义我们自己自己的类型、函数和方法。
+同理为了编写一份可以用于多种具体值的代码,函数并不知道其参数为何值,这时就可以让函数获取泛型而不是像`i32`或`String`这样的具体值。我们已经使用过第六章的`Option`,第八章的`Vec`和`HashMap`,以及第九章的`Result`这些泛型了。本章会探索如何使用泛型定义我们自己自己的类型、函数和方法!
首先,我们将回顾一下提取函数以减少代码重复的机制。接着使用一个只在参数类型上不同的泛型函数来实现相同的功能。我们也会讲到结构体和枚举定义中的泛型。
@@ -20,7 +20,6 @@
考虑一下这个寻找列表中最大值的小程序,如列表 10-1 所示:
-
+
> commit 55d9e75ffec92e922273c997026bb10613a76578
@@ -12,7 +12,6 @@
回到`largest`函数上,列表 10-4 中展示了两个提供了相同的寻找 slice 中最大值功能的函数。第一个是从列表 10-3 中提取的寻找 slice 中`i32`最大值的函数。第二个函数寻找 slice 中`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`函数。注意这些代码还不能编译!
-
-
如果现在就尝试编译这些代码,会出现如下错误:
@@ -142,7 +130,6 @@ what you think. /Carol -->
同样也可以使用`<>`语法来定义拥有一个或多个泛型参数类型字段的结构体。列表 10-6 展示了如何定义和使用一个可以存放任何类型的`x`和`y`坐标值的结构体`Point`:
-
+ {
可以像第五章介绍的那样来为其定义中带有泛型的结构体或枚举实现方法。列表 10-9 中展示了列表 10-6 中定义的结构体`Point`。接着我们在`Point`上定义了一个叫做`x`的方法来返回字段`x`中数据的引用:
-
+`上实现的方法中使用它了。
结构体定义中的泛型类型参数并不总是与结构体方法签名中使用的泛型是同一类型。列表 10-10 中在列表 10-8 中的结构体`Point`上定义了一个方法`mixup`。这个方法获取另一个`Point`作为参数,而它可能与调用`mixup`的`self`是不同的`Point`类型。这个方法用`self`的`Point`类型的`x`值(类型`T`)和参数的`Point`类型的`y`值(类型`W`)来创建一个新`Point`类型的实例:
-
+Filename: src/main.rs
```rust
diff --git a/src/ch10-02-traits.md b/src/ch10-02-traits.md
index f92e3b6..9a5f68e 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 e5a987f5da3fba24e55f5c7102ec63f9dc3bc360
trait 允许我们进行另一种抽象:他们让我们可以抽象类型所通用的行为。*trait* 告诉 Rust 编译器某个特定类型拥有可能与其他类型共享的功能。在使用泛型类型参数的场景中,可以使用 *trait bounds* 在编译时指定泛型可以是任何实现了某个 trait 的类型,并由此在这个场景下拥有我们希望的功能。
@@ -10,13 +10,12 @@ trait 允许我们进行另一种抽象:他们让我们可以抽象类型所
### 定义 trait
-一个类型的行为由其可供调用的方法构成。如果可以对不同类型调用相同的方法的话,这些类型就可以共享相同的行为了。trait 定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必须行为的集合。
+一个类型的行为由其可供调用的方法构成。如果可以对不同类型调用相同的方法的话,这些类型就可以共享相同的行为了。trait 定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必需的行为的集合。
例如,这里有多个存放了不同类型和属性文本的结构体:结构体`NewsArticle`用于存放发生于世界各地的新闻故事,而结构体`Tweet`最多只能存放 140 个字符的内容,以及像是否转推或是否是对推友的回复这样的元数据。
我们想要创建一个多媒体聚合库用来显示可能储存在`NewsArticle`或`Tweet`实例中的数据的总结。每一个结构体都需要的行为是他们是能够被总结的,这样的话就可以调用实例的`summary`方法来请求总结。列表 10-11 中展示了一个表现这个概念的`Summarizable` trait 的定义:
-
+
使用`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 字符以内。
-
+
在类型上实现 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 所示:
-
+
另外这段代码假设`Summarizable`是一个公有 trait,这是因为列表 10-11 中`trait`之前使用了`pub`关键字。
@@ -144,7 +126,6 @@ trait 实现的一个需要注意的限制是:只能在 trait 或对应类型
列表 10-14 中展示了如何为`Summarize` trait 的`summary`方法指定一个默认的字符串值,而不是像列表 10-11 中那样只是定义方法签名:
-
+
如果想要对`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 {
@@ -302,9 +277,8 @@ error[E0507]: cannot move out of borrowed content
错误的核心是`cannot move out of type [T], a non-copy array`,对于非泛型版本的`largest`函数,我们只尝试了寻找最大的`i32`和`char`。正如第四章讨论过的,像`i32`和`char`这样的类型是已知大小的并可以储存在栈上,所以他们实现了`Copy` trait。当我们将`largest`函数改成使用泛型后,现在`list`参数的类型就有可能是没有实现`Copy` trait 的,这意味着我们可能不能将`list[0]`的值移动到`largest`变量中。
-如果只想对实现了`Copy`的类型调用这些带啊吗,可以在`T`的 trait bounds 中增加`Copy`!列表 10-15 中展示了一个可以编译的泛型版本的`largest`函数的完整代码,只要传递给`largest`的 slice 值的类型实现了`PartialOrd`和`Copy`这两个 trait,例如`i32`和`char`:
+如果只想对实现了`Copy`的类型调用这些代码,可以在`T`的 trait bounds 中增加`Copy`!列表 10-15 中展示了一个可以编译的泛型版本的`largest`函数的完整代码,只要传递给`largest`的 slice 值的类型实现了`PartialOrd`和`Copy`这两个 trait,例如`i32`和`char`:
-
+
如果并不希望限制`largest`函数只能用于实现了`Copy` trait 的类型,我们可以在`T`的 trait bounds 中指定`Clone`而不是`Copy`,并克隆 slice 的每一个值使得`largest`函数拥有其所有权。但是使用`clone`函数潜在意味着更多的堆分配,而且堆分配在涉及大量数据时可能会相当缓慢。另一种`largest`的实现方式是返回 slice 中一个`T`值的引用。如果我们将函数返回值从`T`改为`&T`并改变函数体使其能够返回一个引用,我们将不需要任何`Clone`或`Copy`的 trait bounds 而且也不会有任何的堆分配。尝试自己实现这种替代解决方式吧!
trait 和 trait bounds 让我们使用泛型类型参数来减少重复,并仍然能够向编译器明确指定泛型类型需要拥有哪些行为。因为我们向编译器提供了 trait bounds 信息,它就可以检查代码中所用到的具体类型是否提供了正确的行为。在动态类型语言中,如果我们尝试调用一个类型并没有实现的方法,会在运行时出现错误。Rust 将这些错误移动到了编译时,甚至在代码能够运行之前就强迫我们修复错误。另外,我们也无需编写运行时检查行为的代码,因为在编译时就已经检查过了,这样相比其他那些不愿放弃泛型灵活性的语言有更好的性能。
-这里还有一种泛型,我们一直在使用它甚至都没有察觉它的存在,这就是**生命周期**(*lifetimes*)。不同于其他泛型帮助我们确保类型拥有期望的行为,生命周期则有助于确保引用在我们需要他们的时候一直有效。让我们学习生命周期是如何做到这些的。
\ No newline at end of file
+这里还有一种泛型,我们一直在使用它甚至都没有察觉它的存在,这就是**生命周期**(*lifetimes*)。不同于其他泛型帮助我们确保类型拥有期望的行为,生命周期则有助于确保引用在我们需要他们的时候一直有效。让我们学习生命周期是如何做到这些的。
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`的值:
-
+
> ### 未初始化变量不能被使用
>
@@ -60,8 +54,6 @@ error: `x` does not live long enough
编译器的这一部分叫做**借用检查器**(*borrow checker*),它比较作用域来确保所有的借用都是有效的。列表 10-17 展示了与列表 10-16 相同的例子不过带有变量声明周期的注释:
-
让我们看看列表 10-18 中这个并没有产生悬垂引用且可以正常编译的例子:
-
+
`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`:
-
+
注意函数期望获取字符串 slice(如第四章所讲到的这是引用)因为我们并不希望`longest`函数获取其参数的引用。我们希望函数能够接受`String`的 slice(也就是变量`string1`的类型)和字符串字面值(也就是变量`string2`包含的值)。
@@ -171,7 +145,6 @@ interested to know if rereading Chapter 4 clears up that confusion.
如果尝试像列表 10-20 中那样实现`longest`函数,它并不能编译:
-
+
将会出现如下有关生命周期的错误:
@@ -228,7 +197,6 @@ error[E0106]: missing lifetime specifier
来看看我们编写的`longest`函数的上下文中的生命周期。就像泛型类型参数,泛型生命周期参数需要声明在函数名和参数列表间的加括号中。这里我们想要告诉 Rust 关于参数中的引用和返回值之间的限制是他们都必须拥有相同的生命周期,就像列表 10-21 中在每个引用中都加上了`'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`:
-
+
接下来,让我们尝试一个`result`的引用的生命周期必须比两个参数的要短的例子。将`result`变量的声明从内部作用域中移动出来,不过将`result`和`string2`变量的赋值语句一同放在内部作用域里。接下来,我们将使用`result`的`println!`移动到内部作用域之外,就在其结束之后。注意列表 10-23 中的代码不能编译:
-
+
如果尝试编译会出现如下错误:
@@ -395,7 +339,6 @@ at 1:44...
目前为止,我们只定义过有所有权类型的结构体。也可以定义存放引用的结构体,不过需要为结构体定义中的每一个引用添加生命周期注解。列表 10-24 中有一个存放了一个字符串 slice 的结构体`ImportantExcerpt`:
-
+
这个结构体有一个字段,`part`,它存放了一个字符串 slice,这是一个引用。类似于泛型参数类型,必须在结构体名称后面的尖括号中声明泛型生命周期参数,以便在结构体定义中使用生命周期参数。
@@ -428,7 +366,6 @@ lifetime annotation
在这一部分,我们知道了每一个引用都有一个生命周期,而且需要为使用了引用的函数或结构体指定生命周期。然而,第四章的“字符串 slice”部分有一个函数,我们在列表 10-25 中再次展示它,没有生命周期注解却能成功编译:
-
+
这个函数没有生命周期注解却能编译是由于一些历史原因:在早期 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..a03b4b7 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`来运行测试:
+
+
+现在让我们暂时忽略`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,188 +72,555 @@ running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
```
-Cargo 编译并运行了测试。这里有两部分输出:本章我们将关注第一部分。第二部分是文档测试的输出,第十四章会介绍他们。现在注意看这一行:
+
-```text
-test it_works ... ok
-```
+Cargo 编译并运行了测试。在`Compiling`、`Finished`和`Running`这几行之后,可以看到`running 1 test`这一行。下一行显示了生成的测试函数的名称,它是`it_works`,以及测试的运行结果,`ok`。接着可以看到全体测试运行结果的总结:`test result: ok.`意味着所有测试都通过了。`1 passed; 0 failed`表示通过或失败的测试数量。
-`it_works`文本来源于测试函数的名称。
+这里并没有任何被标记为忽略的测试,所以总结表明`0 ignored`。在下一部分关于运行测试的不同方式中会讨论忽略测试。`0 measured`统计是针对测试性能的性能测试的。性能测试(benchmark tests)在编写本书时,仍只属于开发版 Rust(nightly Rust)。请查看附录 D 来了解更多开发版 Rust 的信息。
-这里也有一行总结告诉我们所有测试的聚合结果:
+测试输出中以`Doc-tests adder`开头的下一部分是所有文档测试的结果。现在并没有任何文档测试,不过 Rust 会编译任何出现在 API 文档中的代码示例。这个功能帮助我们使文档和代码保持同步!在第十四章的“文档注释”部分会讲到如何编写文档测试。现在我们将忽略`Doc-tests`部分的输出。
-```
-test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
-```
+
+
-### `assert!`宏
-
-空的测试函数之所以能通过是因为任何没有`panic!`的测试都是通过的,而任何`panic!`的测试都算是失败。让我们使用`assert!宏来使测试失败:
+让我们改变测试的名称并看看这如何改变测试的输出。给`it_works`函数起个不同的名字,比如`exploration`,像这样:
Filename: src/lib.rs
```rust
-#[test]
-fn it_works() {
- assert!(false);
+#[cfg(test)]
+mod tests {
+ #[test]
+ fn exploration() {
+ }
}
```
-`assert!`宏由标准库提供,它获取一个参数,如果参数是`true`,什么也不会发生。如果参数是`false`,这个宏会`panic!`。再次运行测试:
+
+并再次运行`cargo test`。现在输出中将出现`exploration`而不是`it_works`:
```
-$ 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 1 test
-test it_works ... FAILED
+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");
+ }
+}
+```
+
+
+
+
+再次`cargo test`运行测试。输出应该看起来像列表 11-4,它表明`exploration`测试通过了而`another`失败了:
+
+
+```text
+running 2 tests
+test tests::exploration ... ok
+test tests::another ... FAILED
failures:
----- it_works stdout ----
- thread 'it_works' panicked at 'assertion failed: false', src/lib.rs:5
+---- 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:
- it_works
+ tests::another
-test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
+test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
error: test failed
```
-Rust 表明测试失败了:
+
-```
-test it_works ... FAILED
+`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
+ }
+}
```
-并展示了测试是因为src/lib.rs`的第 5 行`assert!`宏得到了一个`false`值而失败的:
+
-```
-thread 'it_works' panicked at 'assertion failed: false', src/lib.rs: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));
+ }
+}
```
-失败的测试也体现在了总结行中:
+
+
+注意在`tests`模块中新增加了一行:`use super::*;`。`tests`是一个普通的模块,它遵循第七章介绍的通常的可见性规则。因为这是一个内部模块,需要将外部模块中被测试的代码引入到内部模块的作用域中。这里选择使用全局导入使得外部模块定义的所有内容在`tests`模块中都是可用的。
+
+我们将测试命名为`larger_can_hold_smaller`,并创建所需的两个`Rectangle`实例。接着调用`assert!`宏并传递`larger.can_hold(&smaller)`调用的结果作为参数。这个表达式预期会返回`true`,所以测试应该通过。让我们拭目以待!
```
-test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
+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));
+ }
+}
+```
+
+因为这里`can_hold`函数的正确结果是`false`,我们需要将这个结果取反后传递给`assert!`宏。这样的话,测试就会通过而`can_hold`将返回`false`:
+
+```
+running 2 tests
+test tests::smaller_can_hold_larger ... ok
+test tests::larger_can_hold_smaller ... ok
+
+test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured
+```
+
+这个通过的测试!现在让我们看看如果引入一个 bug 的话测试结果会发生什么。将`can_hold`方法中比较长度时本应使用大于号的地方改成小于号:
+
+```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
+ }
+}
+```
+
+现在运行测试会产生:
+
+```
+running 2 tests
+test tests::smaller_can_hold_larger ... ok
+test tests::larger_can_hold_smaller ... FAILED
+
+failures:
+
+---- tests::larger_can_hold_smaller stdout ----
+ thread 'tests::larger_can_hold_smaller' panicked at 'assertion failed:
+ larger.can_hold(&smaller)', src/lib.rs:22
+note: Run with `RUST_BACKTRACE=1` for a backtrace.
+
+failures:
+ tests::larger_can_hold_smaller
+
+test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
+```
+
+我们的测试捕获了 bug!因为`larger.length`是 8 而`smaller.length` 是 5,`can_hold`中的长度比较现在返回`false`因为 8 不小于 5。
+
### 使用`assert_eq!`和`assert_ne!`宏来测试相等
-测试功能的一个常用方法是将需要测试代码的值与期望值做比较,并检查是否相等。可以通过向`assert!`宏传递一个使用`==`宏的表达式来做到。不过这个操作实在是太常见了,以至于标注库提供了一对宏来编译处理这些操作:`assert_eq!`和`assert_ne!`。这两个宏分别比较两个值是相等还是不相等。使用这些宏的另一个优势是当断言失败时他们会打印出这两个值具体是什么,以便于观察测试**为什么**失败,而`assert!`只会打印出它从`==`表达式中得到了`false`值。
+测试功能的一个常用方法是将需要测试代码的值与期望值做比较,并检查是否相等。可以通过向`assert!`宏传递一个使用`==`宏的表达式来做到。不过这个操作实在是太常见了,以至于标注库提供了一对宏来方便处理这些操作:`assert_eq!`和`assert_ne!`。这两个宏分别比较两个值是相等还是不相等。当断言失败时他们也会打印出这两个值具体是什么,以便于观察测试**为什么**失败,而`assert!`只会打印出它从`==`表达式中得到了`false`值,而不是导致`false`值的原因。
-下面是分别使用这两个宏其会测试通过的例子:
-
-Filename: src/lib.rs
-
-```
-#[test]
-fn it_works() {
- assert_eq!("Hello", "Hello");
-
- assert_ne!("Hello", "world");
-}
-```
-
-也可以对这些宏指定可选的第三个参数,它是一个会加入错误信息的自定义文本。这两个宏展开后的逻辑看起来像这样:
-
-```rust,ignore
-// assert_eq! - panic if the values aren't equal
-if left_val != right_val {
- panic!(
- "assertion failed: `(left == right)` (left: `{:?}`, right: `{:?}`): {}"
- left_val,
- right_val,
- optional_custom_message
- )
-}
-
-// assert_ne! - panic if the values are equal
-if left_val == right_val {
- panic!(
- "assertion failed: `(left != right)` (left: `{:?}`, right: `{:?}`): {}"
- left_val,
- right_val,
- optional_custom_message
- )
-}
-```
-
-看看这个因为`hello`不等于`world`而失败的测试。我们还增加了一个自定义的错误信息,`greeting operation failed`:
+列表 11-7 中,让我们编写一个对其参数加二并返回结果的函数`add_two`。接着使用`assert_eq!`宏测试这个函数:
Filename: src/lib.rs
```rust
-#[test]
-fn a_simple_case() {
- let result = "hello"; // this value would come from running your code
- assert_eq!(result, "world", "greeting operation failed");
+pub fn add_two(a: i32) -> i32 {
+ a + 2
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn it_adds_two() {
+ assert_eq!(4, add_two(2));
+ }
}
```
-毫无疑问运行这个测试会失败,而错误信息解释了为什么测试失败了并且带有我们的指定的自定义错误信息:
+
+
+测试通过了!
+
+```
+running 1 test
+test tests::it_adds_two ... ok
+
+test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
+```
+
+传递给`assert_eq!`宏的第一个参数,4,等于调用`add_two(2)`的结果。我们将会看到这个测试的那一行说`test tests::it_adds_two ... ok`,`ok`表明测试通过了!
+
+在代码中引入一个 bug 来看看使用`assert_eq!`的测试失败是什么样的。修改`add_two`函数的实现使其加 3:
+
+```rust
+pub fn add_two(a: i32) -> i32 {
+ a + 3
+}
+```
+
+再次运行测试:
+
+```
+running 1 test
+test tests::it_adds_two ... FAILED
+
+failures:
+
+---- tests::it_adds_two stdout ----
+ thread 'tests::it_adds_two' panicked at 'assertion failed: `(left ==
+ right)` (left: `4`, right: `5`)', src/lib.rs:11
+note: Run with `RUST_BACKTRACE=1` for a backtrace.
+
+failures:
+ tests::it_adds_two
+
+test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
+```
+
+测试捕获到了 bug!`it_adds_two`测试失败并显示信息`` assertion failed: `(left == right)` (left: `4`, right: `5`) ``。这个信息有助于我们开始调试:它说`assert_eq!`的`left`参数是 4,而`right`参数,也就是`add_two(2)`的结果,是 5。
+
+注意在一些语言和测试框架中,断言两个值相等的函数的参数叫做`expected`和`actual`,而且指定参数的顺序是需要注意的。然而在 Rust 中,他们则叫做`left`和`right`,同时指定期望的值和被测试代码产生的值的顺序并不重要。这个测试中的断言也可以写成`assert_eq!(add_two(2), 4)`,这时错误信息会变成`` assertion failed: `(left == right)` (left: `5`, right: `4`) ``。
+
+`assert_ne!`宏在传递给它的两个值不相等时通过而在相等时失败。这个宏在代码按照我们期望运行时不确定值**应该**是什么,不过知道他们绝对**不应该**是什么的时候最有用处。例如,如果一个函数确定会以某种方式改变其输出,不过这种方式由运行测试是星期几来决定,这时最好的断言可能就是函数的输出不等于其输入。
+
+`assert_eq!`和`assert_ne!`宏在底层分别使用了`==`和`!=`。当断言失败时,这些宏会使用调试格式打印出其参数,这意味着被比较的值必需实现了`PartialEq`和`Debug` trait。所有的基本类型和大部分标准库类型都实现了这些 trait。对于自定义的结构体和枚举,需要实现 `PartialEq`才能断言他们的值是否相等。需要实现 `Debug`才能在断言失败时打印他们的值。因为这两个 trait 都是可推导 trait,如第五章所提到的,通常可以直接在结构体或枚举上添加`#[derive(PartialEq, Debug)]`注解。附录 C 中有更多关于这些和其他可推导 trait 的详细信息。
+
+### 自定义错误信息
+
+也可以向`assert!`、`assert_eq!`和`assert_ne!`宏传递一个可选的参数来增加用于打印的自定义错误信息。任何在`assert!`必需的一个参数和`assert_eq!`和`assert_ne!`必需的两个参数之后指定的参数都会传递给第八章讲到的`format!`宏,所以可以传递一个包含`{}`占位符的格式字符串和放入占位符的值。自定义信息有助于记录断言的意义,这样到测试失败时,就能更好的例子代码出了什么问题。
+
+例如,比如说有一个根据人名进行问候的函数,而我们希望测试将传递给函数的人名显示在输出中:
+
+Filename: src/lib.rs
+
+```rust
+pub fn greeting(name: &str) -> String {
+ format!("Hello {}!", name)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn greeting_contains_name() {
+ let result = greeting("Carol");
+ assert!(result.contains("Carol"));
+ }
+}
+```
+
+这个程序的需求还没有被确定,而我们非常确定问候开始的`Hello`文本不会改变。我们决定并不想在人名改变时
+不得不更新测试,所以相比检查`greeting`函数返回的确切的值,我们将仅仅断言输出的文本中包含输入参数。
+
+让我们通过改变`greeting`不包含`name`来在代码中引入一个 bug 来测试失败时是怎样的,
+
+```rust
+pub fn greeting(name: &str) -> String {
+ String::from("Hello!")
+}
+```
+
+运行测试会产生:
```text
----- a_simple_case stdout ----
- thread 'a_simple_case' panicked at 'assertion failed: `(left == right)`
- (left: `"hello"`, right: `"world"`): greeting operation failed',
- src/main.rs:4
+running 1 test
+test tests::greeting_contains_name ... FAILED
+
+failures:
+
+---- tests::greeting_contains_name stdout ----
+ thread 'tests::greeting_contains_name' panicked at 'assertion failed:
+ result.contains("Carol")', src/lib.rs:12
+note: Run with `RUST_BACKTRACE=1` for a backtrace.
+
+failures:
+ tests::greeting_contains_name
```
-`assert_eq!`的两个参数被称为 "left" 和 "right" ,而不是 "expected" 和 "actual" ;值的顺序和硬编码的值并没有什么影响。
+这仅仅告诉了我们断言失败了和失败的行号。一个更有用的错误信息应该打印出从`greeting`函数得到的值。让我们改变测试函数来使用一个由包含占位符的格式字符串和从`greeting`函数取得的值组成的自定义错误信息:
-因为这些宏使用了`==`和`!=`运算符并使用调试格式打印这些值,进行比较的值必须实现`PartialEq`和`Debug` trait。Rust 提供的类型实现了这些 trait,不过自定义的结构体和枚举则需要自己实现`PartialEq`以便能够断言这些值是否相等,和实现`Debug`以便在断言失败时打印出这些值。因为第五章提到过这两个 trait 都是 derivable trait,所以通常可以直接在结构体或枚举上加上`#[derive(PartialEq, Debug)]`注解。查看附录 C 来寻找更多关于这些和其他 derivable trait 的信息。
-
-## 使用`should_panic`测试期望的失败
-
-可以使用另一个属性来反转测试中的失败:`should_panic`。这在测试调用特定的函数会产生错误的函数时很有帮助。例如,让我们测试第八章中的一些我们知道会 panic 的代码:尝试使用 range 语法和并不组成完整字母的字节索引来创建一个字符串 slice。在有`#[test]`属性的函数之前增加`#[should_panic]`属性,如列表 11-1 所示:
-
-
+可以在测试输出中看到所取得的确切的值,这会帮助我们理解发生了什么而不是期望发生什么。
-这个测试是成功的,因为我们表示代码应该会 panic。相反如果代码因为某种原因没有产生`panic!`则测试会失败。
+### 使用`should_panic`检查 panic
-使用`should_panic`的测试是脆弱的,因为难以保证测试不会因为一个不同于我们期望的原因失败。为了帮助解决这个问题,`should_panic`属性可以增加一个可选的`expected`参数。测试工具会确保错误信息里包含我们提供的文本。一个比列表 11-1 更健壮的版本如列表 11-2 所示:
+除了检查代码是否返回期望的正确的值之外,检查代码是否按照期望处理错误情况也是很重要的。例如,考虑第九章列表 9-8 创建的`Guess`类型。其他使用`Guess`的代码依赖于`Guess`实例只会包含 1 到 100 的值的保证。可以编写一个测试来确保创建一个超出范围的值的`Guess`实例会 panic。
+
+可以通过对函数增加另一个属性`should_panic`来实现这些。这个属性在函数中的代码 panic 时会通过,而在其中的代码没有 panic 时失败。
+
+列表 11-8 展示了如何编写一个测试来检查`Guess::new`按照我们的期望出现的错误情况:
-
+test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
+```
-请自行尝试当`should_panic`的测试出现 panic 但并不符合期望的信息时会发生什么:在测试中因为不同原因造成`panic!`,或者将期望的 panic 信息改为并不与字母字节边界 panic 信息相匹配。
\ No newline at end of file
+看起来不错!现在在代码中引入 bug,通过移除`new`函数在值大于 100 时会 panic 的条件:
+
+```rust
+# struct Guess {
+# value: u32,
+# }
+#
+impl Guess {
+ pub fn new(value: u32) -> Guess {
+ if value < 1 {
+ panic!("Guess value must be between 1 and 100, got {}.", value);
+ }
+
+ Guess {
+ value: value,
+ }
+ }
+}
+```
+
+如果运行列表 11-8 的测试,它会失败:
+
+```
+running 1 test
+test tests::greater_than_100 ... FAILED
+
+failures:
+
+failures:
+ tests::greater_than_100
+
+test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
+```
+
+这回并没有得到非常有用的信息,不过一旦我们观察测试函数,会发现它标注了`#[should_panic]`。这个错误意味着代码中函数`Guess::new(200)`并没有产生 panic。
+
+然而`should_panic`测试可能是非常含糊不清的,因为他们只是告诉我们代码并没有产生 panic。`should_panic`甚至在测试因为其他不同的原因而不是我们期望发生的那个而 panic 时也会通过。为了使`should_panic`测试更精确,可以给`should_panic`属性增加一个可选的`expected`参数。测试工具会确保错误信息中包含其提供的文本。例如,考虑列表 11-9 中修改过的`Guess`,这里`new`函数更具其值是过大还或者过小而提供不同的 panic 信息:
+
+Filename: src/lib.rs
+
+```rust
+struct Guess {
+ value: u32,
+}
+
+impl Guess {
+ pub fn new(value: u32) -> Guess {
+ if value < 1 {
+ panic!("Guess value must be greater than or equal to 1, got {}.",
+ value);
+ } else if value > 100 {
+ panic!("Guess value must be less than or equal to 100, got {}.",
+ value);
+ }
+
+ Guess {
+ value: value,
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ #[should_panic(expected = "Guess value must be less than or equal to 100")]
+ fn greater_than_100() {
+ Guess::new(200);
+ }
+}
+```
+
+
+
+这个测试会通过,因为`should_panic`属性中`expected`参数提供的值是`Guess::new`函数 panic 信息的子字符串。我们可以指定期望的整个 panic 信息,在这个例子中是`Guess value must be less than or equal to 100, got 200.`。这依赖于 panic 有多独特或动态和你希望测试有多准确。在这个例子中,错误信息的子字符串足以确保函数在`else if value > 100`的情况下运行。
+
+为了观察带有`expected`信息的`should_panic`测试失败时会发生什么,让我们再次引入一个 bug 来将`if value < 1`和`else if value > 100`的代码块对换:
+
+```rust,ignore
+if value < 1 {
+ panic!("Guess value must be less than or equal to 100, got {}.", value);
+} else if value > 100 {
+ panic!("Guess value must be greater than or equal to 1, got {}.", value);
+}
+```
+
+这一次运行`should_panic`测试,它会失败:
+
+```
+running 1 test
+test tests::greater_than_100 ... FAILED
+
+failures:
+
+---- tests::greater_than_100 stdout ----
+ thread 'tests::greater_than_100' panicked at 'Guess value must be greater
+ than or equal to 1, got 200.', src/lib.rs:10
+note: Run with `RUST_BACKTRACE=1` for a backtrace.
+note: Panic did not include expected string 'Guess value must be less than or
+equal to 100'
+
+failures:
+ tests::greater_than_100
+
+test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
+```
+
+错误信息表明测试确实如期望 panic 了,不过 panic 信息`did not include expected string 'Guess value must be less than or equal to 100'`。可以看到我们的到的 panic 信息,在这个例子中是`Guess value must be greater than or equal to 1, got 200.`。这样就可以开始寻找 bug 在哪了!
+
+现在我们讲完了编写测试的方法,让我们看看运行测试时会发生什么并讨论可以用于`cargo test`的不同选项。
\ No newline at end of file
diff --git a/src/ch11-02-running-tests.md b/src/ch11-02-running-tests.md
index 085647a..229bde1 100644
--- a/src/ch11-02-running-tests.md
+++ b/src/ch11-02-running-tests.md
@@ -1,176 +1,210 @@
## 运行测试
-> [ch11-02-running-tests.md](https://github.com/rust-lang/book/blob/master/src/ch11-02-running-tests.md)
+> [ch11-02-running-tests.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch11-02-running-tests.md)
>
-> commit cf52d81371e24e14ce31a5582bfcb8c5b80d26cc
+> commit 55b294f20fc846a13a9be623bf322d8b364cee77
-类似于`cargo run`会编译代码并运行生成的二进制文件,`cargo test`在测试模式下编译代码并运行生成的测试二进制文件。`cargo test`生成的二进制文件默认会并行的运行所有测试并在测试过程中捕获生成的输出,这样就更容易阅读测试结果的输出。
+就像`cargo run`会编译代码并运行生成的二进制文件,`cargo test`在测试模式下编译代码并运行生成的测试二进制文件。这里有一些选项可以用来改变`cargo test`的默认行为。例如,`cargo test`生成的二进制文件的默认行为是并行的运行所有测试,并捕获测试运行过程中产生的输出避免他们被显示出来使得阅读测试结果相关的内容变得更容易。可以指定命令行参数来改变这些默认行为。
-可以通过指定命令行选项来改变这些运行测试的默认行为。这些选项的一部分可以传递给`cargo test`,而另一些则需要传递给生成的测试二进制文件。分隔这些参数的方法是`--`:`cargo test`之后列出了传递给`cargo test`的参数,接着是分隔符`--`,之后是传递给测试二进制文件的参数。
+这些选项的一部分可以传递给`cargo test`,而另一些则需要传递给生成的测试二进制文件。为了分隔两种类型的参数,首先列出传递给`cargo test`的参数,接着是分隔符`--`,再之后是传递给测试二进制文件的参数。运行`cargo test --help`会告诉你`cargo test`的相关参数,而运行`cargo test -- --help`则会告诉你位于分隔符`--`之后的相关参数。
-### 并行运行测试
+### 并行或连续的运行测试
-测试使用线程来并行运行。为此,编写测试时需要注意测试之间不要相互依赖或者存在任何共享状态。共享状态也可能包含在运行环境中,比如当前工作目录或者环境变量。
+
+
-如果你不希望它这样运行,或者想要更加精确的控制使用线程的数量,可以传递`--test-threads`参数和线程的数量给测试二进制文件。将线程数设置为 1 意味着没有任何并行操作:
+当运行多个测试时,他们默认使用线程来并行的运行。这意味着测试会更快的运行完毕,所以可以更快的得到代码能否工作的反馈。因为测试是在同时运行的,你应该小心测试不能相互依赖或任何共享状态,包括类似于当前工作目录或者环境变量这样的共享环境。
+
+例如,每一个测试都运行一些代码在硬盘上创建一个`test-output.txt`文件并写入一些数据。接着每一个测试都读取文件中的数据并断言这个文件包含特定的值,而这个值在每个测试中都是不同的。因为所有测试都是同时运行的,一个测试可能会在另一个测试读写文件过程中覆盖了文件。那么第二个测试就会失败,并不是因为代码不正确,而是因为测试并行运行时相互干涉。一个解决方案是使每一个测试读写不同的文件;另一个是一次运行一个测试。
+
+如果你不希望测试并行运行,或者想要更加精确的控制使用线程的数量,可以传递`--test-threads`参数和希望使用线程的数量给测试二进制文件。例如:
```
$ cargo test -- --test-threads=1
```
-### 捕获测试输出
+这里将测试线程设置为 1,告诉程序不要使用任何并行机制。这也会比并行运行花费更多时间,不过测试就不会在存在共享状态时潜在的相互干涉了。
-Rust 的测试库默认捕获并丢弃标准输出和标准错误中的输出,除非测试失败了。例如,如果在测试中调用了`println!`而测试通过了,你将不会在终端看到`println!`的输出。这个行为可以通过向测试二进制文件传递`--nocapture`参数来禁用:
+### 显示测试输出
+
+如果测试通过了,Rust 的测试库默认会捕获打印到标准输出的任何内容。例如,如果在测试中调用`println!`而测试通过了,我们将不会在终端看到`println!`的输出:只会看到说明测试通过的行。如果测试失败了,就会看到任何标准输出和其他错误信息。
+
+例如,列表 11-20 有一个无意义的函数它打印出其参数的值并接着返回 10。接着还有一个会通过的测试和一个会失败的测试:
+
+Filename: src/lib.rs
+
+```rust
+fn prints_and_returns_10(a: i32) -> i32 {
+ println!("I got the value {}", a);
+ 10
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn this_test_will_pass() {
+ let value = prints_and_returns_10(4);
+ assert_eq!(10, value);
+ }
+
+ #[test]
+ fn this_test_will_fail() {
+ let value = prints_and_returns_10(8);
+ assert_eq!(5, value);
+ }
+}
+```
+
+
+
+运行`cargo test`将会看到这些测试的输出:
+
+```
+running 2 tests
+test tests::this_test_will_pass ... ok
+test tests::this_test_will_fail ... FAILED
+
+failures:
+
+---- tests::this_test_will_fail stdout ----
+ I got the value 8
+thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left ==
+right)` (left: `5`, right: `10`)', src/lib.rs:19
+note: Run with `RUST_BACKTRACE=1` for a backtrace.
+
+failures:
+ tests::this_test_will_fail
+
+test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
+```
+
+注意输出中哪里也不会出现`I got the value 4`,这是当测试通过时打印的内容。这些输出被捕获。失败测试的输出,`I got the value 8`,则出现在输出的测试总结部分,它也显示了测试失败的原因。
+
+如果你希望也能看到通过的测试中打印的值,捕获输出的行为可以通过`--nocapture`参数来禁用:
```
$ cargo test -- --nocapture
```
+使用`--nocapture`参数再次运行列表 11-10 中的测试会显示:
+
+```
+running 2 tests
+I got the value 4
+I got the value 8
+test tests::this_test_will_pass ... ok
+thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left ==
+right)` (left: `5`, right: `10`)', src/lib.rs:19
+note: Run with `RUST_BACKTRACE=1` for a backtrace.
+test tests::this_test_will_fail ... FAILED
+
+failures:
+
+failures:
+ tests::this_test_will_fail
+
+test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
+```
+
+注意测试的输出和测试结果的输出是相互交叉的;这是由于上一部分讲到的测试是并行运行的。尝试一同使用`--test-threads=1`和`--nocapture`功能来看看输出是什么样子!
+
### 通过名称来运行测试的子集
-有时运行整个测试集会耗费很多时间。如果你负责特定位置的代码,你可能会希望只与这些代码相关的测试。`cargo test`有一个参数允许你通过指定名称来运行特定的测试。
+有时运行整个测试集会耗费很多时间。如果你负责特定位置的代码,你可能会希望只与这些代码相关的测试。可以向`cargo test`传递希望运行的测试的(部分)名称作为参数来选择运行哪些测试。
-列表 11-3 中创建了三个如下名称的测试:
+为了展示如何运行测试的子集,列表 11-11 使用`add_two`函数创建了三个测试来供我们选择运行哪一个:
-
-
-使用不同的参数会运行不同的测试子集。没有参数的话,如你所见会运行所有的测试:
-
-```
-$ cargo test
- Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
- Running target/debug/deps/adder-abcabcabc
-
-running 3 tests
-test add_three_and_two ... ok
-test one_hundred ... ok
-test add_two_and_two ... ok
-
-test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured
-```
-
-可以传递任意测试的名称来只运行那个测试:
-
-```
-$ cargo test one_hundred
- Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
- Running target/debug/deps/adder-abcabcabc
-
-running 1 test
-test one_hundred ... ok
-
-test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
-```
-
-也可以传递名称的一部分,`cargo test`会运行所有匹配的测试:
-
-```
-$ cargo test add
- Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
- Running target/debug/deps/adder-abcabcabc
-
-running 2 tests
-test add_three_and_two ... ok
-test add_two_and_two ... ok
-
-test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured
-```
-
-模块名也作为测试名的一部分,所以类似的模块名也可以用来指定测试特定模块。例如,如果将我们的代码组织成一个叫`adding`的模块和一个叫`subtracting`的模块并分别带有测试,如列表 11-4 所示:
-
-
-
-执行`cargo test`会运行所有的测试,而模块名会出现在输出的测试名中:
+如果没有传递任何参数就运行测试,如你所见,所有测试都会并行运行:
```
-$ cargo test
- Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
- Running target/debug/deps/adder-abcabcabc
-
-running 4 tests
-test adding::add_two_and_two ... ok
-test adding::add_three_and_two ... ok
-test subtracting::subtract_three_and_two ... ok
-test adding::one_hundred ... ok
-```
-
-运行`cargo test adding`将只会运行对应模块的测试而不会运行任何 subtracting 模块中的测试:
-
-```
-$ cargo test adding
- Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
- Running target/debug/deps/adder-abcabcabc
-
running 3 tests
-test adding::add_three_and_two ... ok
-test adding::one_hundred ... ok
-test adding::add_two_and_two ... ok
+test tests::add_two_and_two ... ok
+test tests::add_three_and_two ... ok
+test tests::one_hundred ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured
```
+#### 运行单个测试
+
+可以向`cargo test`传递任意测试的名称来只运行这个测试:
+
+```
+$ cargo test one_hundred
+ Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
+ Running target/debug/deps/adder-06a75b4a1f2515e9
+
+running 1 test
+test tests::one_hundred ... ok
+
+test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
+```
+
+不能像这样指定多个测试名称,只有传递给`cargo test`的第一个值才会被使用。
+
+#### 过滤运行多个测试
+
+然而,可以指定测试的部分名称,这样任何名称匹配这个值的测试会被运行。例如,因为头两个测试的名称包含`add`,可以通过`cargo test add`来运行这两个测试:
+
+```
+$ cargo test add
+ Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
+ Running target/debug/deps/adder-06a75b4a1f2515e9
+
+running 2 tests
+test tests::add_two_and_two ... ok
+test tests::add_three_and_two ... ok
+
+test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured
+```
+
+这运行了所有名字中带有`add`的测试。同时注意测试所在的模块作为测试名称的一部分,所以可以通过模块名来过滤运行一个模块中的所有测试。
+
+
+
+
### 除非指定否则忽略某些测试
-有时一些特定的测试执行起来是非常耗费时间的,所以对于大多数`cargo test`命令,我们希望能排除它。无需为`cargo test`创建一个用来在运行所有测试时排除特定测试的参数并每次都要记得使用它,我们可以对这些测试使用`ignore`属性:
+有时一些特定的测试执行起来是非常耗费时间的,所以在运行大多数`cargo test`的时候希望能排除他们。与其通过参数列举出所有希望运行的测试,也可以使用`ignore`属性来标记耗时的测试来排除他们:
Filename: src/lib.rs
@@ -187,13 +221,13 @@ fn expensive_test() {
}
```
-现在运行测试,将会发现`it_works`运行了,而`expensive_test`没有:
+我们对想要排除的测试的`#[test]`之后增加了`#[ignore]`行。现在如果运行测试,就会发现`it_works`运行了,而`expensive_test`没有运行:
```
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished debug [unoptimized + debuginfo] target(s) in 0.24 secs
- Running target/debug/deps/adder-abcabcabc
+ Running target/debug/deps/adder-ce99bcc2479f4607
running 2 tests
test expensive_test ... ignored
@@ -208,12 +242,23 @@ running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
```
-我们可以通过`cargo test -- --ignored`来明确请求只运行那些耗时的测试:
+`expensive_test`被列为`ignored`,如果只希望运行被忽略的测试,可以使用`cargo test -- --ignored`来请求运行他们:
-```
+
+
+
+
+
+
+```text
$ cargo test -- --ignored
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
- Running target/debug/deps/adder-abcabcabc
+ Running target/debug/deps/adder-ce99bcc2479f4607
running 1 test
test expensive_test ... ok
@@ -221,4 +266,5 @@ test expensive_test ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
```
-通过这种方式,大部分时间运行`cargo test`将是快速的。当需要检查`ignored`测试的结果而且你也有时间等待这个结果的话,可以选择执行`cargo test -- --ignored`。
\ No newline at end of file
+
+通过控制运行哪些测试,可以确保运行`cargo test`的结果是快速的。当某个时刻需要检查`ignored`测试的结果而且你也有时间等待这个结果的话,可以选择执行`cargo test -- --ignored`。
\ No newline at end of file
diff --git a/src/ch11-03-test-organization.md b/src/ch11-03-test-organization.md
index 75736aa..81ff91c 100644
--- a/src/ch11-03-test-organization.md
+++ b/src/ch11-03-test-organization.md
@@ -1,20 +1,22 @@
## 测试的组织结构
-> [ch11-03-test-organization.md](https://github.com/rust-lang/book/blob/master/src/ch11-03-test-organization.md)
+> [ch11-03-test-organization.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch11-03-test-organization.md)
>
-> commit cf52d81371e24e14ce31a5582bfcb8c5b80d26cc
+> commit 55b294f20fc846a13a9be623bf322d8b364cee77
-正如之前提到的,测试是一个很广泛的学科,而且不同的人有时也采用不同的技术和组织。Rust 社区倾向于根据测试的两个主要分类来考虑问题:**单元测试**(*unit tests*)与**集成测试**(*unit tests*)。单元测试倾向于更小而更专注,在隔离的环境中一次测试一个模块。他们也可以测试私有接口。集成测试对于你的库来说则完全是外部的。他们与其他用户使用相同的方式使用你得代码,他们只针对共有接口而且每个测试会测试多个模块。这两类测试对于从独立和整体的角度保证你的库符合期望是非常重要的。
+正如之前提到的,测试是一个很广泛的学科,而且不同的开发者也采用不同的技术和组织。Rust 社区倾向于根据测试的两个主要分类来考虑问题:**单元测试**(*unit tests*)与**集成测试**(*unit tests*)。单元测试倾向于更小而更专注,在隔离的环境中一次测试一个模块。他们也可以测试私有接口。集成测试对于你的库来说则完全是外部的。他们与其他用户使用相同的方式使用你的代码,他们只针对共有接口而且每个测试都会测试多个模块。
+
+这两类测试对于从独立和整体的角度保证你的库符合期望是非常重要的。
### 单元测试
-单元测试的目的是在隔离与其他部分的环境中测试每一个单元的代码,以便于快速而准确的定位何处的代码是否符合预期。单元测试位于 *src* 目录中,与他们要测试的代码存在于相同的文件中。他们被分离进每个文件中他们自有的`tests`模块中。
+单元测试的目的是在与其他部分隔离的环境中测试每一个单元的代码,以便于快速而准确的定位何处的代码是否符合预期。单元测试位于 *src* 目录中,与他们要测试的代码存在于相同的文件中。传统做法是在每个文件中创建包含测试函数的`tests`模块,并使用`cfg(test)`标注模块。
#### 测试模块和`cfg(test)`
-通过将测试放进他们自己的模块并对该模块使用`cfg`注解,我们可以告诉 Rust 只在执行`cargo test`时才编译和运行测试代码。这在当我们只希望用`cargo build`编译库代码时可以节省编译时间,并减少编译产物的大小因为并没有包含测试。
+测试模块的`#[cfg(test)]`注解告诉 Rust 只在执行`cargo test`时才编译和运行测试代码,而在运行`cargo build`时不这么做。这在只希望构建库的时候可以节省编译时间,并能节省编译产物的空间因为他们并没有包含测试。我们将会看到因为集成测试位于另一个文件夹,他们并不需要`#[cfg(test)]`注解。但是因为单元测试位于与源码相同的文件中,所以使用`#[cfg(test)]`来指定他们不应该被包含进编译产物中。
-还记得上一部分新建的`adder`项目吗?Cargo 为我们生成了如下代码:
+还记得本章第一部分新建的`adder`项目吗?Cargo 为我们生成了如下代码:
Filename: src/lib.rs
@@ -27,66 +29,12 @@ mod tests {
}
```
-我们忽略了模块相关的信息以便更关注模块中测试代码的机制,不过现在让我们看看测试周围的代码。
-
-首先,这里有一个属性`cfg`。`cfg`属性让我们声明一些内容只在给定特定的**配置**(*configuration*)时才被包含进来。Rust 提供了`test`配置用来编译和运行测试。通过这个属性,Cargo 只会在尝试运行测试时才编译测试代码。
-
-接下来,`tests`包含了所有测试函数,而我们的代码则位于`tests`模块之外。`tests`模块的名称是一个惯例,除此之外这是一个遵守第七章讲到的常见可见性规则的普通模块。因为这是一个内部模块,我们需要将要测试的代码引入作用域。这对于一个大的模块来说是很烦人的,所以这里经常使用全局导入。
-
-从本章到现在,我们一直在为`adder`项目编写并没有实际调用任何代码的测试。现在让我们做一些改变!在 *src/lib.rs* 中,放入`add_two`函数和带有一个检验代码的测试的`tests`模块,如列表 11-5 所示:
-
-
-
-注意除了测试函数之外,我们还在`tests`模块中添加了`use add_two;`。这将我们想要测试的代码引入到了内部的`tests`模块的作用域中,正如任何内部模块需要做的那样。如果现在使用`cargo test`运行测试,它会通过:
-
-```
-running 1 test
-test tests::it_works ... ok
-
-test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
-```
-
-如果我们忘记将`add_two`函数引入作用域,将会得到一个 unresolved name 错误,因为`tests`模块并不知道任何关于`add_two`函数的信息:
-
-```
-error[E0425]: unresolved name `add_two`
- --> src/lib.rs:9:23
- |
-9 | assert_eq!(4, add_two(2));
- | ^^^^^^^ unresolved name
-```
-
-如果这个模块包含很多希望测试的代码,在测试中列出每一个`use`语句将是很烦人的。相反在测试子模块中使用`use super::*;`来一次将所有内容导入作用域中是很常见的。
+这里自动生成了测试模块。`cfg`属性代表 *configuration* ,它告诉 Rust 其之后的项只被包含进特定配置中。在这个例子中,配置是`test`,Rust 所提供的用于编译和运行测试的配置。通过使用这个属性,Cargo 只会在我们主动使用`cargo test`运行测试时才编译测试代码。除了标注为`#[test]`的函数之外,这还包括测试模块中可能存在的帮助函数。
#### 测试私有函数
-测试社区中一直存在关于是否应该对私有函数进行单元测试的论战。不过无论你坚持哪种测试意识形态,Rust 确实允许你测试私有函数,由于私有性规则。考虑列表 11-6 中带有私有函数`internal_adder`的代码:
+测试社区中一直存在关于是否应该对私有函数进行单元测试的论战,而其他语言中难以甚至不可能测试私有函数。不过无论你坚持哪种测试意识形态,Rust 的私有性规则确实允许你测试私有函数,由于私有性规则。考虑列表 11-12 中带有私有函数`internal_adder`的代码:
-
-
-因为测试也不过是 Rust 代码而`tests`也只是另一个模块,我们完全可以在一个测试中导入并调用`internal_adder`。如果你并不认为私有函数应该被测试,Rust 也不会强迫你这么做。
+注意`internal_adder`函数并没有标记为`pub`,不过因为测试也不过是 Rust 代码而`tests`也仅仅是另一个模块,我们完全可以在测试中导入和调用`internal_adder`。如果你并不认为私有函数应该被测试,Rust 也不会强迫你这么做。
### 集成测试
-在 Rust 中,集成测试对于需要测试的库来说是完全独立。他们同其他代码一样使用库文件。他们的目的是测试库的个个部分结合起来能否正常工作。每个能正确运行的代码单元集成在一起也可能会出现问题,所以集成测试的覆盖率也是很重要的。
+在 Rust 中,集成测试对于需要测试的库来说是完全独立。他们同其他代码一样使用库文件,这意味着他们只能调用作为库公有 API 的一部分的函数。他们的目的是测试库的多个部分能否一起正常工作。每个能单独正确运行的代码单元集成在一起也可能会出现问题,所以集成测试的覆盖率也是很重要的。为了创建集成测试,首先需要一个 *tests* 目录。
#### *tests* 目录
-Cargo 支持位于 *tests* 目录中的集成测试。如果创建它并放入 Rust 源文件,Cargo 会将每一个文件当作单独的 crate 来编译。让我们试一试!
+为了编写集成测试,需要在项目根目录创建一个 *tests* 目录,与 *src* 同级。Cargo 知道如何去寻找这个目录中的集成测试文件。接着可以随意在这个文件夹中创建任意多的测试文件,Cargo 会将每一个文件当作单独的 crate 来编译。
-首先,在项目根目录创建一个 *tests* 目录,挨着 *src* 目录。接着新建一个文件 *tests/integration_test.rs*,并写入列表 11-7 中的代码:
+让我们试一试吧!保留列表 11-12 中 *src/lib.rs* 的代码。创建一个 *tests* 目录,新建一个文件 *tests/integration_test.rs*,并输入列表 11-13 中的代码。
-
-
-
-在开头使用了`extern crate adder`,单元测试中并不需要它。`tests`目录中的每一个测试文件都是完全独立的 crate,所以需要在每个文件中导入我们的库。这也就是为何`tests`是编写集成测试的绝佳场所:他们像任何其他用户那样,需要将库导入 crate 并只能使用公有 API。
-
-这个文件中也不需要`tests`模块。除非运行测试否则整个文件夹都不会被编译,所以无需将任何部分标记为`#[cfg(test)]`。另外每个测试文件都被隔离进其自己的 crate 中,无需进一步隔离测试代码。
-
-让我们运行集成测试,同样使用`cargo test`来运行:
+并不需要将 *tests/integration_test.rs* 中的任何代码标注为`#[cfg(test)]`。Cargo 对`tests`文件夹特殊处理并只会在运行`cargo test`时编译这个目录中的文件。现在就试试运行`cargo test`:
```
-$ cargo test
+cargo test
Compiling adder v0.1.0 (file:///projects/adder)
+ Finished debug [unoptimized + debuginfo] target(s) in 0.31 secs
Running target/debug/deps/adder-abcabcabc
running 1 test
-test tests::it_works ... ok
+test tests::internal ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
- Running target/debug/integration_test-952a27e0126bb565
+ Running target/debug/deps/integration_test-ce99bcc2479f4607
running 1 test
test it_adds_two ... ok
@@ -178,9 +122,18 @@ running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
```
-现在有了三个部分的输出:单元测试、集成测试和文档测试。注意当在任何 *src* 目录的文件中增加单元测试时,单元测试部分的对应输出也会增加。增加集成测试文件中的测试函数也会对应增加输出。如果在 *tests* 目录中增加集成测试**文件**,则会增加更多集成测试部分:一个文件对应一个部分。
+
+
-为`cargo test`指定测试函数名称参数也会匹配集成测试文件中的函数。为了只运行某个特定集成测试文件中的所有测试,可以使用`cargo test`的`--test`参数:
+现在有了三个部分的输出:单元测试、集成测试和文档测试。第一部分单元测试与我们之前见过的一样:每一个单元测试一行(列表 11-12 中有一个叫做`internal`的测试),接着是一个单元测试的总结行。
+
+集成测试部分以行`Running target/debug/deps/integration-test-ce99bcc2479f4607`(输出最后的哈希值可能不同)开头。接着是每一个集成测试中的测试函数一行,以及一个就在`Doc-tests adder`部分开始之前的集成测试的总结行。
+
+注意在任意 *src* 文件中增加更多单元测试函数会增加更多单元测试部分的测试结果行。在我们创建的集成测试文件中增加更多测试函数会增加更多集成测试部分的行。每一个集成测试文件有其自己的部分,所以如果在 *tests* 目录中增加更多文件,这里就会有更多集成测试部分。
+
+我们仍然可以通过指定测试函数的名称作为`cargo test`的参数来运行特定集成测试。为了运行某个特定集成测试文件中的所有测试,使用`cargo test`的`--test`后跟文件的名称:
```
$ cargo test --test integration_test
@@ -193,18 +146,86 @@ test it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
```
+这些只是 *tests* 目录中我们指定的文件中的测试。
+
#### 集成测试中的子模块
-随着集成测试的增加,你可能希望在 `tests` 目录增加更多文件,例如根据测试的功能来将测试分组。正如我们之前提到的,这是可以的,Cargo 会将每一个文件当作一个独立的 crate。
+随着集成测试的增加,你可能希望在 `tests` 目录增加更多文件,例如根据测试的功能来将测试分组。正如我们之前提到的,每一个 *tests* 目录中的文件都被编译为单独的 crate。
-最终,可能会有一系列在所有集成测试中通用的帮助函数,例如建立通用场景的函数。如果你将这些函数提取到 *tests* 目录的一个文件中,比如说 *tests/common.rs*,则这个文件将会像这个目录中的其他包含测试的 Rust 文件一样被编译进一个单独的 crate 中。它也会作为一个独立的部分出现在测试输出中。因为这很可能不是你所希望的,所以建议在子目录中使用 *mod.rs* 文件,比如 *tests/common/mod.rs*,来放置帮助函数。*tests* 的子目录不会被作为单独的 crate 编译或者作为单独的部分出现在测试输出中。
+将每个集成测试文件当作其自己的 crate 来对待有助于创建更类似与终端用户使用 crate 那样的单独的作用域。然而,这意味着考虑到像第七章学习的如何将代码分隔进模块和文件那样,*tests* 目录中的文件不能像 *src* 中的文件那样共享相同的行为。
+
+对于 *tests* 目录中文件的不同行为,通常在如果有一系列有助于多个集成测试文件的帮助函数,而你尝试遵循第七章的步骤将他们提取到一个通用的模块中时显得很明显。例如,如果我们创建了 *tests/common.rs* 并将`setup`函数放入其中,这里将放入一些希望能够在多个测试文件的多个测试函数中调用的代码:
+
+Filename: tests/common.rs
+
+```rust
+pub fn setup() {
+ // setup code specific to your library's tests would go here
+}
+```
+
+如果再次运行测试,将会在测试结果中看到一个对应 *common.rs* 文件的新部分,即便这个文件并没有包含任何测试函数,或者没有任何地方调用了`setup`函数:
+
+```
+running 1 test
+test tests::internal ... ok
+
+test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
+
+ Running target/debug/deps/common-b8b07b6f1be2db70
+
+running 0 tests
+
+test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
+
+ Running target/debug/deps/integration_test-d993c68b431d39df
+
+running 1 test
+test it_adds_two ... ok
+
+test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
+
+ Doc-tests adder
+
+running 0 tests
+
+test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
+```
+
+
+
+
+`common`出现在测试结果中并显示`running 0 tests`,这不是我们想要的;我们只是希望能够在其他集成测试文件中分享一些代码罢了。
+
+为了使`common`不出现在测试输出中,需要使用第七章学习到的另一个将代码提取到文件的方式:不再创建*tests/common.rs*,而是创建 *tests/common/mod.rs*。当将`setup`代码移动到 *tests/common/mod.rs* 并去掉 *tests/common.rs* 文件之后,测试输出中将不会出现这一部分。*tests* 目录中的子目录不会被作为单独的 crate 编译或作为一部分出现在测试输出中。
+
+一旦拥有了 *tests/common/mod.rs*,就可以将其作为模块来在任何集成测试文件中使用。这里是一个 *tests/integration_test.rs* 中调用`setup`函数的`it_adds_two`测试的例子:
+
+Filename: tests/integration_test.rs
+
+```rust,ignore
+extern crate adder;
+
+mod common;
+
+#[test]
+fn it_adds_two() {
+ common::setup();
+ assert_eq!(4, adder::add_two(2));
+}
+```
+
+注意`mod common;`声明与第七章中的模块声明相同。接着在测试函数中就可以调用`common::setup()`了。
#### 二进制 crate 的集成测试
-如果项目是二进制 crate 并且只包含 *src/main.rs* 而没有 *src/lib.rs*,这样就不可能在 *tests* 创建集成测试并使用 `extern crate` 导入 *src/main.rs* 中的函数了。这也是 Rust 二进制项目明确采用 *src/main.rs* 调用 *src/lib.rs* 中逻辑的结构的原因之一。通过这种结构,集成测试**就可以**使用`extern crate`测试库 crate 中的主要功能,而如果这些功能没有问题的话,*src/main.rs* 中的少量代码也就会正常工作且不需要测试。
+如果项目是二进制 crate 并且只包含 *src/main.rs* 而没有 *src/lib.rs*,这样就不可能在 *tests* 创建集成测试并使用 `extern crate` 导入 *src/main.rs* 中的函数了。只有库 crate 向其他 crate 暴露了可以调用和使用的函数;二进制 crate 只意在单独运行。
+
+这也是 Rust 二进制项目明确采用 *src/main.rs* 调用 *src/lib.rs* 中逻辑这样的结构的原因之一。通过这种结构,集成测试**就可以**使用`extern crate`测试库 crate 中的主要功能,而如果这些重要的功能没有问题的话,*src/main.rs* 中的少量代码也就会正常工作且不需要测试。
## 总结
-Rust 的测试功能提供了一个确保即使改变代码函数也能继续以指定方式运行的途径。单元测试独立的验证库的每一部分并能够测试私有实现细节。集成测试则涉及多个部分结合起来时能否使用,并像其他代码那样测试库的公有 API。Rust 的类型系统和所有权规则可以帮助避免一些 bug,不过测试对于减少代码是否符合期望的逻辑 bug 是很重要的。
+Rust 的测试功能提供了一个确保即使做出改变函数也能继续以指定方式运行的途径。单元测试独立的验证库的不同部分并能够测试私有实现细节。集成测试则涉及多个部分结合起来工作时的用例,并像其他代码那样测试库的公有 API。即使 Rust 的类型系统和所有权规则可以帮助避免一些 bug,不过测试对于减少代码是否符合期望相关的逻辑 bug 是很重要的。
接下来让我们结合本章所学和其他之前章节的知识,在下一章一起编写一个项目!
\ No newline at end of file
diff --git a/src/ch12-00-an-io-project.md b/src/ch12-00-an-io-project.md
index 54a22aa..cc6d146 100644
--- a/src/ch12-00-an-io-project.md
+++ b/src/ch12-00-an-io-project.md
@@ -1,21 +1,35 @@
-# 一个 I/O 项目
+# 一个 I/O 项目:构建一个小巧的 grep
-> [ch12-00-an-io-project.md](https://github.com/rust-lang/book/blob/master/src/ch12-00-an-io-project.md)
+> [ch12-00-an-io-project.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-00-an-io-project.md)
>
-> commit efd59dd0fe8e3658563fb5fd289af9d862e07a03
+> commit 1f432fc231cfbc310433ab2a354d77058444288c
-之前几个章节我们学习了很多知识。让我们一起运用这些新知识来构建一个项目。在这个过程中,我们还将学习到更多 Rust 标准库的内容。
+
-那么我们应该写点什么呢?这得是一个利用 Rust 优势的项目。Rust 的一个强大的用途是命令行工具:Rust 的运行速度、安全性、“单二进制文件”输出和跨平台支持使得它称为这类工作的绝佳选择。所以我们将创建一个我们自己的经典命令行工具:`grep`。`grep`有着极为简单的应用场景,它完成如下工作:
+
-1. 它获取一个文件和一个字符串作为参数。
-2. 读取文件
-3. 寻找文件中包含字符串参数的行
-4. 打印出这些行
+本章既是一个目前所学的很多技能的概括,也是一个更多标准库功能的探索。我们将构建一个与文件和命令行输入/输出交互的命令行工具来练习现在一些你已经掌握的 Rust 技能。
-另外,我们还将添加一个额外的功能:一个环境变量允许我们大小写不敏感的搜索字符串参数。
+Rust 的运行速度、安全性、“单二进制文件”输出和跨平台支持使其成为创建命令行程序的绝佳选择,所以我们的项目将创建一个我们自己版本的经典命令行工具:`grep`。grep 是“Globally search a Regular Expression and Print.”的首字母缩写。`grep`最简单的使用场景是使用如下步骤在特定文件中搜索指定字符串:
-还有另一个很好的理由使用`grep`作为示例项目:Rust 社区的成员,Andrew Gallant,已经使用 Rust 创建了一个功能非常完整的`grep`版本。它叫做`ripgrep`,并且它非常非常快。这样虽然我们的`grep`将会非常简单,你也会掌握阅读现实生活中项目的基础知识。
+- 获取一个文件和一个字符串作为参数。
+- 读取文件
+- 寻找文件中包含字符串参数的行
+- 打印出这些行
+
+我们还会展示如何使用环境变量和打印到标准错误而不是标准输出;这些功能在命令行工具中是很常用的。
+
+一位 Rust 社区的成员,Andrew Gallant,已经创建了一个功能完整且非常快速的`grep`版本,叫做`ripgrep`。相比之下,我们的`grep`将非常简单,本章将交给你一些帮助你理解像`ripgrep`这样真实项目的背景知识。
这个项目将会结合之前所学的一些内容:
@@ -25,14 +39,12 @@
- 合理的使用 trait 和生命周期(第十章)
- 测试(第十一章)
-另外,我还会简要的讲到闭包、迭代器和 trait 对象,他们分别会在第XX、YY和ZZ章详细介绍。
+另外,我还会简要的讲到闭包、迭代器和 trait 对象,他们分别会在第十三章和第十七章中详细介绍。
-让我们一如既往的使用`cargo new`创建一个新项目:
+让我们一如既往的使用`cargo new`创建一个新项目。我们称之为`greprs`以便与可能已经安装在系统上的`grep`工具相区别:
-```text
+```
$ cargo new --bin greprs
Created binary (application) `greprs` project
$ cd greprs
-```
-
-我们版本的`grep`的叫做“greprs”,这样就不会迷惑用户让他们以为这就是可能已经在系统上安装了功能更完整的`grep`。
\ No newline at end of file
+```
\ No newline at end of file
diff --git a/src/ch12-01-accepting-command-line-arguments.md b/src/ch12-01-accepting-command-line-arguments.md
index 0b8717c..ef608ac 100644
--- a/src/ch12-01-accepting-command-line-arguments.md
+++ b/src/ch12-01-accepting-command-line-arguments.md
@@ -2,18 +2,39 @@
> [ch12-01-accepting-command-line-arguments.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-01-accepting-command-line-arguments.md)
>
-> commit 4f2dc564851dc04b271a2260c834643dfd86c724
+> commit b8e4fcbf289b82c12121b282747ce05180afb1fb
-第一个任务是让`greprs`接受两个命令行参数。crates.io 上有一些现存的库可以帮助我们,不过因为我们正在学习,我们将自己实现一个。
+第一个任务是让`greprs`能够接受两个命令行参数:文件名和要搜索的字符串。也就是说希望能够使用`cargo run`,要搜索的字符串和被搜索的文件的路径来运行程序,像这样:
-我们需要调用一个 Rust 标准库提供的函数:`std::env::args`。这个函数返回一个传递给程序的命令行参数的**迭代器**(*iterator*)。我们还未讨论到迭代器,第十三章会全面的介绍他们。但是对于我们的目的来说,使用他们并不需要知道多少技术细节。我们只需要明白两点:
+```
+$ cargo run searchstring example-filename.txt
+```
+
+现在`cargo new`生成的程序忽略任何传递给它的参数。crates.io 上有一些现存的可以帮助我们接受命令行参数的库,不过因为我们正在学习,让我们实现一个。
+
+
+
+
+### 读取参数值
+
+为了能够获取传递给程序的命令行参数的值,我们需要调用一个 Rust 标准库提供的函数:`std::env::args`。这个函数返回一个传递给程序的命令行参数的**迭代器**(*iterator*)。我们还未讨论到迭代器,第十三章会全面的介绍他们。但是对于我们现在的目的来说只需要明白两点:
1. 迭代器生成一系列的值。
2. 在迭代器上调用`collect`方法可以将其生成的元素转换为一个 vector。
-让我们试试列表 12-1 中的代码:
+让我们尝试一下:使用列表 12-1 中的代码来读取任何传递给`greprs`的命令行参数并将其收集到一个 vector 中。
+
+
+
-
+Listing 12-1: Collect the command line arguments into a vector and print them
+out
-首先使用`use`语句来将`std::env`模块引入作用域。当函数嵌套了多于一层模块时,比如说`std::env::args`,通常使用`use`将父模块引入作用域,而不是引入其本身。`env::args`比单独的`args`要明确一些。当然,如果使用了多余一个`std::env`中的函数,我们也只需要一个`use`语句。
+首先使用`use`语句来将`std::env`模块引入作用域以便可以使用它的`args`函数。注意`std::env::args`函数嵌套进了两层模块中。如第七章讲到的,当所需函数嵌套了多于一层模块时,通常将父模块引入作用域,而不是其自身。这便于我们利用`std::env`中的其他函数。这比增加了`use std::env::args;`后仅仅使用`args`调用函数要更明确一些;这样看起来好像一个定义于当前模块的函数。
-在`main`函数的第一行,我们调用了`env::args`,并立即使用`collect`来创建了一个 vector。这里我们也显式的注明了`args`的类型:`collect`可以被用来创建很多类型的集合。Rust 并不能推断出我们需要什么类型,所以类型注解是必须的。在 Rust 中我们很少会需要注明类型,不过`collect`是就一个通常需要这么做的函数。
+
+
+
+
+> 注意:`std::env::args`在其任何参数包含无效 Unicode 字符时会 panic。如果你需要接受包含无效 Unicode 字符的参数,使用`std::env::args_os`代替。这个函数返回`OsString`值而不是`String`值。出于简单考虑这里使用`std::env::args`,因为`OsString`值每个平台都不一样而且比`String`值处理起来更复杂。
+
+
+
+
+
+
+在`main`函数的第一行,我们调用了`env::args`,并立即使用`collect`来创建了一个包含迭代器所有值的 vector。`collect`可以被用来创建很多类型的集合,所以这里显式注明的`args`类型来指定我们需要一个字符串 vector。虽然在 Rust 中我们很少会需要注明类型,`collect`就是一个经常需要注明类型的函数,因为 Rust 不能推断出你想要什么类型的集合。
最后,我们使用调试格式`:?`打印出 vector。让我们尝试不用参数运行代码,接着用两个参数:
@@ -49,55 +78,58 @@ $ cargo run needle haystack
["target/debug/greprs", "needle", "haystack"]
```
-你会注意一个有趣的事情:二进制文件的名字是第一个参数。其原因超出了本章介绍的范围,不过这是我们必须记住的。
+
+
-现在我们有了一个访问所有参数的方法,让我们如列表 12-2 中所示将需要的变量存放到变量中:
+你可能注意到了 vector 的第一个值是"target/debug/greprs",它是二进制我呢见的名称。其原因超出了本章介绍的范围,不过需要记住的是我们保存了所需的两个参数。
+
+### 将参数值保存进变量
+
+打印出参数 vector 中的值仅仅展示了可以访问程序中指定为命令行参数的值。但是这并不是我们想要做的,我们希望将这两个参数的值保存进变量这样就可以在程序使用这些值。让我们如列表 12-2 这样做:
+
+
+
-
+Listing 12-2: Create variables to hold the query argument and filename argument
-记住,程序名称是是第一个参数,所以并不需要`args[0]`。我们决定从第一个参数将是需要搜索的字符串,所以将第一个参数的引用放入变量`search`中。第二个参数将是文件名,将其放入变量`filename`中。再次尝试运行程序:
+正如我们在打印出 vector 时所看到的,程序的名称占据了 vector 的第一个值`args[0]`,所以我们从索引`1`开始。第一个参数`greprs`是需要搜索的字符串,所以将其将第一个参数的引用存放在变量`query`中。第二个参数将是文件名,所以将第二个参数的引用放入变量`filename`中。
+
+我们将临时打印出出这些变量的值,再一次证明代码如我们期望的那样工作。让我们使用参数`test`和`sample.txt`再次运行这个程序:
```
$ cargo run test sample.txt
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
- Running `target\debug\greprs.exe test sample.txt`
+ Running `target/debug/greprs test sample.txt`
Searching for test
In file sample.txt
```
-很棒!不过有一个问题。让我们不带参数运行:
-
-```
-$ cargo run
- Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
- Running `target\debug\greprs.exe`
-thread 'main' panicked at 'index out of bounds: the len is 1
-but the index is 1', ../src/libcollections\vec.rs:1307
-note: Run with `RUST_BACKTRACE=1` for a backtrace.
-```
-
-因为 vector 中只有一个元素,就是程序名称,不过我们尝试访问第二元素,程序 panic 并提示越界访问。虽然这个错误信息是_准确的_,不过它对程序的用户来说就没有意义了。现在就可以修复这个问题,不过我先继续学习别的内容:在程序结束前我们会改善这个情况。
\ No newline at end of file
+好的,它可以工作!我们将所需的参数值保存进了对应的变量中。之后会增加一些错误处理来应对类似用户没有提供参数的情况,不过现在我们将忽略他们并开始增加读取文件功能。
diff --git a/src/ch12-02-reading-a-file.md b/src/ch12-02-reading-a-file.md
index 860cb7b..2e10185 100644
--- a/src/ch12-02-reading-a-file.md
+++ b/src/ch12-02-reading-a-file.md
@@ -2,13 +2,13 @@
> [ch12-02-reading-a-file.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-02-reading-a-file.md)
>
-> commit 4f2dc564851dc04b271a2260c834643dfd86c724
+> commit b8e4fcbf289b82c12121b282747ce05180afb1fb
-现在有了一些包含我们需要的信息的变量了,让我们试着使用他们。下一步目标是打开需要搜索的文件。为此,我需要一个文件。在项目的根目录创建一个文件`poem.txt`,并写入一些艾米莉·狄金森(Emily Dickinson)的诗:
+接下来我们将读取由命令行文件名参数指定的文件。首先,需要一个用来测试的示例文件——用来确保`greprs`正常工作的最好的文件是拥有少量文本和多个行且有一些重复单词的文件。列表 12-3 是一首艾米莉·狄金森(Emily Dickinson)的诗,它正适合这个工作!在项目根目录创建一个文件`poem.txt`,并输入诗 "I'm nobody! Who are you?":
Filename: poem.txt
-```
+```text
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us — don't tell!
@@ -20,17 +20,21 @@ To tell your name the livelong day
To an admiring bog!
```
+
+
+
+
-创建完这个文件后,让我们编辑 *src/main.rs* 并增加如列表 12-3 所示用来打开文件的代码:
+创建完这个文件之后,修改 *src/main.rs* 并增加如列表 12-4 所示的打开文件的代码:
-
+Listing 12-4: Reading the contents of the file specified by the second argument
-这里增加了一些新内容。首先,需要更多的`use`语句来引入标准库中的相关部分:我们需要`std::fs::File`来处理文件,而`std::io::prelude::*`则包含许多对于 I/O 包括文件 I/O 有帮助的 trait。类似于 Rust 有一个通用的 prelude 来自动引入特定内容,`std::io`也有其自己的 prelude 来引入处理 I/O 时需要的内容。不同于默认的 prelude,必须显式`use`位于`std::io`中的 prelude。
+首先,增加了更多的`use`语句来引入标准库中的相关部分:需要`std::fs::File`来处理文件,而`std::io::prelude::*`则包含许多对于 I/O 包括文件 I/O 有帮助的 trait。类似于 Rust 有一个通用的 prelude 来自动引入特定内容,`std::io`也有其自己的 prelude 来引入处理 I/O 时所需的通用内容。不同于默认的 prelude,必须显式`use`位于`std::io`中的 prelude。
-在`main`中,我们增加了三点内容:第一,我们获取了文件的句柄并使用`File::open`函数与第二个参数中指定的文件名来打开这个文件。第二,我们在变量`contents`中创建了一个空的可变的`String`,接着对文件句柄调用`read_to_string`并以`contents`字符串作为参数,`contents`是`read_to_string`将会放置它读取到的数据地方。最后,我们打印出了整个文件的内容,这是一个确认目前为止的程序能够工作的方法。
+在`main`中,我们增加了三点内容:第一,通过传递变量`filename`的值调用`File::open`函数的值来获取文件的可变句柄。创建了叫做`contents`的变量并将其设置为一个可变的,空的`String`。它将会存放之后读取的文件的内容。第三,对文件句柄调用`read_to_string`并传递`contents`的可变引用作为参数。
-尝试运行这些代码,随意指定第一个参数(因为还未实现搜索功能的部分)而将 *poem.txt* 文件将作为第二个参数:
+在这些代码之后,我们再次增加了临时的`println!`打印出读取文件后`contents`的值,这样就可以检查目前为止的程序能否工作。
+
+尝试运行这些代码,随意指定一个字符串作为第一个命令行参数(因为还未实现搜索功能的部分)而将 *poem.txt* 文件将作为第二个参数:
```
$ cargo run the poem.txt
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
- Running `target\debug\greprs.exe the poem.txt`
+ Running `target/debug/greprs the poem.txt`
Searching for the
In file poem.txt
With text:
@@ -86,4 +87,4 @@ To tell your name the livelong day
To an admiring bog!
```
-好的!我们的代码可以工作了!然而,它还有一些瑕疵。因为程序还很小,这些瑕疵并不是什么大问题,不过随着程序功能的丰富,将会越来越难以用简单的方法修复他们。现在就让我们开始重构而不是等待之后处理。重构在只有少量代码时会显得容易得多。
\ No newline at end of file
+好的!代码读取并打印出了文件的内容。虽然它还有一些瑕疵:`main`函数有着多个功能,同时也没有处理可能出现的错误。虽然我们的程序还很小,这些瑕疵并不是什么大问题。不过随着程序功能的丰富,将会越来越难以用简单的方法修复他们。在开发程序时,及早开始重构是一个最佳实践,因为重构少量代码时要容易的多,所以让我们现在就开始吧。
diff --git a/src/ch12-03-improving-error-handling-and-modularity.md b/src/ch12-03-improving-error-handling-and-modularity.md
index 2d65ec4..6e56835 100644
--- a/src/ch12-03-improving-error-handling-and-modularity.md
+++ b/src/ch12-03-improving-error-handling-and-modularity.md
@@ -1,185 +1,186 @@
-## 读取文件
+## 重构改进模块性和错误处理
> [ch12-03-improving-error-handling-and-modularity.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-03-improving-error-handling-and-modularity.md)
>
-> commit 4f2dc564851dc04b271a2260c834643dfd86c724
+> commit b8e4fcbf289b82c12121b282747ce05180afb1fb
-为了完善我们程序有四个问题需要修复,而他们都与潜在的错误和程序结构有关。第一个问题是在哪打开文件:我们使用了`expect`来在打开文件失败时指定一个错误信息,不过这个错误信息只是说“文件不存在”。还有很多打开文件失败的方式,不过我们总是假设是由于缺少文件导致的。例如,文件存在但是没有打开它的权限:这时,我们就打印出了错误不符合事实的错误信息!
+为了改善我们的程序这里有四个问题需要修复,而且他们都与程序的组织方式和如何处理潜在错误有关。
-第二,我们不停的使用`expect`,这就有点类似我们之前在不传递任何命令行参数时索引会`panic!`时注意到的问题:这虽然时_可以工作_的,不过这有点没有原则性,而且整个程序中都需要他们,将错误处理都置于一处则会显得好很多。
+第一,`main`现在进行了两个任务:它解析了参数并打开了文件。对于一个这样的小函数,这并不是一个大问题。然而如果`main`中的功能持续增加,`main`函数处理的单独的任务也会增加。当函数承担了更多责任,它就更难以推导,更难以测试,并且更难以在不破坏其他部分的情况下做出修改。最好能分离出功能这样每个函数就负责一个任务。
-第三个问题是`main`函数现在处理两个工作:解析参数,并打开文件。对于一个小的函数来说,这不是什么大问题。然而随着程序中的`main`函数不断增长,`main`函数中独立的任务也会越来越多。因为一个函数拥有很多职责,它将难以理解、难以测试并难以在不破坏其他部分的情况下做出修改。
+这同时也关系到第二个问题:`search`和`filename`是程序中的配置变量,而像`f`和`contents`则用来执行程序逻辑。随着`main`函数的增长,就需要引入更多的变量到作用域中,而当作用域中有更多的变量时,将更难以追踪每个变量的目的。最好能将将配置变量组织进一个结构这样就能使他们的目的更明确了。
-这也关系到我们的第四个问题:`search`和`filename`是程序中配置性的变量,而像`f`和`contents`则用来执行程序逻辑。随着`main`函数增长,将引入更多的变量到作用域中,而当作用域中有更多的变量,将更难以追踪哪个变量用于什么目的。如果能够将配置型变量组织进一个结构就能使他们的目的更明确了。
+第三个问题是如果打开文件失败我们使用`expect`来打印出错误信息,不过这个错误信息只是说`file not found`。除了缺少文件之外还有很多打开文件可能失败的方式:例如,文件可能存在,不过可能没有打开它的权限。如果我们现在就出于这种情况,打印出的`file not found`错误信息就给了用户一个不符合事实的建议!
-让我们重新组成程序来解决这些问题。
+第四,我们不停的使用`expect`来处理不同的错误,如果用户没有指定足够的参数来运行程序,他们会从 Rust 得到 "index out of bounds" 错误而这并不能明确的解释问题。如果所有的错误处理都位于一处这样将来的维护者在需要修改错误处理逻辑时就只需要咨询一处代码。将所有的错误处理都放在一处也有助于确保我们打印的错误信息对终端用户来说是有意义的。
+
+让我们通过重构项目来解决这些问题。
### 二进制项目的关注分离
-这类项目组织上的问题在很多相似类型的项目中很常见,所以 Rust 社区开发出一种关注分离的组织模式。这种模式可以用来组织任何用 Rust 构建的二进制项目,所以可以证明应该更早的开始这项重构,以为我们的项目符合这个模式。这个模式看起来像这样:
+`main`函数负责多个任务的组织问题在许多二进制项目中很常见。所以 Rust 社区开发出一个类在`main`函数开始变得庞大时进行二进制程序的关注分离的指导性过程。这些过程有如下步骤:
-1. 将程序拆分成 *main.rs* 和 *lib.rs*。
-2. 将命令行参数解析逻辑放入 *main.rs*。
-3. 将程序逻辑放入 *lib.rs*。
-4. `main`函数的工作是:
- * 解析参数
- * 设置所有配置性变量
+1. 将程序拆分成 *main.rs* 和 *lib.rs* 并将程序的逻辑放入 *lib.rs* 中。
+2. 当命令行解析逻辑比较小时,可以保留在 *main.rs* 中。
+3. 当命令行解析开始变得复杂时,也同样将其从 *main.rs* 提取到 *lib.rs*中。
+4. 经过这些过程之后保留在`main`函数中的责任是:
+ * 使用参数值调用命令行解析逻辑
+ * 设置任何其他的配置
* 调用 *lib.rs* 中的`run`函数
- * 如果`run`返回错误则处理这个错误
+ * 如果`run`返回错误,则处理这个错误
-好的!老实说这个模式好像还很复杂。这就是关注分离的所有内容:*main.rs* 负责实际的程序运行,而 *lib.rs* 处理所有真正的任务逻辑。让我们将程序重构成这种模式。首先,提取出一个目的只在于解析参数的函数。列表 12-4 中展示了一个新的开始,`main`函数调用了一个新函数`parse_config`,它仍然定义于 *src/main.rs* 中:
+这个模式的一切就是为了关注分离:*main.rs* 处理程序运行,而 *lib.rs* 处理所有的真正的任务逻辑。因为不能直接测试`main`函数,这个结构通过将所有的程序逻辑移动到 *lib.rs* 的函数中使得我们可以测试他们。仅仅保留在 *main.rs* 中的代码将足够小以便阅读就可以验证其正确性。
+
+
+
+
+
+### 提取参数解析器
+
+首先,我们将提取解析参数的功能。列表 12-5 中展示了新`main`函数的开头,它调用了新函数`parse_config`。目前它仍将定义在 *src/main.rs* 中:
-
+
-这看起来好像有点复杂,不过我们将一点一点的开展重构。在做出这些改变之后,再次运行程序并验证参数解析是否仍然正常。经常验证你的进展是一个好习惯,这样在遇到问题时就能更好地理解什么修改造成了错误。
+我们仍然将命令行参数收集进一个 vector,不过不同于在`main`函数中将索引 1 的参数值赋值给变量`query`和将索引 2 的值赋值给变量`filename`,我们将整个 vector 传递给`parse_config`函数。接着`parse_config`函数将包含知道哪个参数该放入哪个变量的逻辑,并将这些值返回到`main`。仍然在`main`中创建变量`query`和`filename`,不过`main`不再负责处理命令行参数与变量如何对应。
+
+这对我们这小程序可能有点大材小用,不过我们将采用小的、增量的步骤进行重构。在做出这些改变之后,再次运行程序并验证参数解析是否仍然正常。经常验证你的进展是一个好习惯,这样在遇到问题时能帮助你定位问题的成因。
### 组合配置值
-现在我们有了一个函数了,让我们接着完善它。我们代码还能设计的更好一些:函数返回了一个元组,不过接着立刻就解构成了单独的部分。这些代码本身没有问题,不过有一个地方表明仍有改善的余地:我们调用了`parse_config`方法。函数名中的`config`部分也表明了返回的两个值应该是组合在一起的,因为他们都是某个配置值的一部分。
+我们可以采取另一个小的步骤来进一步改善这个函数。现在函数返回一个元组,不过立刻又就将元组拆成了独立的部分。这是一个我们可能没有进行正确抽象的信号。
-> 注意:一些同学将当使用符合类型更为合适的时候使用基本类型当作一种称为**基本类型偏执**(*primitive obsession*)的反模式。
+另一个表明还有改进空间的迹象是`parse_config`的`config`部分,它暗示了我们返回的两个值是相关的并都是一个配置值的一部分。目前除了将这两个值组合进元组之外并没有表达这个数据结构的意义:我们可以将这两个值放入一个结构体并给每个字段一个有意义的名字。这会让未来的维护者更容易理解不同的值如何相互关联以及他们的目的。
-让我们引入一个结构体来存放所有的配置。列表 12-5 中展示了新增的`Config`结构体定义、重构后的`parse_config`和`main`函数中的相关更新:
+
+
+
+
+> 注意:一些同学将这种当使用符合类型更为合适的时候使用基本类型的反模式称为**基本类型偏执**(*primitive obsession*)。
+
+
+
+
+列表 12-6 展示了新定义的结构体`Config`,它有字段`query`和`filename`。我们也改变了`parse_config`函数来返回一个`Config`结构体的实例,并更新`main`来使用结构体字段而不是单独的变量:
-
-
-`parse_config`的签名现在表明它返回一个`Config`值。在`parse_config`的函数体中,我们之前返回了`args`中`String`值引用的字符串 slice,不过`Config`定义为拥有两个有所有权的`String`值。因为`parse_config`的参数是一个`String`值的 slice,`Config`实例不能获取`String`值的所有权:这违反了 Rust 的借用规则,因为`main`函数中的`args`变量拥有这些`String`值并只允许`parse_config`函数借用他们。
+`parse_config`的签名现在表明它返回一个`Config`值。在`parse_config`的函数体中,之前返回了`args`中`String`值引用的字符串 slice,现在我们选择定义`Config`来使用拥有所有权的`String`值。`main`中的`args`变量是参数值的所有者并只允许`parse_config`函数借用他们,这意味着如果`Config`尝试获取`args`中值的所有权将违反 Rust 的借用规则。
-还有许多不同的方式可以处理`String`的数据;现在我们使用简单但低效率的方式,在字符串 slice 上调用`clone`方法。`clone`调用会生成一个字符串数据的完整拷贝,而且`Config`实例可以拥有它,不过这会消耗更多时间和内存来储存拷贝字符串数据的引用,不过拷贝数据让我们使我们的代码显得更加直白。
+还有许多不同的方式可以处理`String`的数据,而最简单但有些不太高效的方式是调用这些值的`clone`方法。这会生成`Config`实例可以拥有的数据的完整拷贝,不过会比储存字符串数据的引用消耗更多的时间和内存。不过拷贝数据使得代码显得更加直白因为无需管理引用的生命周期,所以在这种情况下牺牲一小部分性能来换取简洁性的取舍是值得的。
> #### 使用`clone`权衡取舍
>
-> 由于其运行时消耗,许多 Rustacean 之间有一个趋势是倾向于不使用`clone`来解决所有权问题。在关于迭代器的第XX章中,我们将会学习如何更有效率的处理这种情况。现在,为了编写我们的程序拷贝一些字符串是没有问题。我们只进行了一次拷贝,而且文件名和要搜索的字符串都比较短。随着你对 Rust 更加熟练,将更轻松的省略这个权衡的步骤,不过现在调用`clone`是完全可以接受的。
+> 由于其运行时消耗,许多 Rustacean 之间有一个趋势是倾向于避免使用`clone`来解决所有权问题。在关于迭代器的第十三章中,我们将会学习如何更有效率的处理这种情况,不过现在,复制一些字符串来取得进展是没有问题的,因为只会进行一次这样的拷贝,而且文件名和要搜索的字符串都比较短。在第一轮编写时拥有一个可以工作但有点低效的程序要比尝试过度优化代码更好一些。随着你对 Rust 更加熟练,将能更轻松的直奔合适的方法,不过现在调用`clone`是完全可以接受的。
-`main`函数更新为将`parse_config`返回的`Config`实例放入变量`config`中,并将分别使用`search`和`filename`变量的代码更新为使用`Config`结构体的字段。
+我们更新`main`将`parse_config`返回的`Config`实例放入变量`config`中,并更新之前分别使用`search`和`filename`变量的代码为现在的使用`Config`结构体的字段。
+
+现在代码更明确的表现了我们的意图,`query`和`filename`是相关联的并且他们的目的是配置程序如何工作的。任何使用这些值的代码就知道在`config`实例中对应目的的字段名中寻找他们。
### 创建一个`Config`构造函数
-现在让我们考虑一下`parse_config`的目的:这是一个创建`Config`示例的函数。我们已经见过了一个创建实例函数的规范:像`String::new`这样的`new`函数。列表 12-6 中展示了将`parse_config`转换为一个`Config`结构体关联函数`new`的代码:
+
+
+
+目前为止,我们将负责解析命令行参数的逻辑从`main`提取到了`parse_config`函数中,这帮助我们看清值`query`和`filename`是相互关联的并应该在代码中表现这种关系。接着我们增加了`Config`结构体来命名`query`和`filename`的相关目的,并能够从`parse_config`函数中将这些值的名称作为结构体字段名称返回。
+
+所以现在`parse_config`函数的目的是创建一个`Config`实例,我们可以将`parse_config`从一个普通函数变为一个叫做`new`的与结构体关联的函数。做出这个改变使得代码更符合习惯:可以像标准库中的`String`调用`String::new`来创建一个该类型的实例那样,将`parse_config`变为一个与`Config`关联的`new`函数。列表 12-7 展示了需要做出的修改:
-
+
-我们将`parse_config`的名字改为`new`并将其移动到`impl`块中。我们也更新了`main`中的调用代码。再次尝试编译并确保程序可以运行。
+这里将`main`中调用`parse_config`的地方更新为调用`Config::new`。我们将`parse_config`的名字改为`new`并将其移动到`impl`块中,这使得`new`函数与`Config`相关联。再次尝试编译并确保它可以工作。
-### 从构造函数返回`Result`
+### 修复错误处理
-这是我们对这个方法最后的重构:还记得当 vector 含有少于三个项时访问索引 1 和 2 会 panic 并给出一个糟糕的错误信息的代码吗?让我们来修改它!列表 12-7 展示了如何在访问这些位置之前检查 slice 是否足够长,并使用一个更好的 panic 信息:
+现在我们开始修复错误处理。回忆一下之前提到过如果`args` vector 包含少于 3 个项并尝试访问 vector 中索引 1 或 索引 2 的值会造成程序 panic。尝试不带任何参数运行程序;这将看起来像这样:
+
+```
+$ cargo run
+ Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
+ Running `target/debug/greprs`
+thread 'main' panicked at 'index out of bounds: the len is 1
+but the index is 1', /stable-dist-rustc/build/src/libcollections/vec.rs:1307
+note: Run with `RUST_BACKTRACE=1` for a backtrace.
+```
+
+`index out of bounds: the len is 1 but the index is 1`是一个针对程序员的错误信息,这并不能真正帮助终端用户理解发生了什么和相反他们应该做什么。现在就让我们修复它吧。
+
+### 改善错误信息
+
+在列表 12-8 中,在`new`函数中增加了一个检查在访问索引 1 和 2 之前检查 slice 是否足够长。如果 slice 不够长,我们使用一个更好的错误信息 panic 而不是`index out of bounds`信息:
-
+
-通过在`new`中添加这额外的几行代码,再次尝试不带参数运行程序:
+这类似于列表 9-8 中的`Guess::new`函数,那里如果`value`参数超出了有效值的范围就调用`panic!`。不同于检查值的范围,这里检查`args`的长度至少是 3,而函数的剩余部分则可以假设这个条件成立的基础上运行。如果
+`args`少于 3 个项,这个条件将为真,并调用`panic!`立即终止程序。
+
+有了`new`中这几行额外的代码,再次不带任何参数运行程序并看看现在错误看起来像什么:
```
$ cargo run
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
- Running `target\debug\greprs.exe`
-thread 'main' panicked at 'not enough arguments', src\main.rs:29
+ Running `target/debug/greprs`
+thread 'main' panicked at 'not enough arguments', src/main.rs:29
note: Run with `RUST_BACKTRACE=1` for a backtrace.
```
-这样就好多了!至少有个一个符合常理的错误信息。然而,还有一堆额外的信息我们并不希望提供给用户。可以通过改变`new`的签名来完善它。现在它只返回了一个`Config`,所有没有办法表示创建`Config`失败的情况。相反,可以如列表 12-8 所示返回一个`Result`:
+这个输出就好多了,现在有了一个合理的错误信息。然而,我们还有一堆额外的信息不希望提供给用户。所以在这里使用列表 9-8 中的技术可能不是最好的;无论如何`panic!`调用更适合程序问题而不是使用问题,正如第九章所讲到的。相反我们可以使用那一章学习的另一个技术:返回一个可以表明成功或错误的`Result`。
+
+
+
+
+#### 从`new`中返回`Result`而不是调用`panic!`
+
+我们可以选择返回一个`Result`值,它在成功时会包含一个`Config`的实例,而在错误时会描述问题。当`Config::new`与`main`交流时,在使用`Result`类型存在问题时可以使用 Rust 的信号方式。接着修改`main`将`Err`成员转换为对用户更友好的错误,而不是`panic!`调用产生的关于`thread 'main'`和`RUST_BACKTRACE`的文本。
+
+列表 12-9 展示了`Config::new`返回值和函数体中返回`Result`所需的改变:
-
+
-现在`new`函数返回一个`Result`,在成功时带有一个`Config`实例而在出现错误时带有一个`&'static str`。回忆一下第十章“静态声明周期”中讲到`&'static str`是一个字符串字面值,他也是现在我们的错误信息。
+
+
+
+现在`new`函数返回一个`Result`,在成功时带有一个`Config`实例而在出现错误时带有一个`&'static str`。回忆一下第十章“静态声明周期”中讲到`&'static str`是一个字符串字面值,也是目前的错误信息。
`new`函数体中有两处修改:当没有足够参数时不再调用`panic!`,而是返回`Err`值。同时我们将`Config`返回值包装进`Ok`成员中。这些修改使得函数符合其新的类型签名。
-### `Config::new`调用和错误处理
+通过让`Config::new`返回一个`Err`值,这就允许`main`函数处理`new`函数返回的`Result`值并在出现错误的情况更明确的结束进程。
-现在我们需要对`main`做一些修改,如列表 12-9 所示:
+### `Config::new`调用并处理错误
+
+为了处理错误情况并打印一个对用户友好的信息,我们需要像列表 12-10 那样更新`main`函数来处理现在`Config::new`返回的`Result`。另外还需要实现一些`panic!`替我们处理的问题:使用错误码 1 退出命令行工具。非零的退出状态是一个告诉调用程序的进程我们的程序以错误状态退出的惯例信号。
-
+
-新增了一个`use`行来从标准库中导入`process`。在`main`函数中我们将处理`new`函数返回的`Result`值,并在其返回`Config::new`时以一种更加清楚的方式结束进程。
+
+
+
-这里使用了一个之前没有讲到的标准库中定义的`Result`的方法:`unwrap_or_else`。当`Result`是`Ok`时其行为类似于`unwrap`:它返回`Ok`内部封装的值。与`unwrap`不同的是,当`Result`是`Err`时,它调用一个**闭包**(*closure*),也就是一个我们定义的作为参数传递给`unwrap_or_else`的匿名函数。第XX章会更详细的介绍闭包;这里需要理解的重要部分是`unwrap_or_else`会将`Err`的内部值传递给闭包中位于两道竖线间的参数`err`。使用`unwrap_or_else`允许我们进行一些自定义的非`panic!`的错误处理。
+在上面的列表中,使用了一个之前没有涉及到的方法:`unwrap_or_else`,它定义于标准库的`Result`上。使用`unwrap_or_else`可以进行一些自定义的非`panic!`的错误处理。当`Result`是`Ok`时,这个方法的行为类似于`unwrap`:它返回`Ok`内部封装的值。然而,当`Result`是`Err`时,它调用一个**闭包**(*closure*),也就是一个我们定义的作为参数传递给`unwrap_or_else`的匿名函数。第十三章会更详细的介绍闭包。现在你需要理解的是`unwrap_or_else`会将`Err`的内部值,也就是列表 12-9 中增加的`not enough arguments`静态字符串的情况,传递给闭包中位于两道竖线间的参数`err`。闭包中的代码在其运行时可以使用这个`err`值。
-上述的错误处理其实只有两行:我们打印出了错误,接着调用了`std::process::exit`。这个函数立刻停止程序的执行并将传递给它的数组作为返回码。依照惯例,零代表成功而任何其他数字表示失败。就结果来说这依然类似于列表 12-7 中的基于`panic!`的错误处理,但是不再会有额外的输出了,让我们试一试:
+
+
-```text
+我们新增了一个`use`行来从标准库中导入`process`。在错误的情况闭包中将被运行的代码只有两行:我们打印出了`err`值,接着调用了`std::process::exit`(在开头增加了新的`use`行从标准库中导入了`process`)。`process::exit`会立即停止程序并将传递给它的数字作为返回状态码。这类似于列表 12-8 中使用的基于`panic!`的错误处理,除了不会在得到所有的额外输出了。让我们试试:
+
+```
$ cargo run
Compiling greprs v0.1.0 (file:///projects/greprs)
Finished debug [unoptimized + debuginfo] target(s) in 0.48 secs
- Running `target\debug\greprs.exe`
+ Running `target/debug/greprs`
Problem parsing arguments: not enough arguments
```
-非常好!现在输出就友好多了。
+非常好!现在输出对于用户来说就友好多了。
-### `run`函数中的错误处理
+### 提取`run`函数
-现在重构完了参数解析部分,让我们再改进一下程序的逻辑。列表 12-10 中展示了在`main`函数中调用提取出函数`run`之后的代码。`run`函数包含之前位于`main`中的部分代码:
+现在我们完成了配置解析的重构:让我们转向程序的逻辑。正如“二进制项目的关注分离”部分的讨论所留下的过程,我们将提取一个叫做`run`的函数来存放目前`main`函数中不属于设置配置或处理错误的所有逻辑。一旦完成这些,`main`函数将简明的足以通过观察来验证,而我们将能够为所有其他逻辑编写测试。
+
+
+
+
+列表 12-11 展示了提取出来的`run`函数。目前我们只进行小的增量式的提取函数的改进并仍将在 *src/main.rs* 中定义这个函数:
-
+`。这让我们进一步以一种对用户友好的方式统一`main`中的错误处理。列表 12-12 展示了`run`签名和函数体中的变化:
-
+>`类型的。之前,函数返回 unit 类型`()`,现在它仍然是`Ok`时的返回值。对于错误类型,我们将使用`Box`。这是一个**trait 对象**(*trait object*),第XX章会讲到。现在可以这样理解它:`Box`意味着函数返回了某个实现了`Error` trait 的类型,不过并没有指定具体的返回值类型。这样就比较灵活,因为在不同的错误场景可能有不同类型的错误返回值。`Box`是一个堆数据的智能指针,第YY章将会详细介绍`Box`。
+这里做出了三个大的改变。第一,改变了`run`函数的返回值为`Result<(), Box>`。之前这个函数返回 unit 类型`()`,现在它仍然保持作为`Ok`时的返回值。
-第二个改变是我们去掉了`expect`调用并替换为第9章讲到的`?`。不同于遇到错误就`panic!`,这会从函数中返回错误值并让调用者来处理它。
+
+
-第三个修改是现在成功时这个函数会返回一个`Ok`值。因为`run`函数签名中声明成功类型返回值是`()`,所以需要将 unit 类型值包装进`Ok`值中。`Ok(())`一开始看起来有点奇怪,不过这样使用`()`是表明我们调用`run`只是为了它的副作用的惯用方式;它并没有返回什么有意义的值。
+对于错误类型,使用了**trait 对象**`Box`(在开头使用了`use`语句将`std::error::Error`引入作用域)。第十七章会涉及 trait 对象。目前只需知道`Box`意味着函数会返回实现了`Error` trait 的类型,不过无需指定具体将会返回的值的类型。这提供了在不同的错误场景可能有不同类型的错误返回值的灵活性。
+
+第二个改变是去掉了`expect`调用并替换为第九章讲到的`?`。不同于遇到错误就`panic!`,这会从函数中返回错误值并让调用者来处理它。
+
+第三个修改是现在成功时这个函数会返回一个`Ok`值。因为`run`函数签名中声明成功类型返回值是`()`,这意味着需要将 unit 类型值包装进`Ok`值中。`Ok(())`一开始看起来有点奇怪,不过这样使用`()`是表明我们调用`run`只是为了它的副作用的惯用方式;它并没有返回什么有意义的值。
上述代码能够编译,不过会有一个警告:
```
warning: unused result which must be used, #[warn(unused_must_use)] on by default
- --> src\main.rs:39:5
+ --> src/main.rs:39:5
|
39 | run(config);
| ^^^^^^^^^^^^
```
-Rust 尝试告诉我们忽略`Result`,它有可能是一个错误值。让我们现在来处理它。我们将采用类似于列表 12-9 中处理`Config::new`错误的技巧,不过还有少许不同:
+Rust 提示我们的代码忽略了`Result`值,它可能表明这里存在一个错误。虽然我们没有检查这里是否有一个错误,而编译器提醒我们这里应该有一些错误处理代码!现在就让我们修正他们。
+
+#### 处理`main`中`run`返回的错误
+
+我们将检查错误并使用与列表 12-10 中处理错误类似的技术来优雅的处理他们,不过有一些细微的不同:
Filename: src/main.rs
@@ -590,7 +459,7 @@ Rust 尝试告诉我们忽略`Result`,它有可能是一个错误值。让我
fn main() {
// ...snip...
- println!("Searching for {}", config.search);
+ println!("Searching for {}", config.query);
println!("In file {}", config.filename);
if let Err(e) = run(config) {
@@ -599,44 +468,36 @@ fn main() {
process::exit(1);
}
}
-
-fn run(config: Config) -> Result<(), Box> {
- let mut f = File::open(config.filename)?;
-
- let mut contents = String::new();
- f.read_to_string(&mut contents)?;
-
- println!("With text:\n{}", contents);
-
- Ok(())
-}
```
-不同于`unwrap_or_else`,我们使用`if let`来检查`run`是否返回`Err`,如果是则调用`process::exit(1)`。为什么呢?这个例子和`Config::new`的区别有些微妙。对于`Config::new`我们关心两件事:
+我们使用`if let`来检查`run`是否返回一个`Err`值,不同于`unwrap_or_else`,并在出错时调用`process::exit(1)`。`run`并不返回像`Config::new`返回的`Config`实例那样需要`unwrap`的值。因为`run`在成功时返回`()`,而我们只关心发现一个错误,所以并不需要`unwrap_or_else`来返回未封装的值,因为它只会是`()`。
-1. 检测出任何可能发生的错误
-2. 如果没有出现错误创建一个`Config`
-
-而在这个情况下,因为`run`在成功的时候返回一个`()`,唯一需要担心的就是第一件事:检测错误。如果我们使用了`unwrap_or_else`,则会得到`()`的返回值。它并没有什么用处。
-
-虽然两种情况下`if let`和`unwrap_or_else`的内容都是一样的:打印出错误并退出。
+不过两个例子中`if let`和`unwrap_or_else`的函数体都一样:打印出错误并退出。
### 将代码拆分到库 crate
-现在项目看起来好多了!还有一件我们尚未开始的工作:拆分 *src/main.rs* 并将一些代码放入 *src/lib.rs* 中。让我们现在就开始吧:将 *src/main.rs* 中的`run`函数移动到新建的 *src/lib.rs* 中。还需要移动相关的`use`语句和`Config`的定义,以及其`new`方法。现在 *src/lib.rs* 应该如列表 12-12 所示:
+现在项目看起来好多了!现在我们将要拆分 *src/main.rs* 并将一些代码放入 *src/lib.rs*,这样就能测试他们并拥有一个小的`main`函数。
+
+让我们将如下代码片段从 *src/main.rs* 移动到新文件 *src/lib.rs* 中:
+
+- `run`函数定义
+- 相关的`use`语句
+- `Config`的定义
+- `Config::new`函数定义
+
+现在 *src/lib.rs* 的内容应该看起来像列表 12-13:
-
+
-> commit 4f2dc564851dc04b271a2260c834643dfd86c724
+> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56
现在为项目的核心功能编写测试将更加容易,因为我们将逻辑提取到了 *src/lib.rs* 中并将参数解析和错误处理都留在了 *src/main.rs* 里。现在我们可以直接使用多种参数调用代码并检查返回值而不用从命令行运行二进制文件了。
我们将要编写的是一个叫做`grep`的函数,它获取要搜索的项以及文本并产生一个搜索结果列表。让我们从`run`中去掉那行`println!`(也去掉 *src/main.rs* 中的,因为再也不需要他们了),并使用之前收集的选项来调用新的`grep`函数。眼下我们只增加一个空的实现,和指定`grep`期望行为的测试。当然,这个测试对于空的实现来说是会失败的,不过可以确保代码是可以编译的并得到期望的错误信息。列表 12-14 展示了这些修改:
-
+
@@ -153,7 +147,6 @@ fn grep<'a>(search: &str, contents: &'a str) -> Vec<&'a str> {
最终,我们需要一个方法来存储包含要搜索字符串的行。为此可以在`for`循环之前创建一个可变的 vector 并调用`push`方法来存放一个`line`。在`for`循环之后,返回这个 vector。列表 12-15 中为完整的实现:
-
+
尝试运行一下:
-
```
$ cargo test
running 1 test
diff --git a/src/ch12-05-working-with-environment-variables.md b/src/ch12-05-working-with-environment-variables.md
index 4969197..5e3a84d 100644
--- a/src/ch12-05-working-with-environment-variables.md
+++ b/src/ch12-05-working-with-environment-variables.md
@@ -2,7 +2,7 @@
> [ch12-05-working-with-environment-variables.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-05-working-with-environment-variables.md)
>
-> commit 4f2dc564851dc04b271a2260c834643dfd86c724
+> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56
让我们再增加一个功能:大小写不敏感搜索。另外,这个设定将不是一个命令行参数:相反它将是一个环境变量。当然可以选择创建一个大小写不敏感的命令行参数,不过用户要求提供一个环境变量这样设置一次之后在整个终端会话中所有的搜索都将是大小写不敏感的了。
@@ -51,7 +51,6 @@ Trust me.";
我们将定义一个叫做`grep_case_insensitive`的新函数。它的实现与`grep`函数大体上相似,不过列表 12-16 展示了一些小的区别:
-
+
diff --git a/src/ch12-06-writing-to-stderr-instead-of-stdout.md b/src/ch12-06-writing-to-stderr-instead-of-stdout.md
index b61709d..c8651cb 100644
--- a/src/ch12-06-writing-to-stderr-instead-of-stdout.md
+++ b/src/ch12-06-writing-to-stderr-instead-of-stdout.md
@@ -2,7 +2,7 @@
> [ch12-06-writing-to-stderr-instead-of-stdout.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-06-writing-to-stderr-instead-of-stdout.md)
>
-> commit 4f2dc564851dc04b271a2260c834643dfd86c724
+> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56
目前为止,我们将所有的输出都`println!`到了终端。这是可以的,不过大部分终端都提供了两种输出:“标准输出”对应大部分信息,而“标准错误”则用于错误信息。这使得处理类似于“将错误打印到终端而将其他信息输出到文件”的情况变得更容易。
@@ -20,7 +20,6 @@ Problem parsing arguments: not enough arguments
我们希望这个信息被打印到屏幕上,而只有成功运行产生的输出写入到文件中。让我们如列表 12-17 中所示改变如何打印错误信息的方法:
-
+
@@ -96,4 +91,4 @@ How dreary to be somebody!
在这一章,我们涉及了如果在 Rust 中进行常规的 I/O 操作。通过使用命令行参数、文件、环境变量和写入`stderr`的功能。现在你已经准备好编写命令行程序了。结合前几章的知识,你的代码将会是组织良好的,并能有效的将数据存储到合适的数据结构中、更好的处理错误,并且还是经过良好测试的。我们也接触了一个真实情况下需要生命周期注解来保证引用一直有效的场景。
-接下来,让我们探索如何利用一些 Rust 中受函数式编程语言影响的功能”闭包和迭代器。
\ No newline at end of file
+接下来,让我们探索如何利用一些 Rust 中受函数式编程语言影响的功能:闭包和迭代器。
\ No newline at end of file
diff --git a/src/ch14-02-publishing-to-crates-io.md b/src/ch14-02-publishing-to-crates-io.md
index f1e4b3e..17584cc 100644
--- a/src/ch14-02-publishing-to-crates-io.md
+++ b/src/ch14-02-publishing-to-crates-io.md
@@ -2,7 +2,7 @@
> [ch14-02-publishing-to-crates-io.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch14-02-publishing-to-crates-io.md)
>
-> commit c49e5ee8859f8eb8f8867cbeafbdf5b802aa5894
+> commit f2eef19b3a39ee68dd363db2fcba173491ba9dc4
我们曾经在项目中增加 crates.io 上的 crate 作为依赖。也可以选择将代码分享给其他人。Crates.io 用来分发包的源代码,所以它主要用于分发开源代码。
@@ -24,10 +24,7 @@ Rust 和 Cargo 有一些帮助人们找到和使用你发布的包的功能。
/// ```
/// let five = 5;
///
-/// assert_eq!(6, add_one(5));
-/// # fn add_one(x: i32) -> i32 {
-/// # x + 1
-/// # }
+/// assert_eq!(6, add_one(five));
/// ```
pub fn add_one(x: i32) -> i32 {
x + 1
@@ -65,7 +62,7 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
第七章介绍了如何使用`mod`关键字来将代码组织进模块中,如何使用`pub`关键字将项变为公有,和如何使用`use`关键字将项引入作用域。当发布 crate 给并不熟悉其使用的库的实现的人时,就值得花时间考虑 crate 的结构对于开发和对于依赖 crate 的人来说是否同样有用。如果结构对于供其他库使用来说并不方便,也无需重新安排内部组织:可以选择使用`pub use`来重新导出一个不同的公有结构。
-例如列表 14-2中,我们创建了一个库`art`,其包含一个`kinds`模块,模块中包含枚举`Color`和包含函数`mix`的模块`utils`:
+例如列表 14-2 中,我们创建了一个库`art`,其包含一个`kinds`模块,模块中包含枚举`Color`和包含函数`mix`的模块`utils`:
Filename: src/lib.rs
diff --git a/src/ch15-02-deref.md b/src/ch15-02-deref.md
index 09d7fc4..f1fdd90 100644
--- a/src/ch15-02-deref.md
+++ b/src/ch15-02-deref.md
@@ -2,9 +2,9 @@
> [ch15-02-deref.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch15-02-deref.md)
>
-> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56
+> commit ecc3adfe0cfa0a4a15a178dc002702fd0ea74b3f
-第一个智能指针相关的重要 trait 是`Deref`,它允许我们重载`*`,解引用运算符(不同于乘法运算符和全局引用运算符)。重载智能指针的`*`方便访问其后的数据,在这个部分的稍后介绍解引用强制多态时我们会讨论方便的意义。
+第一个智能指针相关的重要 trait 是`Deref`,它允许我们重载`*`,解引用运算符(不同于乘法运算符和全局引用运算符)。重载智能指针的`*`能使访问其后的数据更为方便,在这个部分的稍后介绍解引用强制多态时我们会讨论方便的意义。
第八章的哈希 map 的“根据旧值更新一个值”部分简要的提到了解引用运算符。当时有一个可变引用,而我们希望改变这个引用所指向的值。为此,首先我们必须解引用。这是另一个使用`i32`值引用的例子:
@@ -63,21 +63,21 @@ struct that holds mp3 file data and metadata
在`assert_eq!`中,我们验证`vec![1, 2, 3]`是否为`Mp3`实例`*my_favorite_song`解引用的值,结果正是如此因为我们实现了`deref`方法来返回音频数据。如果没有为`Mp3`实现`Deref` trait,Rust 将不会编译`*my_favorite_song`:会出现错误说`Mp3`类型不能被解引用。
-代码能够工作的原因在于调用`*my_favorite_song`时`*`在背后所做的操作:
+没有`Deref` trait 的话,编译器只能解引用`&`引用,而`my_favorite_song`并不是(它是一个`Mp3`结构体)。通过`Deref` trait,编译器知道实现了`Deref` trait 的类型有一个返回引用的`deref`方法(在这个例子中,是`&self.audio`因为列表 15-7 中的`deref`的定义)。所以为了得到一个`*`可以解引用的`&`引用,编译器将`*my_favorite_song`展开为如下:
```rust,ignore
*(my_favorite_song.deref())
```
-这对`my_favorite_song`调用了`deref`方法,它借用了`my_favorite_song`并返回指向`my_favorite_song.audio`的引用,这正是列表 15-5 中`deref`所定义的。引用的`*`被定义为仅仅从引用中返回其数据,所以上面`*`的展开形式对于外部`*`来说并不是递归的。最终的数据类型是`Vec`,它与列表 15-5 中`assert_eq!`的`vec![1, 2, 3]`相匹配。
+这个就是`self.audio`中的结果值。`deref`返回一个引用并接下来必需解引用而不是直接返回值的原因是所有权:如果`deref`方法直接返回值而不是引用,其值将被移动出`self`。这里和大部分使用解引用运算符的地方并不想获取`my_favorite_song.audio`的所有权。
-`deref`方法的返回值类型仍然是引用和为何必须解引用方法的结果的原因是如果`deref`方法就返回值,使用`*`总是会获取其所有权。
+注意将`*`替换为`deref`调用和`*`调用的过程在每次使用`*`的时候都会发生一次。`*`的替换并不会无限递归进行。最终的数据类型是`Vec`,它与列表 15-7 中`assert_eq!`的`vec![1, 2, 3]`相匹配。
### 函数和方法的隐式解引用强制多态
-Rust 倾向于偏爱明确而不是隐晦,不过一个情况下这并不成立,就是函数和方法的参数的**解引用强制多态**(*deref coercions*)。解引用强制多态会自动的将指针或智能指针的引用转换为指针内容的引用。解引用强制多态发生于一个值被传递给函数或方法,并只发生于需要将传递的值类型与签名中参数类型相匹配的时候。解引用强制多态的加入使得 Rust 调用函数或方法时无需很多使用`&`和`*`的引用和解引用。
+Rust 倾向于偏爱明确而不是隐晦,不过一个情况下这并不成立,就是函数和方法的参数的**解引用强制多态**(*deref coercions*)。解引用强制多态会自动的将指针或智能指针的引用转换为指针内容的引用。解引用强制多态发生于当传递给函数的参数类型不同于函数签名中定义参数类型的时候。解引用强制多态的加入使得 Rust 调用函数或方法时无需很多使用`&`和`*`的引用和解引用。
-使用列表 15-5 中的`Mp3`结构体,如下是一个获取`u8` slice 并压缩 mp3 音频数据的函数签名:
+使用列表 15-7 中的`Mp3`结构体,如下是一个获取`u8` slice 并压缩 mp3 音频数据的函数签名:
```rust,ignore
fn compress_mp3(audio: &[u8]) -> Vec {
@@ -99,9 +99,9 @@ compress_mp3(my_favorite_song.audio.as_slice())
let result = compress_mp3(&my_favorite_song);
```
-只有`&`和实例,好的!我们可以把智能指针当成普通的引用。也就是说解引用强制多态意味着 Rust 利用了`Deref`实现的优势:Rust 知道`Mp3`实现了`Deref` trait 并从`deref`方法返回`&Vec`。它也知道标准库实现了`Vec`的`Deref` trait,其`deref`方法返回`&[T]`(我们也可以通过查阅`Vec`的 API 文档来发现这一点)。所以,在编译时,Rust 会发现它可以调用两次`Deref::deref`来将`&Mp3`变成`&Vec`再变成`&[T]`来满足`compress_mp3`的签名。这意味着我们可以少写一些代码!Rust 会多次分析`Deref::deref`的返回值类型直到它满足参数的类型,只要相关类型实现了`Deref` trait。这些间接转换在编译时进行,所以利用解引用强制多态并没有运行时惩罚。
+只有`&`和实例,好的!我们可以把智能指针当成普通的引用。也就是说解引用强制多态意味着 Rust 利用了`Deref`实现的优势:Rust 知道`Mp3`实现了`Deref` trait 并从`deref`方法返回`&Vec`。它也知道标准库实现了`Vec`的`Deref` trait,其`deref`方法返回`&[T]`(我们也可以通过查阅`Vec`的 API 文档来发现这一点)。所以,在编译时,Rust 会发现它可以调用两次`Deref::deref`来将`&Mp3`变成`&Vec`再变成`&[T]`来满足`compress_mp3`的签名。这意味着我们可以少写一些代码!Rust 会多次分析`Deref::deref`的返回值类型直到它满足参数的类型,只要相关类型实现了`Deref` trait。这些间接转换在编译时进行,所以利用解引用强制多态并没有运行时惩罚!
-这里还有一个重载了`&mut T`的`*`的`DerefMut` trait,它以与`Deref`重载`&T`的`*`相同的方式用于参数中。
+类似于如何使用`Deref` trait 重载`&T`的`*`运算符,`DerefMut` trait用于重载`&mut T`的`*`运算符。
Rust 在发现类型和 trait 实现满足三种情况时会进行解引用强制多态:
diff --git a/src/ch16-03-shared-state.md b/src/ch16-03-shared-state.md
index e69de29..5280f8d 100644
--- a/src/ch16-03-shared-state.md
+++ b/src/ch16-03-shared-state.md
@@ -0,0 +1,314 @@
+## 共享状态并发
+
+> [ch16-03-shared-state.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch16-03-shared-state.md)
+>
+> commit 9df612e93e038b05fc959db393c15a5402033f47
+
+虽然消息传递是一个很好的处理并发的方式,但并不是唯一的一个。再次考虑一下它的口号:
+
+> Do not communicate by sharing memory; instead, share memory by
+> communicating.
+>
+> 不要共享内存来通讯;而是要通讯来共享内存。
+
+那么“共享内存来通讯”看起来是怎样的呢?共享内存并发有点像多所有权:多个线程可以同时访问相同的内存位置。正如第十五章中智能指针使得多所有权成为可能时我们所看到的,这会增加额外的复杂性,因为需要以某种方式管理这些不同的所有者。
+
+但是 Rust 的类型系统和所有权可以很好的帮助我们正确的进行管理。例如,让我们看看一个共享内存中更常见的并发原语:互斥器(mutexes)。
+
+### 互斥器一次只允许一个线程访问数据
+
+**互斥器**(*mutex*)是一个用于共享内存的并发原语。它是“mutual exclusion”的缩写,也就是说,任何给定时间它只允许一个线程访问某些数据。互斥器以难以使用著称,因为你不得不记住:
+
+1. 必须记住在使用数据之前尝试获取锁。
+2. 一旦处理完被互斥器所保护的数据之后,必须记得解锁数据这样其他线程才能够获取锁。
+
+对于一个现实中的互斥器的例子,想象一下在一个会议中的专门小组讨论会上,不过只有一个麦克风。在一个小组成员可能发言之前,他们必须请求或示意他们需要使用麦克风。一旦得到了麦克风,他们可以发言任意长的时间,接着将麦克风交给系一个希望讲话的小组成员。如果小组成员在没有麦克风的时候就开始叫喊或者在其他成员发言结束之前就取得麦克风将是很无理的。如果对这个共享的麦克风的管理因为任何这些原因出现问题,讨论会将无法如期进行。
+
+正确的管理互斥器是异常复杂的,这也就是为什么这么多人都热衷于通道。然而,在 Rust 中,得益于类型系统和所有权,我们不可能会在锁和解锁上出错。
+
+### `Mutex`的 API
+
+让我们看看列表 16-12 中使用互斥器的例子,现在并不涉及到多线程:
+
+Filename: src/main.rs
+
+```rust
+use std::sync::Mutex;
+
+fn main() {
+ let m = Mutex::new(5);
+
+ {
+ let mut num = m.lock().unwrap();
+ *num = 6;
+ }
+
+ println!("m = {:?}", m);
+}
+```
+
+
+
+与很多类型一样,我们通过叫做`new`的关联函数来创建一个`Mutex`。为了访问互斥器中的数据,使用`lock`方法来获取锁。这个调用会阻塞到直到轮到我们拥有锁为止。如果另一个线程拥有锁接着那个线程 panic 了则这个调用会失败。类似于上一部分列表 16-6 那样,我们暂时使用`unwrap()`而不是更好的错误处理。请查看第九章中提供的更好的工具。
+
+一旦获取了锁,就可以将返回值(在这里是`num`)作为一个数据的可变引用使用了。类型系统是 Rust 如何保证使用值之前必须获取锁的:`Mutex`并不是一个`i32`,所以**必须**获取锁才能使用这个`i32`值。我们是不会忘记这么做的;类否则型系统是不会允许的。
+
+与你可能怀疑的一样,`Mutex`是一个智能指针。好吧,更准确的说,`lock`调用返回一个叫做`MutexGuard`的智能指针。类似我们在第十五章见过的智能指针,它实现了`Deref`来指向其内部数据。另外`MutexGuard`有一个用来释放锁的`Drop`实现。这样就不会忘记释放锁了。这在`MutexGuard`离开作用域时会自动发生,例如它发生于列表 16-12 中内部作用域的结尾。接着可以打印出互斥器的值并发现能够将其内部的`i32`改为 6。
+
+#### 在线程间共享`Mutex`
+
+现在让我们尝试使用`Mutex`在多个线程间共享值。我们将启动十个线程,并在每一个线程中对一个计数器值加一,这样计数器将从 0 变为 10。注意接下来的几个例子会有编译错误,而我们将利用这些错误来学习如何使用
+`Mutex`以及 Rust 又是怎样帮助我们正确使用它的。列表 16-13 是最开始的例子:
+
+Filename: src/main.rs
+
+```rust,ignore
+use std::sync::Mutex;
+use std::thread;
+
+fn main() {
+ let counter = Mutex::new(0);
+ let mut handles = vec![];
+
+ for _ in 0..10 {
+ let handle = thread::spawn(|| {
+ let mut num = counter.lock().unwrap();
+
+ *num += 1;
+ });
+ handles.push(handle);
+ }
+
+ for handle in handles {
+ handle.join().unwrap();
+ }
+
+ println!("Result: {}", *counter.lock().unwrap());
+}
+```
+
+
+
+这里创建了一个`counter`变量来存放内含`i32`的`Mutex`,类似列表 16-12 那样。接下来使用 range 创建了 10 个线程。这里使用了`thread::spawn`并对所有线程使用了相同的闭包:他们每一个都将调用`lock`方法来获取`Mutex`上的锁并对接着互斥器中的值加一。当一个线程结束执行其闭包,`num`会离开作用域并释放锁这样另一个线程就可以获取它了。
+
+在主线程中,我们像列表 16-2 那样收集了所有的 join 句柄,并接着每一个的`join`方法来确保所有线程都会结束。那时,主线程会获取锁并打印出程序的结果。
+
+之前提示过这个例子不能编译,让我们看看为什么!
+
+```
+error[E0373]: closure may outlive the current function, but it borrows
+`counter`, which is owned by the current function
+ -->
+ |
+9 | let handle = thread::spawn(|| {
+ | ^^ may outlive borrowed value `counter`
+10 | let mut num = counter.lock().unwrap();
+ | ------- `counter` is borrowed here
+ |
+help: to force the closure to take ownership of `counter` (and any other
+referenced variables), use the `move` keyword, as shown:
+ | let handle = thread::spawn(move || {
+```
+
+这类似于列表 16-5 中解决了的问题。考虑到启动了多个线程,Rust 无法知道这些线程会运行多久而`counter`是否在每一个线程尝试借用它时仍然保持有效。帮助信息提醒了我们如何解决它:可以使用`move`来给予每个线程其所有权。试试将这个修改用到闭包上:
+
+```rust,ignore
+thread::spawn(move || {
+```
+
+再次尝试编译。这会出现了一个不同的错误!
+
+```
+error[E0382]: capture of moved value: `counter`
+ -->
+ |
+9 | let handle = thread::spawn(move || {
+ | ------- value moved (into closure) here
+10 | let mut num = counter.lock().unwrap();
+ | ^^^^^^^ value captured here after move
+ |
+ = note: move occurs because `counter` has type `std::sync::Mutex`,
+ which does not implement the `Copy` trait
+
+error[E0382]: use of moved value: `counter`
+ -->
+ |
+9 | let handle = thread::spawn(move || {
+ | ------- value moved (into closure) here
+...
+21 | println!("Result: {}", *counter.lock().unwrap());
+ | ^^^^^^^ value used here after move
+ |
+ = note: move occurs because `counter` has type `std::sync::Mutex`,
+ which does not implement the `Copy` trait
+
+error: aborting due to 2 previous errors
+```
+
+`move`并没有像列表 16-5 中那样解决这个程序中的问题。为什么没有呢?这个错误信息有些难以理解,因为它表明`counter`被移动进了闭包,接着它在调用`lock`时被捕获。这听起来像是我们希望的,不过这是不允许的。
+
+让我们推理一下。现在不再使用`for`循环创建 10 个线程,让我们不用循环而只创建两个线程来看看会发生什么。将列表 16-13 中第一个`for`循环替换为如下代码:
+
+```rust,ignore
+let handle = thread::spawn(move || {
+ let mut num = counter.lock().unwrap();
+
+ *num += 1;
+});
+handles.push(handle);
+
+let handle2 = thread::spawn(move || {
+ let mut num2 = counter.lock().unwrap();
+
+ *num2 += 1;
+});
+handles.push(handle2);
+```
+
+这里创建了两个线程,并将用于第二个线程的变量名改为`handle2`和`num2`。现在我们简化了例子来看看是否能够理解错误信息。这一次编译给出如下信息:
+
+```text
+error[E0382]: capture of moved value: `counter`
+ -->
+ |
+8 | let handle = thread::spawn(move || {
+ | ------- value moved (into closure) here
+...
+16 | let mut num2 = counter.lock().unwrap();
+ | ^^^^^^^ value captured here after move
+ |
+ = note: move occurs because `counter` has type `std::sync::Mutex`,
+ which does not implement the `Copy` trait
+
+error[E0382]: use of moved value: `counter`
+ -->
+ |
+8 | let handle = thread::spawn(move || {
+ | ------- value moved (into closure) here
+...
+26 | println!("Result: {}", *counter.lock().unwrap());
+ | ^^^^^^^ value used here after move
+ |
+ = note: move occurs because `counter` has type `std::sync::Mutex`,
+ which does not implement the `Copy` trait
+
+error: aborting due to 2 previous errors
+```
+
+啊哈!在第一个错误信息中,Rust 表明了`counter`被移动进了`handle`所代表线程的闭包中。这个移动阻止我们在对其调用`lock`并将结果储存在`num2`中时捕获`counter`,这是已经在第二个线程中了!所以 Rust 告诉我们不能将`counter`的所有权移动到多个线程中。这在之前很难看出是因为我们在循环中创建多个线程,而 Rust 无法在循环的迭代中指明不同的线程(没有临时变量)。
+
+#### 多线程和多所有权
+
+在第十五章中,我们可以通过使用智能指针`Rc`来创建引用计数的值来拥有多所有权。同时第十五章提到了`Rc`只能用于单线程上下文,不过还是让我们在这里试用`Rc`来观察会发生什么。列表 16-14 将`Mutex`封装进了`Rc`中,并在移动到线程中之前克隆了`Rc`。切换回循环来创建线程,并保留闭包中的`move`关键字:
+
+Filename: src/main.rs
+
+```rust,ignore
+use std::rc::Rc;
+use std::sync::Mutex;
+use std::thread;
+
+fn main() {
+ let counter = Rc::new(Mutex::new(0));
+ let mut handles = vec![];
+
+ for _ in 0..10 {
+ let counter = counter.clone();
+ let handle = thread::spawn(move || {
+ let mut num = counter.lock().unwrap();
+
+ *num += 1;
+ });
+ handles.push(handle);
+ }
+
+ for handle in handles {
+ handle.join().unwrap();
+ }
+
+ println!("Result: {}", *counter.lock().unwrap());
+}
+```
+
+
+
+又一次,编译并...出现了不同的错误!编译器真是教会了我们很多东西!
+
+```
+error[E0277]: the trait bound `std::rc::Rc>:
+std::marker::Send` is not satisfied
+ -->
+ |
+11 | let handle = thread::spawn(move || {
+ | ^^^^^^^^^^^^^ the trait `std::marker::Send` is not
+ implemented for `std::rc::Rc>`
+ |
+ = note: `std::rc::Rc>` cannot be sent between threads
+ safely
+ = note: required because it appears within the type
+ `[closure@src/main.rs:11:36: 15:10
+ counter:std::rc::Rc>]`
+ = note: required by `std::thread::spawn`
+```
+
+哇哦,太长不看!需要指出一些重要的部分:第一个提示表明`Rc>`不能安全的在线程间传递。理由也在错误信息中,经过提取之后,表明“不满足`Send` trait bound”(`the trait bound Send is not satisfied`)。下一部分将会讨论`Send`,它是一个确保确保用于线程的类型是适合并发环境的 trait。
+
+不幸的是,`Rc`并不能安全的在线程间共享。当`Rc`管理引用计数时,它必须在每一个`clone`调用时增加计数并在每一个克隆被丢弃时减少计数。`Rc`并没有使用任何并发原语来确保改变计数的操作不会被其他线程打断。在计数出错时这可能会导致诡异的 bug,比如可能会造成内存泄漏或在使用结束之前就丢弃一个值。那么如果有一个正好与`Rc`类似,不过以一种线程安全的方式改变引用计数的类型会怎么样呢?
+
+#### 原子引用计数`Arc`
+
+如果你思考过像之前那样的问题的话,你就是正确的。确实有一个类似`Rc`并可以安全的用于并发环境的类型:`Arc`。字母“a”代表**原子性**(*atomic*),所以这是一个**原子引用计数**(*atomically reference counted*)类型。原子性是另一类这里还未涉及到的并发原语;请查看标准库中`std::sync::atomic`的文档来获取更多细节。其中的要点就是:原子性类型工作起来类似原始类型,不过可以安全的在线程间共享。
+
+那为什么不是所有的原始类型都是原子性的呢,然后为什么不是所有标准库中的类型都默认使用`Arc`实现呢?线程安全伴随一些性能惩罚,我们只希望在需要时才为此付出代价。如果只是在单线程中会值进行操作,因为并不需要原子性提供的保证代码可以运行的更快。
+
+回到之前的例子:`Arc`和`Rc`除了`Arc`内部的原子性之外他们是等价的。其 API 也是一样的,所以可以修改`use`行和`new`调用。列表 16-15 中的代码最终可以编译和运行:
+
+Filename: src/main.rs
+
+```rust
+use std::sync::{Mutex, Arc};
+use std::thread;
+
+fn main() {
+ let counter = Arc::new(Mutex::new(0));
+ let mut handles = vec![];
+
+ for _ in 0..10 {
+ let counter = counter.clone();
+ let handle = thread::spawn(move || {
+ let mut num = counter.lock().unwrap();
+
+ *num += 1;
+ });
+ handles.push(handle);
+ }
+
+ for handle in handles {
+ handle.join().unwrap();
+ }
+
+ println!("Result: {}", *counter.lock().unwrap());
+}
+```
+
+
+
+这会打印出:
+
+```
+Result: 10
+```
+
+成功了!我们从 0 数到了 10,这可能并不是很显眼,不过一路上我们学习了很多关于`Mutex`和线程安全的内容!这个例子中构建的结构可以用于比增加计数更为复杂的操作。可以被分解为独立部分的计算可以像这样被分散到多个线程中,并可以使用`Mutex`来允许每个线程在他们自己的部分更新最终的结果。
+
+你可能注意到了,因为`counter`是不可变的,不过可以获取其内部值的可变引用,这意味着`Mutex`提供了内部可变性,就像`Cell`系列类型那样。正如第十五章中使用`RefCell`可以改变`Rc`中的内容那样,同样的可以使用`Mutex`来改变`Arc`中的内容。
+
+回忆一下`Rc`并没有避免所有可能的问题:我们也讨论了当两个`Rc`相互引用时的引用循环的可能性,这可能造成内存泄露。`Mutex`有一个类似的 Rust 同样也不能避免的问题:死锁。**死锁**(*deadlock*)是一个场景中操作需要锁定两个资源,而两个线程分别拥有一个锁并永远相互等待的问题。如果你对这个主题感兴趣,尝试编写一个带有死锁的 Rust 程序,接着研究任何其他语言中使用互斥器的死锁规避策略并尝试在 Rust 中实现他们。标准库中`Mutex`和`MutexGuard`的 API 文档会提供拥有的信息。
+
+Rust 的类型系统和所有权确保了线程在更新共享值时拥有独占的访问权限,所以线程不会以一种不可预测的方式覆盖彼此的操作。为了和编译器一起使一切正确运行花了一些时间,不过我们节省了未来可能需要重现只在线程以特定顺序执行才会出现的诡异错误场景的时间。
+
+接下来,为了丰富本章的内容,让我们讨论一下`Send`和`Sync` trait 以及如何对自定义类型使用他们。
\ No newline at end of file
diff --git a/src/ch16-04-extensible-concurrency-sync-and-send.md b/src/ch16-04-extensible-concurrency-sync-and-send.md
index e69de29..b9f3bde 100644
--- a/src/ch16-04-extensible-concurrency-sync-and-send.md
+++ b/src/ch16-04-extensible-concurrency-sync-and-send.md
@@ -0,0 +1,41 @@
+## 使用`Sync`和`Send` trait 的可扩展并发
+
+> [ch16-04-extensible-concurrency-sync-and-send.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch16-04-extensible-concurrency-sync-and-send.md)
+>
+> commit 55b294f20fc846a13a9be623bf322d8b364cee77
+
+Rust 的并发模型中一个有趣的方面是语言本身对并发知道的**很少**。我们讨论过的几乎所有内容都是标准库的一部分,而不是语言本身的内容。因为并不需要语言提供任何用于并发上下文中的内容,并发选择也不仅限于标准库或语言所提供的:我们可以编写自己的或使用别人编写的内容。
+
+我们说了**几乎**所有内容都不在语言本身,那么位于语言本身的是什么呢?这是两个 trait,都位于`std::marker`:`Sync`和`Send`。
+
+### `Send`用于表明所有权可能被传送给其他线程
+
+`Send`标记 trait 表明类型的所有权可能被在线程间传递。几乎所有的 Rust 类型都是`Send`的,不过有一些例外。标准库中提供的一个不是`Send`的类型是`Rc`:如果克隆`Rc`值并尝试将克隆的所有权传递给另一个线程,这两个线程可能会同时更新引用计数。正如上一部分提到的,`Rc`被实现为用于单线程场景,这时不需要为拥有线程安全的引用计数而付出性能代价。
+
+因为`Rc`没有标记为`Send`,Rust 的类型系统和 trait bound 会确保我们永远也不会忘记或错误的把一个`Rc`值不安全的在线程间传递。列表 16-14 曾尝试这么做,不过得到了一个错误说`the trait Send is not implemented for Rc>`。当切换为标记为`Send`的`Arc`时,代码就可以编译了。
+
+任何完全由`Send`的类型组成的类型也会自动被标记为`Send`。几乎所有基本类型都是`Send`的,除了第十九章将会讨论的裸指针(raw pointer)之外。大部分标准库类型是`Send`的,除了`Rc`之外。
+
+### `Sync`表明多线程访问是安全的
+
+`Sync`标记 trait 表明一个类型可以安全的在多个线程中拥有其值的引用。换一种方式来说就是对于任意类型`T`,如果`&T`(`T`的引用)是`Send`的话`T`就是`Sync`的,这样其引用就可以安全的发送到另一个线程。类似于`Send`的情况,基本类型是`Sync`的,完全由`Sync`的类型组成的类型也是`Sync`的。
+
+`Rc`也不是`Sync`的,出于其不是`Send`的相同的原因。`RefCell`(第十五章讨论过)和`Cell`系列类型不是`Sync`的。`RefCell`在运行时所进行的借用检查也不是线程安全的。`Mutex`是`Sync`的,正如上一部分所讲的它可以被用来在多线程中共享访问。
+
+### 手动实现`Send`和`Sync`是不安全的
+
+通常并不需要实现`Send`和`Sync` trait,因为由是`Send`和`Sync`的类型组成的类型也自动就是`Send`和`Sync`的了。因为他们是标记 trait,甚至都不需要实现任何方法。他们只是用来加强并发相关的不可变行性的。
+
+实现这些标记 trait 涉及到实现不安全的 Rust 代码。第十九章将会讲到如何使用不安全 Rust 代码;现在,重要的是在创建新的由不是`Send`和`Sync`的部分构成的并发类型时需要多加小心,以确保维持其安全保证。[The Nomicon] 中由更多关于这些保证和如何维持他们的信息。
+
+[The Nomicon]: https://doc.rust-lang.org/stable/nomicon/vec.html
+
+## 总结
+
+这不会是本书最后一个出现并发的章节;第二十章的项目会在更现实的场景中使用这些概念,而不像本章中讨论的这些小例子。
+
+正如我们提到的,因为 Rust 本身很少有处理并发的部分内容,有很多的并发方案都由 crate 实现。他们比标准库要发展的更快;请在网上搜索当前最新的用于多线程场景的 crate。
+
+Rust 提供了用于消息传递的通道,和像`Mutex`和`Arc`这样可以安全的用于并发上下文的智能指针。类型系统和借用检查器会确保这些场景中的代码不会出现数据竞争和无效的引用。一旦代码可以编译了,我们就可以坚信这些代码可以正确的运行于多线程环境,而不会出现其他语言中经常出现的那些难以追踪的 bug。并发编程不再是什么可怕的概念:无所畏惧的使你的程序使用并发吧!
+
+接下来,让我们讨论一下当 Rust 程序变得更大时那些符合习惯的模拟问题和结构的解决方案,以及 Rust 风格如何与面向对象编程(Object Oriented Programming)中那些你所熟悉的概念相联系。
\ No newline at end of file
diff --git a/src/ch17-00-oop.md b/src/ch17-00-oop.md
new file mode 100644
index 0000000..00513c0
--- /dev/null
+++ b/src/ch17-00-oop.md
@@ -0,0 +1,7 @@
+## Rust 是一个面向对象的编程语言吗?
+
+> [ch17-00-oop.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch17-00-oop.md)
+>
+> commit 759801361bde74b47e81755fff545c66020e6e63
+
+面向对象编程是一种起源于20世纪60年代Simula的模式化编程的方式,然后在90年代在C++语言开始流行。为了描述OOP有很多种复杂的定义:在一些定义下,Rust是面向对象的;在其他定义下,Rust不是。在本章节中,我们会探索一些被普遍认为是面向对象的特性和这些特性是如何转换为Rust的方言的。
\ No newline at end of file
diff --git a/src/ch17-01-what-is-oo.md b/src/ch17-01-what-is-oo.md
new file mode 100644
index 0000000..dac9067
--- /dev/null
+++ b/src/ch17-01-what-is-oo.md
@@ -0,0 +1,99 @@
+## 什么是面向对象?
+
+> [ch17-01-what-is-oo.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch17-01-what-is-oo.md)
+>
+> commit 46334522e22d6217b392451cff8b4feca2d69d79
+
+关于一门语言是否需要是面向对象,在编程社区内并达成一致意见。Rust被很多不同的编程模式影响,我们探索了13章提到的函数式编程的特性。面向对象编程语言的一些特性往往是对象、封装和继承。我们看一下每个的含义和Rust是否支持它们。
+
+## 对象包含数据和行为
+
+`Design Patterns: Elements of Reusable Object-Oriented Software`这本书被俗称为`The Gang of Four book`,是面向对象编程模式的目录。它这样定义面向对象编程:
+
+> 面向对象的程序是由对象组成的。一个对象包数据和操作这些数据的程序。程序通常被称为方法或操作。
+
+在这个定一下,Rust是面向对象的:结构体和枚举包含数据和impl块提供了在结构体和枚举上的方法。虽然带有方法的结构体和枚举不称为对象,但是他们提供了和对象相同的功能,使用了` Gang of Four`定义的对象。
+
+
+## 隐藏了实现细节的封装
+
+通常与面向对象编程相关的另一个方面是封装的思想:对象的实现细节不能被使用对象的代码获取到。唯一与对象交互的方式是通过对象提供的public API,使用对象的代码无法深入到对象内部并直接改变数据或者行为。封装使得改变和重构对象的内部,无需改变使用对象的代码。
+
+就像我们在第7张讨论的那样,我们可以使用pub关键字来决定模块、类型函数和方法是public的(默认情况下一切都是private)。比如,我们可以定义一个结构体`AveragedCollection `包含一个`i32`类型的vector。结构体也可以有一个字段,该字段保存了vector中所有值的平均值。这样,希望知道结构体中的vector的平均值的人可以随着获取到,而无需自己计算。`AveragedCollection` 会为我们缓存平均值结果。 Listing 17-1有`AveragedCollection` 结构体的定义。
+
+Filename: src/lib.rs
+
+```
+pub struct AveragedCollection {
+ list: Vec,
+ average: f64,
+}
+```
+
+`AveragedCollection`结构体维护了一个Integer列表和集合中所有元素的平均值。
+
+
+注意,结构体本身被标记为pub,这样其他代码可以使用这个结构体,但是在结构体内部的字段仍然是private。这是非常重要的,因为我们希望保证变量被增加到列表或者被从列表删除时,也会同时更新平均值。我们通过在结构体上实现add、remove和average方法来做到这一点( Listing 17-2:):
+
+Filename: src/lib.rs
+
+
+```
+# pub struct AveragedCollection {
+# list: Vec,
+# average: f64,
+# }
+impl AveragedCollection {
+ pub fn add(&mut self, value: i32) {
+ self.list.push(value);
+ self.update_average();
+ }
+
+ pub fn remove(&mut self) -> Option {
+ let result = self.list.pop();
+ match result {
+ Some(value) => {
+ self.update_average();
+ Some(value)
+ },
+ None => None,
+ }
+ }
+
+ pub fn average(&self) -> f64 {
+ self.average
+ }
+
+ fn update_average(&mut self) {
+ let total: i32 = self.list.iter().sum();
+ self.average = total as f64 / self.list.len() as f64;
+ }
+}
+```
+
+Listing 17-2:在`AveragedCollection`结构体上实现了add、remove和average public方法
+
+public方法`add`、`remove`和`average`是修改`AveragedCollection`实例的唯一方式。当使用add方法把一个元素加入到`list`或者使用`remove`方法来删除它,这些方法的实现同时会调用私有的`update_average`方法来更新`average`成员变量。因为`list`和`average`是私有的,没有其他方式来使得外部的代码直接向`list`增加或者删除元素,直接操作`list`可能会引发`average`字段不同步。`average`方法返回`average`字段的值,这指的外部的代码只能读取`average`而不能修改它。
+
+因为我们已经封装好了`AveragedCollection`的实现细节,所以我们也可以像使用`list`一样使用的一个不同的数据结构,比如用`HashSet`代替`Vec`。只要签名`add`、`remove`和`average`公有函数保持相同,使用`AveragedCollection`的代码无需改变。如果我们暴露`List`给外部代码时,未必都是这样,因为`HashSet`和`Vec`使用不同的函数增加元素,所以如果要想直接修改`list`的话,外部的代码可能还得修改。
+
+如果封装是一个语言被认为是面向对象语言必要的方面的话,那么Rust满足要求。在代码中不同的部分使用或者不使用`pub`决定了实现细节的封装。
+
+## 作为类型系统的继承和作为代码共享的继承
+
+继承是一个很多编程语言都提供的机制,一个对象可以从另外一个对象的定义继承,这使得可以获得父对象的数据和行为,而不用重新定义。很多人定义面向对象语言时,认为继承是一个特色。
+
+如果一个语言必须有继承才能被称为面向对象的语言,那么Rust就不是面向对象的。没有办法定义一个结构体继承自另外一个结构体,从而获得父结构体的成员和方法。然而,如果你过去常常在你的编程工具箱使用继承,依赖于你要使用继承的原因,在Rust中有其他的方式。
+
+使用继承有两个主要的原因。第一个是为了重用代码:一旦一个特殊的行为从一个类型继承,继承可以在另外一个类型实现代码重用。Rust代码可以被共享通过使用默认的trait方法实现,可以在Listing 10-14看到,我们增加一个`summary`方法到`Summarizable`trait。任何继承了`Summarizable`trait的类型上会有`summary`方法,而无需任何的父代码。这类似于父类有一个继承的方法,一个从父类继承的子类也因为继承有了继承的方法。当实现`Summarizable`trait时,我们也可以选择覆写默认的`summary`方法,这类似于子类覆写了从父类继承的实现方法。
+
+第二个使用继承的原因是,使用类型系统:子类型可以在父类型被使用的地方使用。这也称为多态,意味着如果多种对象有一个相同的shape,它们可以被其他替代。
+
+>虽然很多人使用多态来描述继承,但是它实际上是一种特殊的多态,称为子类型多态。也有很多种其他形式,在Rust中带有通用的ttait绑定的一个参数
+>也是多态——更特殊的类型多态。在多种类型的多态间的细节不是关键的,所以不要过于担心细节,只需要知道Rust有多种多态相关的特色就好,不像很多其他OOP语言。
+
+为了支持这种样式,Rust有trait对象,这样我们可以指定给任何类型的值,只要值实现了一种特定的trait。
+
+继承最近在很多编程语言的设计方案中失宠了。使用继承类实现代码重用需要共享比你需要共享的代码。子类不应该经常共享它们的父类的所有特色,但是继承意味着子类得到了它的父类的数据和行为。这使得一个程序的设计不灵活,创建了无意义的子类的方法被调用的可能性或者由于方法不适用于子类但是必须从父类继承,从而触发错误。另外,很多语言只允许从一个类继承,更加限制了程序设计的灵活性。
+
+因为这些原因,Rust选择了一个另外的途径,使用trait替代继承。让我们看一下在Rust中trait对象是如何实现多态的。
-注意我们还需要使用公有的`pub`:在`Config`和其字段、它的`new`方法和`run`函数上。
+这里使用了公有的`pub`:在`Config`、其字段和其`new`方法,以及`run`函数上。现在我们有了一个拥有可以测试的公有 API 的库 crate 了。
-现在在 *src/main.rs* 中,我们需要通过`extern crate greprs`来引入现在位于 *src/lib.rs* 的代码。接着需要增加一行`use greprs::Config`来引入`Config`到作用域,并对`run`函数加上 crate 名称前缀,如列表 12-13 所示:
+#### 从二进制 crate 中调用库 crate
+
+现在需要在 *src/main.rs* 中使用`extern crate greprs`将移动到 *src/lib.rs* 的代码引入二进制 crate 的作用域。接着我们将增加一个`use greprs::Config`行将`Config`类型引入作用域,并使用库 crate 的名称作为`run`函数的前缀,如列表 12-14 所示:
-
+
-通过这些重构,所有代码应该都能运行了。运行几次`cargo run`来确保你没有破坏什么内容。好的!确实有很多的内容,不过已经为将来的成功奠定了基础。我们采用了一种更加优秀的方式来处理错误,并使得代码更模块化了一些。从现在开始几乎所有的工作都将在 *src/lib.rs* 中进行。
-让我们利用这新创建的模块的优势来进行一些在旧代码中难以开开展的工作,他们在新代码中却很简单:编写测试!
\ No newline at end of file
+
+通过这些重构,所有功能应该抖联系在一起并可以运行了。运行`cargo run`来确保一切都正确的衔接在一起。
+
+
+
+
+哇哦!这可有很多的工作,不过我们为将来成功打下了基础。现在处理错误将更容易,同时代码也更模块化。从现在开始几乎所有的工作都将在 *src/lib.rs* 中进行。
+
+让我们利用这些新创建的模块的优势来进行一些在旧代码中难以展开的工作,他们在新代码中却很简单:编写测试!
\ No newline at end of file
diff --git a/src/ch12-04-testing-the-librarys-functionality.md b/src/ch12-04-testing-the-librarys-functionality.md
index 03d9f58..0132c74 100644
--- a/src/ch12-04-testing-the-librarys-functionality.md
+++ b/src/ch12-04-testing-the-librarys-functionality.md
@@ -2,13 +2,12 @@
> [ch12-04-testing-the-librarys-functionality.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-04-testing-the-librarys-functionality.md)
>
-这里有三个大的修改。第一个是现在`run`函数的返回值是`Result<(), Box
-`run`函数的内容是之前位于`main`中的几行,而且`run`函数获取一个`Config`作为参数。现在有了一个单独的函数了,我们就可以像列表 12-8 中的`Config::new`那样进行类似的改进了。列表 12-11 展示了另一个`use`语句将`std::error::Error`结构引入了作用域,还有使`run`函数返回`Result`的修改:
+现在`run`函数包含了`main`中从读取文件开始的剩余的所有逻辑。`run`函数获取一个`Config`实例作为参数。
+
+#### 从`run`函数中返回错误
+
+通过将剩余的逻辑分离进`run`函数而不是留在`main`中,就可以像列表 12-9 中的`Config::new`那样改进错误处理。不再通过通过`expect`允许程序 panic,`run`函数将会在出错时返回一个`Result
在`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`替换为编译器创建的具体定义后的用例代码:
-
注意必须在`impl`后面声明`T`,这样就可以在`Point
其语法类似于函数定义中的泛型应用。首先,必须在结构体名称后面的尖括号中声明泛型参数的名称。接着在结构体定义中可以指定具体数据类型的位置使用泛型类型。
注意`Point`的定义中是使用了要给泛型类型,我们想要表达的是结构体`Point`对于一些类型`T`是泛型的,而且无论这个泛型是什么,字段`x`和`y`**都是**相同类型的。如果尝试创建一个有不同类型值的`Point`的实例,像列表 10-7 中的代码就不能编译:
-
-
+
尝试编译会得到如下错误:
@@ -209,7 +185,6 @@ error[E0308]: mismatched types
如果想要一个`x`和`y`可以有不同类型且仍然是泛型的`Point`结构体,我们可以使用多个泛型类型参数。在列表 10-8 中,我们修改`Point`的定义为拥有两个泛型类型`T`和`U`。其中字段`x`是`T`类型的,而字段`y`是`U`类型的:
-
+
现在所有这些`Point`实例都是被允许的了!你可以在定义中使用任意多的泛型类型参数,不过太多的话代码将难以阅读和理解。如果你处于一个需要很多泛型类型的位置,这可能是一个需要重新组织代码并分隔成一些更小部分的信号。
@@ -265,7 +235,6 @@ enum Result
这段代码获取一个整型列表,存放在变量`numbers`中。它将列表的第一项放入了变量`largest`中。接着遍历了列表中的所有数字,如果当前值大于`largest`中储存的值,将`largest`替换为这个值。如果当前值小于目前为止的最大值,`largest`保持不变。当列表中所有值都被考虑到之后,`largest`将会是最大值,在这里也就是 100。
如果需要在两个不同的列表中寻找最大值,我们可以重复列表 10-1 中的代码这样程序中就会存在两段相同逻辑的代码,如列表 10-2 所示:
-
+
虽然代码能够执行,但是重复的代码是冗余且已于出错的,并且意味着当更新逻辑时需要修改多处地方的代码。
@@ -100,7 +90,6 @@ Listing 10-2: Code to find the largest number in *two* lists of numbers
在列表 10-3 的程序中将寻找最大值的代码提取到了一个叫做`largest`的函数中。这个程序可以找出两个不同数字列表的最大值,不过列表 10-1 中的代码只存在于一个位置:
-
+
这个函数有一个参数`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)
>
首先,我们定义了一个包含`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)
>
`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)
>
如何知道`File::open`返回一个`Result`呢?我们可以查看标准库 API 文档,或者可以直接问编译器!如果给`f`某个我们知道**不是**函数返回值类型的类型注解,接着尝试编译代码,编译器会告诉我们类型不匹配。然后错误信息会告诉我们`f`的类型**应该**是什么,为此我们将`let f`语句改为:
@@ -65,7 +61,6 @@ error[E0308]: mismatched types
我们需要在列表 9-2 的代码中增加根据`File::open`返回值进行不同处理的逻辑。列表 9-3 展示了一个处理`Result`的基本工具:第六章学习过的`match`表达式。
-
+
注意与`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`增加了另一个分支:
-
+
`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 展示了一个从文件中读取用户名的函数。如果文件不存在或不能读取,这个函数会将这些错误返回给调用它的代码:
-
+
首先让我们看看函数的返回值:`Result
+
+Rust 在编译时就必须准确的知道 vector 中类型的原因是它需要知道储存每个元素到底需要多少内存。第二个好处是可以准确的知道这个 vector 中允许什么类型。如果 Rust 允许 vector 存放任意类型,那么当对 vector 元素执行操作时一个或多个类型的值就有可能会造成错误。使用枚举外加`match`意味着 Rust 能在编译时就保证总是会处理所有可能的情况,正如第六章讲到的那样。
如果在编写程序时不能确切无遗的知道运行时会储存进 vector 的所有类型,枚举技术就行不通了。相反,你可以使用 trait 对象,第十七章会讲到它。
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)
>
我们想要对`Some(3)`匹配进行操作不过不想处理任何其他`Some
#### 匹配`Some(T)`
@@ -189,7 +171,7 @@ None => None,
匹配上了!这里没有值来加一,所以程序结束并返回`=>`右侧的值`None`,因为第一个分支就匹配到了,其他的分支将不再比较。
-将`match`与枚举相结合在很多场景中都是有用的。你会在 Rust 代码中看到很多这样的模式:`match`一个枚举,绑定其中的值到一个变量,接着根据其值执行代码。这在一开有点复杂,不过一旦习惯了,你将希望所有语言都拥有它!这一直是用户的最爱。
+将`match`与枚举相结合在很多场景中都是有用的。你会在 Rust 代码中看到很多这样的模式:`match`一个枚举,绑定其中的值到一个变量,接着根据其值执行代码。这在一开有点复杂,不过一旦习惯了,你会希望所有语言都拥有它!这一直都是用户的最爱。
### 匹配是穷尽的
diff --git a/src/ch06-03-if-let.md b/src/ch06-03-if-let.md
index c7b3cb7..17ff210 100644
--- a/src/ch06-03-if-let.md
+++ b/src/ch06-03-if-let.md
@@ -1,13 +1,11 @@
## `if let`简单控制流
-> [ch06-03-if-let.md](https://github.com/rust-lang/book/blob/master/src/ch06-03-if-let.md)
+> [ch06-03-if-let.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch06-03-if-let.md)
>