From 5428b4647ffdd7cd4f55209621cda47903fab422 Mon Sep 17 00:00:00 2001 From: KaiserY Date: Wed, 22 Mar 2017 23:21:49 +0800 Subject: [PATCH] wip check ch07-01 --- README.md | 2 +- docs/ch05-00-structs.html | 2 +- docs/ch05-01-method-syntax.html | 2 +- docs/ch06-00-enums.html | 2 +- docs/ch06-01-defining-an-enum.html | 48 +++--- docs/ch06-02-match.html | 42 ++---- docs/ch06-03-if-let.html | 14 +- docs/ch07-00-modules.html | 4 +- docs/ch07-01-mod-and-the-filesystem.html | 66 ++++----- docs/print.html | 180 +++++++++++------------ src/PREFACE.md | 2 +- src/ch05-00-structs.md | 2 +- src/ch05-01-method-syntax.md | 2 +- src/ch06-00-enums.md | 2 +- src/ch06-01-defining-an-enum.md | 57 ++++--- src/ch06-02-match.md | 52 +++---- src/ch06-03-if-let.md | 17 +-- src/ch07-00-modules.md | 4 +- src/ch07-01-mod-and-the-filesystem.md | 75 +++++----- 19 files changed, 256 insertions(+), 319 deletions(-) diff --git a/README.md b/README.md index 99233d2..0564a6d 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,4 @@ 还在施工中:目前翻译到第十六章 -目前正在解决代码排版问题:已检查到第五章 \ No newline at end of file +目前正在解决代码排版问题:已检查到第六章 \ No newline at end of file diff --git a/docs/ch05-00-structs.html b/docs/ch05-00-structs.html index 308bf7c..1a24889 100644 --- a/docs/ch05-00-structs.html +++ b/docs/ch05-00-structs.html @@ -69,7 +69,7 @@

结构体

-

ch05-00-structs.md +

ch05-00-structs.md
commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

diff --git a/docs/ch05-01-method-syntax.html b/docs/ch05-01-method-syntax.html index 3c19190..85bac45 100644 --- a/docs/ch05-01-method-syntax.html +++ b/docs/ch05-01-method-syntax.html @@ -69,7 +69,7 @@

方法语法

-

ch05-01-method-syntax.md +

ch05-01-method-syntax.md
commit 8c1c1a55d5c0f9bc3c866ee79b267df9dc5c04e2

diff --git a/docs/ch06-00-enums.html b/docs/ch06-00-enums.html index 0057196..de1b336 100644 --- a/docs/ch06-00-enums.html +++ b/docs/ch06-00-enums.html @@ -69,7 +69,7 @@

枚举和模式匹配

-

ch06-00-enums.md +

ch06-00-enums.md
commit 4f2dc564851dc04b271a2260c834643dfd86c724

diff --git a/docs/ch06-01-defining-an-enum.html b/docs/ch06-01-defining-an-enum.html index bfc2c27..0e443c3 100644 --- a/docs/ch06-01-defining-an-enum.html +++ b/docs/ch06-01-defining-an-enum.html @@ -69,12 +69,12 @@

定义枚举

-

ch06-01-defining-an-enum.md +

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 地址类型,V4V6。这被称为枚举的成员variants):

enum IpAddrKind {
     V4,
@@ -111,8 +111,7 @@ fn route(ip_type: IpAddrKind) { }
 route(IpAddrKind::V4);
 route(IpAddrKind::V6);
 
-

使用枚举甚至还有更多优势。进一步考虑一下我们的 IP 地址类型,目前没有一个储存实际 IP 地址数据的方法;只知道它是什么类型的。考虑到已经在第五章学习过结构体了,你可以想如列表 6-1 那样修改这个问题:

-
+

使用枚举甚至还有更多优势。进一步考虑一下我们的 IP 地址类型,目前没有一个储存实际 IP 地址数据的方法;只知道它是什么类型的。考虑到已经在第五章学习过结构体了,你可以像列表 6-1 那样修改这个问题:

enum IpAddrKind {
     V4,
     V6,
@@ -133,11 +132,8 @@ let loopback = IpAddr {
     address: String::from("::1"),
 };
 
-
-

Listing 6-1: Storing the data and IpAddrKind variant of an IP address using a -struct

-
-
+

Listing 6-1: Storing the data and IpAddrKind variant of +an IP address using a struct

这里我们定义了一个有两个字段的结构体IpAddrkind字段是IpAddrKind(之前定义的枚举)类型的而address字段是String类型的。这里有两个结构体的实例。第一个,home,它的kind的值是IpAddrKind::V4与之相关联的地址数据是127.0.0.1。第二个实例,loopbackkind的值是IpAddrKind的另一个成员,V6,关联的地址是::1。我们使用了要给结构体来将kindaddress打包在一起,现在枚举成员就与值相关联了。

我们可以使用一种更简洁的方式来表达相同的概念,仅仅使用枚举并将数据直接放进每一个枚举成员而不是将枚举作为结构体的一部分。IpAddr枚举的新定义表明了V4V6成员都关联了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 中的另一个枚举的例子:它的成员中内嵌了多种多样的类型:

-
enum Message {
     Quit,
     Move { x: i32, y: i32 },
@@ -185,11 +180,8 @@ enum IpAddr {
     ChangeColor(i32, i32, i32),
 }
 
-
-

Listing 6-2: A Message enum whose variants each store different amounts and -types of values

-
-
+

Listing 6-2: A Message enum whose variants each store +different amounts and types of values

这个枚举有四个含有不同类型的成员:

  • 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表达式就是这么一个处理枚举的控制流结构:它会根据枚举的成员运行不同的代码,这些代码可以使用匹配到的值中的数据。

diff --git a/docs/ch06-02-match.html b/docs/ch06-02-match.html index f1502a2..0c482b1 100644 --- a/docs/ch06-02-match.html +++ b/docs/ch06-02-match.html @@ -69,14 +69,13 @@

match控制流运算符

-

ch06-02-match.md +

ch06-02-match.md
-commit 396e2db4f7de2e5e7869b1f8bc905c45c631ad7d

+commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

Rust 有一个叫做match的极为强大的控制流运算符,它允许我们将一个值与一系列的模式相比较并根据匹配的模式执行代码。模式可由字面值、变量、通配符和许多其他内容构成;第十八章会讲到所有不同种类的模式以及他们的作用。match的力量来源于模式的表现力以及编译器检查,它确保了所有可能的情况都得到处理。

-

match表达式想象成某种硬币分啦机:硬币滑入有着不同大小孔洞的轨道,每一个硬币都会掉入符合它大小的孔洞。同样地,值也会检查match的每一个模式,并且在遇到第一个“符合”的模式时,值会进入相关联的代码块并在执行中被使用。

+

match表达式想象成某种硬币分类器:硬币滑入有着不同大小孔洞的轨道,每一个硬币都会掉入符合它大小的孔洞。同样地,值也会检查match的每一个模式,并且在遇到第一个“符合”的模式时,值会进入相关联的代码块并在执行中被使用。

因为刚刚提到了硬币,让我们用他们来作为一个使用match的例子!我们可以编写一个函数来获取一个未知的(美国)硬币,并以一种类似验钞机的方式,确定它是何种硬币并返回它的美分值,如列表 6-3 中所示:

-
enum Coin {
     Penny,
     Nickel,
@@ -93,15 +92,13 @@ fn value_in_cents(coin: Coin) -> i32 {
     }
 }
 
-
-

Listing 6-3: An enum and a match expression that has the variants of the enum -as its patterns.

-
-
+

Listing 6-3: An enum and a match expression that has +the variants of the enum as its patterns.

拆开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 中完成了这些修改:

-
+

作为一个例子,让我们修改枚举的一个成员来存放数据。1999 年到 2008 年间,美帝在 25 美分的硬币的一侧为 50 个州的每一个都印刷了不同的设计。其他的硬币都没有这种区分州的设计,所以只有这些 25 美分硬币有特殊的价值。可以将这些信息加入我们的enum,通过改变Quarter成员来包含一个State值,列表 6-4 中完成了这些修改:

#[derive(Debug)] // So we can inspect the state in a minute
 enum UsState {
     Alabama,
@@ -139,13 +135,10 @@ enum Coin {
     Quarter(UsState),
 }
 
-
-

Listing 6-4: A Coin enum where the Quarter variant also holds a UsState -value

-
-
-

想象一下我们的一个朋友尝试收集所有 50 个州的 25 美分硬币。在根据硬币类型分类零钱的同时,也可以报告出每个 25 美分硬币所对应的州名称,这样如何我们的朋友没有的话,他可以把它加入收藏。

-

在这些代码的匹配表达式中,我们在匹配Coin::Quarter成员的分支的模式中增加了一个叫做state的变量。当匹配到Coin::Quarter时,变量state将会绑定 25 美分硬币所对应州的值。接着在代码那个分支中使用state,如下:

+

Listing 6-4: A Coin enum where the Quarter variant +also holds a UsState value

+

想象一下我们的一个朋友尝试收集所有 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 中这样:

-
fn plus_one(x: Option<i32>) -> Option<i32> {
     match x {
         None => None,
@@ -188,10 +180,8 @@ let five = Some(5);
 let six = plus_one(five);
 let none = plus_one(None);
 
-
-

Listing 6-5: A function that uses a match expression on an Option<i32>

-
-
+

Listing 6-5: A function that uses a match expression on +an Option<i32>

匹配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..30c0605 100644
--- a/docs/ch06-03-if-let.html
+++ b/docs/ch06-03-if-let.html
@@ -69,23 +69,19 @@
                 

if let简单控制流

-

ch06-03-if-let.md +

ch06-03-if-let.md
-commit 396e2db4f7de2e5e7869b1f8bc905c45c631ad7d

+commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

if let语法让我们以一种不那么冗长的方式结合iflet,来处理匹配一个模式的值而忽略其他的值。考虑列表 6-6 中的程序,它匹配一个Option<u8>值并只希望当值是三时执行代码:

-
let some_u8_value = Some(0u8);
 match some_u8_value {
     Some(3) => println!("three"),
     _ => (),
 }
 
-
-

Listing 6-6: A match that only cares about executing code when the value is -Some(3)

-
-
+

Listing 6-6: A match that only cares about executing +code when the value is Some(3)

我们想要对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>类型是如何帮助你利用类型系统来避免出错。当枚举值包含数据时,你可以根据需要处理多少情况来选择使用matchif let来获取并使用这些值。

你的 Rust 程序现在能够使用结构体和枚举在自己的作用域内表现其内容了。在你的 API 中使用自定义类型保证了类型安全:编译器会确保你的函数只会得到它期望的类型的值。

-

为了向你的用户提供一个组织良好的 API,它使用直观且只向用户暴露他们确实需要的部分,那么让我们转向 Rust 的模块系统吧。

+

为了向你的用户提供一个组织良好的 API,它使用起来很直观并且只向用户暴露他们确实需要的部分,那么现在就让我们转向 Rust 的模块系统吧。

diff --git a/docs/ch07-00-modules.html b/docs/ch07-00-modules.html index 00cac28..e087ded 100644 --- a/docs/ch07-00-modules.html +++ b/docs/ch07-00-modules.html @@ -69,9 +69,9 @@

模块

-

ch07-00-modules.md +

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..24d0e20 100644 --- a/docs/ch07-01-mod-and-the-filesystem.html +++ b/docs/ch07-01-mod-and-the-filesystem.html @@ -69,11 +69,11 @@

mod和文件系统

-

ch07-01-mod-and-the-filesystem.md +

ch07-01-mod-and-the-filesystem.md
-commit e2a129961ae346f726f8b342455ec2255cdfed68

+commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

-

我们将通过使用 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 中所示那样增加这个模块:

-
-Filename: src/lib.rs +

Filename: src/lib.rs

mod network {
     fn connect() {
     }
@@ -112,15 +111,11 @@ mod client {
     }
 }
 
-
-

Listing 7-1: The network module and the client module defined side-by-side -in src/lib.rs

-
-
-

现在我们有了network::connect函数和client::connect函数。他们可能有着完全不同的功能,同时他们也不会彼此冲突因为他们位于不同的模块。

+

Listing 7-1: The network module and the client module +defined side-by-side in src/lib.rs

+

现在我们有了network::connect函数和client::connect函数。他们可能有着完全不同的功能,同时他们也不会彼此冲突,因为他们位于不同的模块。

虽然在这个例子中,我们构建了一个库,但是 src/lib.rs 并没有什么特殊意义。也可以在 src/main.rs 中使用子模块。事实上,也可以将模块放入其他模块中。这有助于随着模块的增长,将相关的功能组织在一起并又保持各自独立。如何选择组织代码依赖于如何考虑代码不同部分之间的关系。例如,对于库的用户来说,client模块和它的函数connect可能放在network命名空间里显得更有道理,如列表 7-2 所示:

-
-Filename: src/lib.rs +

Filename: src/lib.rs

mod network {
     fn connect() {
     }
@@ -131,10 +126,8 @@ in src/lib.rs

} }
-
-

Listing 7-2: Moving the client module inside of the network module

-
-
+

Listing 7-2: Moving the client module inside of the +network module

src/lib.rs 文件中,将现有的mod networkmod client的定义替换为client模块作为network的一个内部模块。现在我们有了network::connectnetwork::client::connect函数:又一次,这两个connect函数也不相冲突,因为他们在不同的命名空间中。

这样,模块之间形成了一个层次结构。src/lib.rs 的内容位于最顶层,而其子模块位于较低的层次。这是列表 7-1 中的例子以这种方式考虑的组织结构:

communicator
@@ -149,8 +142,7 @@ in src/lib.rs

可以看到列表 7-2 中,clientnetwork的子模块,而不是它的同级模块。更为负责的项目可以有很多的模块,所以他们需要符合逻辑地组合在一起以便记录他们。在项目中“符合逻辑”的意义全凭你得理解和库的用户对你项目领域的认识。利用我们这里讲到的技术来创建同级模块和嵌套的模块将是你会喜欢的结构。

将模块移动到其他文件

位于层级结构中的模块,非常类似计算机领域的另一个我们非常熟悉的结构:文件系统!我们可以利用 Rust 的模块系统连同多个文件一起分解 Rust 项目,这样就不是所有的内容都落到 src/lib.rs 中了。作为例子,我们将从列表 7-3 中的代码开始:

-
-Filename: src/lib.rs +

Filename: src/lib.rs

mod client {
     fn connect() {
     }
@@ -166,24 +158,36 @@ mod network {
     }
 }
 
-
-

Listing 7-3: Three modules, client, network, and network::server, all -defined in src/lib.rs

-
-
+

Listing 7-3: Three modules, client, network, and +network::server, all defined in src/lib.rs

这是模块层次结构:

communicator
  ├── client
  └── network
      └── server
 
+

如果这些模块有很多函数,而这些函数又很长,将难以在文件中寻找我们需要的代码。因为这些函数被嵌套进一个或多个模块中,同时函数中的代码也会开始变长。这就有充分的理由将clientnetworkserver每一个模块从 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
@@ -238,7 +242,6 @@ mod server;
 }
 

当尝试运行cargo build时,会出现如列表 7-4 中所示的错误:

-
$ cargo build
    Compiling communicator v0.1.0 (file:///projects/communicator)
 error: cannot declare a new module at this location
@@ -258,14 +261,11 @@ note: ... or maybe `use` the module `server` instead of possibly redeclaring it
 4 | mod server;
   |     ^^^^^^
 
-
-

Listing 7-4: Error when trying to extract the server submodule into -src/server.rs

-
-
+

Listing 7-4: Error when trying to extract the server +submodule into src/server.rs

这个错误说明“不能在这个位置新声明一个模块”并指出 src/network.rs 中的mod server;这一行。看来 src/network.rssrc/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/print.html b/docs/print.html index 3d43f04..4705ae7 100644 --- a/docs/print.html +++ b/docs/print.html @@ -2231,7 +2231,7 @@ let slice = &a[1..3];

    所有权系统影响了 Rust 中其他很多部分如何工作,所以我们会继续讲到这些概念,贯穿本书的余下内容。让我们开始下一个章节,来看看如何将多份数据组合进一个struct中。

    结构体

    -

    ch05-00-structs.md +

    ch05-00-structs.md
    commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

    @@ -2435,7 +2435,7 @@ trait and printing the Rectangle instance using debug formatting我们的area函数是非常明确的————它只是计算了长方形的面积。如果这个行为与Rectangle结构体再结合得更紧密一些就更好了,因为这明显就是Rectangle类型的行为。现在让我们看看如何继续重构这些代码,来将area函数协调进Rectangle类型定义的area方法中。

    方法语法

    -

    ch05-01-method-syntax.md +

    ch05-01-method-syntax.md
    commit 8c1c1a55d5c0f9bc3c866ee79b267df9dc5c04e2

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

    结构体并不是创建自定义类型的唯一方法;让我们转向 Rust 的enum功能并为自己的工具箱再填一个工具。

    枚举和模式匹配

    -

    ch06-00-enums.md +

    ch06-00-enums.md
    commit 4f2dc564851dc04b271a2260c834643dfd86c724

    @@ -2569,12 +2569,12 @@ commit 4f2dc564851dc04b271a2260c834643dfd86c724

    枚举是一个很多语言都有的功能,不过不同语言中的功能各不相同。Rust 的枚举与像F#、OCaml 和 Haskell这样的函数式编程语言中的代数数据类型algebraic data types)最为相似。

    定义枚举

    -

    ch06-01-defining-an-enum.md +

    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 地址类型,V4V6。这被称为枚举的成员variants):

    enum IpAddrKind {
         V4,
    @@ -2611,8 +2611,7 @@ fn route(ip_type: IpAddrKind) { }
     route(IpAddrKind::V4);
     route(IpAddrKind::V6);
     
    -

    使用枚举甚至还有更多优势。进一步考虑一下我们的 IP 地址类型,目前没有一个储存实际 IP 地址数据的方法;只知道它是什么类型的。考虑到已经在第五章学习过结构体了,你可以想如列表 6-1 那样修改这个问题:

    -
    +

    使用枚举甚至还有更多优势。进一步考虑一下我们的 IP 地址类型,目前没有一个储存实际 IP 地址数据的方法;只知道它是什么类型的。考虑到已经在第五章学习过结构体了,你可以像列表 6-1 那样修改这个问题:

    enum IpAddrKind {
         V4,
         V6,
    @@ -2633,11 +2632,8 @@ let loopback = IpAddr {
         address: String::from("::1"),
     };
     
    -
    -

    Listing 6-1: Storing the data and IpAddrKind variant of an IP address using a -struct

    -
    -
    +

    Listing 6-1: Storing the data and IpAddrKind variant of +an IP address using a struct

    这里我们定义了一个有两个字段的结构体IpAddrkind字段是IpAddrKind(之前定义的枚举)类型的而address字段是String类型的。这里有两个结构体的实例。第一个,home,它的kind的值是IpAddrKind::V4与之相关联的地址数据是127.0.0.1。第二个实例,loopbackkind的值是IpAddrKind的另一个成员,V6,关联的地址是::1。我们使用了要给结构体来将kindaddress打包在一起,现在枚举成员就与值相关联了。

    我们可以使用一种更简洁的方式来表达相同的概念,仅仅使用枚举并将数据直接放进每一个枚举成员而不是将枚举作为结构体的一部分。IpAddr枚举的新定义表明了V4V6成员都关联了String值:

    enum IpAddr {
    @@ -2660,7 +2656,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 +2670,9 @@ enum IpAddr {
         V6(Ipv6Addr),
     }
     
    -

    这些代码展示了可以将任意类型的数据放入枚举成员中:例如字符串、数字类型或者结构体。甚至可以包含另一个枚举!另外,标准库中的类型通常并不比你可能设想出来的要复杂多少。

    +

    这些代码展示了可以将任意类型的数据放入枚举成员中:例如字符串、数字类型或者结构体。甚至可以包含另一个枚举!另外,标准库中的类型通常并不比你设想出来的要复杂多少。

    注意虽然标准库中包含一个IpAddr的定义,仍然可以创建和使用我们自己的定义而不会有冲突,因为我们并没有将标准库中的定义引入作用域。第七章会讲到如何导入类型。

    来看看列表 6-2 中的另一个枚举的例子:它的成员中内嵌了多种多样的类型:

    -
    enum Message {
         Quit,
         Move { x: i32, y: i32 },
    @@ -2685,11 +2680,8 @@ enum IpAddr {
         ChangeColor(i32, i32, i32),
     }
     
    -
    -

    Listing 6-2: A Message enum whose variants each store different amounts and -types of values

    -
    -
    +

    Listing 6-2: A Message enum whose variants each store +different amounts and types of values

    这个枚举有四个含有不同类型的成员:

    • Quit没有关联任何数据。
    • @@ -2697,7 +2689,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 +2720,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 +2766,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 +

    ch06-02-match.md
    -commit 396e2db4f7de2e5e7869b1f8bc905c45c631ad7d

    +commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

    Rust 有一个叫做match的极为强大的控制流运算符,它允许我们将一个值与一系列的模式相比较并根据匹配的模式执行代码。模式可由字面值、变量、通配符和许多其他内容构成;第十八章会讲到所有不同种类的模式以及他们的作用。match的力量来源于模式的表现力以及编译器检查,它确保了所有可能的情况都得到处理。

    -

    match表达式想象成某种硬币分啦机:硬币滑入有着不同大小孔洞的轨道,每一个硬币都会掉入符合它大小的孔洞。同样地,值也会检查match的每一个模式,并且在遇到第一个“符合”的模式时,值会进入相关联的代码块并在执行中被使用。

    +

    match表达式想象成某种硬币分类器:硬币滑入有着不同大小孔洞的轨道,每一个硬币都会掉入符合它大小的孔洞。同样地,值也会检查match的每一个模式,并且在遇到第一个“符合”的模式时,值会进入相关联的代码块并在执行中被使用。

    因为刚刚提到了硬币,让我们用他们来作为一个使用match的例子!我们可以编写一个函数来获取一个未知的(美国)硬币,并以一种类似验钞机的方式,确定它是何种硬币并返回它的美分值,如列表 6-3 中所示:

    -
    enum Coin {
         Penny,
         Nickel,
    @@ -2796,15 +2795,13 @@ fn value_in_cents(coin: Coin) -> i32 {
         }
     }
     
    -
    -

    Listing 6-3: An enum and a match expression that has the variants of the enum -as its patterns.

    -
    -
    +

    Listing 6-3: An enum and a match expression that has +the variants of the enum as its patterns.

    拆开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 +2823,7 @@ fn value_in_cents(coin: Coin) -> i32 {
     

    绑定值的模式

    匹配分支的另一个有用的功能是可以绑定匹配的模式的部分值。这也就是如何从枚举成员中提取值。

    -

    作为一个例子,让我们修改枚举的一个成员来存放数据。1999 年到 2008 年间,美帝在 25 美分的硬币的一侧为 50 个州每一个都印刷了不同的设计。其他的硬币都没有这种区分州的设计,所以只有这些 25 美分硬币有特殊的价值。可以将这些信息加入我们的enum,通过改变Quarter成员来包含一个State值,列表 6-4 中完成了这些修改:

    -
    +

    作为一个例子,让我们修改枚举的一个成员来存放数据。1999 年到 2008 年间,美帝在 25 美分的硬币的一侧为 50 个州的每一个都印刷了不同的设计。其他的硬币都没有这种区分州的设计,所以只有这些 25 美分硬币有特殊的价值。可以将这些信息加入我们的enum,通过改变Quarter成员来包含一个State值,列表 6-4 中完成了这些修改:

    #[derive(Debug)] // So we can inspect the state in a minute
     enum UsState {
         Alabama,
    @@ -2842,13 +2838,10 @@ enum Coin {
         Quarter(UsState),
     }
     
    -
    -

    Listing 6-4: A Coin enum where the Quarter variant also holds a UsState -value

    -
    -
    -

    想象一下我们的一个朋友尝试收集所有 50 个州的 25 美分硬币。在根据硬币类型分类零钱的同时,也可以报告出每个 25 美分硬币所对应的州名称,这样如何我们的朋友没有的话,他可以把它加入收藏。

    -

    在这些代码的匹配表达式中,我们在匹配Coin::Quarter成员的分支的模式中增加了一个叫做state的变量。当匹配到Coin::Quarter时,变量state将会绑定 25 美分硬币所对应州的值。接着在代码那个分支中使用state,如下:

    +

    Listing 6-4: A Coin enum where the Quarter variant +also holds a UsState value

    +

    想象一下我们的一个朋友尝试收集所有 50 个州的 25 美分硬币。在根据硬币类型分类零钱的同时,也可以报告出每个 25 美分硬币所对应的州名称,这样如果我们的朋友没有的话,他可以把它加入收藏。

    +

    在这些代码的匹配表达式中,我们在匹配Coin::Quarter成员的分支的模式中增加了一个叫做state的变量。当匹配到Coin::Quarter时,变量state将会绑定 25 美分硬币所对应州的值。接着在那个分支的代码中使用state,如下:

    # #[derive(Debug)]
     # enum UsState {
     #    Alabama,
    @@ -2877,9 +2870,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 中这样:

    -
    fn plus_one(x: Option<i32>) -> Option<i32> {
         match x {
             None => None,
    @@ -2891,10 +2883,8 @@ let five = Some(5);
     let six = plus_one(five);
     let none = plus_one(None);
     
    -
    -

    Listing 6-5: A function that uses a match expression on an Option<i32>

    -
    -
    +

    Listing 6-5: A function that uses a match expression on +an Option<i32>

    匹配Some(T)

    更仔细的检查plus_one的第一行操作。当调用plus_one(five)时,plus_one函数体中的x将会是值Some(5)。接着将其与每个分支比较。

    None => None,
    @@ -2908,7 +2898,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 +2930,19 @@ match some_u8_value {
     

    然而,match在只关心一个情况的场景中可能就有点啰嗦了。为此 Rust 提供了if let

    if let简单控制流

    -

    ch06-03-if-let.md +

    ch06-03-if-let.md
    -commit 396e2db4f7de2e5e7869b1f8bc905c45c631ad7d

    +commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

    if let语法让我们以一种不那么冗长的方式结合iflet,来处理匹配一个模式的值而忽略其他的值。考虑列表 6-6 中的程序,它匹配一个Option<u8>值并只希望当值是三时执行代码:

    -
    let some_u8_value = Some(0u8);
     match some_u8_value {
         Some(3) => println!("three"),
         _ => (),
     }
     
    -
    -

    Listing 6-6: A match that only cares about executing code when the value is -Some(3)

    -
    -
    +

    Listing 6-6: A match that only cares about executing +code when the value is Some(3)

    我们想要对Some(3)匹配进行操作不过不想处理任何其他Some<u8>值或None值。为了满足match表达式(穷尽性)的要求,必须在处理完这唯一的成员后加上_ => (),这样也要增加很多样板代码。

    不过我们可以使用if let这种更短的方式编写。如下代码与列表 6-6 中的match行为一致:

    # let some_u8_value = Some(0u8);
    @@ -3012,12 +2998,12 @@ if let Coin::Quarter(state) = coin {
     

    总结

    现在我们涉及到了如何使用枚举来创建有一系列可列举值的自定义类型。我们也展示了标准库的Option<T>类型是如何帮助你利用类型系统来避免出错。当枚举值包含数据时,你可以根据需要处理多少情况来选择使用matchif let来获取并使用这些值。

    你的 Rust 程序现在能够使用结构体和枚举在自己的作用域内表现其内容了。在你的 API 中使用自定义类型保证了类型安全:编译器会确保你的函数只会得到它期望的类型的值。

    -

    为了向你的用户提供一个组织良好的 API,它使用直观且只向用户暴露他们确实需要的部分,那么让我们转向 Rust 的模块系统吧。

    +

    为了向你的用户提供一个组织良好的 API,它使用起来很直观并且只向用户暴露他们确实需要的部分,那么现在就让我们转向 Rust 的模块系统吧。

    模块

    -

    ch07-00-modules.md +

    ch07-00-modules.md
    -commit e2a129961ae346f726f8b342455ec2255cdfed68

    +commit 4f2dc564851dc04b271a2260c834643dfd86c724

    在你刚开始编写 Rust 程序时,代码可能仅仅位于main函数里。随着代码数量的增长,最终你会将功能移动到其他函数中,为了复用也为了更好的组织。通过将代码分隔成更小的块,每一个块代码自身就更易于理解。不过当你发现自己有太多的函数了该怎么办呢?Rust 有一个模块系统来处理编写可复用代码同时保持代码组织度的问题。

    就跟你将代码行提取到一个函数中一样,也可以将函数(和其他类似结构体和枚举的代码)提取到不同模块中。模块module)是一个包含函数或类型定义的命名空间,你可以选择这些定义是能(公有)还是不能(私有)在其模块外可见。这是一个模块如何工作的概括:

    @@ -3029,11 +3015,11 @@ commit e2a129961ae346f726f8b342455ec2255cdfed68

    我们会逐一了解这每一部分并学习如何将他们结合在一起。

    mod和文件系统

    -

    ch07-01-mod-and-the-filesystem.md +

    ch07-01-mod-and-the-filesystem.md
    -commit e2a129961ae346f726f8b342455ec2255cdfed68

    +commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

    -

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

    +

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

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

    $ cargo new communicator
     $ cd communicator
    @@ -3051,7 +3037,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 +3046,7 @@ mod tests {
     

    mod关键字的后面是模块的名字,network,接着是位于大括号中的代码块。代码块中的一切都位于network命名空间中。在这个例子中,只有一个函数,connect。如果想要在network模块外面的代码中调用这个函数,需要指定模块名并使用命名空间语法::,像这样:network::connect(),而不是只是connect()

    也可以在 src/lib.rs 文件中同时存在多个模块。例如,再拥有一个client模块,它也有一个叫做connect的函数,如列表 7-1 中所示那样增加这个模块:

    -
    -Filename: src/lib.rs +

    Filename: src/lib.rs

    mod network {
         fn connect() {
         }
    @@ -3072,15 +3057,11 @@ mod client {
         }
     }
     
    -
    -

    Listing 7-1: The network module and the client module defined side-by-side -in src/lib.rs

    -
    -
    -

    现在我们有了network::connect函数和client::connect函数。他们可能有着完全不同的功能,同时他们也不会彼此冲突因为他们位于不同的模块。

    +

    Listing 7-1: The network module and the client module +defined side-by-side in src/lib.rs

    +

    现在我们有了network::connect函数和client::connect函数。他们可能有着完全不同的功能,同时他们也不会彼此冲突,因为他们位于不同的模块。

    虽然在这个例子中,我们构建了一个库,但是 src/lib.rs 并没有什么特殊意义。也可以在 src/main.rs 中使用子模块。事实上,也可以将模块放入其他模块中。这有助于随着模块的增长,将相关的功能组织在一起并又保持各自独立。如何选择组织代码依赖于如何考虑代码不同部分之间的关系。例如,对于库的用户来说,client模块和它的函数connect可能放在network命名空间里显得更有道理,如列表 7-2 所示:

    -
    -Filename: src/lib.rs +

    Filename: src/lib.rs

    mod network {
         fn connect() {
         }
    @@ -3091,10 +3072,8 @@ in src/lib.rs

    } }
    -
    -

    Listing 7-2: Moving the client module inside of the network module

    -
    -
    +

    Listing 7-2: Moving the client module inside of the +network module

    src/lib.rs 文件中,将现有的mod networkmod client的定义替换为client模块作为network的一个内部模块。现在我们有了network::connectnetwork::client::connect函数:又一次,这两个connect函数也不相冲突,因为他们在不同的命名空间中。

    这样,模块之间形成了一个层次结构。src/lib.rs 的内容位于最顶层,而其子模块位于较低的层次。这是列表 7-1 中的例子以这种方式考虑的组织结构:

    communicator
    @@ -3109,8 +3088,7 @@ in src/lib.rs

    可以看到列表 7-2 中,clientnetwork的子模块,而不是它的同级模块。更为负责的项目可以有很多的模块,所以他们需要符合逻辑地组合在一起以便记录他们。在项目中“符合逻辑”的意义全凭你得理解和库的用户对你项目领域的认识。利用我们这里讲到的技术来创建同级模块和嵌套的模块将是你会喜欢的结构。

    将模块移动到其他文件

    位于层级结构中的模块,非常类似计算机领域的另一个我们非常熟悉的结构:文件系统!我们可以利用 Rust 的模块系统连同多个文件一起分解 Rust 项目,这样就不是所有的内容都落到 src/lib.rs 中了。作为例子,我们将从列表 7-3 中的代码开始:

    -
    -Filename: src/lib.rs +

    Filename: src/lib.rs

    mod client {
         fn connect() {
         }
    @@ -3126,24 +3104,36 @@ mod network {
         }
     }
     
    -
    -

    Listing 7-3: Three modules, client, network, and network::server, all -defined in src/lib.rs

    -
    -
    +

    Listing 7-3: Three modules, client, network, and +network::server, all defined in src/lib.rs

    这是模块层次结构:

    communicator
      ├── client
      └── network
          └── server
     
    +

    如果这些模块有很多函数,而这些函数又很长,将难以在文件中寻找我们需要的代码。因为这些函数被嵌套进一个或多个模块中,同时函数中的代码也会开始变长。这就有充分的理由将clientnetworkserver每一个模块从 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
    @@ -3198,7 +3188,6 @@ mod server;
     }
     

    当尝试运行cargo build时,会出现如列表 7-4 中所示的错误:

    -
    $ cargo build
        Compiling communicator v0.1.0 (file:///projects/communicator)
     error: cannot declare a new module at this location
    @@ -3218,14 +3207,11 @@ note: ... or maybe `use` the module `server` instead of possibly redeclaring it
     4 | mod server;
       |     ^^^^^^
     
    -
    -

    Listing 7-4: Error when trying to extract the server submodule into -src/server.rs

    -
    -
    +

    Listing 7-4: Error when trying to extract the server +submodule into src/server.rs

    这个错误说明“不能在这个位置新声明一个模块”并指出 src/network.rs 中的mod server;这一行。看来 src/network.rssrc/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/src/PREFACE.md b/src/PREFACE.md index 99233d2..0564a6d 100644 --- a/src/PREFACE.md +++ b/src/PREFACE.md @@ -2,4 +2,4 @@ 还在施工中:目前翻译到第十六章 -目前正在解决代码排版问题:已检查到第五章 \ No newline at end of file +目前正在解决代码排版问题:已检查到第六章 \ No newline at end of file diff --git a/src/ch05-00-structs.md b/src/ch05-00-structs.md index 407acef..2475a4d 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 150b2cb..24648f7 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 18fd7d0..569f16a 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 那样修改这个问题: - -
      +使用枚举甚至还有更多优势。进一步考虑一下我们的 IP 地址类型,目前没有一个储存实际 IP 地址**数据**的方法;只知道它是什么**类型**的。考虑到已经在第五章学习过结构体了,你可以像列表 6-1 那样修改这个问题: ```rust enum IpAddrKind { @@ -84,13 +82,8 @@ let loopback = IpAddr { }; ``` -
      - -Listing 6-1: Storing the data and `IpAddrKind` variant of an IP address using a -`struct` - -
      -
      +Listing 6-1: Storing the data and `IpAddrKind` variant of +an IP address using a `struct` 这里我们定义了一个有两个字段的结构体`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 中的另一个枚举的例子:它的成员中内嵌了多种多样的类型: -
      - ```rust enum Message { Quit, @@ -158,13 +149,8 @@ enum Message { } ``` -
      - -Listing 6-2: A `Message` enum whose variants each store different amounts and -types of values - -
      -
      +Listing 6-2: A `Message` enum whose variants each store +different amounts and types of values 这个枚举有四个含有不同类型的成员: @@ -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 d739cbd..e9ea8d8 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 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56 Rust 有一个叫做`match`的极为强大的控制流运算符,它允许我们将一个值与一系列的模式相比较并根据匹配的模式执行代码。模式可由字面值、变量、通配符和许多其他内容构成;第十八章会讲到所有不同种类的模式以及他们的作用。`match`的力量来源于模式的表现力以及编译器检查,它确保了所有可能的情况都得到处理。 -把`match`表达式想象成某种硬币分啦机:硬币滑入有着不同大小孔洞的轨道,每一个硬币都会掉入符合它大小的孔洞。同样地,值也会检查`match`的每一个模式,并且在遇到第一个“符合”的模式时,值会进入相关联的代码块并在执行中被使用。 +把`match`表达式想象成某种硬币分类器:硬币滑入有着不同大小孔洞的轨道,每一个硬币都会掉入符合它大小的孔洞。同样地,值也会检查`match`的每一个模式,并且在遇到第一个“符合”的模式时,值会进入相关联的代码块并在执行中被使用。 因为刚刚提到了硬币,让我们用他们来作为一个使用`match`的例子!我们可以编写一个函数来获取一个未知的(美国)硬币,并以一种类似验钞机的方式,确定它是何种硬币并返回它的美分值,如列表 6-3 中所示: -
      - ```rust enum Coin { Penny, @@ -30,21 +28,18 @@ fn value_in_cents(coin: Coin) -> i32 { } ``` -
      - -Listing 6-3: An enum and a `match` expression that has the variants of the enum -as its patterns. - -
      -
      +Listing 6-3: An enum and a `match` expression that has +the variants of the enum as its patterns. 拆开`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 中完成了这些修改: - -
      +作为一个例子,让我们修改枚举的一个成员来存放数据。1999 年到 2008 年间,美帝在 25 美分的硬币的一侧为 50 个州的每一个都印刷了不同的设计。其他的硬币都没有这种区分州的设计,所以只有这些 25 美分硬币有特殊的价值。可以将这些信息加入我们的`enum`,通过改变`Quarter`成员来包含一个`State`值,列表 6-4 中完成了这些修改: ```rust #[derive(Debug)] // So we can inspect the state in a minute @@ -91,17 +84,12 @@ enum Coin { } ``` -
      +Listing 6-4: A `Coin` enum where the `Quarter` variant +also holds a `UsState` value -Listing 6-4: A `Coin` enum where the `Quarter` variant also holds a `UsState` -value +想象一下我们的一个朋友尝试收集所有 50 个州的 25 美分硬币。在根据硬币类型分类零钱的同时,也可以报告出每个 25 美分硬币所对应的州名称,这样如果我们的朋友没有的话,他可以把它加入收藏。 -
      -
      - -想象一下我们的一个朋友尝试收集所有 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 中这样: -
      - ```rust fn plus_one(x: Option) -> Option { match x { @@ -155,12 +141,8 @@ let six = plus_one(five); let none = plus_one(None); ``` -
      - -Listing 6-5: A function that uses a `match` expression on an `Option` - -
      -
      +Listing 6-5: A function that uses a `match` expression on +an `Option` #### 匹配`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) >
      -> commit 396e2db4f7de2e5e7869b1f8bc905c45c631ad7d +> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56 `if let`语法让我们以一种不那么冗长的方式结合`if`和`let`,来处理匹配一个模式的值而忽略其他的值。考虑列表 6-6 中的程序,它匹配一个`Option`值并只希望当值是三时执行代码: -
      - ```rust let some_u8_value = Some(0u8); match some_u8_value { @@ -16,13 +14,8 @@ match some_u8_value { } ``` -
      - -Listing 6-6: A `match` that only cares about executing code when the value is -`Some(3)` - -
      -
      +Listing 6-6: A `match` that only cares about executing +code when the value is `Some(3)` 我们想要对`Some(3)`匹配进行操作不过不想处理任何其他`Some`值或`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..47b3491 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 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56 -我们将通过使用 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 中所示那样增加这个模块: - -
      Filename: src/lib.rs ```rust @@ -65,19 +63,13 @@ mod client { } ``` -
      +Listing 7-1: The `network` module and the `client` module +defined side-by-side in *src/lib.rs* -Listing 7-1: The `network` module and the `client` module defined side-by-side -in *src/lib.rs* - -
      -
      - -现在我们有了`network::connect`函数和`client::connect`函数。他们可能有着完全不同的功能,同时他们也不会彼此冲突因为他们位于不同的模块。 +现在我们有了`network::connect`函数和`client::connect`函数。他们可能有着完全不同的功能,同时他们也不会彼此冲突,因为他们位于不同的模块。 虽然在这个例子中,我们构建了一个库,但是 *src/lib.rs* 并没有什么特殊意义。也可以在 *src/main.rs* 中使用子模块。事实上,也可以将模块放入其他模块中。这有助于随着模块的增长,将相关的功能组织在一起并又保持各自独立。如何选择组织代码依赖于如何考虑代码不同部分之间的关系。例如,对于库的用户来说,`client`模块和它的函数`connect`可能放在`network`命名空间里显得更有道理,如列表 7-2 所示: -
      Filename: src/lib.rs ```rust @@ -92,12 +84,8 @@ mod network { } ``` -
      - -Listing 7-2: Moving the `client` module inside of the `network` module - -
      -
      +Listing 7-2: Moving the `client` module inside of the +`network` module 在 *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 中的代码开始: -
      Filename: src/lib.rs ```rust @@ -143,13 +130,8 @@ mod network { } ``` -
      - -Listing 7-3: Three modules, `client`, `network`, and `network::server`, all -defined in *src/lib.rs* - -
      -
      +Listing 7-3: Three modules, `client`, `network`, and +`network::server`, all defined in *src/lib.rs* 这是模块层次结构: @@ -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*。 @@ -250,8 +252,6 @@ fn connect() { 当尝试运行`cargo build`时,会出现如列表 7-4 中所示的错误: -
      - ```text $ cargo build Compiling communicator v0.1.0 (file:///projects/communicator) @@ -273,19 +273,14 @@ note: ... or maybe `use` the module `server` instead of possibly redeclaring it | ^^^^^^ ``` -
      - -Listing 7-4: Error when trying to extract the `server` submodule into -*src/server.rs* - -
      -
      +Listing 7-4: Error when trying to extract the `server` +submodule into *src/server.rs* 这个错误说明“不能在这个位置新声明一个模块”并指出 *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` ```