diff --git a/docs/ch01-02-hello-world.html b/docs/ch01-02-hello-world.html index f6922a0..5e55907 100644 --- a/docs/ch01-02-hello-world.html +++ b/docs/ch01-02-hello-world.html @@ -117,7 +117,7 @@ Hello, world!
    println!("Hello, world!");
 

这行代码做了这个小程序的所有工作:它在屏幕上打印文本。这里有很多需要注意的细节。第一个是 Rust 代码风格使用 4 个空格缩进,而不是 1 个制表符(tab)。

-

第二个重要的部分是println!()。这叫做 Rust ,是如何进行 Rust 元编程(metaprogramming)的关键所在。相反如果是调用一个函数的话,它应该看起来像这样:println(没有!)。我们将在 24 章更加详细的讨论 Rust 宏,不过现在你只需记住当看到符号!的时候,就代表在调用一个宏而不是一个普通的函数。

+

第二个重要的部分是println!()。这叫做 Rust ,是如何进行 Rust 元编程(metaprogramming)的关键所在。相反如果是调用一个函数的话,它应该看起来像这样:println(没有!)。我们将在 21 章 E 小节中更加详细的讨论 Rust 宏,不过现在你只需记住当看到符号!的时候,就代表在调用一个宏而不是一个普通的函数。

接下来,"Hello, world!" 是一个 字符串。我们把这个字符串作为一个参数传递给println!,它负责在屏幕上打印这个字符串。轻松加愉快!(⊙o⊙)

这一行以一个分号结尾(;)。;代表这个表达式的结束和下一个表达式的开始。大部分 Rust 代码行以;结尾。

编译和运行是两个步骤

@@ -142,7 +142,7 @@ main.rs

仅仅使用rustc编译简单程序是没问题的,不过随着项目的增长,你将想要能够控制你项目拥有的所有选项,并使其易于分享你的代码给别人或别的项目。接下来,我们将介绍一个叫做 Cargo 的工具,它将帮助你编写现实生活中的 Rust 程序。

Hello, Cargo!

Cargo 是 Rust 的构建系统和包管理工具,同时 Rustacean 们使用 Cargo 来管理他们的 Rust 项目,因为它使得很多任务变得更轻松。例如,Cargo 负责构建代码、下载代码依赖的库并编译这些库。我们把代码需要的库叫做 依赖dependencies)。

-

最简单的 Rust 程序,例如我们刚刚编写的,并没有任何依赖,所以目前我们只使用了 Cargo 负责构建代码的那一部分。随着编写更加复杂的 Rust 程序,你会想要添加依赖,那么如果你使用 Cargo 开始的话,这将会变得简单许多。

+

最简单的 Rust 程序,例如我们刚刚编写的,并没有任何依赖,所以目前我们只使用了 Cargo 负责构建代码的那一部分。随着编写更加复杂的 Rust 程序,你会想要添加依赖,如果你使用 Cargo 开始的话,这将会变得简单许多。

由于绝大部分 Rust 项目使用 Cargo,本书接下来的部分将假设你使用它。如果使用安装章节介绍的官方安装包的话,Rust 自带 Cargo。如果通过其他方式安装 Rust 的话,可以在终端输入如下命令检查是否安装了 Cargo:

$ cargo --version
 
diff --git a/docs/ch02-00-guessing-game-tutorial.html b/docs/ch02-00-guessing-game-tutorial.html index cdeb3ce..a2b0b8a 100644 --- a/docs/ch02-00-guessing-game-tutorial.html +++ b/docs/ch02-00-guessing-game-tutorial.html @@ -536,7 +536,7 @@ fn main() { Err(_) => continue, }; -

expect调用切换到expect语句是如何从遇到错误就崩溃到真正处理错误的常用手段。记住parse返回一个Result类型,而Result是一个拥有OkErr两个成员的枚举。在这里使用match表达式,就像之前处理cmp方法返回的Ordering一样。

+

expect调用切换到match语句是如何从遇到错误就崩溃到真正处理错误的常用手段。记住parse返回一个Result类型,而Result是一个拥有OkErr两个成员的枚举。在这里使用match表达式,就像之前处理cmp方法返回的Ordering一样。

如果parse能够成功的将字符串转换为一个数字,它会返回一个包含结果数字Ok值。这个Ok值会匹配第一个分支的模式,这时match表达式仅仅返回parse产生的Ok值之中的num值。这个数字会最终如期变成新创建的guess变量。

如果parse能将字符串转换为一个数字,它会返回一个包含更多错误信息的Err值。Err值不能匹配第一个match分支的Ok(num)模式,但是会匹配第二个分支的Err(_)模式。_是一个包罗万象的值;在这个例子中,我们想要匹配所有Err值,不管其中有何种信息。所以程序会执行第二个分支的代码,continue,这意味着进入loop的下一次循环并请求另一个猜测。这样程序就有效地忽略了parse可能遇到的所有错误!

现在万事俱备(只欠东风)了。运行cargo run来尝试一下:

diff --git a/docs/ch03-02-data-types.html b/docs/ch03-02-data-types.html index 29a6681..fb4f079 100644 --- a/docs/ch03-02-data-types.html +++ b/docs/ch03-02-data-types.html @@ -71,7 +71,7 @@

ch03-02-data-types.md
-commit 04aa3a45eb72855b34213703718f50a12a3eeec8

+commit fe4833a8ef2853c55424e7747a4ef8dd64c35b32

Rust 中的任何值都有一个具体的类型type),这告诉了 Rust 它被指定了何种数据,这样 Rust 就知道如何处理这些数据了。这一部分将讲到一些语言内建的类型。我们将这些类型分为两个子集:标量(scalar)和复合(compound)。

贯穿整个部分,请记住 Rust 是一个静态类型statically typed)语言,也就是说必须在编译时就知道所有变量的类型。编译器通常可以通过值以及如何使用他们来推断出我们想要用的类型。当多个类型都是可能的时候,比如第二章中parseString转换为数字类型,必须增加类型注解,像这样:

@@ -153,7 +153,7 @@ commit 04aa3a45eb72855b34213703718f50a12a3eeec8

let f: bool = false; // with explicit type annotation } -

使用布尔值的主要场景是条件语句,例如if。在“控制流”(“Control Flow”)部分将讲到if语句在 Rust 中如何工作。

+

使用布尔值的主要场景是条件表达式,例如if。在“控制流”(“Control Flow”)部分将讲到if表达式在 Rust 中如何工作。

字符类型

目前为止只使用到了数字,不过 Rust 也支持字符。Rust 的char类型是大部分语言中基本字母字符类型,如下代码展示了如何使用它:

Filename: src/main.rs

diff --git a/docs/ch04-01-what-is-ownership.html b/docs/ch04-01-what-is-ownership.html index 10d7b59..329d79b 100644 --- a/docs/ch04-01-what-is-ownership.html +++ b/docs/ch04-01-what-is-ownership.html @@ -71,7 +71,7 @@

ch04-01-what-is-ownership.md
-commit fae5fa82d728b5965ecbba84060689430345e509

+commit 6d4ef020095a375483b2121d4fa2b1661062cc92

Rust 的核心功能(之一)是所有权ownership)。虽然这个功能理解起来很直观,不过它对语言的其余部分有着更深层的含义。

所有程序都必须管理他们运行时使用计算机内存的方式。一些语言中使用垃圾回收在程序运行过程中来时刻寻找不再被使用的内存;在另一些语言中,程序员必须亲自分配和释放内存。Rust 则选择了第三种方式:内存被一个所有权系统管理,它拥有一系列的规则使编译器在编译时进行检查。任何所有权系统的功能都不会导致运行时开销。

@@ -91,17 +91,17 @@ commit fae5fa82d728b5965ecbba84060689430345e509

所有权规则

-

首先,让我们看一下所有权的规则。请记住这些规则因为我们将讲解一些说明这些规则的例子:

+

首先,让我们看一下所有权的规则。请记住它们,我们将讲解一些它们的例子:

    -
  1. Rust 中的每一个值都有一个叫做它的所有者owner)的变量。
  2. -
  3. 同时一次只能有一个所有者
  4. -
  5. 当所有者变量离开作用域,这个值将被丢弃。
  6. +
  7. 每一个值都被它的所有者owner)变量拥有。
  8. +
  9. 值在任意时刻只能被一个所有者拥有。
  10. +
  11. 当所有者离开作用域,这个值将被丢弃。

变量作用域

-

我们在第二章已经完成过一个 Rust 程序的例子了。现在我们已经掌握了基本语法,所以不会在所有的例子中包含fn main() {代码了,所以如果你是一路跟过来的,必须手动将之后例子的代码放入一个main函数中。为此,例子将显得更加具体,使我们可以关注具体细节而不是样板代码。

-

作为所有权的第一个例子,我们看看一些变量的作用域scope)。作用域是一个项在程序中有效的范围。假如有一个这样的变量:

+

我们已经在第二章完成过一个 Rust 程序的例子。现在我们已经掌握了基本语法,所以不会在所有的例子中包含fn main() {代码了,所以如果你是一路跟过来的,必须手动将之后例子的代码放入一个main函数中。为此,例子将显得更加具体,使我们可以关注具体细节而不是样板代码。

+

作为所有权的第一个例子,我们看看一些变量的作用域scope)。作用域是一个项(原文:item)在程序中有效的范围。假如有一个这样的变量:

let s = "hello";
 

变量s绑定到了一个字符串字面值,这个字符串值是硬编码进我们程序代码中的。这个变量从声明的点开始直到当前作用域结束时都是有效的。列表 4-1 的注释标明了变量s在哪里是有效的:

@@ -195,8 +195,8 @@ println!("{}", s1); | 3 | let s2 = s1; | -- value moved here -4 | println!("{}, world!",s1); - | ^^ value used here after move +4 | println!("{}, world!", s1); + | ^^ value used here after move | = note: move occurs because `s1` has type `std::string::String`, which does not implement the `Copy` trait @@ -296,7 +296,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

diff --git a/docs/ch04-02-references-and-borrowing.html b/docs/ch04-02-references-and-borrowing.html index 6320a96..508979f 100644 --- a/docs/ch04-02-references-and-borrowing.html +++ b/docs/ch04-02-references-and-borrowing.html @@ -109,7 +109,7 @@ let len = calculate_length(&s1);

变量s有效的作用域与函数参数的作用域一样,不过当引用离开作用域后并不丢弃它指向的数据因为我们没有所有权。函数使用引用而不是实际值作为参数意味着无需返回值来交还所有权,因为就不曾拥有它。

我们将获取引用作为函数参数称为借用borrowing)。正如现实生活中,如果一个人拥有某样东西,你可以从它哪里借来。当你使用完毕,必须还回去。

-

那么如果我们尝试修改借用的变量呢?尝试列表 4-9 中的代码。剧透:这行不通!

+

如果我们尝试修改借用的变量呢?尝试列表 4-9 中的代码。剧透:这行不通!

Filename: src/main.rs

fn main() {
     let s = String::from("hello");
@@ -165,11 +165,11 @@ let r2 = &mut s;
 

这个限制允许可变性,不过是以一种受限制的方式。新 Rustacean 们经常与此作斗争,因为大部分语言任何时候都是可变的。这个限制的好处是 Rust 可以在编译时就避免数据竞争(data races)。

数据竞争是一种特定类型的竞争状态,它可由这三个行为造成:

    -
  1. 两个或更多指针同时访问相同的数据。
  2. -
  3. 至少有一个指针被用来写数据。
  4. -
  5. 没有被用来同步数据访问的机制。
  6. +
  7. 两个或更多指针同时访问同一数据。
  8. +
  9. 至少有一个指针被写入。
  10. +
  11. 没有同步数据访问的机制。
-

数据竞争会导致未定义行为并且当在运行时尝试追踪时可能会变得难以诊断和修复;Rust 阻止了这种情况的发生,因为存在数据竞争的代码根本就不能编译!

+

数据竞争会导致未定义行为,在运行时难以追踪,并且难以诊断和修复;Rust 避免了这种情况,它拒绝编译存在数据竞争的代码!

一如既往,可以使用大括号来创建一个新的作用域来允许拥有多个可变引用,只是不能同时拥有:

let mut s = String::from("hello");
 
diff --git a/docs/ch05-00-structs.html b/docs/ch05-00-structs.html
index 5dc74e9..6628023 100644
--- a/docs/ch05-00-structs.html
+++ b/docs/ch05-00-structs.html
@@ -133,7 +133,7 @@ error[E0106]: missing lifetime specifier
 3 |     email: &str,
   |            ^ expected lifetime parameter
 
-

第十章会讲到如何修复这个问题以便在结构体中储存引用,不过现在,通过通过从像&str这样的引用切换到像String这类拥有所有权的类型来修改修改这个错误。

+

第十章会讲到如何修复这个问题以便在结构体中储存引用,不过现在,通过从像&str这样的引用切换到像String这类拥有所有权的类型来修改修改这个错误。

一个示例程序

为了理解何时会需要使用结构体,让我们编写一个计算长方形面积的程序。我们会从单独的变量开始,接着重构程序直到使用结构体替代他们为止。

使用 Cargo 来创建一个叫做 rectangles 的新二进制程序,它会获取一个长方形以像素为单位的长度和宽度并计算它的面积。列表 5-2 中是项目的 src/main.rs 文件中为此实现的一个小程序:

diff --git a/docs/ch05-01-method-syntax.html b/docs/ch05-01-method-syntax.html index 38c0488..ec5950e 100644 --- a/docs/ch05-01-method-syntax.html +++ b/docs/ch05-01-method-syntax.html @@ -73,7 +73,7 @@
commit 8c1c1a55d5c0f9bc3c866ee79b267df9dc5c04e2

-

方法与函数类似:他们使用fn关键和名字声明,他们可以拥有参数和返回值,同时包含一些代码会在某处被调用时执行。不过方法与方法是不同的,因为他们在结构体(或者枚举或者 trait 对象,将分别在第六章和第十七章讲解)的上下文中被定义,并且他们第一个参数总是self,它代表方法被调用的结构体的实例。

+

方法与函数类似:他们使用fn关键和名字声明,他们可以拥有参数和返回值,同时包含一些代码会在某处被调用时执行。不过方法与函数是不同的,因为他们在结构体(或者枚举或者 trait 对象,将分别在第六章和第十七章讲解)的上下文中被定义,并且他们第一个参数总是self,它代表方法被调用的结构体的实例。

定义方法

让我们将获取一个Rectangle实例作为参数的area函数改写成一个定义于Rectangle结构体上的area方法,如列表 5-7 所示:

Filename: src/main.rs

@@ -108,7 +108,7 @@ struct

->运算符到哪去了?

-

像在 C++ 这样的语言中,又两个不同的运算符来调用方法:.直接在对象上调用方法,而->在一个对象的指针上调用方法,这时需要先解引用(dereference)指针。换句话说,如果object是一个指针,那么object->something()就像(*object).something()一样。

+

像在 C++ 这样的语言中,有两个不同的运算符来调用方法:.直接在对象上调用方法,而->在一个对象的指针上调用方法,这时需要先解引用(dereference)指针。换句话说,如果object是一个指针,那么object->something()就像(*object).something()一样。

Rust 并没有一个与->等效的运算符;相反,Rust 有一个叫自动引用和解引用automatic referencing and dereferencing)的功能。方法调用是 Rust 中少数几个拥有这种行为的地方。

这是它如何工作的:当使用object.something()调用方法时,Rust 会自动增加&&mut*以便使object符合方法的签名。也就是说,这些代码是等同的:

# #[derive(Debug,Copy,Clone)]
diff --git a/docs/ch06-01-defining-an-enum.html b/docs/ch06-01-defining-an-enum.html
index de0d5f3..9f06b72 100644
--- a/docs/ch06-01-defining-an-enum.html
+++ b/docs/ch06-01-defining-an-enum.html
@@ -73,7 +73,7 @@
 
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 地址的场景时应该把他们当作相同的类型。

可以通过在代码中定义一个IpAddrKind枚举来表现这个概念并列出可能的 IP 地址类型,V4V6。这被称为枚举的成员variants):

enum IpAddrKind {
@@ -92,7 +92,7 @@ commit e6d6caab41471f7115a621029bd428a812c5260e

let four = IpAddrKind::V4; let six = IpAddrKind::V6;
-

注意枚举的成员位于其标识符的命名空间中,并使用两个冒号分开。这么设计的益处是现在IpAddrKind::V4IpAddrKind::V6是相同类型的:IpAddrKind。例如,接着我们可以顶一个函数来获取IpAddrKind

+

注意枚举的成员位于其标识符的命名空间中,并使用两个冒号分开。这么设计的益处是现在IpAddrKind::V4IpAddrKind::V6是相同类型的:IpAddrKind。例如,接着我们可以定义一个函数来获取IpAddrKind

# enum IpAddrKind {
 #     V4,
 #     V6,
diff --git a/docs/ch06-02-match.html b/docs/ch06-02-match.html
index 768e0b8..6e83627 100644
--- a/docs/ch06-02-match.html
+++ b/docs/ch06-02-match.html
@@ -164,7 +164,7 @@ 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成员中内部的州的值。

+

如果调用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值并不尝试执行任何操作。

diff --git a/docs/ch06-03-if-let.html b/docs/ch06-03-if-let.html index 8a2f0e7..5ce9473 100644 --- a/docs/ch06-03-if-let.html +++ b/docs/ch06-03-if-let.html @@ -90,7 +90,7 @@ if let Some(3) = some_u8_value { }

if let获取通过=分隔的一个模式和一个表达式。它的工作方式与match相同,这里的表达式对应match而模式则对应第一个分支。

-

使用if let意味着编写更少代码,更少的缩进和更少的样板代码。然而,这样会失去match强制要求的穷进行检查。matchif let之间的选择依赖特定的环境以及增加简洁度和失去穷尽性检查的权衡取舍。

+

使用if let意味着编写更少代码,更少的缩进和更少的样板代码。然而,这样会失去match强制要求的穷尽性检查。matchif let之间的选择依赖特定的环境以及增加简洁度和失去穷尽性检查的权衡取舍。

换句话说,可以认为if letmatch的一个语法糖,它当值匹配某一模式时执行代码而忽略所有其他值。

可以在if let中包含一个elseelse块中的代码与match表达式中的_分支块中的代码相同,这样的match表达式就等同于if letelse。回忆一下列表 6-4 中Coin枚举的定义,它的Quarter成员包含一个UsState值。如果想要计数所有不是 25 美分的硬币的同时也报告 25 美分硬币所属的州,可以使用这样一个match表达式:

# #[derive(Debug)]
diff --git a/docs/ch07-01-mod-and-the-filesystem.html b/docs/ch07-01-mod-and-the-filesystem.html
index 7bbfae1..a7cc9c6 100644
--- a/docs/ch07-01-mod-and-the-filesystem.html
+++ b/docs/ch07-01-mod-and-the-filesystem.html
@@ -71,7 +71,7 @@
 

ch07-01-mod-and-the-filesystem.md
-commit 6fc32eabcd09f7a130094767abadb691dfcdddf7

+commit b0481ac44ff2594c6c240baa36357737739db445

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

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

diff --git a/docs/ch08-02-strings.html b/docs/ch08-02-strings.html index 9976e83..fc77d05 100644 --- a/docs/ch08-02-strings.html +++ b/docs/ch08-02-strings.html @@ -202,14 +202,14 @@ let answer = &hello[0]; let s = &hello[0..4];

这里,s是一个&str,它包含字符串的头四个字节。早些时候,我们提到了这些字母都是两个字节长的,所以这意味着s将会是“Зд”。

-

那么如果获取&hello[0..1]会发生什么呢?回答是:在运行时会 panic,就跟访问 vector 中的无效索引时一样:

+

如果获取&hello[0..1]会发生什么呢?答案是:在运行时会 panic,就跟访问 vector 中的无效索引时一样:

thread 'main' panicked at 'index 0 and/or 1 in `Здравствуйте` do not lie on
 character boundary', ../src/libcore/str/mod.rs:1694
 

你应该小心谨慎的使用这个操作,因为它可能会使你的程序崩溃。

遍历字符串的方法

幸运的是,这里还有其他获取字符串元素的方式。

-

如果你需要操作单独的 Unicode 标量值,最好的选择是使用chars方法。堆“नमस्ते”调用chars方法会将其分开并返回六个char类型的值,接着就可以遍历结果来访问每一个元素了:

+

如果你需要操作单独的 Unicode 标量值,最好的选择是使用chars方法。对“नमस्ते”调用chars方法会将其分开并返回六个char类型的值,接着就可以遍历结果来访问每一个元素了:

for c in "नमस्ते".chars() {
     println!("{}", c);
 }
diff --git a/docs/ch08-03-hash-maps.html b/docs/ch08-03-hash-maps.html
index 0ed67a9..4c04dc2 100644
--- a/docs/ch08-03-hash-maps.html
+++ b/docs/ch08-03-hash-maps.html
@@ -73,7 +73,7 @@
 
commit 4f2dc564851dc04b271a2260c834643dfd86c724

-

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

+

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

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

本章我们会介绍哈希 map 的基本 API,不过还有更多吸引人的功能隐藏于标准库中的HashMap定义的函数中。请一如既往地查看标准库文档来了解更多信息。

新建一个哈希 map

@@ -85,9 +85,9 @@ let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50);
-

注意必须首先use标准库中集合部分的HashMap。在这三个常用集合中,这个是最不常用的,所以并不包含在被 prelude 自动引用的功能中。标准库中对哈希 map 的支持也相对较少;例如,并没有内建的用于构建的宏。

-

就像 vector 一样,哈希 map 将他们的数据储存在堆上。这个HashMap的键类型是String而值类型是i32。同样类似于 vector,哈希 map 是同质的:所有的键必须是相同类型,值也必须都是相同类型。

-

另一个构建哈希 map 的方法是使用一个元组的 vector 的collect方法,其中每个元组包含一个键值对。collect方法可以将数据收集进一系列的集合类型,包括HashMap。例如,如果队伍的名字和初始分数分别在两个 vector 中,可以使用zip方法来创建一个元组的 vector,其中“Blue”与 10 是一对,依此类推。接着就可以使用collect方法将这个元组 vector 转换成一个HashMap

+

注意必须首先 use 标准库中集合部分的 HashMap。在这三个常用集合中,HashMap 是最不常用的,所以并没有被 prelude 自动引用。标准库中对 HashMap 的支持也相对较少,例如,并没有内建的构建宏。 +像 vector 一样,哈希 map 将他们的数据储存在堆上,这个 HashMap 的键类型是 String 而值类型是 i32。同样类似于 vector,哈希 map 是同质的:所有的键必须是相同类型,值也必须都是相同类型。

+

另一个构建哈希 map 的方法是使用一个元组的 vector 的 collect 方法,其中每个元组包含一个键值对。collect 方法可以将数据收集进一系列的集合类型,包括 HashMap。例如,如果队伍的名字和初始分数分别在两个 vector 中,可以使用 zip 方法来创建一个元组的 vector,其中“Blue”与 10 是一对,依此类推。接着就可以使用 collect 方法将这个元组 vector 转换成一个 HashMap

use std::collections::HashMap;
 
 let teams  = vec![String::from("Blue"), String::from("Yellow")];
@@ -95,7 +95,7 @@ let initial_scores = vec![10, 50];
 
 let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();
 
-

这里HashMap<_, _>类型注解是必要的,因为可能collect进很多不同的数据结构,而除非显式指定 Rust 无从得知你需要的类型。但是对于键和值的参数来说,可以使用下划线而 Rust 可以根据 vector 中数据的类型推断出哈希 map 所包含的类型。

+

这里HashMap<_, _>类型注解是必要的,因为可能collect进很多不同的数据结构,而除非显式指定 Rust 无从得知你需要的类型。但是对于键和值的类型参数来说,可以使用下划线占位,而 Rust 能够根据 vector 中数据的类型推断出 HashMap 所包含的类型。

哈希 map 和所有权

对于像i32这样的实现了Copy trait 的类型,其值可以拷贝进哈希 map。对于像String这样拥有所有权的值,其值将被移动而哈希 map 会成为这些值的所有者:

use std::collections::HashMap;
@@ -121,7 +121,7 @@ scores.insert(String::from("Yellow"), 50);
 let team_name = String::from("Blue");
 let score = scores.get(&team_name);
 
-

这里,score将会是与蓝队分数相关的值,而这个值将是Some(10)。因为get返回Option<V>所以结果被封装进Some;如果某个键在哈希 map 中没有对应的值,get会返回None。程序将需要采用第六章提到的方法中之一来处理Option

+

这里,score 是与蓝队分数相关的值,应为 Some(10)。因为 get 返回 Option<V>,所以结果被装进 Some;如果某个键在哈希 map 中没有对应的值,get 会返回 None。这时就要用某种第六章提到的方法来处理 Option

可以使用与 vector 类似的方式来遍历哈希 map 中的每一个键值对,也就是for循环:

use std::collections::HashMap;
 
@@ -139,7 +139,7 @@ for (key, value) in &scores {
 Blue: 10
 

更新哈希 map

-

虽然键值对的数量是可以增长的,不过每个单独的键同时只能关联一个值。当你想要改变哈希 map 中的数据时,必须选择是用新值替代旧值,还是完全无视旧值。我们也可以选择保留旧值而忽略新值,并只在键没有对应一个值时增加新值。或者可以结合新值和旧值。让我们看看着每一种方式是如何工作的!

+

尽管键值对的数量是可以增长的,不过任何时候,每个键只能关联一个值。当你想要改变哈希 map 中的数据时,根据目标键是否有值以及值的更新策略分成多种情况,下面我们了解一下:

覆盖一个值

如果我们插入了一个键值对,接着用相同的键插入一个不同的值,与这个键相关联的旧值将被替换。即便下面的代码调用了两次insert,哈希 map 也只会包含一个键值对,因为两次都是对蓝队的键插入的值:

use std::collections::HashMap;
@@ -181,9 +181,9 @@ for word in text.split_whitespace() {
 
 println!("{:?}", map);
 
-

这会打印出{"world": 2, "hello": 1, "wonderful": 1}or_insert方法事实上会返回这个键的值的一个可变引用(&mut V)。这里我们将这个可变引用储存在count变量中,所以为了赋值必须首先使用星号(*)解引用count。这个可变引用在for循环的结尾离开作用域,这样所有这些改变都是安全的并被借用规则所允许。

+

这会打印出{"world": 2, "hello": 1, "wonderful": 1}or_insert方法事实上会返回这个键的值的一个可变引用(&mut V)。这里我们将这个可变引用储存在count变量中,所以为了赋值必须首先使用星号(*)解引用count。这个可变引用在for循环的结尾离开作用域,这样所有这些改变都是安全的并符合借用规则。

哈希函数

-

HashMap默认使用一个密码学上是安全的哈希函数,它可以提供抵抗拒绝服务(Denial of Service, DoS)攻击的能力。这并不是现有最快的哈希函数,不过为了更好的安全性带来一些性能下降也是值得的。如果你监控你的代码并发现默认哈希函数对你来说非常慢,可以通过指定一个不同的 hasher 来切换为另一个函数。hasher 是一个实现了BuildHasher trait 的类型。第十章会讨论 trait 和如何实现他们。你并不需要从头开始实现你自己的 hasher;crates.io 有其他人分享的实现了许多常用哈希算法的 hasher 的库。

+

HashMap默认使用一种密码学安全的哈希函数,它可以抵抗拒绝服务(Denial of Service, DoS)攻击。然而并不是最快的,不过为了更高的安全性值得付出一些性能的代价。如果性能监测显示此哈希函数非常慢,以致于你无法接受,你可以指定一个不同的 hasher 来切换为其它函数。hasher 是一个实现了BuildHasher trait 的类型。第十章会讨论 trait 和如何实现他们。你并不需要从头开始实现你自己的 hasher;crates.io 有其他人分享的实现了许多常用哈希算法的 hasher 的库。

总结

vector、字符串和哈希 map 会在你的程序需要储存、访问和修改数据时帮助你。这里有一些你应该能够解决的练习问题:

头两个情况除了可变性之外是相同的:如果有一个&T,而T实现了返回U类型的Deref,可以直接得到&U。对于可变引用也是一样。最后一个有些微妙:如果有一个可变引用,它也可以强转为一个不可变引用。反之则是_不可能_的:不可变引用永远也不能强转为可变引用。

-

Deref trait 对于智能指针模式十分重要的原因在于智能指针可以被看作普通引用并用于期望使用引用的地方。例如,无需重新编写方法和函数来直接获取智能指针。

+

Deref trait 对于智能指针模式十分重要的原因在于智能指针可以被看作普通引用并被用于期望使用普通引用的地方。例如,无需重新编写方法和函数来直接获取智能指针。

- + + - - + - + - + + - - + - + @@ -189,7 +189,7 @@ struct that holds mp3 file data and metadata

- + diff --git a/docs/ch15-03-drop.html b/docs/ch15-03-drop.html index ebee845..b312b02 100644 --- a/docs/ch15-03-drop.html +++ b/docs/ch15-03-drop.html @@ -74,9 +74,9 @@ commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

对于智能指针模式来说另一个重要的 trait 是DropDrop运行我们在值要离开作用域时执行一些代码。智能指针在被丢弃时会执行一些重要的清理工作,比如释放内存或减少引用计数。更一般的来讲,数据类型可以管理多于内存的资源,比如文件或网络连接,而使用Drop在代码处理完他们之后释放这些资源。我们在智能指针上下文中讨论Drop是因为其功能几乎总是用于实现智能指针。

-

在其他一些语言中,必须每次总是必须记住在使用完智能指针实例后调用清理内存或资源的代码。如果忘记的话,运行代码的系统可能会因为负荷过重而崩溃。在 Rust 中,可以指定一些代码应该在值离开作用域时被执行,而编译器会自动插入这些代码。这意味着无需记住在所有处理完这些类型实例后调用清理代码,而仍然不会泄露资源!

+

在其他一些语言中,我们不得不记住在每次使用完智能指针实例后调用清理内存或资源的代码。如果忘记的话,运行代码的系统可能会因为负荷过重而崩溃。在 Rust 中,可以指定一些代码应该在值离开作用域时被执行,而编译器会自动插入这些代码。这意味着无需记住在所有处理完这些类型实例后调用清理代码,而仍然不会泄露资源!

指定在值离开作用域时应该执行的代码的方式是实现Drop trait。Drop trait 要求我们实现一个叫做drop的方法,它获取一个self的可变引用。

-

列表 15-8 展示了并没有实际功能的结构体CustomSmartPointer,不过我们会在创建实例之后打印出CustomSmartPointer created.,而在实例离开作用域时打印出Dropping CustomSmartPointer!,这样就能看出哪些代码被执行了。不同于println!语句,我们在智能指针需要执行清理代码时使用drop

+

列表 15-8 展示了并没有实际功能的结构体CustomSmartPointer,不过我们会在创建实例之后打印出CustomSmartPointer created.,而在实例离开作用域时打印出Dropping CustomSmartPointer!,这样就能看出每一段代码是何时被执行的。实际的项目中,我们应该在drop中清理任何智能指针运行所需要的资源,而不是这个例子中的println!语句:

Filename: src/main.rs

struct CustomSmartPointer {
     data: String,
@@ -97,7 +97,7 @@ fn main() {
 

Listing 15-8: A CustomSmartPointer struct that implements the Drop trait, where we could put code that would clean up after the CustomSmartPointer.

-

Drop trait 位于 prelude 中,所以无需导入它。drop方法的实现调用了println!;这里是你需要实际需要放入关闭套接字代码的地方。在main函数中,我们创建一个CustomSmartPointer的新实例并打印出CustomSmartPointer created.以便在运行时知道代码运行到此出。在main的结尾,CustomSmartPointer的实例会离开作用域。注意我们没有显式调用drop方法:

+

Drop trait 位于 prelude 中,所以无需导入它。drop方法的实现调用了println!;这里是你需要放入实际关闭套接字代码的地方。在main函数中,我们创建一个CustomSmartPointer的新实例并打印出CustomSmartPointer created.以便在运行时知道代码运行到此处。在main的结尾,CustomSmartPointer的实例会离开作用域。注意我们没有显式调用drop方法:

当运行这个程序,我们会看到:

CustomSmartPointer created.
 Wait for it...
diff --git a/docs/ch15-04-rc.html b/docs/ch15-04-rc.html
index 0904282..c30ee2a 100644
--- a/docs/ch15-04-rc.html
+++ b/docs/ch15-04-rc.html
@@ -114,7 +114,7 @@ to share ownership of a third list won't work

implement the `Copy` trait

Cons成员拥有其储存的数据,所以当创建b列表时将a的所有权移动到了b。接着当再次尝使用a创建c时,这不被允许因为a的所有权已经被移动。

-

相反可以改变Cons的定义来存放一个引用,不过接着必须指定生命周期参数,而且也必须以使得列表的每一个元素都与列表本身存在的一样久那样构造列表的元素。否则借用检查器甚至都不会允许我们编译代码。

+

相反可以改变Cons的定义来存放一个引用,不过接着必须指定生命周期参数,而且在构造列表时,也必须使列表中的每一个元素都至少与列表本身存在的一样久。否则借用检查器甚至都不会允许我们编译代码。

如列表 15-12 所示,可以将List的定义从Box<T>改为Rc<T>

Filename: src/main.rs

enum List {
@@ -135,7 +135,7 @@ fn main() {
 Rc<T>

注意必须为Rc增加use语句因为它不在 prelude 中。在main中创建了存放 5 和 10 的列表并将其存放在一个叫做a的新的Rc中。接着当创建bc时,我们对a调用了clone方法。

克隆Rc<T>会增加引用计数

-

之前我们见过clone方法,当时使用它来创建某些数据的完整拷贝。但是对于Rc<T>来说,它并不创建一个完整的拷贝。Rc<T>存放了引用计数,也就是说,一个存在多少个克隆的数量。让我们像列表 15-13 那样在创建c时增加一个内部作用域,并在不同的位置打印出关联函数Rc::strong_count的结果。Rc::strong_count返回传递给它的Rc值的引用计数,而在本章的稍后部分介绍避免引用循环时讲到它为什么叫做strong_count

+

之前我们见过clone方法,当时使用它来创建某些数据的完整拷贝。但是对于Rc<T>来说,它并不创建一个完整的拷贝。Rc<T>存放了引用计数,也就是说,一个存在多少个克隆的计数器。让我们像列表 15-13 那样在创建c时增加一个内部作用域,并在不同的位置打印出关联函数Rc::strong_count的结果。Rc::strong_count返回传递给它的Rc值的引用计数,而在本章的稍后部分介绍避免引用循环时讲到它为什么叫做strong_count

Filename: src/main.rs

# enum List {
 #     Cons(i32, Rc<List>),
diff --git a/docs/ch15-05-interior-mutability.html b/docs/ch15-05-interior-mutability.html
index bcd2469..1b251de 100644
--- a/docs/ch15-05-interior-mutability.html
+++ b/docs/ch15-05-interior-mutability.html
@@ -88,8 +88,8 @@ commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

  • 引用必须总是有效的。
  • 对于引用和Box<T>,借用规则的不可变性作用于编译时。对于RefCell<T>,这些不可变性作用于运行时。对于引用,如果违反这些规则,会得到一个编译错误。而对于RefCell<T>,违反这些规则会panic!

    -

    Rust 编译器执行的静态分析时天生保守的。代码的一些属性则不可能通过分析代码发现:其中最著名的就是停机问题(停机问题),这超出了本书的范畴,不过如果你感兴趣的话这是一个值得研究的有趣主题。

    -

    因为一些分析是不可能的,Rust 编译器在其不确定的时候甚至都不尝试猜测,所以说它是保守的而且有时会拒绝事实上不会违反 Rust 保证的正确的程序。换句话说,如果 Rust 接受不正确的程序,那么人们也就不会相信 Rust 所做的保证了。如果 Rust 拒绝正确的程序,会给程序员带来不变,但不会带来灾难。RefCell<T>正是用于当你知道代码遵守借用规则,而编译器不能理解的时候。

    +

    Rust 编译器执行的静态分析天生是保守的。代码的一些属性则不可能通过分析代码发现:其中最著名的就是停机问题(停机问题),这超出了本书的范畴,不过如果你感兴趣的话这是一个值得研究的有趣主题。

    +

    因为一些分析是不可能的,Rust 编译器在其不确定的时候甚至都不尝试猜测,所以说它是保守的而且有时会拒绝事实上不会违反 Rust 保证的正确的程序。换句话说,如果 Rust 接受不正确的程序,那么人们也就不会相信 Rust 所做的保证了。如果 Rust 拒绝正确的程序,会给程序员带来不便,但不会带来灾难。RefCell<T>正是用于当你知道代码遵守借用规则,而编译器不能理解的时候。

    类似于Rc<T>RefCell<T>只能用于单线程场景。在并发章节会介绍如何在多线程程序中使用RefCell<T>的功能。现在所有你需要知道的就是如果尝试在多线程上下文中使用RefCell<T>,会得到一个编译错误。

    对于引用,可以使用&&mut语法来分别创建不可变和可变的引用。不过对于RefCell<T>,我们使用borrowborrow_mut方法,它是RefCell<T>拥有的安全 API 的一部分。borrow返回Ref类型的智能指针,而borrow_mut返回RefMut类型的智能指针。这两个类型实现了Deref所以可以被当作常规引用处理。RefRefMut动态的借用所有权,而他们的Drop实现也动态的释放借用。

    列表 15-14 展示了如何使用RefCell<T>来使函数不可变的和可变的借用它的参数。注意data变量使用let data而不是let mut data来声明为不可变的,而a_fn_that_mutably_borrows则允许可变的借用数据并修改它!

    @@ -158,7 +158,7 @@ thread 'main' panicked at 'already borrowed: BorrowMutError', /stable-dist-rustc/build/src/libcore/result.rs:868 note: Run with `RUST_BACKTRACE=1` for a backtrace.
    -

    这个运行时BorrowMutError类似于编译错误:它表明已经可变的借用过s一次了,所以不允许再次借用它。我们并没有绕过借用规则,只是选择让 Rust 在运行时而不是编译时执行他们。你可以选择在任何时候任何地方使用RefCell<T>,不过除了不得不编写很多RefCell之外,最终还是可能会发现其中的问题(可能是在生产环境而不是开发环境)。另外,在运行时检查借用规则有性能惩罚。

    +

    这个运行时BorrowMutError类似于编译错误:它表明我们已经可变得借用过一次s了,所以不允许再次借用它。我们并没有绕过借用规则,只是选择让 Rust 在运行时而不是编译时执行他们。你可以选择在任何时候任何地方使用RefCell<T>,不过除了不得不编写很多RefCell之外,最终还是可能会发现其中的问题(可能是在生产环境而不是开发环境)。另外,在运行时检查借用规则有性能惩罚。

    结合Rc<T>RefCell<T>来拥有多个可变数据所有者

    那么为什么要权衡考虑选择引入RefCell<T>呢?好吧,还记得我们说过Rc<T>只能拥有一个T的不可变引用吗?考虑到RefCell<T>是不可变的,但是拥有内部可变性,可以将Rc<T>RefCell<T>结合来创造一个既有引用计数又可变的类型。列表 15-15 展示了一个这么做的例子,再次回到列表 15-5 中的 cons list。在这个例子中,不同于在 cons list 中储存i32值,我们储存一个Rc<RefCell<i32>>值。希望储存这个类型是因为其可以拥有不属于列表一部分的这个值的所有者(Rc<T>提供的多个所有者功能),而且还可以改变内部的i32值(RefCell<T>提供的内部可变性功能):

    Filename: src/main.rs

    diff --git a/docs/ch15-06-reference-cycles.html b/docs/ch15-06-reference-cycles.html index add2cc5..0055473 100644 --- a/docs/ch15-06-reference-cycles.html +++ b/docs/ch15-06-reference-cycles.html @@ -71,11 +71,11 @@

    ch15-06-reference-cycles.md
    -commit c49e5ee8859f8eb8f8867cbeafbdf5b802aa5894

    +commit 9430a3d28a2121a938d704ce48b15d21062f880e

    我们讨论过 Rust 做出的一些保证,例如永远也不会遇到一个空值,而且数据竞争也会在编译时被阻止。Rust 的内存安全保证也使其更难以制造从不被清理的内存,这被称为内存泄露。然而 Rust 并不是不可能出现内存泄漏,避免内存泄露不是 Rust 的保证之一。换句话说,内存泄露是安全的。

    在使用Rc<T>RefCell<T>时,有可能创建循环引用,这时各个项相互引用并形成环。这是不好的因为每一项的引用计数将永远也到不了 0,其值也永远也不会被丢弃。让我们看看这是如何发生的以及如何避免它。

    -

    在列表 15-16 中,我们将使用列表 15-5 中List定义的另一个变体。我们将回到储存i32值作为Cons成员的第一个元素。现在Cons成员的第二个元素是RefCell<Rc<List>>:这时就不能修改i32值了,但是能够修改Cons成员指向的哪个List。还需要增加一个tail方法来方便我们在拥有一个Cons成员时访问第二个项:

    +

    在列表 15-16 中,我们将使用列表 15-5 中List定义的另一个变体。我们将回到储存i32值作为Cons成员的第一个元素。现在Cons成员的第二个元素是RefCell<Rc<List>>:这时就不能修改i32值了,但是能够修改Cons成员指向的那个List。还需要增加一个tail方法来方便我们在拥有一个Cons成员时访问第二个项:

    Filename: src/main.rs

    #[derive(Debug)]
     enum List {
    @@ -152,7 +152,7 @@ pointing to each other

    举例来说,对于像图这样的数据结构,为了创建父节点指向子节点的边和以相反方向从子节点指向父节点的边,有时需要创建这样的引用循环。如果一个方向拥有所有权而另一个方向没有,对于模拟这种数据关系的一种不会创建引用循环和内存泄露的方式是使用Weak<T>。接下来让我们探索一下!

    避免引用循环:将Rc<T>变为Weak<T>

    Rust 标准库中提供了Weak<T>,一个用于存在引用循环但只有一个方向有所有权的智能指针。我们已经展示过如何克隆Rc<T>来增加引用的strong_countWeak<T>是一种引用Rc<T>但不增加strong_count的方式:相反它增加Rc引用的weak_count。当Rc离开作用域,其内部值会在strong_count为 0 的时候被丢弃,即便weak_count不为 0 。为了能够从Weak<T>中获取值,首先需要使用upgrade方法将其升级为Option<Rc<T>>。升级Weak<T>的结果在Rc还未被丢弃时是Some,而在Rc被丢弃时是None。因为upgrade返回一个Option,我们知道 Rust 会确保SomeNone的情况都被处理并不会尝试使用一个无效的指针。

    -

    不同于列表 15-17 中每个项只知道它的下一项,加入我们需要一个树,它的项知道它的子项父项。

    +

    不同于列表 15-17 中每个项只知道它的下一项,假如我们需要一个树,它的项知道它的子项父项。

    让我们从一个叫做Node的存放拥有所有权的i32值和其子Node值的引用的结构体开始:

    use std::rc::Rc;
     use std::cell::RefCell;
    @@ -280,8 +280,8 @@ examining strong and weak reference counts of leaf and branch
     

    总结

    现在我们学习了如何选择不同类型的智能指针来选择不同的保证并与 Rust 的常规引用向取舍。Box<T>有一个已知的大小并指向分配在堆上的数据。Rc<T>记录了堆上数据的引用数量这样就可以拥有多个所有者。RefCell<T>和其内部可变性使其可以用于需要不可变类型,但希望在运行时而不是编译时检查借用规则的场景。

    我们还介绍了提供了很多智能指针功能的 trait DerefDrop。同时探索了形成引用循环和造成内存泄漏的可能性,以及如何使用Weak<T>避免引用循环。

    -

    如果本章内容引起了你的兴趣并希望现在就实现你自己的智能指针的话,请阅读 The Nomicon 来获取更多有用的信息。

    -

    接下来,让我们谈谈 Rust 的并发。我们还会学习到一些新的堆并发有帮助的智能指针。

    +

    如果本章内容引起了你的兴趣并希望现在就实现你自己的智能指针的话,请阅读 The Nomicon 来获取更多有用的信息。

    +

    接下来,让我们谈谈 Rust 的并发。我们还会学习到一些新的对并发有帮助的智能指针。

    diff --git a/docs/ch16-00-concurrency.html b/docs/ch16-00-concurrency.html index 13aa771..23da46b 100644 --- a/docs/ch16-00-concurrency.html +++ b/docs/ch16-00-concurrency.html @@ -73,19 +73,19 @@
    commit da15de39eaabd50100d6fa662c653169254d9175

    -

    确保内存安全并不是 Rust 的唯一目标:作为一个能更好的处理并发和并行编程一直是 Rust 的另一个主要目标。 +

    确保内存安全并不是 Rust 的唯一目标:更好的处理并发和并行编程一直是 Rust 的另一个主要目标。 并发编程(concurrent programming)代表程序的不同部分相互独立的执行,而并行编程代表程序不同部分同时执行,这两个概念在计算机拥有更多处理器可供程序利用时变得更加重要。由于历史的原因,在此类上下文中编程一直是困难且容易出错的:Rust 希望能改变这一点。

    最开始,我们认为内存安全和防止并发问题是需要通过两个不同的方法解决的两个相互独立的挑战。然而,随着时间的推移,我们发现所有权和类型系统是一系列解决内存安全并发问题的强用力的工具!通过改进所有权和类型检查,很多并发错误在 Rust 中都是编译时错误,而不是运行时错误。我们给 Rust 的这一部分起了一个绰号无畏并发fearless concurrency)。无畏并发意味着 Rust 不光允许你自信代码不会出现诡异的错误,也让你可以轻易重构这种代码而无需担心会引入新的 bug。

    -

    注意:对于 Rust 的口号无畏并发,这里用并发指代很多问题而不是更精确的区分并发和(或)并行,是处于简化问题的原因。如果这是一本专注于并发和/或并行的书,我们肯定会更精确的。对于本章,请自行脑补任何并发并发和(或)并行

    +

    注意:对于 Rust 的口号无畏并发,这里用并发指代很多问题而不是更精确的区分并发和(或)并行,是出于简化问题的原因。如果这是一本专注于并发和/或并行的书,我们肯定会更精确的。对于本章,当我们谈到并发时,请自行替换为并发和(或)并行

    -

    很多语言对于其所提供的处理并发并发问题的解决方法是非常固执己见的。这是一个非常合理的策略,尤其是对于更高级的语言来说,不过对于底层语言来说可没有奢侈的选择。底层语言被期望为能在任何给定的场景中启用提供最高性能的方法,同时他们对硬件有更少的抽象。因此,Rust 给了我们多种工具来以适合场景和要求的方式来为问题建模。

    +

    很多语言所提供的处理并发问题的解决方法都非常有特色,尤其是对于更高级的语言,这是一个非常合理的策略。然而对于底层语言则没有奢侈的选择。在任何给定的情况下,我们都期望底层语言可以提供最高的性能,并且对硬件有更薄的抽象。因此,Rust 给了我们多种工具,并以适合实际情况和需求的方式来为问题建模。

    如下是本章将要涉及到的内容:

      -
    • 如果创建线程来同时运行多段代码。
    • +
    • 如何创建线程来同时运行多段代码。
    • 并发消息传递Message passing),其中通道(channel)被用来在线程间传递消息。
    • 并发共享状态Shared state),其中多个线程可以访问同一片数据。
    • -
    • SyncSend trait,他们允许 Rust 的并发保证能扩展到用户定义的和标准库中提供的类型中。
    • +
    • SyncSend trait,他们允许 Rust 的并发保证能被扩展到用户定义的和标准库中提供的类型中。
    diff --git a/docs/ch16-01-threads.html b/docs/ch16-01-threads.html index a3f8bea..e0125fa 100644 --- a/docs/ch16-01-threads.html +++ b/docs/ch16-01-threads.html @@ -75,10 +75,10 @@ commit 55b294f20fc846a13a9be623bf322d8b364cee77

    在今天使用的大部分操作系统中,当程序执行时,操作系统运行代码的上下文称为进程process)。操作系统可以运行很多进程,而操作系统也管理这些进程使得多个程序可以在电脑上同时运行。

    我们可以将每个进程运行一个程序的概念再往下抽象一层:程序也可以在其上下文中同时运行独立的部分。这个功能叫做线程thread)。

    -

    将程序需要执行的计算拆分到多个线程中可以提高性能,因为程序可以在同时进行很多工作。不过使用线程会增加程序复杂性。因为线程是同时运行的,所以无法预先保证不同线程中的代码的执行顺序。这可能会由于线程以不一致的顺序访问数据或资源而导致竞争状态,或由于两个线程相互阻止对方继续运行而造成死锁,以及仅仅出现于特定场景并难以稳定重现的 bug。Rust 减少了这些或那些使用线程的负面影响,不过在多线程上下文中编程仍然需要以与只期望在单个线程中编程不同的方式思考和组织代码。

    +

    将程序需要执行的计算拆分到多个线程中可以提高性能,因为程序可以在同时进行很多工作。不过使用线程会增加程序复杂性。因为线程是同时运行的,所以无法预先保证不同线程中的代码的执行顺序。这可能会由于线程以不一致的顺序访问数据或资源而导致竞争状态,或由于两个线程相互阻止对方继续运行而造成死锁,以及仅仅出现于特定场景并难以稳定重现的 bug。Rust 减少了这些或那些使用线程的负面影响,不过在多线程上下文中编程,相比只期望在单个线程中运行的程序,仍然要采用不同的思考方式和代码结构。

    编程语言有一些不同的方法来实现线程。很多操作系统提供了创建新线程的 API。另外,很多编程语言提供了自己的特殊的线程实现。编程语言提供的线程有时被称作轻量级lightweight)或绿色green)线程。这些语言将一系列绿色线程放入不同数量的操作系统线程中执行。因为这个原因,语言调用操作系统 API 创建线程的模型有时被称为 1:1,一个 OS 线程对应一个语言线程。绿色线程模型被称为 M:N 模型,M个绿色线程对应N个 OS 线程,这里MN不必相同。

    -

    每一个模型都有其自己的优势和取舍。对于 Rust 来说最重要的取舍是运行时支持。运行时是一个令人迷惑的概念;在不同上下文中它可能有不同的含义。这里其代表二进制文件中包含的语言自身的代码。对于一些语言,这些代码是庞大的,另一些则很小。通俗的说,“没有运行时”通常被人们用来指代“小运行时”,因为任何非汇编语言都存在一定数量的运行时。更小的运行时拥有更少的功能不过其优势在于更小的二进制输出。更小的二进制文件更容易在更多上下文中与其他语言结合。虽然很多语言觉得增加运行时来换取更多功能没有什么问题,但是 Rust 需要做到几乎没有运行时,同时不能在为了维持性能而能够在 C 语言中调用方面做出妥协。

    -

    绿色线程模型功能要求更大的运行时来管理这些线程。为此,Rust 标准库只提供了 1:1 线程模型实现。因为 Rust 是这么一个底层语言,所以有相应的 crate 实现了 M:N 线程模型,如果你宁愿牺牲性能来换取例如更多的线程控制和更低的上下文切换消耗。

    +

    每一个模型都有其自己的优势和取舍。对于 Rust 来说最重要的取舍是运行时支持。运行时是一个令人迷惑的概念;在不同上下文中它可能有不同的含义。这里其代表二进制文件中包含的语言自身的代码。对于一些语言,这些代码是庞大的,另一些则很小。通俗的说,“没有运行时”通常被人们用来指代“小运行时”,因为任何非汇编语言都存在一定数量的运行时。更小的运行时拥有更少的功能不过其优势在于更小的二进制输出。更小的二进制文件更容易在更多上下文中与其他语言结合。虽然很多语言觉得增加运行时来换取更多功能没有什么问题,但是 Rust 需要做到几乎没有运行时,同时为了保持高性能必需能够调用 C 语言,这点也是不能妥协的。

    +

    绿色线程模型功能要求更大的运行时来管理这些线程。为此,Rust 标准库只提供了 1:1 线程模型实现。因为 Rust 是这么一个底层语言,所以有相应的 crate 实现了 M:N 线程模型,如果你宁愿牺牲性能来换取例如更好的线程运行控制和更低的上下文切换成本。

    现在我们明白了 Rust 中的线程是如何定义的,让我们开始探索如何使用标准库提供的线程相关的 API吧。

    使用spawn创建新线程

    为了创建一个新线程,调用thread::spawn函数并传递一个闭包(第十三章学习了闭包),它包含希望在新线程运行的代码。列表 16-1 中的例子在新线程中打印了一些文本而其余的文本在主线程中打印:

    @@ -238,7 +238,7 @@ fn main() {

    Listing 16-4: A thread with a closure that attempts to capture a reference to v from a main thread that drops v

    -

    这些代码可以运行,而新建线程则可能直接就出错了并完全没有机会运行。新建线程内部有一个v的引用,不过主线程仍在执行:它立刻丢弃了v,使用了第十五章提到的显式丢弃其参数的drop函数。接着,新建线程开始执行,现在v是无效的了,所以它的引用也就是无效得的。噢,这太糟了!

    +

    这些代码可以运行,而新建线程则可能直接就出错了并完全没有机会运行。新建线程内部有一个v的引用,不过主线程仍在执行:它立刻丢弃了v,使用了第十五章提到的显式丢弃其参数的drop函数。接着,新建线程开始执行,现在v是无效的了,所以它的引用也就是无效的。噢,这太糟了!

    为了修复这个问题,我们可以听取错误信息的建议:

    help: to force the closure to take ownership of `v` (and any other referenced
     variables), use the `move` keyword, as shown:
    diff --git a/docs/ch16-02-message-passing.html b/docs/ch16-02-message-passing.html
    index e9c29e7..cb2db47 100644
    --- a/docs/ch16-02-message-passing.html
    +++ b/docs/ch16-02-message-passing.html
    @@ -173,7 +173,7 @@ it down the channel

    我们的并发错误会造成一个编译时错误!send获取其参数的所有权并移动这个值归接收者所有。这个意味着不可能意外的在发送后再次使用这个值;所有权系统检查一切是否合乎规则。

    在这一点上,消息传递非常类似于 Rust 的单所有权系统。消息传递的拥护者出于相似的原因支持消息传递,就像 Rustacean 们欣赏 Rust 的所有权一样:单所有权意味着特定类型问题的消失。如果一次只有一个线程可以使用某些内存,就没有出现数据竞争的机会。

    发送多个值并观察接收者的等待

    -

    列表 16-8 中的代码可以编译和运行,不过这并不是很有趣:通过它难以看出两个独立的线程在一个通道上相互通讯。列表 16-10 则有一些改进会证明这些代码是并发执行的:新建线程现在会发送多个消息并在每个消息之间暂定一段时间。

    +

    列表 16-8 中的代码可以编译和运行,不过这并不是很有趣:通过它难以看出两个独立的线程在一个通道上相互通讯。列表 16-10 则有一些改进会证明这些代码是并发执行的:新建线程现在会发送多个消息并在每个消息之间暂停一段时间。

    Filename: src/main.rs

    use std::thread;
     use std::sync::mpsc;
    diff --git a/docs/ch16-03-shared-state.html b/docs/ch16-03-shared-state.html
    index beb6686..5322647 100644
    --- a/docs/ch16-03-shared-state.html
    +++ b/docs/ch16-03-shared-state.html
    @@ -79,18 +79,18 @@ commit 9df612e93e038b05fc959db393c15a5402033f47

    communicating.

    不要共享内存来通讯;而是要通讯来共享内存。

    -

    那么“共享内存来通讯”看起来是怎样的呢?共享内存并发有点像多所有权:多个线程可以同时访问相同的内存位置。正如第十五章中智能指针使得多所有权成为可能时我们所看到的,这会增加额外的复杂性,因为需要以某种方式管理这些不同的所有者。

    -

    但是 Rust 的类型系统和所有权可以很好的帮助我们正确的进行管理。例如,让我们看看一个共享内存中更常见的并发原语:互斥器(mutexes)。

    +

    那么“共享内存来通讯”是怎样的呢?共享内存并发有点像多所有权:多个线程可以同时访问相同的内存位置。第十五章介绍了智能指针如何使得多所有权成为可能,然而这会增加额外的复杂性,因为需要以某种方式管理这些不同的所有者。

    +

    不过 Rust 的类型系统和所有权可以很好的帮助我们,正确的管理它们。以共享内存中更常见的并发原语:互斥器(mutexes)为例,让我们看看具体的情况。

    互斥器一次只允许一个线程访问数据

    -

    互斥器mutex)是一个用于共享内存的并发原语。它是“mutual exclusion”的缩写,也就是说,任何给定时间它只允许一个线程访问某些数据。互斥器以难以使用著称,因为你不得不记住:

    +

    互斥器mutex)是一种用于共享内存的并发原语。它是“mutual exclusion”的缩写,也就是说,任意时间,它只允许一个线程访问某些数据。互斥器以难以使用著称,因为你不得不记住:

      -
    1. 必须记住在使用数据之前尝试获取锁。
    2. -
    3. 一旦处理完被互斥器所保护的数据之后,必须记得解锁数据这样其他线程才能够获取锁。
    4. +
    5. 在使用数据之前尝试获取锁。
    6. +
    7. 处理完被互斥器所保护的数据之后,必须解锁数据,这样其他线程才能够获取锁。
    -

    对于一个现实中的互斥器的例子,想象一下在一个会议中的专门小组讨论会上,不过只有一个麦克风。在一个小组成员可能发言之前,他们必须请求或示意他们需要使用麦克风。一旦得到了麦克风,他们可以发言任意长的时间,接着将麦克风交给系一个希望讲话的小组成员。如果小组成员在没有麦克风的时候就开始叫喊或者在其他成员发言结束之前就取得麦克风将是很无理的。如果对这个共享的麦克风的管理因为任何这些原因出现问题,讨论会将无法如期进行。

    -

    正确的管理互斥器是异常复杂的,这也就是为什么这么多人都热衷于通道。然而,在 Rust 中,得益于类型系统和所有权,我们不可能会在锁和解锁上出错。

    +

    现实中也有互斥器的例子,想象一下在一个会议中,只有一个麦克风。如果一个成员要发言,他必须请求使用麦克风。一旦得到了麦克风,他可以畅所欲言,然后将麦克风交给下一个希望讲话的成员。如果成员在没有麦克风的时候就开始叫喊,或者在其他成员发言结束之前就拿走麦克风,是很不合适的。如果这个共享的麦克风因为此类原因而出现问题,会议将无法正常进行。

    +

    正确的管理互斥器异常复杂,这也是许多人之所以热衷于通道的原因。然而,在 Rust 中,得益于类型系统和所有权,我们不会在锁和解锁上出错。

    Mutex<T>的 API

    -

    让我们看看列表 16-12 中使用互斥器的例子,现在并不涉及到多线程:

    +

    让我们看看列表 16-12 中使用互斥器的例子,现在不涉及多线程:

    Filename: src/main.rs

    use std::sync::Mutex;
     
    @@ -107,12 +107,12 @@ fn main() {
     

    Listing 16-12: Exploring the API of Mutex<T> in a single threaded context for simplicity

    -

    与很多类型一样,我们通过叫做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。

    +

    像很多类型一样,我们使用关联函数 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 是最开始的例子:

    +

    现在让我们尝试使用Mutex<T>在多个线程间共享值。我们将启动十个线程,并在各个线程中对同一个计数器值加一,这样计数器将从 0 变为 10。注意,接下来的几个例子会出现编译错误,而我们将通过这些错误来学习如何使用 +Mutex<T>,以及 Rust 又是如何辅助我们以确保正确。列表 16-13 是最开始的例子:

    Filename: src/main.rs

    use std::sync::Mutex;
     use std::thread;
    @@ -139,8 +139,8 @@ fn main() {
     

    Listing 16-13: The start of a program having 10 threads each increment a counter guarded by a Mutex<T>

    -

    这里创建了一个counter变量来存放内含i32Mutex<T>,类似列表 16-12 那样。接下来使用 range 创建了 10 个线程。这里使用了thread::spawn并对所有线程使用了相同的闭包:他们每一个都将调用lock方法来获取Mutex<T>上的锁并对接着互斥器中的值加一。当一个线程结束执行其闭包,num会离开作用域并释放锁这样另一个线程就可以获取它了。

    -

    在主线程中,我们像列表 16-2 那样收集了所有的 join 句柄,并接着每一个的join方法来确保所有线程都会结束。那时,主线程会获取锁并打印出程序的结果。

    +

    这里创建了一个 counter 变量来存放内含 i32Mutex<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
    @@ -155,10 +155,10 @@ 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来给予每个线程其所有权。试试将这个修改用到闭包上:

    +

    这类似于列表 16-5 中解决了的问题。考虑到启动了多个线程,Rust 无法知道这些线程会运行多久,而在每一个线程尝试借用 counter 时它是否仍然有效。帮助信息提醒了我们如何解决它:可以使用 move 来给予每个线程其所有权。尝试在闭包上做一点改动:

    thread::spawn(move || {
     
    -

    再次尝试编译。这会出现了一个不同的错误!

    +

    再次编译。这回出现了一个不同的错误!

    error[E0382]: capture of moved value: `counter`
       -->
        |
    @@ -184,8 +184,8 @@ error[E0382]: use of moved value: `counter`
     
     error: aborting due to 2 previous errors
     
    -

    move并没有像列表 16-5 中那样解决这个程序中的问题。为什么没有呢?这个错误信息有些难以理解,因为它表明counter被移动进了闭包,接着它在调用lock时被捕获。这听起来像是我们希望的,不过这是不允许的。

    -

    让我们推理一下。现在不再使用for循环创建 10 个线程,让我们不用循环而只创建两个线程来看看会发生什么。将列表 16-13 中第一个for循环替换为如下代码:

    +

    move 并没有像列表 16-5 中那样解决问题。为什么呢?错误信息有点难懂,因为它表明 counter 被移动进了闭包,接着它在调用 lock 时被捕获。这似乎是我们希望的,然而不被允许。

    +

    让我们推理一下。这次不再使用 for 循环创建 10 个线程,只创建两个线程,看看会发生什么。将列表 16-13 中第一个for循环替换为如下代码:

    let handle = thread::spawn(move || {
         let mut num = counter.lock().unwrap();
     
    @@ -200,15 +200,15 @@ let handle2 = thread::spawn(move || {
     });
     handles.push(handle2);
     
    -

    这里创建了两个线程,并将用于第二个线程的变量名改为handle2num2。现在我们简化了例子来看看是否能够理解错误信息。这一次编译给出如下信息:

    +

    这里创建了两个线程,并将第二个线程所用的变量改名为 handle2num2。我们简化了例子,看是否能理解错误信息。此次编译给出如下信息:

    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
    +16 |         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
    @@ -227,11 +227,11 @@ error[E0382]: use of moved value: `counter`
     
     error: aborting due to 2 previous errors
     
    -

    啊哈!在第一个错误信息中,Rust 表明了counter被移动进了handle所代表线程的闭包中。这个移动阻止我们在对其调用lock并将结果储存在num2中时捕获counter,这是已经在第二个线程中了!所以 Rust 告诉我们不能将counter的所有权移动到多个线程中。这在之前很难看出是因为我们在循环中创建多个线程,而 Rust 无法在循环的迭代中指明不同的线程(没有临时变量)。

    +

    啊哈!第一个错误信息中说,counter 被移动进了 handle 所代表线程的闭包中。因此我们无法在第二个线程中对其调用 lock,并将结果储存在 num2 中时捕获counter!所以 Rust 告诉我们不能将 counter 的所有权移动到多个线程中。这在之前很难看出,因为我们在循环中创建了多个线程,而 Rust 无法在每次迭代中指明不同的线程(没有临时变量 num2)。

    多线程和多所有权

    -

    在第十五章中,我们可以通过使用智能指针Rc<T>来创建引用计数的值来拥有多所有权。同时第十五章提到了Rc<T>只能用于单线程上下文,不过还是让我们在这里试用Rc<T>来观察会发生什么。列表 16-14 将Mutex<T>封装进了Rc<T>中,并在移动到线程中之前克隆了Rc<T>。切换回循环来创建线程,并保留闭包中的move关键字:

    +

    在第十五章中,我们通过使用智能指针 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::rc::Rc;
     use std::sync::Mutex;
     use std::thread;
     
    @@ -258,7 +258,7 @@ fn main() {
     

    Listing 16-14: Attempting to use Rc<T> to allow multiple threads to own the Mutex<T>

    -

    又一次,编译并...出现了不同的错误!编译器真是教会了我们很多东西!

    +

    再一次编译并...出现了不同的错误!编译器真是教会了我们很多!

    error[E0277]: the trait bound `std::rc::Rc<std::sync::Mutex<i32>>:
     std::marker::Send` is not satisfied
       -->
    @@ -274,12 +274,12 @@ std::marker::Send` is not satisfied
        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 中的代码最终可以编译和运行:

    +

    哇哦,太长不看!说重点:第一个提示表明 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;
    @@ -310,10 +310,10 @@ to be able to share ownership across multiple threads

    这会打印出:

    Result: 10
     
    -

    成功了!我们从 0 数到了 10,这可能并不是很显眼,不过一路上我们学习了很多关于Mutex<T>和线程安全的内容!这个例子中构建的结构可以用于比增加计数更为复杂的操作。可以被分解为独立部分的计算可以像这样被分散到多个线程中,并可以使用Mutex<T>来允许每个线程在他们自己的部分更新最终的结果。

    +

    成功了!我们从 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 的类型系统和所有权确保了线程在更新共享值时拥有独占的访问权限,所以线程不会以一种不可预测的方式覆盖彼此的操作。为了和编译器一起使一切正确运行花了一些时间,不过我们节省了未来可能需要重现只在线程以特定顺序执行才会出现的诡异错误场景的时间。

    +

    回忆一下Rc<T>并没有避免所有可能的问题:我们也讨论了当两个Rc<T>相互引用时的引用循环的可能性,这可能造成内存泄露。Mutex<T>有一个类似的 Rust 同样也不能避免的问题:死锁。死锁deadlock)是一个场景中操作需要锁定两个资源,而两个线程分别拥有一个锁并永远相互等待的问题。如果你对这个主题感兴趣,尝试编写一个带有死锁的 Rust 程序,接着研究任何其他语言中使用互斥器的死锁规避策略并尝试在 Rust 中实现他们。标准库中Mutex<T>MutexGuard的 API 文档会提供有用的信息。

    +

    Rust 的类型系统和所有权规则,确保了线程在更新共享值时拥有独占的访问权限,所以线程不会以不可预测的方式覆盖彼此的操作。虽然为了使一切正确运行而在编译器上花了一些时间,但是我们节省了未来的时间,尤其是线程以特定顺序执行才会出现的诡异错误难以重现。

    接下来,为了丰富本章的内容,让我们讨论一下SendSync trait 以及如何对自定义类型使用他们。

    diff --git a/docs/ch16-04-extensible-concurrency-sync-and-send.html b/docs/ch16-04-extensible-concurrency-sync-and-send.html index 5707f24..2ab691d 100644 --- a/docs/ch16-04-extensible-concurrency-sync-and-send.html +++ b/docs/ch16-04-extensible-concurrency-sync-and-send.html @@ -71,25 +71,25 @@

    ch16-04-extensible-concurrency-sync-and-send.md
    -commit 55b294f20fc846a13a9be623bf322d8b364cee77

    +commit 9430a3d28a2121a938d704ce48b15d21062f880e

    -

    Rust 的并发模型中一个有趣的方面是语言本身对并发知道的很少。我们讨论过的几乎所有内容都是标准库的一部分,而不是语言本身的内容。因为并不需要语言提供任何用于并发上下文中的内容,并发选择也不仅限于标准库或语言所提供的:我们可以编写自己的或使用别人编写的内容。

    -

    我们说了几乎所有内容都不在语言本身,那么位于语言本身的是什么呢?这是两个 trait,都位于std::markerSyncSend

    +

    Rust 的并发模型中一个有趣的方面是:语言本身对并发知之甚少。我们之前讨论的几乎所有内容,都属于标准库,而不是语言本身的内容。由于不需要语言提供并发相关的基础设施,并发方案不受标准库或语言所限:我们可以编写自己的或使用别人编写的。

    +

    我们说“几乎所有内容都不属于语言本身”,那么属于语言本身的是什么呢?是两个 trait,都位于std::markerSyncSend

    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>>。当切换为标记为SendArc<T>时,代码就可以编译了。

    -

    任何完全由Send的类型组成的类型也会自动被标记为Send。几乎所有基本类型都是Send的,除了第十九章将会讨论的裸指针(raw pointer)之外。大部分标准库类型是Send的,除了Rc<T>之外。

    +

    Send标记 trait 表明类型的所有权可能被在线程间传递。几乎所有的 Rust 类型都是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>>。当切换为标记为SendArc<T>时,就没有问题了。

    +

    任何完全由Send的类型组成的类型也会自动被标记为Send。几乎所有基本类型都是Send的,大部分标准库类型是Send的,除了Rc<T>,以及第十九章将会讨论的裸指针(raw pointer)。

    Sync表明多线程访问是安全的

    -

    Sync标记 trait 表明一个类型可以安全的在多个线程中拥有其值的引用。换一种方式来说就是对于任意类型T,如果&TT的引用)是Send的话T就是Sync的,这样其引用就可以安全的发送到另一个线程。类似于Send的情况,基本类型是Sync的,完全由Sync的类型组成的类型也是Sync的。

    +

    Sync标记 trait 表明一个类型可以安全的在多个线程中拥有其值的引用。换一种方式来说就是,对于任意类型T,如果&TT的引用)是Send的话T就是Sync的,这样其引用就可以安全的发送到另一个线程。类似于Send的情况,基本类型是Sync的,完全由Sync的类型组成的类型也是Sync的。

    Rc<T>也不是Sync的,出于其不是Send的相同的原因。RefCell<T>(第十五章讨论过)和Cell<T>系列类型不是Sync的。RefCell<T>在运行时所进行的借用检查也不是线程安全的。Mutex<T>Sync的,正如上一部分所讲的它可以被用来在多线程中共享访问。

    手动实现SendSync是不安全的

    -

    通常并不需要实现SendSync trait,因为由是SendSync的类型组成的类型也自动就是SendSync的了。因为他们是标记 trait,甚至都不需要实现任何方法。他们只是用来加强并发相关的不可变行性的。

    -

    实现这些标记 trait 涉及到实现不安全的 Rust 代码。第十九章将会讲到如何使用不安全 Rust 代码;现在,重要的是在创建新的由不是SendSync的部分构成的并发类型时需要多加小心,以确保维持其安全保证。The Nomicon 中由更多关于这些保证和如何维持他们的信息。

    +

    通常并不需要实现SendSync trait,由属于SendSync的类型组成的类型,自动就是SendSync的。因为他们是标记 trait,甚至都不需要实现任何方法。他们只是用来加强并发相关的不可变性的。

    +

    实现这些标记 trait 涉及到编写不安全的 Rust 代码,第十九章将会讲述具体的方法;当前重要的是,在创建新的由不是SendSync的部分构成的并发类型时需要多加小心,以确保维持其安全保证。The Nomicon 中有更多关于这些保证以及如何维持他们的信息。

    总结

    这不会是本书最后一个出现并发的章节;第二十章的项目会在更现实的场景中使用这些概念,而不像本章中讨论的这些小例子。

    正如我们提到的,因为 Rust 本身很少有处理并发的部分内容,有很多的并发方案都由 crate 实现。他们比标准库要发展的更快;请在网上搜索当前最新的用于多线程场景的 crate。

    -

    Rust 提供了用于消息传递的通道,和像Mutex<T>Arc<T>这样可以安全的用于并发上下文的智能指针。类型系统和借用检查器会确保这些场景中的代码不会出现数据竞争和无效的引用。一旦代码可以编译了,我们就可以坚信这些代码可以正确的运行于多线程环境,而不会出现其他语言中经常出现的那些难以追踪的 bug。并发编程不再是什么可怕的概念:无所畏惧的使你的程序使用并发吧!

    -

    接下来,让我们讨论一下当 Rust 程序变得更大时那些符合习惯的模拟问题和结构的解决方案,以及 Rust 风格如何与面向对象编程(Object Oriented Programming)中那些你所熟悉的概念相联系。

    +

    Rust 提供了用于消息传递的通道,和像Mutex<T>Arc<T>这样可以安全的用于并发上下文的智能指针。类型系统和借用检查器会确保这些场景中的代码,不会出现数据竞争和无效的引用。一旦代码可以编译了,我们就可以坚信这些代码可以正确的运行于多线程环境,而不会出现其他语言中经常出现的那些难以追踪的 bug。并发编程不再是什么可怕的概念:无所畏惧地并发吧!

    +

    接下来,让我们讨论一下当 Rust 程序变得更大时,有哪些符合语言习惯的问题建模方法和结构化解决方案,以及 Rust 的风格是如何与面向对象编程(Object Oriented Programming)中那些你所熟悉的概念相联系的。

    diff --git a/docs/ch17-00-oop.html b/docs/ch17-00-oop.html index bf30b9b..1c56662 100644 --- a/docs/ch17-00-oop.html +++ b/docs/ch17-00-oop.html @@ -73,7 +73,7 @@
    commit 759801361bde74b47e81755fff545c66020e6e63

    -

    面向对象编程(Object-Oriented Programming)是一种起源于 20 世纪 60 年代的 Simula 编程语言的模式化编程方式,然后在 90 年代随着 C++ 语言开始流行。为了描述 OOP 有很多种复杂的定义:在一些定义下,Rust 是面向对象的;在其他定义下,Rust 不是。在本章节中,我们会探索一些被普遍认为是面向对象的特性和这些特性是如何转换为 Rust 方言的。

    +

    面向对象编程(Object-Oriented Programming)是一种起源于 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 index 51af9f0..9c9a70c 100644 --- a/docs/ch17-01-what-is-oo.html +++ b/docs/ch17-01-what-is-oo.html @@ -73,18 +73,18 @@
    commit 2a9b2a1b019ad6d4832ff3e56fbcba5be68b250e

    -

    关于一门语言是否需要是面向对象,在编程社区内并未达成一致意见。Rust 被很多不同的编程模式影响,我们探索了十三章提到的函数式编程的特性。面向对象编程语言的一些特性往往是对象、封装和继承。我们看一下这每一个概念的含义以及 Rust 是否支持他们。

    -

    对象包含数据和行为

    +

    关于一个语言被称为面向对象所需的功能,在编程社区内并未达成一致意见。Rust 被很多不同的编程范式影响;我们探索了十三章提到的来自函数式编程的特性。面向对象编程语言所共享的一些特性往往是对象、封装和继承。让我们看一下这每一个概念的含义以及 Rust 是否支持他们。

    +

    对象包含数据和行为

    Design Patterns: Elements of Reusable Object-Oriented Software这本书被俗称为The Gang of Four book,是面向对象编程模式的目录。它这样定义面向对象编程:

    Object-oriented programs are made up of objects. An object packages both data and the procedures that operate on that data. The procedures are typically called methods or operations.

    -

    面向对象的程序是由对象组成的。一个对象包数据和操作这些数据的过程。这些过程通常被称为方法操作

    +

    面向对象的程序是由对象组成的。一个对象包含数据和操作这些数据的过程。这些过程通常被称为方法操作

    -

    在这个定义下,Rust 是面向对象的:结构体和枚举包含数据而 impl 块提供了在结构体和枚举之上的方法。虽然带有方法的结构体和枚举并不被称为对象,但是他们提供了与对象相同的功能,参考 Gang of Four 所定义的对象。

    -

    隐藏了实现细节的封装

    -

    另一个通常与面向对象编程相关的方面是封装的思想:对象的实现细节不能被使用对象的代码获取到。唯一与对象交互的方式是通过对象提供的公有 API;使用对象的代码无法深入到对象内部并直接改变数据或者行为。封装使得改变和重构对象的内部时无需改变使用对象的代码。

    +

    在这个定义下,Rust 是面向对象的:结构体和枚举包含数据而 impl 块提供了在结构体和枚举之上的方法。虽然带有方法的结构体和枚举并不被称为对象,但是他们提供了与对象相同的功能,参考 Gang of Four 中对象的定义。

    +

    隐藏了实现细节的封装

    +

    另一个通常与面向对象编程相关的方面是封装encapsulation)的思想:对象的实现细节不能被使用对象的代码获取到。唯一与对象交互的方式是通过对象提供的公有 API;使用对象的代码无法深入到对象内部并直接改变数据或者行为。封装使得改变和重构对象的内部时无需改变使用对象的代码。

    就像我们在第七章讨论的那样,可以使用pub关键字来决定模块、类型函数和方法是公有的,而默认情况下一切都是私有的。比如,我们可以定义一个包含一个i32类型的 vector 的结构体AveragedCollection。结构体也可以有一个字段,该字段保存了 vector 中所有值的平均值。这样,希望知道结构体中的 vector 的平均值的人可以随时获取它,而无需自己计算。AveragedCollection会为我们缓存平均值结果。列表 17-1 有AveragedCollection结构体的定义:

    文件名: src/lib.rs

    pub struct AveragedCollection {
    @@ -93,7 +93,7 @@ typically called methods or operations.

    }

    列表 17-1: AveragedCollection结构体维护了一个整型列表和集合中所有元素的平均值。

    -

    注意,结构体自身被标记为pub,这样其他代码可以使用这个结构体,但是在结构体内部的字段仍然是私有的。这是非常重要的,因为我们希望保证变量被增加到列表或者被从列表删除时,也会同时更新平均值。我们通过在结构体上实现addremoveaverage方法来做到这一点,如列表 17-2 所示:

    +

    注意,结构体自身被标记为pub,这样其他代码可以使用这个结构体,但是在结构体内部的字段仍然是私有的。这是非常重要的,因为我们希望保证变量被增加到列表或者被从列表删除时,也会同时更新平均值。可以通过在结构体上实现addremoveaverage方法来做到这一点,如列表 17-2 所示:

    文件名: src/lib.rs

    # pub struct AveragedCollection {
     #     list: Vec<i32>,
    @@ -132,16 +132,16 @@ impl AveragedCollection {
     

    如果封装是一个语言被认为是面向对象语言所必要的方面的话,那么 Rust 就满足这个要求。在代码中不同的部分使用或者不使用pub决定了实现细节的封装。

    作为类型系统的继承和作为代码共享的继承

    继承Inheritance)是一个很多编程语言都提供的机制,一个对象可以定义为继承另一个对象的定义,这使其可以获得父对象的数据和行为,而不用重新定义。一些人定义面向对象语言时,认为继承是一个特色。

    -

    如果一个语言必须有继承才能被称为面向对象的语言的话,那么 Rust 就不是面向对象的。无法定义一个结构体继承自另外一个结构体,从而获得父结构体的成员和方法。然而,如果你过去常常在你的编程工具箱使用继承,根据你希望使用继承的原因,Rust 提供了其他的解决方案。

    +

    如果一个语言必须有继承才能被称为面向对象语言的话,那么 Rust 就不是面向对象的。无法定义一个结构体继承自另外一个结构体,从而获得父结构体的成员和方法。然而,如果你过去常常在你的编程工具箱使用继承,根据你希望使用继承的原因,Rust 也提供了其他的解决方案。

    使用继承有两个主要的原因。第一个是为了重用代码:一旦为一个类型实现了特定行为,继承可以对一个不同的类型重用这个实现。相反 Rust 代码可以使用默认 trait 方法实现来进行共享,在列表 10-14 中我们见过在Summarizable trait 上增加的summary方法的默认实现。任何实现了Summarizable trait 的类型都可以使用summary方法而无须进一步实现。这类似于父类有一个方法的实现,而通过继承子类也拥有这个方法的实现。当实现Summarizable trait 时也可以选择覆盖summary的默认实现,这类似于子类覆盖从父类继承的方法实现。

    第二个使用继承的原因与类型系统有关:用来表现子类型可以在父类型被使用的地方使用。这也被称为多态polymorphism),意味着如果多种对象有一个相同的形态大小,它们可以替代使用。

    -

    虽然很多人使用“多态”来描述继承,但是它实际上是一种特殊的多态,称为“子类型多态”。也有很多种其他形式的多态,在 Rust 中带有泛型参数的 trait bound 也是多态,更具体的说是“参数多态”。不同类型多态的确切细节在这里并不关键,所以不要过于担心细节,只需要知道 Rust 有多种多态相关的特色就好,不同于很多其他 OOP 语言。

    +

    虽然很多人使用“多态”("polymorphism")来描述继承,但是它实际上是一种特殊的多态,称为“子类型多态”("sub-type polymorphism")。也有很多种其他形式的多态,在 Rust 中带有泛型参数的 trait bound 也是多态,更具体的说是“参数多态”("parametric polymorphism")。不同类型多态的确切细节在这里并不关键,所以不要过于担心细节,只需要知道 Rust 有多种多态相关的特色就好,不同于很多其他 OOP 语言。

    -

    为了支持这种模式,Rust 有 trait 对象trait objects),这样我们可以指定给任何类型的值,只要值实现了一种特定的 trait。

    -

    继承最近在很多编程语言的设计方案中失宠了。使用继承来实现代码重用需要共享比你需要共享的代码。子类不应该总是共享它们的父类的所有特色,但是继承意味着子类得到了它父类所有的数据和行为。这使得程序的设计更加不灵活,并产生了无意义的方法调用或子类,或者由于方法并不适用于子类不过必需从父类继承而造成错误的可能性。另外,一些语言只允许子类继承一个父类,这进一步限制了程序设计的灵活性。

    +

    为了支持这种模式,Rust 有 trait 对象trait objects),这样就可以使用任意类型的值,只要这个值实现了指定的 trait。

    +

    继承最近在很多编程语言的设计方案中失宠了。使用继承来实现代码重用,会共享更多非必需的代码。子类不应该总是共享其父类的所有特性,然而继承意味着子类得到了其父类全部的数据和行为。这使得程序的设计更不灵活,并产生了无意义的方法调用或子类,以及由于方法并不适用于子类,却必需从父类继承而可能造成的错误。另外,某些语言只允许子类继承一个父类,进一步限制了程序设计的灵活性。

    因为这些原因,Rust 选择了一个另外的途径,使用 trait 对象替代继承。让我们看一下在 Rust 中 trait 对象是如何实现多态的。

    diff --git a/docs/ch17-02-trait-objects.html b/docs/ch17-02-trait-objects.html index 8932ea5..9afdcfd 100644 --- a/docs/ch17-02-trait-objects.html +++ b/docs/ch17-02-trait-objects.html @@ -73,17 +73,17 @@
    commit 67876e3ef5323ce9d394f3ea6b08cb3d173d9ba9

    -

    在第八章,我们谈到了 vector 的局限是 vector 只能存储同种类型的元素。在列表 8-1 中有一个例子,其中定义了一个有存放整型、浮点型和文本的成员的枚举类型SpreadsheetCell,这样就可以在每一个单元格储存不同类型的数据并使得 vector 仍让代表一行单元格。这在那类代码被编译时就知晓需要可交换处理的数据的类型是一个固定集合的情况下是可行的。

    +

    在第八章,我们谈到了 vector 只能存储同种类型元素的局限。在列表 8-1 中有一个例子,其中定义了存放包含整型、浮点型和文本型成员的枚举类型SpreadsheetCell,这样就可以在每一个单元格储存不同类型的数据,并使得 vector 仍然代表一行单元格。当编译时就知道类型集合全部元素的情况下,这种方案是可行的。

    -

    有时,我们想我们使用的类型集合是可扩展的,可以被使用我们的库的程序员扩展。比如很多图形化接口工具有一个条目列表,从这个列表迭代和调用draw方法在每个条目上。我们将要创建一个库crate,包含称为rust_gui的CUI库的结构体。我们的GUI库可以包含一些给开发者使用的类型,比如Button或者TextField。使用rust_gui的程序员会创建更多可以在屏幕绘图的类型:一个程序员可能会增加Image,另外一个可能会增加SelectBox。我们不会在本章节实现一个完善的GUI库,但是我们会展示如何把各部分组合在一起。

    -

    当要写一个rust_gui库时,我们不知道其他程序员要创建什么类型,所以我们无法定义一个enum来包含所有的类型。我们知道的是rust_gui需要有能力跟踪所有这些不同类型的大量的值,需要有能力在每个值上调用draw方法。我们的GUI库不需要确切地知道当调用draw方法时会发生什么,只要值有可用的方法供我们调用就可以。

    -

    在有继承的语言里,我们可能会定义一个名为Component的类,该类上有一个draw方法。其他的类比如ButtonImageSelectBox会从Component继承并继承draw方法。它们会各自覆写draw方法来自定义行为,但是框架会把所有的类型当作是Component的实例,并在它们上调用draw

    +

    有时,我们希望使用的类型的集合对于使用库的程序员来说是可扩展的。例如,很多图形用户接口(GUI)工具有一个条目列表的概念,它通过遍历列表并对每一个条目调用draw方法来绘制在屏幕上。我们将要创建一个叫做rust_gui的包含一个 GUI 库结构的库 crate。GUI 库可以包含一些供开发者使用的类型,比如ButtonTextField。使用rust_gui的程序员会想要创建更多可以绘制在屏幕上的类型:一个程序员可能会增加一个Image,而另一个可能会增加一个SelectBox。我们不会在本章节实现一个功能完善的 GUI 库,不过会展示各个部分是如何结合在一起的。

    +

    当写 rust_gui 库时,我们不知道其他程序员需要什么类型,所以无法定义一个 enum 来包含所有的类型。然而 rust_gui 需要跟踪所有这些不同类型的值,需要有在每个值上调用 draw 方法能力。我们的 GUI 库不需要确切地知道调用 draw 方法会发生什么,只需要有可用的方法供我们调用。

    +

    在可以继承的语言里,我们会定义一个名为 Component 的类,该类上有一个draw方法。其他的类比如ButtonImageSelectBox会从Component继承并拥有draw方法。它们各自覆写draw方法以自定义行为,但是框架会把所有的类型当作是Component的实例,并在其上调用draw

    定义一个带有自定义行为的Trait

    -

    不过,在Rust语言中,我们可以定义一个名为Draw的trait,其上有一个名为draw的方法。我们定义一个带有trait对象的vector,绑定了一种指针的trait,比如&引用或者一个Box<T>智能指针。

    -

    我们提到,我们不会调用结构体和枚举的对象,从而区分于其他语言的对象。在结构体的数据或者枚举的字段和impl块中的行为是分开的,而其他语言则是数据和行为被组合到一个概念里。Trait对象更像其他语言的对象,在这种场景下,他们组合了由指针组成的数据到实体对象,该对象带有在trait中定义的方法行为。但是,trait对象是和其他语言是不同的,因为我们不能向一个trait对象增加数据。trait对象不像其他语言那样有用:它们的目的是允许从公有的行为上抽象。

    -

    trait定义了在给定场景下我们所需要的行为。在我们会使用一个实体类型或者一个通用类型的地方,我们可以把trait当作trait对象使用。Rust的类型系统会保证我们为trait对象带入的任何值会实现trait的方法。我们不需要在编译阶段知道所有可能的类型,我们可以把所有的实例统一对待。Listing 17-03展示了如何定义一个名为Draw的带有draw方法的trait。

    +

    不过,在Rust语言中,我们可以定义一个 Draw trait,包含名为 draw 的方法。我们定义一个由trait对象组成的vector,绑定了某种指针的trait,比如&引用或者一个Box<T>智能指针。

    +

    之前提到,我们不会称结构体和枚举为对象,以区分其他语言的结构体和枚举对象。结构体或者枚举成员中的数据和impl块中的行为是分开的,而其他语言则是数据和行为被组合到一个对象里。Trait 对象更像其他语言的对象,因为他们将其指针指向的具体对象作为数据,将在trait 中定义的方法作为行为,组合在了一起。但是,trait 对象和其他语言是不同的,我们不能向一个 trait 对象增加数据。trait 对象不像其他语言那样有用:它们的目的是允许从公有行为上抽象。

    +

    trait 对象定义了给定情况下应有的行为。当需要具有某种特性的不确定具体类型时,我们可以把 trait 对象当作 trait 使用。Rust 的类型系统会保证我们为 trait 对象带入的任何值会实现 trait 的方法。我们不需要在编译阶段知道所有可能的类型,却可以把所有的实例统一对待。Listing 17-03展示了如何定义一个名为Draw的带有draw方法的trait。

    Filename: src/lib.rs

    pub trait Draw {
         fn draw(&self);
    @@ -91,7 +91,7 @@ get Chapter 8 for editing. /Carol -->
     

    Listing 17-3:Draw trait的定义

    -

    因为我们已经在第10章讨论过如何定义trait,你可能比较熟悉。下面是新的定义:Listing 17-4有一个名为Screen的结构体,里面有一个名为components的vector,components的类型是BoxBox<Draw>是一个trait对象:它是一个任何Box内部的实现了Drawtrait的类型的替身。

    +

    因为我们已经在第10章讨论过如何定义 trait,你可能比较熟悉。下面是新的定义:Listing 17-4有一个名为 Screen 的结构体,里面有一个名为 components 的 vector,components 的类型是BoxBox<Draw> 是一个 trait 对象:它是 Box 内部任意一个实现了 Draw trait 的类型的替身。

    Filename: src/lib.rs

    # pub trait Draw {
     #     fn draw(&self);
    @@ -101,9 +101,9 @@ pub struct Screen {
         pub components: Vec<Box<Draw>>,
     }
     
    -

    Listing 17-4: 定义一个Screen结构体,带有一个含有实现了Drawtrait的components vector成员

    +

    Listing 17-4: 定义一个 Screen 结构体,带有一个含有实现了 Draw trait 的 components vector 成员

    -

    Screen结构体上,我们将要定义一个run方法,该方法会在它的components上调用draw方法,如Listing 17-5所示:

    +

    Screen 结构体上,我们将要定义一个 run 方法,该方法会在它的 components 上调用 draw 方法,如Listing 17-5所示:

    Filename: src/lib.rs

    # pub trait Draw {
     #     fn draw(&self);
    @@ -121,9 +121,9 @@ impl Screen {
         }
     }
     
    -

    Listing 17-5:在Screen上实现一个run方法,该方法在每个组件上调用draw方法 +

    Listing 17-5:在 Screen 上实现一个 run 方法,该方法在每个组件上调用 draw 方法

    -

    这是区别于定义一个使用带有trait绑定的通用类型参数的结构体。通用类型参数一次只能被一个实体类型替代,而trait对象可以在运行时允许多种实体类型填充trait对象。比如,我们已经定义了Screen结构体使用通用类型和一个trait绑定,如Listing 17-6所示:

    +

    这与带 trait 约束的泛型结构体不同(trait 约束泛型参数)。泛型参数一次只能被一个具体类型替代,而 trait 对象可以在运行时允许多种具体类型填充 trait 对象。比如,我们已经定义了 Screen 结构体使用泛型和一个 trait 约束,如Listing 17-6所示:

    Filename: src/lib.rs

    # pub trait Draw {
     #     fn draw(&self);
    @@ -142,12 +142,12 @@ impl<T> Screen<T>
         }
     }
     
    -

    Listing 17-6: 一种Screen结构体的替代实现,它的run方法使用通用类型和trait绑定 +

    Listing 17-6: 一种 Screen 结构体的替代实现,它的 run 方法使用通用类型和 trait 绑定

    -

    这个例子只能使我们有一个Screen实例,这个实例有一个组件列表,所有的组件类型是Button或者TextField。如果你有同种的集合,那么可以优先使用通用和trait绑定,这是因为为了使用具体的类型,定义是在编译阶段是单一的。

    -

    而如果使用内部有Vec<Box<Draw>> trait对象的列表的Screen结构体,Screen实例可以同时包含Box<Button>Box<TextField>Vec。我们看它是怎么工作的,然后讨论运行时性能的实现。

    +

    这个例子中,Screen 实例所有组件类型必需全是 Button,或者全是 TextField。如果你的组件集合是单一类型的,那么可以优先使用泛型和 trait 约束,因为其使用的具体类型在编译阶段即可确定。

    +

    Screen 结构体内部的 Vec<Box<Draw>> trait 对象列表,则可以同时包含 Box<Button>Box<TextField>。我们看它是怎么工作的,然后讨论运行时性能。

    来自我们或者库使用者的实现

    -

    现在,我们增加一些实现了Drawtrait的类型。我们会再次提供Button,实际上实现一个GUI库超出了本书的范围,所以draw方法的内部不会有任何有用的实现。为了想象一下实现可能的样子,Button结构体可能有 widthheightlabel`字段,如Listing 17-7所示:

    +

    现在,我们增加一些实现了 Draw trait 的类型,再次提供 Button。实现一个 GUI 库实际上超出了本书的范围,因此 draw 方法留空。为了想象实现可能的样子,Button 结构体有 widthheightlabel字段,如Listing 17-7所示:

    Filename: src/lib.rs

    # pub trait Draw {
     #     fn draw(&self);
    @@ -166,11 +166,11 @@ impl Draw for Button {
     }
     

    Listing 17-7: 实现了Draw trait的Button 结构体

    -

    Button上的 widthheightlabel会和其他组件不同,比如TextField可能有widthheight, -labelplaceholder字段。每个我们可以在屏幕上绘制的类型会实现Drawtrait,在draw方法中使用不同的代码,定义了如何绘制Button(GUI代码的具体实现超出了本章节的范围)。除了Draw trait,Button可能也有另一个impl块,包含了当按钮被点击的时候的响应方法。这类方法不适用于TextField这样的类型。

    -

    有时,使用我们的库决定了实现一个包含widthheightoptions``SelectBox结构体。它们在SelectBox类型上实现了Drawtrait,如 Listing 17-8所示:

    +

    Button 上的 widthheightlabel 会和其他组件不同,比如 TextField 可能有 widthheight, +label 以及 placeholder 字段。每个我们可以在屏幕上绘制的类型都会实现 Draw trait,在 draw 方法中使用不同的代码,定义了如何绘制 Button。除了 Draw trait,Button 也可能有一个 impl 块,包含按钮被点击时的响应方法。这类方法不适用于 TextField 这样的类型。

    +

    假定我们的库的用户相要实现一个包含 widthheightoptionsSelectBox 结构体。同时也在 SelectBox 类型上实现了 Draw trait,如 Listing 17-8所示:

    Filename: src/main.rs

    -
    extern crate rust_gui;
    +
    extern crate rust_gui;
     use rust_gui::Draw;
     
     struct SelectBox {
    @@ -185,11 +185,11 @@ impl Draw for SelectBox {
         }
     }
     
    -

    Listing 17-8: 另外一个crate中,在SelectBox结构体上使用rust_gui和实现了Draw trait +

    Listing 17-8: 另外一个 crate 中,在 SelectBox 结构体上使用 rust_gui 和实现了Draw trait

    -

    我们的库的使用者现在可以写他们的main函数来创建一个Screen实例,然后通过把自身放入Box<T>变成trait对象,向screen增加SelectBoxButton。它们可以在每个Screen实例上调用run方法,这会调用每个组件的draw方法。 Listing 17-9展示了实现:

    +

    库的用户现在可以在他们的 main 函数中创建一个 Screen 实例,然后把自身放入 Box<T> 变成 trait 对象,向 screen 增加 SelectBoxButton。他们可以在这个 Screen 实例上调用 run 方法,这又会调用每个组件的 draw 方法。 Listing 17-9 展示了实现:

    Filename: src/main.rs

    -
    use rust_gui::{Screen, Button};
    +
    use rust_gui::{Screen, Button};
     
     fn main() {
         let screen = Screen {
    @@ -214,14 +214,14 @@ fn main() {
         screen.run();
     }
     
    -

    Listing 17-9: 使用trait对象来存储实现了相同trait的不同类型 +

    Listing 17-9: 使用 trait 对象来存储实现了相同 trait 的不同类型

    -

    虽然我们不知道有些人可能有一天会增加SelectBox类型,但是我们的Screen 有能力操作SelectBox和绘制,因为SelectBox实现了Draw类型,这意味着它实现了draw方法。

    -

    只关心值响应的消息,而不关心值的具体类型,这类似于动态类型语言中的duck typing:如果它像鸭子一样走路,像鸭子一样叫,那么它肯定是只鸭子!在Listing 17-5的Screenrun方法的实现中,run不需要知道每个组件的具体类型。它也不检查是否一个组件是Button或者SelectBox的实例,只是调用组件的draw方法即可。通过指定Box<Draw>作为componentsvector中的值类型,我们定义了:Screen需要可以被调用其draw方法的值。

    -

    使用trait对象和支持duck typing的Rust类型系统的好处是,我们永远不需要在运行时检查一个值是否实现了一个特殊方法,或者担心因为调用了一个值没有实现方法而遇到错误。如果值没有实现trait对象需要的trait,Rust不会编译我们的代码。

    -

    比如,Listing 17-10展示了当我们创建一个把String当做其成员的Screen时发生的情况:

    +

    虽然我们不知道哪一天会有人增加 SelectBox 类型,但是我们的 Screen 能够操作 SelectBox 并绘制它,因为 SelectBox 实现了 Draw 类型,这意味着它实现了 draw 方法。

    +

    只关心值的响应,而不关心其具体类型,这类似于动态类型语言中的 duck typing:如果它像鸭子一样走路,像鸭子一样叫,那么它就是只鸭子!在 Listing 17-5 Screenrun 方法实现中,run 不需要知道每个组件的具体类型。它也不检查组件是 Button 还是 SelectBox 的实例,只管调用组件的 draw 方法。通过指定 Box<Draw> 作为 components 列表中元素的类型,我们约束了 Screen 需要这些实现了 draw 方法的值。

    +

    Rust 类型系统使用 trait 对象来支持 duck typing 的好处是,我们无需在运行时检查一个值是否实现了特定方法,或是担心调用了一个值没有实现的方法。如果值没有实现 trait 对象需要的 trait(方法),Rust 不会编译。

    +

    比如,Listing 17-10 展示了当我们创建一个使用 String 做为其组件的 Screen 时发生的情况:

    Filename: src/main.rs

    -
    extern crate rust_gui;
    +
    extern crate rust_gui;
     use rust_gui::Draw;
     
     fn main() {
    @@ -234,9 +234,9 @@ fn main() {
         screen.run();
     }
     
    -

    Listing 17-10: 尝试使用一种没有实现trait对象的trait的类型

    +

    Listing 17-10: 尝试使用一种没有实现 trait 对象的类型

    -

    我们会遇到这个错误,因为String没有实现 Drawtrait:

    +

    我们会遇到这个错误,因为 String 没有实现 Draw trait:

    error[E0277]: the trait bound `std::string::String: Draw` is not satisfied
       -->
        |
    @@ -246,10 +246,10 @@ fn main() {
        |
        = note: required for the cast to the object type `Draw`
     
    -

    这个报错让我们知道,或者我们传入了本来不想传给Screen的东西,我们应该传入一个不同的类型,或者是我们应该在String上实现Draw,这样,Screen才能调用它的draw方法。

    -

    Trait对象执行动态分发

    -

    回忆一下第10章,我们讨论过当我们使用通用类型的trait绑定时,编译器执行单类型的处理过程:在我们需要使用通用类型参数的地方,编译器为每个实体类型产生了非通用的函数实现和方法。由于非单类型而产生的代码是 static dispatch:当方法被调用,代码会执行在编译阶段就决定的方法,这样寻找那段代码是非常快速的。

    -

    当我们使用trait对象,编译器不能执行单类型的,因为我们不知道可能被代码调用的类型。而,当方法被调用的时候,Rust跟踪可能被使用的代码,然后在运行时找出为了方法被调用时该使用哪些代码。这也是我们熟知的dynamic dispatch,当运行时的查找发生时是比较耗费资源的。动态分发也防止编译器选择内联函数的代码,这样防止了一些优化。虽然我们写代码时得到了额外的代码灵活性,不过,这是一个权衡考虑。

    +

    这个错误告诉我们,要么传入 Screen 需要的类型,要么在 String 上实现 Draw,以便 Screen 调用它的 draw 方法。

    +

    Trait 对象执行动态分发

    +

    回忆一下第10章我们讨论过的,当我们在泛型上使用 trait 约束时,编译器按单态类型处理:在需要使用范型参数的地方,编译器为每个具体类型生成非泛型的函数和方法实现。单态类型处理产生的代码实际就是做 static dispatch:方法的代码在编译阶段就已经决定了,当调用时,寻找那段代码非常快速。

    +

    当我们使用 trait 对象,编译器不能按单态类型处理,因为无法知道使用代码的所有可能类型。而是调用方法的时候,Rust 跟踪可能被使用的代码,在运行时找出调用该方法时应使用的代码。这也是我们熟知的 dynamic dispatch,查找过程会产生运行时开销。动态分发也会阻止编译器内联函数,失去一些优化途径。尽管获得了额外的灵活性,但仍然需要权衡取舍。

    Trait 对象需要对象安全

    所有权规则

    -

    首先,让我们看一下所有权的规则。请记住这些规则因为我们将讲解一些说明这些规则的例子:

    +

    首先,让我们看一下所有权的规则。请记住它们,我们将讲解一些它们的例子:

      -
    1. Rust 中的每一个值都有一个叫做它的所有者owner)的变量。
    2. -
    3. 同时一次只能有一个所有者
    4. -
    5. 当所有者变量离开作用域,这个值将被丢弃。
    6. +
    7. 每一个值都被它的所有者owner)变量拥有。
    8. +
    9. 值在任意时刻只能被一个所有者拥有。
    10. +
    11. 当所有者离开作用域,这个值将被丢弃。

    变量作用域

    -

    我们在第二章已经完成过一个 Rust 程序的例子了。现在我们已经掌握了基本语法,所以不会在所有的例子中包含fn main() {代码了,所以如果你是一路跟过来的,必须手动将之后例子的代码放入一个main函数中。为此,例子将显得更加具体,使我们可以关注具体细节而不是样板代码。

    -

    作为所有权的第一个例子,我们看看一些变量的作用域scope)。作用域是一个项在程序中有效的范围。假如有一个这样的变量:

    +

    我们已经在第二章完成过一个 Rust 程序的例子。现在我们已经掌握了基本语法,所以不会在所有的例子中包含fn main() {代码了,所以如果你是一路跟过来的,必须手动将之后例子的代码放入一个main函数中。为此,例子将显得更加具体,使我们可以关注具体细节而不是样板代码。

    +

    作为所有权的第一个例子,我们看看一些变量的作用域scope)。作用域是一个项(原文:item)在程序中有效的范围。假如有一个这样的变量:

    let s = "hello";
     

    变量s绑定到了一个字符串字面值,这个字符串值是硬编码进我们程序代码中的。这个变量从声明的点开始直到当前作用域结束时都是有效的。列表 4-1 的注释标明了变量s在哪里是有效的:

    @@ -1712,8 +1712,8 @@ println!("{}", s1); | 3 | let s2 = s1; | -- value moved here -4 | println!("{}, world!",s1); - | ^^ value used here after move +4 | println!("{}, world!", s1); + | ^^ value used here after move | = note: move occurs because `s1` has type `std::string::String`, which does not implement the `Copy` trait @@ -1813,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

    @@ -1874,7 +1874,7 @@ let len = calculate_length(&s1);

    变量s有效的作用域与函数参数的作用域一样,不过当引用离开作用域后并不丢弃它指向的数据因为我们没有所有权。函数使用引用而不是实际值作为参数意味着无需返回值来交还所有权,因为就不曾拥有它。

    我们将获取引用作为函数参数称为借用borrowing)。正如现实生活中,如果一个人拥有某样东西,你可以从它哪里借来。当你使用完毕,必须还回去。

    -

    那么如果我们尝试修改借用的变量呢?尝试列表 4-9 中的代码。剧透:这行不通!

    +

    如果我们尝试修改借用的变量呢?尝试列表 4-9 中的代码。剧透:这行不通!

    Filename: src/main.rs

    fn main() {
         let s = String::from("hello");
    @@ -1930,11 +1930,11 @@ let r2 = &mut s;
     

    这个限制允许可变性,不过是以一种受限制的方式。新 Rustacean 们经常与此作斗争,因为大部分语言任何时候都是可变的。这个限制的好处是 Rust 可以在编译时就避免数据竞争(data races)。

    数据竞争是一种特定类型的竞争状态,它可由这三个行为造成:

      -
    1. 两个或更多指针同时访问相同的数据。
    2. -
    3. 至少有一个指针被用来写数据。
    4. -
    5. 没有被用来同步数据访问的机制。
    6. +
    7. 两个或更多指针同时访问同一数据。
    8. +
    9. 至少有一个指针被写入。
    10. +
    11. 没有同步数据访问的机制。
    -

    数据竞争会导致未定义行为并且当在运行时尝试追踪时可能会变得难以诊断和修复;Rust 阻止了这种情况的发生,因为存在数据竞争的代码根本就不能编译!

    +

    数据竞争会导致未定义行为,在运行时难以追踪,并且难以诊断和修复;Rust 避免了这种情况,它拒绝编译存在数据竞争的代码!

    一如既往,可以使用大括号来创建一个新的作用域来允许拥有多个可变引用,只是不能同时拥有:

    let mut s = String::from("hello");
     
    @@ -2296,7 +2296,7 @@ error[E0106]: missing lifetime specifier
     3 |     email: &str,
       |            ^ expected lifetime parameter
     
    -

    第十章会讲到如何修复这个问题以便在结构体中储存引用,不过现在,通过通过从像&str这样的引用切换到像String这类拥有所有权的类型来修改修改这个错误。

    +

    第十章会讲到如何修复这个问题以便在结构体中储存引用,不过现在,通过从像&str这样的引用切换到像String这类拥有所有权的类型来修改修改这个错误。

    一个示例程序

    为了理解何时会需要使用结构体,让我们编写一个计算长方形面积的程序。我们会从单独的变量开始,接着重构程序直到使用结构体替代他们为止。

    使用 Cargo 来创建一个叫做 rectangles 的新二进制程序,它会获取一个长方形以像素为单位的长度和宽度并计算它的面积。列表 5-2 中是项目的 src/main.rs 文件中为此实现的一个小程序:

    @@ -2440,7 +2440,7 @@ trait and printing the Rectangle instance using debug formatting commit 8c1c1a55d5c0f9bc3c866ee79b267df9dc5c04e2

    -

    方法与函数类似:他们使用fn关键和名字声明,他们可以拥有参数和返回值,同时包含一些代码会在某处被调用时执行。不过方法与方法是不同的,因为他们在结构体(或者枚举或者 trait 对象,将分别在第六章和第十七章讲解)的上下文中被定义,并且他们第一个参数总是self,它代表方法被调用的结构体的实例。

    +

    方法与函数类似:他们使用fn关键和名字声明,他们可以拥有参数和返回值,同时包含一些代码会在某处被调用时执行。不过方法与函数是不同的,因为他们在结构体(或者枚举或者 trait 对象,将分别在第六章和第十七章讲解)的上下文中被定义,并且他们第一个参数总是self,它代表方法被调用的结构体的实例。

    定义方法

    让我们将获取一个Rectangle实例作为参数的area函数改写成一个定义于Rectangle结构体上的area方法,如列表 5-7 所示:

    Filename: src/main.rs

    @@ -2475,7 +2475,7 @@ struct

    ->运算符到哪去了?

    -

    像在 C++ 这样的语言中,又两个不同的运算符来调用方法:.直接在对象上调用方法,而->在一个对象的指针上调用方法,这时需要先解引用(dereference)指针。换句话说,如果object是一个指针,那么object->something()就像(*object).something()一样。

    +

    像在 C++ 这样的语言中,有两个不同的运算符来调用方法:.直接在对象上调用方法,而->在一个对象的指针上调用方法,这时需要先解引用(dereference)指针。换句话说,如果object是一个指针,那么object->something()就像(*object).something()一样。

    Rust 并没有一个与->等效的运算符;相反,Rust 有一个叫自动引用和解引用automatic referencing and dereferencing)的功能。方法调用是 Rust 中少数几个拥有这种行为的地方。

    这是它如何工作的:当使用object.something()调用方法时,Rust 会自动增加&&mut*以便使object符合方法的签名。也就是说,这些代码是等同的:

    # #[derive(Debug,Copy,Clone)]
    @@ -2574,7 +2574,7 @@ commit 4f2dc564851dc04b271a2260c834643dfd86c724


    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 地址的场景时应该把他们当作相同的类型。

    可以通过在代码中定义一个IpAddrKind枚举来表现这个概念并列出可能的 IP 地址类型,V4V6。这被称为枚举的成员variants):

    enum IpAddrKind {
    @@ -2593,7 +2593,7 @@ commit e6d6caab41471f7115a621029bd428a812c5260e

    let four = IpAddrKind::V4; let six = IpAddrKind::V6;
    -

    注意枚举的成员位于其标识符的命名空间中,并使用两个冒号分开。这么设计的益处是现在IpAddrKind::V4IpAddrKind::V6是相同类型的:IpAddrKind。例如,接着我们可以顶一个函数来获取IpAddrKind

    +

    注意枚举的成员位于其标识符的命名空间中,并使用两个冒号分开。这么设计的益处是现在IpAddrKind::V4IpAddrKind::V6是相同类型的:IpAddrKind。例如,接着我们可以定义一个函数来获取IpAddrKind

    # enum IpAddrKind {
     #     V4,
     #     V6,
    @@ -2868,7 +2868,7 @@ 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成员中内部的州的值。

    +

    如果调用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值并不尝试执行任何操作。

    @@ -2952,7 +2952,7 @@ if let Some(3) = some_u8_value { }

    if let获取通过=分隔的一个模式和一个表达式。它的工作方式与match相同,这里的表达式对应match而模式则对应第一个分支。

    -

    使用if let意味着编写更少代码,更少的缩进和更少的样板代码。然而,这样会失去match强制要求的穷进行检查。matchif let之间的选择依赖特定的环境以及增加简洁度和失去穷尽性检查的权衡取舍。

    +

    使用if let意味着编写更少代码,更少的缩进和更少的样板代码。然而,这样会失去match强制要求的穷尽性检查。matchif let之间的选择依赖特定的环境以及增加简洁度和失去穷尽性检查的权衡取舍。

    换句话说,可以认为if letmatch的一个语法糖,它当值匹配某一模式时执行代码而忽略所有其他值。

    可以在if let中包含一个elseelse块中的代码与match表达式中的_分支块中的代码相同,这样的match表达式就等同于if letelse。回忆一下列表 6-4 中Coin枚举的定义,它的Quarter成员包含一个UsState值。如果想要计数所有不是 25 美分的硬币的同时也报告 25 美分硬币所属的州,可以使用这样一个match表达式:

    # #[derive(Debug)]
    @@ -3018,7 +3018,7 @@ commit 4f2dc564851dc04b271a2260c834643dfd86c724

    ch07-01-mod-and-the-filesystem.md
    -commit 6fc32eabcd09f7a130094767abadb691dfcdddf7

    +commit b0481ac44ff2594c6c240baa36357737739db445

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

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

    @@ -3836,14 +3836,14 @@ let answer = &hello[0]; let s = &hello[0..4];

    这里,s是一个&str,它包含字符串的头四个字节。早些时候,我们提到了这些字母都是两个字节长的,所以这意味着s将会是“Зд”。

    -

    那么如果获取&hello[0..1]会发生什么呢?回答是:在运行时会 panic,就跟访问 vector 中的无效索引时一样:

    +

    如果获取&hello[0..1]会发生什么呢?答案是:在运行时会 panic,就跟访问 vector 中的无效索引时一样:

    thread 'main' panicked at 'index 0 and/or 1 in `Здравствуйте` do not lie on
     character boundary', ../src/libcore/str/mod.rs:1694
     

    你应该小心谨慎的使用这个操作,因为它可能会使你的程序崩溃。

    遍历字符串的方法

    幸运的是,这里还有其他获取字符串元素的方式。

    -

    如果你需要操作单独的 Unicode 标量值,最好的选择是使用chars方法。堆“नमस्ते”调用chars方法会将其分开并返回六个char类型的值,接着就可以遍历结果来访问每一个元素了:

    +

    如果你需要操作单独的 Unicode 标量值,最好的选择是使用chars方法。对“नमस्ते”调用chars方法会将其分开并返回六个char类型的值,接着就可以遍历结果来访问每一个元素了:

    for c in "नमस्ते".chars() {
         println!("{}", c);
     }
    @@ -3879,7 +3879,7 @@ character boundary', ../src/libcore/str/mod.rs:1694
     
    commit 4f2dc564851dc04b271a2260c834643dfd86c724

    -

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

    +

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

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

    本章我们会介绍哈希 map 的基本 API,不过还有更多吸引人的功能隐藏于标准库中的HashMap定义的函数中。请一如既往地查看标准库文档来了解更多信息。

    新建一个哈希 map

    @@ -3891,9 +3891,9 @@ let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50);
    -

    注意必须首先use标准库中集合部分的HashMap。在这三个常用集合中,这个是最不常用的,所以并不包含在被 prelude 自动引用的功能中。标准库中对哈希 map 的支持也相对较少;例如,并没有内建的用于构建的宏。

    -

    就像 vector 一样,哈希 map 将他们的数据储存在堆上。这个HashMap的键类型是String而值类型是i32。同样类似于 vector,哈希 map 是同质的:所有的键必须是相同类型,值也必须都是相同类型。

    -

    另一个构建哈希 map 的方法是使用一个元组的 vector 的collect方法,其中每个元组包含一个键值对。collect方法可以将数据收集进一系列的集合类型,包括HashMap。例如,如果队伍的名字和初始分数分别在两个 vector 中,可以使用zip方法来创建一个元组的 vector,其中“Blue”与 10 是一对,依此类推。接着就可以使用collect方法将这个元组 vector 转换成一个HashMap

    +

    注意必须首先 use 标准库中集合部分的 HashMap。在这三个常用集合中,HashMap 是最不常用的,所以并没有被 prelude 自动引用。标准库中对 HashMap 的支持也相对较少,例如,并没有内建的构建宏。 +像 vector 一样,哈希 map 将他们的数据储存在堆上,这个 HashMap 的键类型是 String 而值类型是 i32。同样类似于 vector,哈希 map 是同质的:所有的键必须是相同类型,值也必须都是相同类型。

    +

    另一个构建哈希 map 的方法是使用一个元组的 vector 的 collect 方法,其中每个元组包含一个键值对。collect 方法可以将数据收集进一系列的集合类型,包括 HashMap。例如,如果队伍的名字和初始分数分别在两个 vector 中,可以使用 zip 方法来创建一个元组的 vector,其中“Blue”与 10 是一对,依此类推。接着就可以使用 collect 方法将这个元组 vector 转换成一个 HashMap

    use std::collections::HashMap;
     
     let teams  = vec![String::from("Blue"), String::from("Yellow")];
    @@ -3901,7 +3901,7 @@ let initial_scores = vec![10, 50];
     
     let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();
     
    -

    这里HashMap<_, _>类型注解是必要的,因为可能collect进很多不同的数据结构,而除非显式指定 Rust 无从得知你需要的类型。但是对于键和值的参数来说,可以使用下划线而 Rust 可以根据 vector 中数据的类型推断出哈希 map 所包含的类型。

    +

    这里HashMap<_, _>类型注解是必要的,因为可能collect进很多不同的数据结构,而除非显式指定 Rust 无从得知你需要的类型。但是对于键和值的类型参数来说,可以使用下划线占位,而 Rust 能够根据 vector 中数据的类型推断出 HashMap 所包含的类型。

    哈希 map 和所有权

    对于像i32这样的实现了Copy trait 的类型,其值可以拷贝进哈希 map。对于像String这样拥有所有权的值,其值将被移动而哈希 map 会成为这些值的所有者:

    use std::collections::HashMap;
    @@ -3927,7 +3927,7 @@ scores.insert(String::from("Yellow"), 50);
     let team_name = String::from("Blue");
     let score = scores.get(&team_name);
     
    -

    这里,score将会是与蓝队分数相关的值,而这个值将是Some(10)。因为get返回Option<V>所以结果被封装进Some;如果某个键在哈希 map 中没有对应的值,get会返回None。程序将需要采用第六章提到的方法中之一来处理Option

    +

    这里,score 是与蓝队分数相关的值,应为 Some(10)。因为 get 返回 Option<V>,所以结果被装进 Some;如果某个键在哈希 map 中没有对应的值,get 会返回 None。这时就要用某种第六章提到的方法来处理 Option

    可以使用与 vector 类似的方式来遍历哈希 map 中的每一个键值对,也就是for循环:

    use std::collections::HashMap;
     
    @@ -3945,7 +3945,7 @@ for (key, value) in &scores {
     Blue: 10
     

    更新哈希 map

    -

    虽然键值对的数量是可以增长的,不过每个单独的键同时只能关联一个值。当你想要改变哈希 map 中的数据时,必须选择是用新值替代旧值,还是完全无视旧值。我们也可以选择保留旧值而忽略新值,并只在键没有对应一个值时增加新值。或者可以结合新值和旧值。让我们看看着每一种方式是如何工作的!

    +

    尽管键值对的数量是可以增长的,不过任何时候,每个键只能关联一个值。当你想要改变哈希 map 中的数据时,根据目标键是否有值以及值的更新策略分成多种情况,下面我们了解一下:

    覆盖一个值

    如果我们插入了一个键值对,接着用相同的键插入一个不同的值,与这个键相关联的旧值将被替换。即便下面的代码调用了两次insert,哈希 map 也只会包含一个键值对,因为两次都是对蓝队的键插入的值:

    use std::collections::HashMap;
    @@ -3987,9 +3987,9 @@ for word in text.split_whitespace() {
     
     println!("{:?}", map);
     
    -

    这会打印出{"world": 2, "hello": 1, "wonderful": 1}or_insert方法事实上会返回这个键的值的一个可变引用(&mut V)。这里我们将这个可变引用储存在count变量中,所以为了赋值必须首先使用星号(*)解引用count。这个可变引用在for循环的结尾离开作用域,这样所有这些改变都是安全的并被借用规则所允许。

    +

    这会打印出{"world": 2, "hello": 1, "wonderful": 1}or_insert方法事实上会返回这个键的值的一个可变引用(&mut V)。这里我们将这个可变引用储存在count变量中,所以为了赋值必须首先使用星号(*)解引用count。这个可变引用在for循环的结尾离开作用域,这样所有这些改变都是安全的并符合借用规则。

    哈希函数

    -

    HashMap默认使用一个密码学上是安全的哈希函数,它可以提供抵抗拒绝服务(Denial of Service, DoS)攻击的能力。这并不是现有最快的哈希函数,不过为了更好的安全性带来一些性能下降也是值得的。如果你监控你的代码并发现默认哈希函数对你来说非常慢,可以通过指定一个不同的 hasher 来切换为另一个函数。hasher 是一个实现了BuildHasher trait 的类型。第十章会讨论 trait 和如何实现他们。你并不需要从头开始实现你自己的 hasher;crates.io 有其他人分享的实现了许多常用哈希算法的 hasher 的库。

    +

    HashMap默认使用一种密码学安全的哈希函数,它可以抵抗拒绝服务(Denial of Service, DoS)攻击。然而并不是最快的,不过为了更高的安全性值得付出一些性能的代价。如果性能监测显示此哈希函数非常慢,以致于你无法接受,你可以指定一个不同的 hasher 来切换为其它函数。hasher 是一个实现了BuildHasher trait 的类型。第十章会讨论 trait 和如何实现他们。你并不需要从头开始实现你自己的 hasher;crates.io 有其他人分享的实现了许多常用哈希算法的 hasher 的库。

    总结

    vector、字符串和哈希 map 会在你的程序需要储存、访问和修改数据时帮助你。这里有一些你应该能够解决的练习问题:

      @@ -4401,7 +4401,7 @@ values between 1 and 100

      如此获取一个参数并只返回 1 到 100 之间数字的函数就可以声明为获取或返回一个Guess,而不是u32,同时其函数体中也无需进行任何额外的检查。

      总结

      Rust 的错误处理功能被设计为帮助你编写更加健壮的代码。panic!宏代表一个程序无法处理的状态,并停止执行而不是使用无效或不正确的值继续处理。Rust 类型系统的Result枚举代表操作可能会在一种可以恢复的情况下失败。可以使用Result来告诉代码调用者他需要处理潜在的成功或失败。在适当的场景使用panic!Result将会使你的代码在面对无处不在的错误时显得更加可靠。

      -

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

      +

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

      泛型、trait 和生命周期

      ch10-00-generics.md @@ -4562,7 +4562,7 @@ fn main() { names and the types in their signatures

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

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

      -

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

      +

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

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

      fn largest<T>(list: &[T]) -> T {
       
      @@ -4603,7 +4603,7 @@ uses generic type parameters but doesn't compile yet

      | note: an implementation of `std::cmp::PartialOrd` might be missing for `T`
    -

    注释中提到了std::cmp::PartialOrd,这是一个 trait。下一部分会讲到 trait,不过简单来说,这个错误表明largest的函数体对T的所有可能的类型都无法工作;因为在函数体需要比较T类型的值,不过它只能用于我们知道如何排序的类型。标准库中定义的std::cmp::PartialOrd trait 可以实现类型的排序功能。在下一部分会再次回到 trait 并讲解如何为泛型指定一个 trait,不过让我们先把这个例子放在一边并探索其他那些可以使用泛型类型参数的地方。

    +

    注释中提到了std::cmp::PartialOrd,这是一个 trait。下一部分会讲到 trait,不过简单来说,这个错误表明largest的函数体不能适用于T的所有可能的类型;因为在函数体需要比较T类型的值,不过它只能用于我们知道如何排序的类型。标准库中定义的std::cmp::PartialOrd trait 可以实现类型的排序功能。在下一部分会再次回到 trait 并讲解如何为泛型指定一个 trait,不过让我们先把这个例子放在一边并探索其他那些可以使用泛型类型参数的地方。

    当为带有生命周期的结构体实现方法时,其语法依然类似列表 10-10 中展示的泛型类型参数的语法:包括声明生命周期参数的位置和生命周期参数是否与结构体字段或方法的参数与返回值相关联。

    -

    (实现方法时)结构体字段的生命周期必须总是在impl关键字之后声明并在结构体名称之后被适用,因为这些生命周期是结构体类型的一部分。

    +

    (实现方法时)结构体字段的生命周期必须总是在impl关键字之后声明并在结构体名称之后被使用,因为这些生命周期是结构体类型的一部分。

    impl块里的方法签名中,引用可能与结构体字段中的引用相关联,也可能是独立的。另外,生命周期省略规则也经常让我们无需在方法签名中使用生命周期注解。让我们看看一些使用列表 10-24 中定义的结构体ImportantExcerpt的例子。

    首先,这里有一个方法level。其唯一的参数是self的引用,而且返回值只是一个i32,并不引用任何值:

    # struct ImportantExcerpt<'a> {
    @@ -5399,7 +5399,7 @@ commit b7ab6668bbcb73b93c6464d8354c94a8e6c90395

    ch11-01-writing-tests.md
    -commit 55b294f20fc846a13a9be623bf322d8b364cee77

    +commit c6162d22288253b2f2a017cfe96cf1aa765c2955

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

    测试函数剖析

    @@ -5734,7 +5734,7 @@ fn greeting_contains_name() {

    现在如果再次运行测试,将会看到更有价值的错误信息:

    ---- tests::greeting_contains_name stdout ----
    -    thread 'tests::greeting_contains_name' panicked at 'Result did not contain
    +    thread 'tests::greeting_contains_name' panicked at 'Greeting did not contain
         name, value was `Hello`', src/lib.rs:12
     note: Run with `RUST_BACKTRACE=1` for a backtrace.
     
    @@ -7023,7 +7023,7 @@ error: test failed

    Listing 12-17: Iterating through each line in contents

    -

    lines方法返回一个迭代器。第十三张会深入了解迭代器,不过我们已经在列表 3-6 中见过使用迭代器的方法,在那里使用了一个for循环和迭代器在一个集合的每一项上运行一些代码。

    +

    lines方法返回一个迭代器。第十三章会深入了解迭代器,不过我们已经在列表 3-6 中见过使用迭代器的方法,在那里使用了一个for循环和迭代器在一个集合的每一项上运行一些代码。

    之后我们将修复Config::new的函数体。因为标准库文档也表明,std::env::Args实现了Iterator trait,所以我们知道可以调用其next方法!如下就是新的代码:

    # struct Config {
    -#     search: String,
    +#     query: String,
     #     filename: String,
     # }
     #
    @@ -7767,9 +7766,9 @@ impl Config {
         fn new(mut args: std::env::Args) -> Result<Config, &'static str> {
             args.next();
     
    -        let search = match args.next() {
    +        let query = match args.next() {
                 Some(arg) => arg,
    -            None => return Err("Didn't get a search string"),
    +            None => return Err("Didn't get a query string"),
             };
     
             let filename = match args.next() {
    @@ -7778,25 +7777,25 @@ impl Config {
             };
     
             Ok(Config {
    -            search: search,
    +            query: query,
                 filename: filename,
             })
         }
     }
     
    -

    还记得env::args返回值的第一个值是程序的名称吗。我们希望忽略它,所以首先调用next并不处理其返回值。第二次调用next的返回值应该是希望放入Configsearch字段的值。使用match来在next返回Some时提取值,而在因为没有足够的参数(这会造成next调用返回None)而提早返回Err值。

    -

    filename值也进行相同处理。稍微有些可惜的是searchfilenamematch表达式是如此的相似。如果可以对next返回的Option使用?就好了,不过目前?只能用于Result值。即便我们可以像Result一样对Option使用?,得到的值也是借用的,而我们希望能够将迭代器中的String移动到Config中。

    +

    还记得env::args返回值的第一个值是程序的名称吗。我们希望忽略它,所以首先调用next并不处理其返回值。第二次调用next的返回值应该是希望放入Configquery字段的值。使用match来在next返回Some时提取值,而在因为没有足够的参数(这会造成next调用返回None)而提早返回Err值。

    +

    filename值也进行相同处理。稍微有些可惜的是queryfilenamematch表达式是如此的相似。如果可以对next返回的Option使用?就好了,不过目前?只能用于Result值。即便我们可以像Result一样对Option使用?,得到的值也是借用的,而我们希望能够将迭代器中的String移动到Config中。

    使用迭代器适配器来使代码更简明

    -

    另一部分可以利用迭代器的代码位于列表 12-15 中实现的grep函数中:

    +

    另一部分可以利用迭代器的代码位于列表 12-15 中实现的search函数中:

    -
    fn grep<'a>(search: &str, contents: &'a str) -> Vec<&'a str> {
    +
    fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
         let mut results = Vec::new();
     
         for line in contents.lines() {
    -        if line.contains(search) {
    +        if line.contains(query) {
                 results.push(line);
             }
         }
    @@ -7805,22 +7804,22 @@ reviewed it. /Carol -->
     }
     

    我们可以用一种更简短的方式来编写这些代码,并避免使用了一个作为可变中间值的results vector,像这样使用迭代器适配器方法来实现:

    -
    fn grep<'a>(search: &str, contents: &'a str) -> Vec<&'a str> {
    +
    fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
         contents.lines()
    -        .filter(|line| line.contains(search))
    +        .filter(|line| line.contains(query))
             .collect()
     }
     
    -

    这里使用了filter适配器来只保留line.contains(search)为真的那些行。接着使用collect将他们放入另一个 vector 中。这就简单多了!

    -

    也可以对列表 12-16 中定义的grep_case_insensitive函数使用如下同样的技术:

    +

    这里使用了filter适配器来只保留line.contains(query)为真的那些行。接着使用collect将他们放入另一个 vector 中。这就简单多了!

    +

    也可以对列表 12-16 中定义的search_case_insensitive函数使用如下同样的技术:

    -
    fn grep_case_insensitive<'a>(search: &str, contents: &'a str) -> Vec<&'a str> {
    -    let search = search.to_lowercase();
    +
    fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    +    let query = query.to_lowercase();
     
         contents.lines()
             .filter(|line| {
    -            line.to_lowercase().contains(&search)
    +            line.to_lowercase().contains(&query)
             }).collect()
     }
     
    @@ -8228,7 +8227,7 @@ test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
    commit 4f2dc564851dc04b271a2260c834643dfd86c724

    -

    cargo install命令用于在本地安装和使用二进制 crate。它并不打算替换系统中的包;它意在作为一个方便 Rust 开发者安装他人在 crates.io 共享的工具的手段。只有有二进制目标文件的包能够安装,而且所有二进制文件都被安装到 Rust 安装根目录的 bin 文件夹中。如果你使用 rustup.rs 安装的 Rust 且没有自定义任何配置,这将是$HOME/.cargo/bin。将这个目录添加到$PATH环境变量中就能够运行通过cargo install安装的程序了。

    +

    cargo install命令用于在本地安装和使用二进制 crate。它并不打算替换系统中的包;它意在作为一个方便 Rust 开发者们安装其他人已经在 crates.io 上共享的工具的手段。只有有二进制目标文件的包能够安装,而且所有二进制文件都被安装到 Rust 安装根目录的 bin 文件夹中。如果你使用 rustup.rs 安装的 Rust 且没有自定义任何配置,这将是$HOME/.cargo/bin。将这个目录添加到$PATH环境变量中就能够运行通过cargo install安装的程序了。

    例如,第十二章提到的叫做ripgrep的用于搜索文件的grep的 Rust 实现。如果想要安装ripgrep,可以运行如下:

    $ cargo install ripgrep
     Updating registry `https://github.com/rust-lang/crates.io-index`
    @@ -8274,7 +8273,7 @@ commit 4f2dc564851dc04b271a2260c834643dfd86c724


    commit 85b2c9ac704c9dc4bbedb97209d336afb9809dc1

    -

    最简单直接的智能指针是 box,它的类型是Box<T>。 box 允许你将一个单独的值放在堆上(第四章介绍或栈与堆)。列表 15-1 展示了如何使用 box 在堆上储存一个i32

    +

    最简单直接的智能指针是 box,它的类型是Box<T>。 box 允许你将一个值放在堆上(第四章介绍过栈与堆)。列表 15-1 展示了如何使用 box 在堆上储存一个i32

    Filename: src/main.rs

    fn main() {
         let b = Box::new(5);
    @@ -8293,7 +8292,7 @@ box

    Listing 15-2: The first attempt of defining an enum to represent a cons list data structure of i32 values

    -

    我们实现了一个只存放i32值的 cons list。也可以选择实用第十章介绍的泛型来实现一个类型无关的 cons list。

    +

    我们实现了一个只存放i32值的 cons list。也可以选择使用第十章介绍的泛型来实现一个类型无关的 cons list。

    cons list 的更多内容

    cons list 是一个来源于 Lisp 编程语言及其方言的数据结构。在 Lisp 中,cons函数("construct function"的缩写)利用两个参数来构造一个新的列表,他们通常是一个单独的值和另一个列表。

    @@ -8360,7 +8359,7 @@ fn main() {

    Listing 15-5: Definition of List that uses Box<T> in order to have a known size

    -

    这样编译器就能够计算出储存一个List值需要的大小了。Rust 将会检查List,同样的从Cons成员开始检查。Cons成员需要i32的大小加上一个usize的大小,因为 box 总是usize大小的,不管它指向的是什么。接着 Rust 检查Nil成员,它并储存一个值,所以Nil并不需要任何空间。我们通过 box 打破了这无限递归的连锁。图 15-6 展示了现在Cons成员看起来像什么:

    +

    这样编译器就能够计算出储存一个List值需要的大小了。Rust 将会检查List,同样的从Cons成员开始检查。Cons成员需要i32的大小加上一个usize的大小,因为 box 总是usize大小的,不管它指向的是什么。接着 Rust 检查Nil成员,它并不储存一个值,所以Nil并不需要任何空间。我们通过 box 打破了这无限递归的连锁。图 15-6 展示了现在Cons成员看起来像什么:

    A finite Cons list

    Figure 15-6: A List that is not infinitely sized since Cons holds a Box

    @@ -8372,7 +8371,7 @@ order to have a known size


    commit ecc3adfe0cfa0a4a15a178dc002702fd0ea74b3f

    -

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

    +

    第一个智能指针相关的重要 trait 是Deref,它允许我们重载*,解引用运算符(不同于乘法运算符和全局引用运算符)。重载智能指针的*能使访问其持有的数据更为方便,在本章结束前谈到解引用强制多态时我们会说明方便的意义。

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

    let mut x = 5;
     {
    @@ -8383,7 +8382,7 @@ commit ecc3adfe0cfa0a4a15a178dc002702fd0ea74b3f

    assert_eq!(6, x);
    -

    我们使用*y来访问可变引用y所指向的数据,而是可变引用本身。接着可以修改它的数据,在这里对其加一。

    +

    我们使用*y来访问可变引用y所指向的数据,而不是可变引用本身。接着可以修改它的数据,在这里对其加一。

    引用并不是智能指针,他们只是引用指向的一个值,所以这个解引用操作是很直接的。智能指针还会储存指针或数据的元数据。当解引用一个智能指针时,我们只想要数据,而不需要元数据。我们希望能在使用常规引用的地方也能使用智能指针。为此,可以通过实现Deref trait 来重载*运算符的行为。

    列表 15-7 展示了一个定义为储存 mp3 数据和元数据的结构体通过Deref trait 来重载*的例子。Mp3,在某种意义上是一个智能指针:它拥有包含音频的Vec<u8>数据。另外,它储存了一些可选的元数据,在这个例子中是音频数据中艺术家和歌曲的名称。我们希望能够方便的访问音频数据而不是元数据,所以需要实现Deref trait 来返回音频数据。实现Deref trait 需要一个叫做deref的方法,它借用self并返回其内部数据:

    Filename: src/main.rs

    @@ -8421,7 +8420,7 @@ struct that holds mp3 file data and metadata

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

    *(my_favorite_song.deref())
     
    -

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

    +

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

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

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

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

    @@ -8446,7 +8445,7 @@ struct that holds mp3 file data and metadata

  • &mut T&UT: Deref<Target=U>
  • 头两个情况除了可变性之外是相同的:如果有一个&T,而T实现了返回U类型的Deref,可以直接得到&U。对于可变引用也是一样。最后一个有些微妙:如果有一个可变引用,它也可以强转为一个不可变引用。反之则是_不可能_的:不可变引用永远也不能强转为可变引用。

    -

    Deref trait 对于智能指针模式十分重要的原因在于智能指针可以被看作普通引用并用于期望使用引用的地方。例如,无需重新编写方法和函数来直接获取智能指针。

    +

    Deref trait 对于智能指针模式十分重要的原因在于智能指针可以被看作普通引用并被用于期望使用普通引用的地方。例如,无需重新编写方法和函数来直接获取智能指针。

    Drop Trait 运行清理代码

    ch15-03-drop.md @@ -8454,9 +8453,9 @@ struct that holds mp3 file data and metadata

    commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

    对于智能指针模式来说另一个重要的 trait 是DropDrop运行我们在值要离开作用域时执行一些代码。智能指针在被丢弃时会执行一些重要的清理工作,比如释放内存或减少引用计数。更一般的来讲,数据类型可以管理多于内存的资源,比如文件或网络连接,而使用Drop在代码处理完他们之后释放这些资源。我们在智能指针上下文中讨论Drop是因为其功能几乎总是用于实现智能指针。

    -

    在其他一些语言中,必须每次总是必须记住在使用完智能指针实例后调用清理内存或资源的代码。如果忘记的话,运行代码的系统可能会因为负荷过重而崩溃。在 Rust 中,可以指定一些代码应该在值离开作用域时被执行,而编译器会自动插入这些代码。这意味着无需记住在所有处理完这些类型实例后调用清理代码,而仍然不会泄露资源!

    +

    在其他一些语言中,我们不得不记住在每次使用完智能指针实例后调用清理内存或资源的代码。如果忘记的话,运行代码的系统可能会因为负荷过重而崩溃。在 Rust 中,可以指定一些代码应该在值离开作用域时被执行,而编译器会自动插入这些代码。这意味着无需记住在所有处理完这些类型实例后调用清理代码,而仍然不会泄露资源!

    指定在值离开作用域时应该执行的代码的方式是实现Drop trait。Drop trait 要求我们实现一个叫做drop的方法,它获取一个self的可变引用。

    -

    列表 15-8 展示了并没有实际功能的结构体CustomSmartPointer,不过我们会在创建实例之后打印出CustomSmartPointer created.,而在实例离开作用域时打印出Dropping CustomSmartPointer!,这样就能看出哪些代码被执行了。不同于println!语句,我们在智能指针需要执行清理代码时使用drop

    +

    列表 15-8 展示了并没有实际功能的结构体CustomSmartPointer,不过我们会在创建实例之后打印出CustomSmartPointer created.,而在实例离开作用域时打印出Dropping CustomSmartPointer!,这样就能看出每一段代码是何时被执行的。实际的项目中,我们应该在drop中清理任何智能指针运行所需要的资源,而不是这个例子中的println!语句:

    Filename: src/main.rs

    struct CustomSmartPointer {
         data: String,
    @@ -8477,7 +8476,7 @@ fn main() {
     

    Listing 15-8: A CustomSmartPointer struct that implements the Drop trait, where we could put code that would clean up after the CustomSmartPointer.

    -

    Drop trait 位于 prelude 中,所以无需导入它。drop方法的实现调用了println!;这里是你需要实际需要放入关闭套接字代码的地方。在main函数中,我们创建一个CustomSmartPointer的新实例并打印出CustomSmartPointer created.以便在运行时知道代码运行到此出。在main的结尾,CustomSmartPointer的实例会离开作用域。注意我们没有显式调用drop方法:

    +

    Drop trait 位于 prelude 中,所以无需导入它。drop方法的实现调用了println!;这里是你需要放入实际关闭套接字代码的地方。在main函数中,我们创建一个CustomSmartPointer的新实例并打印出CustomSmartPointer created.以便在运行时知道代码运行到此处。在main的结尾,CustomSmartPointer的实例会离开作用域。注意我们没有显式调用drop方法:

    当运行这个程序,我们会看到:

    CustomSmartPointer created.
     Wait for it...
    @@ -8557,7 +8556,7 @@ to share ownership of a third list won't work

    implement the `Copy` trait

    Cons成员拥有其储存的数据,所以当创建b列表时将a的所有权移动到了b。接着当再次尝使用a创建c时,这不被允许因为a的所有权已经被移动。

    -

    相反可以改变Cons的定义来存放一个引用,不过接着必须指定生命周期参数,而且也必须以使得列表的每一个元素都与列表本身存在的一样久那样构造列表的元素。否则借用检查器甚至都不会允许我们编译代码。

    +

    相反可以改变Cons的定义来存放一个引用,不过接着必须指定生命周期参数,而且在构造列表时,也必须使列表中的每一个元素都至少与列表本身存在的一样久。否则借用检查器甚至都不会允许我们编译代码。

    如列表 15-12 所示,可以将List的定义从Box<T>改为Rc<T>

    Filename: src/main.rs

    enum List {
    @@ -8578,7 +8577,7 @@ fn main() {
     Rc<T>

    注意必须为Rc增加use语句因为它不在 prelude 中。在main中创建了存放 5 和 10 的列表并将其存放在一个叫做a的新的Rc中。接着当创建bc时,我们对a调用了clone方法。

    克隆Rc<T>会增加引用计数

    -

    之前我们见过clone方法,当时使用它来创建某些数据的完整拷贝。但是对于Rc<T>来说,它并不创建一个完整的拷贝。Rc<T>存放了引用计数,也就是说,一个存在多少个克隆的数量。让我们像列表 15-13 那样在创建c时增加一个内部作用域,并在不同的位置打印出关联函数Rc::strong_count的结果。Rc::strong_count返回传递给它的Rc值的引用计数,而在本章的稍后部分介绍避免引用循环时讲到它为什么叫做strong_count

    +

    之前我们见过clone方法,当时使用它来创建某些数据的完整拷贝。但是对于Rc<T>来说,它并不创建一个完整的拷贝。Rc<T>存放了引用计数,也就是说,一个存在多少个克隆的计数器。让我们像列表 15-13 那样在创建c时增加一个内部作用域,并在不同的位置打印出关联函数Rc::strong_count的结果。Rc::strong_count返回传递给它的Rc值的引用计数,而在本章的稍后部分介绍避免引用循环时讲到它为什么叫做strong_count

    Filename: src/main.rs

    # enum List {
     #     Cons(i32, Rc<List>),
    @@ -8630,8 +8629,8 @@ commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

  • 引用必须总是有效的。
  • 对于引用和Box<T>,借用规则的不可变性作用于编译时。对于RefCell<T>,这些不可变性作用于运行时。对于引用,如果违反这些规则,会得到一个编译错误。而对于RefCell<T>,违反这些规则会panic!

    -

    Rust 编译器执行的静态分析时天生保守的。代码的一些属性则不可能通过分析代码发现:其中最著名的就是停机问题(停机问题),这超出了本书的范畴,不过如果你感兴趣的话这是一个值得研究的有趣主题。

    -

    因为一些分析是不可能的,Rust 编译器在其不确定的时候甚至都不尝试猜测,所以说它是保守的而且有时会拒绝事实上不会违反 Rust 保证的正确的程序。换句话说,如果 Rust 接受不正确的程序,那么人们也就不会相信 Rust 所做的保证了。如果 Rust 拒绝正确的程序,会给程序员带来不变,但不会带来灾难。RefCell<T>正是用于当你知道代码遵守借用规则,而编译器不能理解的时候。

    +

    Rust 编译器执行的静态分析天生是保守的。代码的一些属性则不可能通过分析代码发现:其中最著名的就是停机问题(停机问题),这超出了本书的范畴,不过如果你感兴趣的话这是一个值得研究的有趣主题。

    +

    因为一些分析是不可能的,Rust 编译器在其不确定的时候甚至都不尝试猜测,所以说它是保守的而且有时会拒绝事实上不会违反 Rust 保证的正确的程序。换句话说,如果 Rust 接受不正确的程序,那么人们也就不会相信 Rust 所做的保证了。如果 Rust 拒绝正确的程序,会给程序员带来不便,但不会带来灾难。RefCell<T>正是用于当你知道代码遵守借用规则,而编译器不能理解的时候。

    类似于Rc<T>RefCell<T>只能用于单线程场景。在并发章节会介绍如何在多线程程序中使用RefCell<T>的功能。现在所有你需要知道的就是如果尝试在多线程上下文中使用RefCell<T>,会得到一个编译错误。

    对于引用,可以使用&&mut语法来分别创建不可变和可变的引用。不过对于RefCell<T>,我们使用borrowborrow_mut方法,它是RefCell<T>拥有的安全 API 的一部分。borrow返回Ref类型的智能指针,而borrow_mut返回RefMut类型的智能指针。这两个类型实现了Deref所以可以被当作常规引用处理。RefRefMut动态的借用所有权,而他们的Drop实现也动态的释放借用。

    列表 15-14 展示了如何使用RefCell<T>来使函数不可变的和可变的借用它的参数。注意data变量使用let data而不是let mut data来声明为不可变的,而a_fn_that_mutably_borrows则允许可变的借用数据并修改它!

    @@ -8700,7 +8699,7 @@ thread 'main' panicked at 'already borrowed: BorrowMutError', /stable-dist-rustc/build/src/libcore/result.rs:868 note: Run with `RUST_BACKTRACE=1` for a backtrace.
    -

    这个运行时BorrowMutError类似于编译错误:它表明已经可变的借用过s一次了,所以不允许再次借用它。我们并没有绕过借用规则,只是选择让 Rust 在运行时而不是编译时执行他们。你可以选择在任何时候任何地方使用RefCell<T>,不过除了不得不编写很多RefCell之外,最终还是可能会发现其中的问题(可能是在生产环境而不是开发环境)。另外,在运行时检查借用规则有性能惩罚。

    +

    这个运行时BorrowMutError类似于编译错误:它表明我们已经可变得借用过一次s了,所以不允许再次借用它。我们并没有绕过借用规则,只是选择让 Rust 在运行时而不是编译时执行他们。你可以选择在任何时候任何地方使用RefCell<T>,不过除了不得不编写很多RefCell之外,最终还是可能会发现其中的问题(可能是在生产环境而不是开发环境)。另外,在运行时检查借用规则有性能惩罚。

    结合Rc<T>RefCell<T>来拥有多个可变数据所有者

    那么为什么要权衡考虑选择引入RefCell<T>呢?好吧,还记得我们说过Rc<T>只能拥有一个T的不可变引用吗?考虑到RefCell<T>是不可变的,但是拥有内部可变性,可以将Rc<T>RefCell<T>结合来创造一个既有引用计数又可变的类型。列表 15-15 展示了一个这么做的例子,再次回到列表 15-5 中的 cons list。在这个例子中,不同于在 cons list 中储存i32值,我们储存一个Rc<RefCell<i32>>值。希望储存这个类型是因为其可以拥有不属于列表一部分的这个值的所有者(Rc<T>提供的多个所有者功能),而且还可以改变内部的i32值(RefCell<T>提供的内部可变性功能):

    Filename: src/main.rs

    @@ -8745,11 +8744,11 @@ c after = Cons(RefCell { value: 10 }, Cons(RefCell { value: 15 }, Nil))

    ch15-06-reference-cycles.md
    -commit c49e5ee8859f8eb8f8867cbeafbdf5b802aa5894

    +commit 9430a3d28a2121a938d704ce48b15d21062f880e

    我们讨论过 Rust 做出的一些保证,例如永远也不会遇到一个空值,而且数据竞争也会在编译时被阻止。Rust 的内存安全保证也使其更难以制造从不被清理的内存,这被称为内存泄露。然而 Rust 并不是不可能出现内存泄漏,避免内存泄露不是 Rust 的保证之一。换句话说,内存泄露是安全的。

    在使用Rc<T>RefCell<T>时,有可能创建循环引用,这时各个项相互引用并形成环。这是不好的因为每一项的引用计数将永远也到不了 0,其值也永远也不会被丢弃。让我们看看这是如何发生的以及如何避免它。

    -

    在列表 15-16 中,我们将使用列表 15-5 中List定义的另一个变体。我们将回到储存i32值作为Cons成员的第一个元素。现在Cons成员的第二个元素是RefCell<Rc<List>>:这时就不能修改i32值了,但是能够修改Cons成员指向的哪个List。还需要增加一个tail方法来方便我们在拥有一个Cons成员时访问第二个项:

    +

    在列表 15-16 中,我们将使用列表 15-5 中List定义的另一个变体。我们将回到储存i32值作为Cons成员的第一个元素。现在Cons成员的第二个元素是RefCell<Rc<List>>:这时就不能修改i32值了,但是能够修改Cons成员指向的那个List。还需要增加一个tail方法来方便我们在拥有一个Cons成员时访问第二个项:

    Filename: src/main.rs

    #[derive(Debug)]
     enum List {
    @@ -8826,7 +8825,7 @@ pointing to each other

    举例来说,对于像图这样的数据结构,为了创建父节点指向子节点的边和以相反方向从子节点指向父节点的边,有时需要创建这样的引用循环。如果一个方向拥有所有权而另一个方向没有,对于模拟这种数据关系的一种不会创建引用循环和内存泄露的方式是使用Weak<T>。接下来让我们探索一下!

    避免引用循环:将Rc<T>变为Weak<T>

    Rust 标准库中提供了Weak<T>,一个用于存在引用循环但只有一个方向有所有权的智能指针。我们已经展示过如何克隆Rc<T>来增加引用的strong_countWeak<T>是一种引用Rc<T>但不增加strong_count的方式:相反它增加Rc引用的weak_count。当Rc离开作用域,其内部值会在strong_count为 0 的时候被丢弃,即便weak_count不为 0 。为了能够从Weak<T>中获取值,首先需要使用upgrade方法将其升级为Option<Rc<T>>。升级Weak<T>的结果在Rc还未被丢弃时是Some,而在Rc被丢弃时是None。因为upgrade返回一个Option,我们知道 Rust 会确保SomeNone的情况都被处理并不会尝试使用一个无效的指针。

    -

    不同于列表 15-17 中每个项只知道它的下一项,加入我们需要一个树,它的项知道它的子项父项。

    +

    不同于列表 15-17 中每个项只知道它的下一项,假如我们需要一个树,它的项知道它的子项父项。

    让我们从一个叫做Node的存放拥有所有权的i32值和其子Node值的引用的结构体开始:

    use std::rc::Rc;
     use std::cell::RefCell;
    @@ -8954,27 +8953,27 @@ examining strong and weak reference counts of leaf and branch
     

    总结

    现在我们学习了如何选择不同类型的智能指针来选择不同的保证并与 Rust 的常规引用向取舍。Box<T>有一个已知的大小并指向分配在堆上的数据。Rc<T>记录了堆上数据的引用数量这样就可以拥有多个所有者。RefCell<T>和其内部可变性使其可以用于需要不可变类型,但希望在运行时而不是编译时检查借用规则的场景。

    我们还介绍了提供了很多智能指针功能的 trait DerefDrop。同时探索了形成引用循环和造成内存泄漏的可能性,以及如何使用Weak<T>避免引用循环。

    -

    如果本章内容引起了你的兴趣并希望现在就实现你自己的智能指针的话,请阅读 The Nomicon 来获取更多有用的信息。

    -

    接下来,让我们谈谈 Rust 的并发。我们还会学习到一些新的堆并发有帮助的智能指针。

    +

    如果本章内容引起了你的兴趣并希望现在就实现你自己的智能指针的话,请阅读 The Nomicon 来获取更多有用的信息。

    +

    接下来,让我们谈谈 Rust 的并发。我们还会学习到一些新的对并发有帮助的智能指针。

    无畏并发

    ch16-00-concurrency.md
    commit da15de39eaabd50100d6fa662c653169254d9175

    -

    确保内存安全并不是 Rust 的唯一目标:作为一个能更好的处理并发和并行编程一直是 Rust 的另一个主要目标。 +

    确保内存安全并不是 Rust 的唯一目标:更好的处理并发和并行编程一直是 Rust 的另一个主要目标。 并发编程(concurrent programming)代表程序的不同部分相互独立的执行,而并行编程代表程序不同部分同时执行,这两个概念在计算机拥有更多处理器可供程序利用时变得更加重要。由于历史的原因,在此类上下文中编程一直是困难且容易出错的:Rust 希望能改变这一点。

    最开始,我们认为内存安全和防止并发问题是需要通过两个不同的方法解决的两个相互独立的挑战。然而,随着时间的推移,我们发现所有权和类型系统是一系列解决内存安全并发问题的强用力的工具!通过改进所有权和类型检查,很多并发错误在 Rust 中都是编译时错误,而不是运行时错误。我们给 Rust 的这一部分起了一个绰号无畏并发fearless concurrency)。无畏并发意味着 Rust 不光允许你自信代码不会出现诡异的错误,也让你可以轻易重构这种代码而无需担心会引入新的 bug。

    -

    注意:对于 Rust 的口号无畏并发,这里用并发指代很多问题而不是更精确的区分并发和(或)并行,是处于简化问题的原因。如果这是一本专注于并发和/或并行的书,我们肯定会更精确的。对于本章,请自行脑补任何并发并发和(或)并行

    +

    注意:对于 Rust 的口号无畏并发,这里用并发指代很多问题而不是更精确的区分并发和(或)并行,是出于简化问题的原因。如果这是一本专注于并发和/或并行的书,我们肯定会更精确的。对于本章,当我们谈到并发时,请自行替换为并发和(或)并行

    -

    很多语言对于其所提供的处理并发并发问题的解决方法是非常固执己见的。这是一个非常合理的策略,尤其是对于更高级的语言来说,不过对于底层语言来说可没有奢侈的选择。底层语言被期望为能在任何给定的场景中启用提供最高性能的方法,同时他们对硬件有更少的抽象。因此,Rust 给了我们多种工具来以适合场景和要求的方式来为问题建模。

    +

    很多语言所提供的处理并发问题的解决方法都非常有特色,尤其是对于更高级的语言,这是一个非常合理的策略。然而对于底层语言则没有奢侈的选择。在任何给定的情况下,我们都期望底层语言可以提供最高的性能,并且对硬件有更薄的抽象。因此,Rust 给了我们多种工具,并以适合实际情况和需求的方式来为问题建模。

    如下是本章将要涉及到的内容:

      -
    • 如果创建线程来同时运行多段代码。
    • +
    • 如何创建线程来同时运行多段代码。
    • 并发消息传递Message passing),其中通道(channel)被用来在线程间传递消息。
    • 并发共享状态Shared state),其中多个线程可以访问同一片数据。
    • -
    • SyncSend trait,他们允许 Rust 的并发保证能扩展到用户定义的和标准库中提供的类型中。
    • +
    • SyncSend trait,他们允许 Rust 的并发保证能被扩展到用户定义的和标准库中提供的类型中。

    使用线程同时运行代码

    @@ -8984,10 +8983,10 @@ commit 55b294f20fc846a13a9be623bf322d8b364cee77

    在今天使用的大部分操作系统中,当程序执行时,操作系统运行代码的上下文称为进程process)。操作系统可以运行很多进程,而操作系统也管理这些进程使得多个程序可以在电脑上同时运行。

    我们可以将每个进程运行一个程序的概念再往下抽象一层:程序也可以在其上下文中同时运行独立的部分。这个功能叫做线程thread)。

    -

    将程序需要执行的计算拆分到多个线程中可以提高性能,因为程序可以在同时进行很多工作。不过使用线程会增加程序复杂性。因为线程是同时运行的,所以无法预先保证不同线程中的代码的执行顺序。这可能会由于线程以不一致的顺序访问数据或资源而导致竞争状态,或由于两个线程相互阻止对方继续运行而造成死锁,以及仅仅出现于特定场景并难以稳定重现的 bug。Rust 减少了这些或那些使用线程的负面影响,不过在多线程上下文中编程仍然需要以与只期望在单个线程中编程不同的方式思考和组织代码。

    +

    将程序需要执行的计算拆分到多个线程中可以提高性能,因为程序可以在同时进行很多工作。不过使用线程会增加程序复杂性。因为线程是同时运行的,所以无法预先保证不同线程中的代码的执行顺序。这可能会由于线程以不一致的顺序访问数据或资源而导致竞争状态,或由于两个线程相互阻止对方继续运行而造成死锁,以及仅仅出现于特定场景并难以稳定重现的 bug。Rust 减少了这些或那些使用线程的负面影响,不过在多线程上下文中编程,相比只期望在单个线程中运行的程序,仍然要采用不同的思考方式和代码结构。

    编程语言有一些不同的方法来实现线程。很多操作系统提供了创建新线程的 API。另外,很多编程语言提供了自己的特殊的线程实现。编程语言提供的线程有时被称作轻量级lightweight)或绿色green)线程。这些语言将一系列绿色线程放入不同数量的操作系统线程中执行。因为这个原因,语言调用操作系统 API 创建线程的模型有时被称为 1:1,一个 OS 线程对应一个语言线程。绿色线程模型被称为 M:N 模型,M个绿色线程对应N个 OS 线程,这里MN不必相同。

    -

    每一个模型都有其自己的优势和取舍。对于 Rust 来说最重要的取舍是运行时支持。运行时是一个令人迷惑的概念;在不同上下文中它可能有不同的含义。这里其代表二进制文件中包含的语言自身的代码。对于一些语言,这些代码是庞大的,另一些则很小。通俗的说,“没有运行时”通常被人们用来指代“小运行时”,因为任何非汇编语言都存在一定数量的运行时。更小的运行时拥有更少的功能不过其优势在于更小的二进制输出。更小的二进制文件更容易在更多上下文中与其他语言结合。虽然很多语言觉得增加运行时来换取更多功能没有什么问题,但是 Rust 需要做到几乎没有运行时,同时不能在为了维持性能而能够在 C 语言中调用方面做出妥协。

    -

    绿色线程模型功能要求更大的运行时来管理这些线程。为此,Rust 标准库只提供了 1:1 线程模型实现。因为 Rust 是这么一个底层语言,所以有相应的 crate 实现了 M:N 线程模型,如果你宁愿牺牲性能来换取例如更多的线程控制和更低的上下文切换消耗。

    +

    每一个模型都有其自己的优势和取舍。对于 Rust 来说最重要的取舍是运行时支持。运行时是一个令人迷惑的概念;在不同上下文中它可能有不同的含义。这里其代表二进制文件中包含的语言自身的代码。对于一些语言,这些代码是庞大的,另一些则很小。通俗的说,“没有运行时”通常被人们用来指代“小运行时”,因为任何非汇编语言都存在一定数量的运行时。更小的运行时拥有更少的功能不过其优势在于更小的二进制输出。更小的二进制文件更容易在更多上下文中与其他语言结合。虽然很多语言觉得增加运行时来换取更多功能没有什么问题,但是 Rust 需要做到几乎没有运行时,同时为了保持高性能必需能够调用 C 语言,这点也是不能妥协的。

    +

    绿色线程模型功能要求更大的运行时来管理这些线程。为此,Rust 标准库只提供了 1:1 线程模型实现。因为 Rust 是这么一个底层语言,所以有相应的 crate 实现了 M:N 线程模型,如果你宁愿牺牲性能来换取例如更好的线程运行控制和更低的上下文切换成本。

    现在我们明白了 Rust 中的线程是如何定义的,让我们开始探索如何使用标准库提供的线程相关的 API吧。

    使用spawn创建新线程

    为了创建一个新线程,调用thread::spawn函数并传递一个闭包(第十三章学习了闭包),它包含希望在新线程运行的代码。列表 16-1 中的例子在新线程中打印了一些文本而其余的文本在主线程中打印:

    @@ -9147,7 +9146,7 @@ fn main() {

    Listing 16-4: A thread with a closure that attempts to capture a reference to v from a main thread that drops v

    -

    这些代码可以运行,而新建线程则可能直接就出错了并完全没有机会运行。新建线程内部有一个v的引用,不过主线程仍在执行:它立刻丢弃了v,使用了第十五章提到的显式丢弃其参数的drop函数。接着,新建线程开始执行,现在v是无效的了,所以它的引用也就是无效得的。噢,这太糟了!

    +

    这些代码可以运行,而新建线程则可能直接就出错了并完全没有机会运行。新建线程内部有一个v的引用,不过主线程仍在执行:它立刻丢弃了v,使用了第十五章提到的显式丢弃其参数的drop函数。接着,新建线程开始执行,现在v是无效的了,所以它的引用也就是无效的。噢,这太糟了!

    为了修复这个问题,我们可以听取错误信息的建议:

    help: to force the closure to take ownership of `v` (and any other referenced
     variables), use the `move` keyword, as shown:
    @@ -9290,7 +9289,7 @@ it down the channel

    我们的并发错误会造成一个编译时错误!send获取其参数的所有权并移动这个值归接收者所有。这个意味着不可能意外的在发送后再次使用这个值;所有权系统检查一切是否合乎规则。

    在这一点上,消息传递非常类似于 Rust 的单所有权系统。消息传递的拥护者出于相似的原因支持消息传递,就像 Rustacean 们欣赏 Rust 的所有权一样:单所有权意味着特定类型问题的消失。如果一次只有一个线程可以使用某些内存,就没有出现数据竞争的机会。

    发送多个值并观察接收者的等待

    -

    列表 16-8 中的代码可以编译和运行,不过这并不是很有趣:通过它难以看出两个独立的线程在一个通道上相互通讯。列表 16-10 则有一些改进会证明这些代码是并发执行的:新建线程现在会发送多个消息并在每个消息之间暂定一段时间。

    +

    列表 16-8 中的代码可以编译和运行,不过这并不是很有趣:通过它难以看出两个独立的线程在一个通道上相互通讯。列表 16-10 则有一些改进会证明这些代码是并发执行的:新建线程现在会发送多个消息并在每个消息之间暂停一段时间。

    Filename: src/main.rs

    use std::thread;
     use std::sync::mpsc;
    @@ -9402,18 +9401,18 @@ commit 9df612e93e038b05fc959db393c15a5402033f47

    communicating.

    不要共享内存来通讯;而是要通讯来共享内存。

    -

    那么“共享内存来通讯”看起来是怎样的呢?共享内存并发有点像多所有权:多个线程可以同时访问相同的内存位置。正如第十五章中智能指针使得多所有权成为可能时我们所看到的,这会增加额外的复杂性,因为需要以某种方式管理这些不同的所有者。

    -

    但是 Rust 的类型系统和所有权可以很好的帮助我们正确的进行管理。例如,让我们看看一个共享内存中更常见的并发原语:互斥器(mutexes)。

    +

    那么“共享内存来通讯”是怎样的呢?共享内存并发有点像多所有权:多个线程可以同时访问相同的内存位置。第十五章介绍了智能指针如何使得多所有权成为可能,然而这会增加额外的复杂性,因为需要以某种方式管理这些不同的所有者。

    +

    不过 Rust 的类型系统和所有权可以很好的帮助我们,正确的管理它们。以共享内存中更常见的并发原语:互斥器(mutexes)为例,让我们看看具体的情况。

    互斥器一次只允许一个线程访问数据

    -

    互斥器mutex)是一个用于共享内存的并发原语。它是“mutual exclusion”的缩写,也就是说,任何给定时间它只允许一个线程访问某些数据。互斥器以难以使用著称,因为你不得不记住:

    +

    互斥器mutex)是一种用于共享内存的并发原语。它是“mutual exclusion”的缩写,也就是说,任意时间,它只允许一个线程访问某些数据。互斥器以难以使用著称,因为你不得不记住:

      -
    1. 必须记住在使用数据之前尝试获取锁。
    2. -
    3. 一旦处理完被互斥器所保护的数据之后,必须记得解锁数据这样其他线程才能够获取锁。
    4. +
    5. 在使用数据之前尝试获取锁。
    6. +
    7. 处理完被互斥器所保护的数据之后,必须解锁数据,这样其他线程才能够获取锁。
    -

    对于一个现实中的互斥器的例子,想象一下在一个会议中的专门小组讨论会上,不过只有一个麦克风。在一个小组成员可能发言之前,他们必须请求或示意他们需要使用麦克风。一旦得到了麦克风,他们可以发言任意长的时间,接着将麦克风交给系一个希望讲话的小组成员。如果小组成员在没有麦克风的时候就开始叫喊或者在其他成员发言结束之前就取得麦克风将是很无理的。如果对这个共享的麦克风的管理因为任何这些原因出现问题,讨论会将无法如期进行。

    -

    正确的管理互斥器是异常复杂的,这也就是为什么这么多人都热衷于通道。然而,在 Rust 中,得益于类型系统和所有权,我们不可能会在锁和解锁上出错。

    +

    现实中也有互斥器的例子,想象一下在一个会议中,只有一个麦克风。如果一个成员要发言,他必须请求使用麦克风。一旦得到了麦克风,他可以畅所欲言,然后将麦克风交给下一个希望讲话的成员。如果成员在没有麦克风的时候就开始叫喊,或者在其他成员发言结束之前就拿走麦克风,是很不合适的。如果这个共享的麦克风因为此类原因而出现问题,会议将无法正常进行。

    +

    正确的管理互斥器异常复杂,这也是许多人之所以热衷于通道的原因。然而,在 Rust 中,得益于类型系统和所有权,我们不会在锁和解锁上出错。

    Mutex<T>的 API

    -

    让我们看看列表 16-12 中使用互斥器的例子,现在并不涉及到多线程:

    +

    让我们看看列表 16-12 中使用互斥器的例子,现在不涉及多线程:

    Filename: src/main.rs

    use std::sync::Mutex;
     
    @@ -9430,12 +9429,12 @@ fn main() {
     

    Listing 16-12: Exploring the API of Mutex<T> in a single threaded context for simplicity

    -

    与很多类型一样,我们通过叫做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。

    +

    像很多类型一样,我们使用关联函数 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 是最开始的例子:

    +

    现在让我们尝试使用Mutex<T>在多个线程间共享值。我们将启动十个线程,并在各个线程中对同一个计数器值加一,这样计数器将从 0 变为 10。注意,接下来的几个例子会出现编译错误,而我们将通过这些错误来学习如何使用 +Mutex<T>,以及 Rust 又是如何辅助我们以确保正确。列表 16-13 是最开始的例子:

    Filename: src/main.rs

    use std::sync::Mutex;
     use std::thread;
    @@ -9462,8 +9461,8 @@ fn main() {
     

    Listing 16-13: The start of a program having 10 threads each increment a counter guarded by a Mutex<T>

    -

    这里创建了一个counter变量来存放内含i32Mutex<T>,类似列表 16-12 那样。接下来使用 range 创建了 10 个线程。这里使用了thread::spawn并对所有线程使用了相同的闭包:他们每一个都将调用lock方法来获取Mutex<T>上的锁并对接着互斥器中的值加一。当一个线程结束执行其闭包,num会离开作用域并释放锁这样另一个线程就可以获取它了。

    -

    在主线程中,我们像列表 16-2 那样收集了所有的 join 句柄,并接着每一个的join方法来确保所有线程都会结束。那时,主线程会获取锁并打印出程序的结果。

    +

    这里创建了一个 counter 变量来存放内含 i32Mutex<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
    @@ -9478,10 +9477,10 @@ 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来给予每个线程其所有权。试试将这个修改用到闭包上:

    +

    这类似于列表 16-5 中解决了的问题。考虑到启动了多个线程,Rust 无法知道这些线程会运行多久,而在每一个线程尝试借用 counter 时它是否仍然有效。帮助信息提醒了我们如何解决它:可以使用 move 来给予每个线程其所有权。尝试在闭包上做一点改动:

    thread::spawn(move || {
     
    -

    再次尝试编译。这会出现了一个不同的错误!

    +

    再次编译。这回出现了一个不同的错误!

    error[E0382]: capture of moved value: `counter`
       -->
        |
    @@ -9507,8 +9506,8 @@ error[E0382]: use of moved value: `counter`
     
     error: aborting due to 2 previous errors
     
    -

    move并没有像列表 16-5 中那样解决这个程序中的问题。为什么没有呢?这个错误信息有些难以理解,因为它表明counter被移动进了闭包,接着它在调用lock时被捕获。这听起来像是我们希望的,不过这是不允许的。

    -

    让我们推理一下。现在不再使用for循环创建 10 个线程,让我们不用循环而只创建两个线程来看看会发生什么。将列表 16-13 中第一个for循环替换为如下代码:

    +

    move 并没有像列表 16-5 中那样解决问题。为什么呢?错误信息有点难懂,因为它表明 counter 被移动进了闭包,接着它在调用 lock 时被捕获。这似乎是我们希望的,然而不被允许。

    +

    让我们推理一下。这次不再使用 for 循环创建 10 个线程,只创建两个线程,看看会发生什么。将列表 16-13 中第一个for循环替换为如下代码:

    let handle = thread::spawn(move || {
         let mut num = counter.lock().unwrap();
     
    @@ -9523,15 +9522,15 @@ let handle2 = thread::spawn(move || {
     });
     handles.push(handle2);
     
    -

    这里创建了两个线程,并将用于第二个线程的变量名改为handle2num2。现在我们简化了例子来看看是否能够理解错误信息。这一次编译给出如下信息:

    +

    这里创建了两个线程,并将第二个线程所用的变量改名为 handle2num2。我们简化了例子,看是否能理解错误信息。此次编译给出如下信息:

    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
    +16 |         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
    @@ -9550,11 +9549,11 @@ error[E0382]: use of moved value: `counter`
     
     error: aborting due to 2 previous errors
     
    -

    啊哈!在第一个错误信息中,Rust 表明了counter被移动进了handle所代表线程的闭包中。这个移动阻止我们在对其调用lock并将结果储存在num2中时捕获counter,这是已经在第二个线程中了!所以 Rust 告诉我们不能将counter的所有权移动到多个线程中。这在之前很难看出是因为我们在循环中创建多个线程,而 Rust 无法在循环的迭代中指明不同的线程(没有临时变量)。

    +

    啊哈!第一个错误信息中说,counter 被移动进了 handle 所代表线程的闭包中。因此我们无法在第二个线程中对其调用 lock,并将结果储存在 num2 中时捕获counter!所以 Rust 告诉我们不能将 counter 的所有权移动到多个线程中。这在之前很难看出,因为我们在循环中创建了多个线程,而 Rust 无法在每次迭代中指明不同的线程(没有临时变量 num2)。

    多线程和多所有权

    -

    在第十五章中,我们可以通过使用智能指针Rc<T>来创建引用计数的值来拥有多所有权。同时第十五章提到了Rc<T>只能用于单线程上下文,不过还是让我们在这里试用Rc<T>来观察会发生什么。列表 16-14 将Mutex<T>封装进了Rc<T>中,并在移动到线程中之前克隆了Rc<T>。切换回循环来创建线程,并保留闭包中的move关键字:

    +

    在第十五章中,我们通过使用智能指针 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::rc::Rc;
     use std::sync::Mutex;
     use std::thread;
     
    @@ -9581,7 +9580,7 @@ fn main() {
     

    Listing 16-14: Attempting to use Rc<T> to allow multiple threads to own the Mutex<T>

    -

    又一次,编译并...出现了不同的错误!编译器真是教会了我们很多东西!

    +

    再一次编译并...出现了不同的错误!编译器真是教会了我们很多!

    error[E0277]: the trait bound `std::rc::Rc<std::sync::Mutex<i32>>:
     std::marker::Send` is not satisfied
       -->
    @@ -9597,12 +9596,12 @@ std::marker::Send` is not satisfied
        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 中的代码最终可以编译和运行:

    +

    哇哦,太长不看!说重点:第一个提示表明 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;
    @@ -9633,59 +9632,59 @@ to be able to share ownership across multiple threads

    这会打印出:

    Result: 10
     
    -

    成功了!我们从 0 数到了 10,这可能并不是很显眼,不过一路上我们学习了很多关于Mutex<T>和线程安全的内容!这个例子中构建的结构可以用于比增加计数更为复杂的操作。可以被分解为独立部分的计算可以像这样被分散到多个线程中,并可以使用Mutex<T>来允许每个线程在他们自己的部分更新最终的结果。

    +

    成功了!我们从 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 的类型系统和所有权确保了线程在更新共享值时拥有独占的访问权限,所以线程不会以一种不可预测的方式覆盖彼此的操作。为了和编译器一起使一切正确运行花了一些时间,不过我们节省了未来可能需要重现只在线程以特定顺序执行才会出现的诡异错误场景的时间。

    +

    回忆一下Rc<T>并没有避免所有可能的问题:我们也讨论了当两个Rc<T>相互引用时的引用循环的可能性,这可能造成内存泄露。Mutex<T>有一个类似的 Rust 同样也不能避免的问题:死锁。死锁deadlock)是一个场景中操作需要锁定两个资源,而两个线程分别拥有一个锁并永远相互等待的问题。如果你对这个主题感兴趣,尝试编写一个带有死锁的 Rust 程序,接着研究任何其他语言中使用互斥器的死锁规避策略并尝试在 Rust 中实现他们。标准库中Mutex<T>MutexGuard的 API 文档会提供有用的信息。

    +

    Rust 的类型系统和所有权规则,确保了线程在更新共享值时拥有独占的访问权限,所以线程不会以不可预测的方式覆盖彼此的操作。虽然为了使一切正确运行而在编译器上花了一些时间,但是我们节省了未来的时间,尤其是线程以特定顺序执行才会出现的诡异错误难以重现。

    接下来,为了丰富本章的内容,让我们讨论一下SendSync trait 以及如何对自定义类型使用他们。

    使用SyncSend trait 的可扩展并发

    ch16-04-extensible-concurrency-sync-and-send.md
    -commit 55b294f20fc846a13a9be623bf322d8b364cee77

    +commit 9430a3d28a2121a938d704ce48b15d21062f880e

    -

    Rust 的并发模型中一个有趣的方面是语言本身对并发知道的很少。我们讨论过的几乎所有内容都是标准库的一部分,而不是语言本身的内容。因为并不需要语言提供任何用于并发上下文中的内容,并发选择也不仅限于标准库或语言所提供的:我们可以编写自己的或使用别人编写的内容。

    -

    我们说了几乎所有内容都不在语言本身,那么位于语言本身的是什么呢?这是两个 trait,都位于std::markerSyncSend

    +

    Rust 的并发模型中一个有趣的方面是:语言本身对并发知之甚少。我们之前讨论的几乎所有内容,都属于标准库,而不是语言本身的内容。由于不需要语言提供并发相关的基础设施,并发方案不受标准库或语言所限:我们可以编写自己的或使用别人编写的。

    +

    我们说“几乎所有内容都不属于语言本身”,那么属于语言本身的是什么呢?是两个 trait,都位于std::markerSyncSend

    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>>。当切换为标记为SendArc<T>时,代码就可以编译了。

    -

    任何完全由Send的类型组成的类型也会自动被标记为Send。几乎所有基本类型都是Send的,除了第十九章将会讨论的裸指针(raw pointer)之外。大部分标准库类型是Send的,除了Rc<T>之外。

    +

    Send标记 trait 表明类型的所有权可能被在线程间传递。几乎所有的 Rust 类型都是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>>。当切换为标记为SendArc<T>时,就没有问题了。

    +

    任何完全由Send的类型组成的类型也会自动被标记为Send。几乎所有基本类型都是Send的,大部分标准库类型是Send的,除了Rc<T>,以及第十九章将会讨论的裸指针(raw pointer)。

    Sync表明多线程访问是安全的

    -

    Sync标记 trait 表明一个类型可以安全的在多个线程中拥有其值的引用。换一种方式来说就是对于任意类型T,如果&TT的引用)是Send的话T就是Sync的,这样其引用就可以安全的发送到另一个线程。类似于Send的情况,基本类型是Sync的,完全由Sync的类型组成的类型也是Sync的。

    +

    Sync标记 trait 表明一个类型可以安全的在多个线程中拥有其值的引用。换一种方式来说就是,对于任意类型T,如果&TT的引用)是Send的话T就是Sync的,这样其引用就可以安全的发送到另一个线程。类似于Send的情况,基本类型是Sync的,完全由Sync的类型组成的类型也是Sync的。

    Rc<T>也不是Sync的,出于其不是Send的相同的原因。RefCell<T>(第十五章讨论过)和Cell<T>系列类型不是Sync的。RefCell<T>在运行时所进行的借用检查也不是线程安全的。Mutex<T>Sync的,正如上一部分所讲的它可以被用来在多线程中共享访问。

    手动实现SendSync是不安全的

    -

    通常并不需要实现SendSync trait,因为由是SendSync的类型组成的类型也自动就是SendSync的了。因为他们是标记 trait,甚至都不需要实现任何方法。他们只是用来加强并发相关的不可变行性的。

    -

    实现这些标记 trait 涉及到实现不安全的 Rust 代码。第十九章将会讲到如何使用不安全 Rust 代码;现在,重要的是在创建新的由不是SendSync的部分构成的并发类型时需要多加小心,以确保维持其安全保证。The Nomicon 中由更多关于这些保证和如何维持他们的信息。

    +

    通常并不需要实现SendSync trait,由属于SendSync的类型组成的类型,自动就是SendSync的。因为他们是标记 trait,甚至都不需要实现任何方法。他们只是用来加强并发相关的不可变性的。

    +

    实现这些标记 trait 涉及到编写不安全的 Rust 代码,第十九章将会讲述具体的方法;当前重要的是,在创建新的由不是SendSync的部分构成的并发类型时需要多加小心,以确保维持其安全保证。The Nomicon 中有更多关于这些保证以及如何维持他们的信息。

    总结

    这不会是本书最后一个出现并发的章节;第二十章的项目会在更现实的场景中使用这些概念,而不像本章中讨论的这些小例子。

    正如我们提到的,因为 Rust 本身很少有处理并发的部分内容,有很多的并发方案都由 crate 实现。他们比标准库要发展的更快;请在网上搜索当前最新的用于多线程场景的 crate。

    -

    Rust 提供了用于消息传递的通道,和像Mutex<T>Arc<T>这样可以安全的用于并发上下文的智能指针。类型系统和借用检查器会确保这些场景中的代码不会出现数据竞争和无效的引用。一旦代码可以编译了,我们就可以坚信这些代码可以正确的运行于多线程环境,而不会出现其他语言中经常出现的那些难以追踪的 bug。并发编程不再是什么可怕的概念:无所畏惧的使你的程序使用并发吧!

    -

    接下来,让我们讨论一下当 Rust 程序变得更大时那些符合习惯的模拟问题和结构的解决方案,以及 Rust 风格如何与面向对象编程(Object Oriented Programming)中那些你所熟悉的概念相联系。

    +

    Rust 提供了用于消息传递的通道,和像Mutex<T>Arc<T>这样可以安全的用于并发上下文的智能指针。类型系统和借用检查器会确保这些场景中的代码,不会出现数据竞争和无效的引用。一旦代码可以编译了,我们就可以坚信这些代码可以正确的运行于多线程环境,而不会出现其他语言中经常出现的那些难以追踪的 bug。并发编程不再是什么可怕的概念:无所畏惧地并发吧!

    +

    接下来,让我们讨论一下当 Rust 程序变得更大时,有哪些符合语言习惯的问题建模方法和结构化解决方案,以及 Rust 的风格是如何与面向对象编程(Object Oriented Programming)中那些你所熟悉的概念相联系的。

    Rust 是一个面向对象的编程语言吗?

    ch17-00-oop.md
    commit 759801361bde74b47e81755fff545c66020e6e63

    -

    面向对象编程(Object-Oriented Programming)是一种起源于 20 世纪 60 年代的 Simula 编程语言的模式化编程方式,然后在 90 年代随着 C++ 语言开始流行。为了描述 OOP 有很多种复杂的定义:在一些定义下,Rust 是面向对象的;在其他定义下,Rust 不是。在本章节中,我们会探索一些被普遍认为是面向对象的特性和这些特性是如何转换为 Rust 方言的。

    +

    面向对象编程(Object-Oriented Programming)是一种起源于 20 世纪 60 年代的 Simula 编程语言的模式化编程方式,然后在 90 年代随着 C++ 语言开始流行。关于 OOP 是什么有很多相互矛盾的定义:在一些定义下,Rust 是面向对象的;在其他定义下,Rust 不是。在本章节中,我们会探索一些被普遍认为是面向对象的特性和这些特性是如何体现在 Rust 语言习惯中的。

    什么是面向对象?

    ch17-01-what-is-oo.md
    commit 2a9b2a1b019ad6d4832ff3e56fbcba5be68b250e

    -

    关于一门语言是否需要是面向对象,在编程社区内并未达成一致意见。Rust 被很多不同的编程模式影响,我们探索了十三章提到的函数式编程的特性。面向对象编程语言的一些特性往往是对象、封装和继承。我们看一下这每一个概念的含义以及 Rust 是否支持他们。

    -

    对象包含数据和行为

    +

    关于一个语言被称为面向对象所需的功能,在编程社区内并未达成一致意见。Rust 被很多不同的编程范式影响;我们探索了十三章提到的来自函数式编程的特性。面向对象编程语言所共享的一些特性往往是对象、封装和继承。让我们看一下这每一个概念的含义以及 Rust 是否支持他们。

    +

    对象包含数据和行为

    Design Patterns: Elements of Reusable Object-Oriented Software这本书被俗称为The Gang of Four book,是面向对象编程模式的目录。它这样定义面向对象编程:

    Object-oriented programs are made up of objects. An object packages both data and the procedures that operate on that data. The procedures are typically called methods or operations.

    -

    面向对象的程序是由对象组成的。一个对象包数据和操作这些数据的过程。这些过程通常被称为方法操作

    +

    面向对象的程序是由对象组成的。一个对象包含数据和操作这些数据的过程。这些过程通常被称为方法操作

    -

    在这个定义下,Rust 是面向对象的:结构体和枚举包含数据而 impl 块提供了在结构体和枚举之上的方法。虽然带有方法的结构体和枚举并不被称为对象,但是他们提供了与对象相同的功能,参考 Gang of Four 所定义的对象。

    -

    隐藏了实现细节的封装

    -

    另一个通常与面向对象编程相关的方面是封装的思想:对象的实现细节不能被使用对象的代码获取到。唯一与对象交互的方式是通过对象提供的公有 API;使用对象的代码无法深入到对象内部并直接改变数据或者行为。封装使得改变和重构对象的内部时无需改变使用对象的代码。

    +

    在这个定义下,Rust 是面向对象的:结构体和枚举包含数据而 impl 块提供了在结构体和枚举之上的方法。虽然带有方法的结构体和枚举并不被称为对象,但是他们提供了与对象相同的功能,参考 Gang of Four 中对象的定义。

    +

    隐藏了实现细节的封装

    +

    另一个通常与面向对象编程相关的方面是封装encapsulation)的思想:对象的实现细节不能被使用对象的代码获取到。唯一与对象交互的方式是通过对象提供的公有 API;使用对象的代码无法深入到对象内部并直接改变数据或者行为。封装使得改变和重构对象的内部时无需改变使用对象的代码。

    就像我们在第七章讨论的那样,可以使用pub关键字来决定模块、类型函数和方法是公有的,而默认情况下一切都是私有的。比如,我们可以定义一个包含一个i32类型的 vector 的结构体AveragedCollection。结构体也可以有一个字段,该字段保存了 vector 中所有值的平均值。这样,希望知道结构体中的 vector 的平均值的人可以随时获取它,而无需自己计算。AveragedCollection会为我们缓存平均值结果。列表 17-1 有AveragedCollection结构体的定义:

    文件名: src/lib.rs

    pub struct AveragedCollection {
    @@ -9694,7 +9693,7 @@ typically called methods or operations.

    }

    列表 17-1: AveragedCollection结构体维护了一个整型列表和集合中所有元素的平均值。

    -

    注意,结构体自身被标记为pub,这样其他代码可以使用这个结构体,但是在结构体内部的字段仍然是私有的。这是非常重要的,因为我们希望保证变量被增加到列表或者被从列表删除时,也会同时更新平均值。我们通过在结构体上实现addremoveaverage方法来做到这一点,如列表 17-2 所示:

    +

    注意,结构体自身被标记为pub,这样其他代码可以使用这个结构体,但是在结构体内部的字段仍然是私有的。这是非常重要的,因为我们希望保证变量被增加到列表或者被从列表删除时,也会同时更新平均值。可以通过在结构体上实现addremoveaverage方法来做到这一点,如列表 17-2 所示:

    文件名: src/lib.rs

    # pub struct AveragedCollection {
     #     list: Vec<i32>,
    @@ -9733,16 +9732,16 @@ impl AveragedCollection {
     

    如果封装是一个语言被认为是面向对象语言所必要的方面的话,那么 Rust 就满足这个要求。在代码中不同的部分使用或者不使用pub决定了实现细节的封装。

    作为类型系统的继承和作为代码共享的继承

    继承Inheritance)是一个很多编程语言都提供的机制,一个对象可以定义为继承另一个对象的定义,这使其可以获得父对象的数据和行为,而不用重新定义。一些人定义面向对象语言时,认为继承是一个特色。

    -

    如果一个语言必须有继承才能被称为面向对象的语言的话,那么 Rust 就不是面向对象的。无法定义一个结构体继承自另外一个结构体,从而获得父结构体的成员和方法。然而,如果你过去常常在你的编程工具箱使用继承,根据你希望使用继承的原因,Rust 提供了其他的解决方案。

    +

    如果一个语言必须有继承才能被称为面向对象语言的话,那么 Rust 就不是面向对象的。无法定义一个结构体继承自另外一个结构体,从而获得父结构体的成员和方法。然而,如果你过去常常在你的编程工具箱使用继承,根据你希望使用继承的原因,Rust 也提供了其他的解决方案。

    使用继承有两个主要的原因。第一个是为了重用代码:一旦为一个类型实现了特定行为,继承可以对一个不同的类型重用这个实现。相反 Rust 代码可以使用默认 trait 方法实现来进行共享,在列表 10-14 中我们见过在Summarizable trait 上增加的summary方法的默认实现。任何实现了Summarizable trait 的类型都可以使用summary方法而无须进一步实现。这类似于父类有一个方法的实现,而通过继承子类也拥有这个方法的实现。当实现Summarizable trait 时也可以选择覆盖summary的默认实现,这类似于子类覆盖从父类继承的方法实现。

    第二个使用继承的原因与类型系统有关:用来表现子类型可以在父类型被使用的地方使用。这也被称为多态polymorphism),意味着如果多种对象有一个相同的形态大小,它们可以替代使用。

    -

    虽然很多人使用“多态”来描述继承,但是它实际上是一种特殊的多态,称为“子类型多态”。也有很多种其他形式的多态,在 Rust 中带有泛型参数的 trait bound 也是多态,更具体的说是“参数多态”。不同类型多态的确切细节在这里并不关键,所以不要过于担心细节,只需要知道 Rust 有多种多态相关的特色就好,不同于很多其他 OOP 语言。

    +

    虽然很多人使用“多态”("polymorphism")来描述继承,但是它实际上是一种特殊的多态,称为“子类型多态”("sub-type polymorphism")。也有很多种其他形式的多态,在 Rust 中带有泛型参数的 trait bound 也是多态,更具体的说是“参数多态”("parametric polymorphism")。不同类型多态的确切细节在这里并不关键,所以不要过于担心细节,只需要知道 Rust 有多种多态相关的特色就好,不同于很多其他 OOP 语言。

    -

    为了支持这种模式,Rust 有 trait 对象trait objects),这样我们可以指定给任何类型的值,只要值实现了一种特定的 trait。

    -

    继承最近在很多编程语言的设计方案中失宠了。使用继承来实现代码重用需要共享比你需要共享的代码。子类不应该总是共享它们的父类的所有特色,但是继承意味着子类得到了它父类所有的数据和行为。这使得程序的设计更加不灵活,并产生了无意义的方法调用或子类,或者由于方法并不适用于子类不过必需从父类继承而造成错误的可能性。另外,一些语言只允许子类继承一个父类,这进一步限制了程序设计的灵活性。

    +

    为了支持这种模式,Rust 有 trait 对象trait objects),这样就可以使用任意类型的值,只要这个值实现了指定的 trait。

    +

    继承最近在很多编程语言的设计方案中失宠了。使用继承来实现代码重用,会共享更多非必需的代码。子类不应该总是共享其父类的所有特性,然而继承意味着子类得到了其父类全部的数据和行为。这使得程序的设计更不灵活,并产生了无意义的方法调用或子类,以及由于方法并不适用于子类,却必需从父类继承而可能造成的错误。另外,某些语言只允许子类继承一个父类,进一步限制了程序设计的灵活性。

    因为这些原因,Rust 选择了一个另外的途径,使用 trait 对象替代继承。让我们看一下在 Rust 中 trait 对象是如何实现多态的。

    为使用不同类型的值而设计的 trait 对象

    @@ -9750,17 +9749,17 @@ impl AveragedCollection {
    commit 67876e3ef5323ce9d394f3ea6b08cb3d173d9ba9

    -

    在第八章,我们谈到了 vector 的局限是 vector 只能存储同种类型的元素。在列表 8-1 中有一个例子,其中定义了一个有存放整型、浮点型和文本的成员的枚举类型SpreadsheetCell,这样就可以在每一个单元格储存不同类型的数据并使得 vector 仍让代表一行单元格。这在那类代码被编译时就知晓需要可交换处理的数据的类型是一个固定集合的情况下是可行的。

    +

    在第八章,我们谈到了 vector 只能存储同种类型元素的局限。在列表 8-1 中有一个例子,其中定义了存放包含整型、浮点型和文本型成员的枚举类型SpreadsheetCell,这样就可以在每一个单元格储存不同类型的数据,并使得 vector 仍然代表一行单元格。当编译时就知道类型集合全部元素的情况下,这种方案是可行的。

    -

    有时,我们想我们使用的类型集合是可扩展的,可以被使用我们的库的程序员扩展。比如很多图形化接口工具有一个条目列表,从这个列表迭代和调用draw方法在每个条目上。我们将要创建一个库crate,包含称为rust_gui的CUI库的结构体。我们的GUI库可以包含一些给开发者使用的类型,比如Button或者TextField。使用rust_gui的程序员会创建更多可以在屏幕绘图的类型:一个程序员可能会增加Image,另外一个可能会增加SelectBox。我们不会在本章节实现一个完善的GUI库,但是我们会展示如何把各部分组合在一起。

    -

    当要写一个rust_gui库时,我们不知道其他程序员要创建什么类型,所以我们无法定义一个enum来包含所有的类型。我们知道的是rust_gui需要有能力跟踪所有这些不同类型的大量的值,需要有能力在每个值上调用draw方法。我们的GUI库不需要确切地知道当调用draw方法时会发生什么,只要值有可用的方法供我们调用就可以。

    -

    在有继承的语言里,我们可能会定义一个名为Component的类,该类上有一个draw方法。其他的类比如ButtonImageSelectBox会从Component继承并继承draw方法。它们会各自覆写draw方法来自定义行为,但是框架会把所有的类型当作是Component的实例,并在它们上调用draw

    +

    有时,我们希望使用的类型的集合对于使用库的程序员来说是可扩展的。例如,很多图形用户接口(GUI)工具有一个条目列表的概念,它通过遍历列表并对每一个条目调用draw方法来绘制在屏幕上。我们将要创建一个叫做rust_gui的包含一个 GUI 库结构的库 crate。GUI 库可以包含一些供开发者使用的类型,比如ButtonTextField。使用rust_gui的程序员会想要创建更多可以绘制在屏幕上的类型:一个程序员可能会增加一个Image,而另一个可能会增加一个SelectBox。我们不会在本章节实现一个功能完善的 GUI 库,不过会展示各个部分是如何结合在一起的。

    +

    当写 rust_gui 库时,我们不知道其他程序员需要什么类型,所以无法定义一个 enum 来包含所有的类型。然而 rust_gui 需要跟踪所有这些不同类型的值,需要有在每个值上调用 draw 方法能力。我们的 GUI 库不需要确切地知道调用 draw 方法会发生什么,只需要有可用的方法供我们调用。

    +

    在可以继承的语言里,我们会定义一个名为 Component 的类,该类上有一个draw方法。其他的类比如ButtonImageSelectBox会从Component继承并拥有draw方法。它们各自覆写draw方法以自定义行为,但是框架会把所有的类型当作是Component的实例,并在其上调用draw

    定义一个带有自定义行为的Trait

    -

    不过,在Rust语言中,我们可以定义一个名为Draw的trait,其上有一个名为draw的方法。我们定义一个带有trait对象的vector,绑定了一种指针的trait,比如&引用或者一个Box<T>智能指针。

    -

    我们提到,我们不会调用结构体和枚举的对象,从而区分于其他语言的对象。在结构体的数据或者枚举的字段和impl块中的行为是分开的,而其他语言则是数据和行为被组合到一个概念里。Trait对象更像其他语言的对象,在这种场景下,他们组合了由指针组成的数据到实体对象,该对象带有在trait中定义的方法行为。但是,trait对象是和其他语言是不同的,因为我们不能向一个trait对象增加数据。trait对象不像其他语言那样有用:它们的目的是允许从公有的行为上抽象。

    -

    trait定义了在给定场景下我们所需要的行为。在我们会使用一个实体类型或者一个通用类型的地方,我们可以把trait当作trait对象使用。Rust的类型系统会保证我们为trait对象带入的任何值会实现trait的方法。我们不需要在编译阶段知道所有可能的类型,我们可以把所有的实例统一对待。Listing 17-03展示了如何定义一个名为Draw的带有draw方法的trait。

    +

    不过,在Rust语言中,我们可以定义一个 Draw trait,包含名为 draw 的方法。我们定义一个由trait对象组成的vector,绑定了某种指针的trait,比如&引用或者一个Box<T>智能指针。

    +

    之前提到,我们不会称结构体和枚举为对象,以区分其他语言的结构体和枚举对象。结构体或者枚举成员中的数据和impl块中的行为是分开的,而其他语言则是数据和行为被组合到一个对象里。Trait 对象更像其他语言的对象,因为他们将其指针指向的具体对象作为数据,将在trait 中定义的方法作为行为,组合在了一起。但是,trait 对象和其他语言是不同的,我们不能向一个 trait 对象增加数据。trait 对象不像其他语言那样有用:它们的目的是允许从公有行为上抽象。

    +

    trait 对象定义了给定情况下应有的行为。当需要具有某种特性的不确定具体类型时,我们可以把 trait 对象当作 trait 使用。Rust 的类型系统会保证我们为 trait 对象带入的任何值会实现 trait 的方法。我们不需要在编译阶段知道所有可能的类型,却可以把所有的实例统一对待。Listing 17-03展示了如何定义一个名为Draw的带有draw方法的trait。

    Filename: src/lib.rs

    pub trait Draw {
         fn draw(&self);
    @@ -9768,7 +9767,7 @@ get Chapter 8 for editing. /Carol -->
     

    Listing 17-3:Draw trait的定义

    -

    因为我们已经在第10章讨论过如何定义trait,你可能比较熟悉。下面是新的定义:Listing 17-4有一个名为Screen的结构体,里面有一个名为components的vector,components的类型是BoxBox<Draw>是一个trait对象:它是一个任何Box内部的实现了Drawtrait的类型的替身。

    +

    因为我们已经在第10章讨论过如何定义 trait,你可能比较熟悉。下面是新的定义:Listing 17-4有一个名为 Screen 的结构体,里面有一个名为 components 的 vector,components 的类型是BoxBox<Draw> 是一个 trait 对象:它是 Box 内部任意一个实现了 Draw trait 的类型的替身。

    Filename: src/lib.rs

    # pub trait Draw {
     #     fn draw(&self);
    @@ -9778,9 +9777,9 @@ pub struct Screen {
         pub components: Vec<Box<Draw>>,
     }
     
    -

    Listing 17-4: 定义一个Screen结构体,带有一个含有实现了Drawtrait的components vector成员

    +

    Listing 17-4: 定义一个 Screen 结构体,带有一个含有实现了 Draw trait 的 components vector 成员

    -

    Screen结构体上,我们将要定义一个run方法,该方法会在它的components上调用draw方法,如Listing 17-5所示:

    +

    Screen 结构体上,我们将要定义一个 run 方法,该方法会在它的 components 上调用 draw 方法,如Listing 17-5所示:

    Filename: src/lib.rs

    # pub trait Draw {
     #     fn draw(&self);
    @@ -9798,9 +9797,9 @@ impl Screen {
         }
     }
     
    -

    Listing 17-5:在Screen上实现一个run方法,该方法在每个组件上调用draw方法 +

    Listing 17-5:在 Screen 上实现一个 run 方法,该方法在每个组件上调用 draw 方法

    -

    这是区别于定义一个使用带有trait绑定的通用类型参数的结构体。通用类型参数一次只能被一个实体类型替代,而trait对象可以在运行时允许多种实体类型填充trait对象。比如,我们已经定义了Screen结构体使用通用类型和一个trait绑定,如Listing 17-6所示:

    +

    这与带 trait 约束的泛型结构体不同(trait 约束泛型参数)。泛型参数一次只能被一个具体类型替代,而 trait 对象可以在运行时允许多种具体类型填充 trait 对象。比如,我们已经定义了 Screen 结构体使用泛型和一个 trait 约束,如Listing 17-6所示:

    Filename: src/lib.rs

    # pub trait Draw {
     #     fn draw(&self);
    @@ -9819,12 +9818,12 @@ impl<T> Screen<T>
         }
     }
     
    -

    Listing 17-6: 一种Screen结构体的替代实现,它的run方法使用通用类型和trait绑定 +

    Listing 17-6: 一种 Screen 结构体的替代实现,它的 run 方法使用通用类型和 trait 绑定

    -

    这个例子只能使我们有一个Screen实例,这个实例有一个组件列表,所有的组件类型是Button或者TextField。如果你有同种的集合,那么可以优先使用通用和trait绑定,这是因为为了使用具体的类型,定义是在编译阶段是单一的。

    -

    而如果使用内部有Vec<Box<Draw>> trait对象的列表的Screen结构体,Screen实例可以同时包含Box<Button>Box<TextField>Vec。我们看它是怎么工作的,然后讨论运行时性能的实现。

    +

    这个例子中,Screen 实例所有组件类型必需全是 Button,或者全是 TextField。如果你的组件集合是单一类型的,那么可以优先使用泛型和 trait 约束,因为其使用的具体类型在编译阶段即可确定。

    +

    Screen 结构体内部的 Vec<Box<Draw>> trait 对象列表,则可以同时包含 Box<Button>Box<TextField>。我们看它是怎么工作的,然后讨论运行时性能。

    来自我们或者库使用者的实现

    -

    现在,我们增加一些实现了Drawtrait的类型。我们会再次提供Button,实际上实现一个GUI库超出了本书的范围,所以draw方法的内部不会有任何有用的实现。为了想象一下实现可能的样子,Button结构体可能有 widthheightlabel`字段,如Listing 17-7所示:

    +

    现在,我们增加一些实现了 Draw trait 的类型,再次提供 Button。实现一个 GUI 库实际上超出了本书的范围,因此 draw 方法留空。为了想象实现可能的样子,Button 结构体有 widthheightlabel字段,如Listing 17-7所示:

    Filename: src/lib.rs

    # pub trait Draw {
     #     fn draw(&self);
    @@ -9843,11 +9842,11 @@ impl Draw for Button {
     }
     

    Listing 17-7: 实现了Draw trait的Button 结构体

    -

    Button上的 widthheightlabel会和其他组件不同,比如TextField可能有widthheight, -labelplaceholder字段。每个我们可以在屏幕上绘制的类型会实现Drawtrait,在draw方法中使用不同的代码,定义了如何绘制Button(GUI代码的具体实现超出了本章节的范围)。除了Draw trait,Button可能也有另一个impl块,包含了当按钮被点击的时候的响应方法。这类方法不适用于TextField这样的类型。

    -

    有时,使用我们的库决定了实现一个包含widthheightoptions``SelectBox结构体。它们在SelectBox类型上实现了Drawtrait,如 Listing 17-8所示:

    +

    Button 上的 widthheightlabel 会和其他组件不同,比如 TextField 可能有 widthheight, +label 以及 placeholder 字段。每个我们可以在屏幕上绘制的类型都会实现 Draw trait,在 draw 方法中使用不同的代码,定义了如何绘制 Button。除了 Draw trait,Button 也可能有一个 impl 块,包含按钮被点击时的响应方法。这类方法不适用于 TextField 这样的类型。

    +

    假定我们的库的用户相要实现一个包含 widthheightoptionsSelectBox 结构体。同时也在 SelectBox 类型上实现了 Draw trait,如 Listing 17-8所示:

    Filename: src/main.rs

    -
    extern crate rust_gui;
    +
    extern crate rust_gui;
     use rust_gui::Draw;
     
     struct SelectBox {
    @@ -9862,11 +9861,11 @@ impl Draw for SelectBox {
         }
     }
     
    -

    Listing 17-8: 另外一个crate中,在SelectBox结构体上使用rust_gui和实现了Draw trait +

    Listing 17-8: 另外一个 crate 中,在 SelectBox 结构体上使用 rust_gui 和实现了Draw trait

    -

    我们的库的使用者现在可以写他们的main函数来创建一个Screen实例,然后通过把自身放入Box<T>变成trait对象,向screen增加SelectBoxButton。它们可以在每个Screen实例上调用run方法,这会调用每个组件的draw方法。 Listing 17-9展示了实现:

    +

    库的用户现在可以在他们的 main 函数中创建一个 Screen 实例,然后把自身放入 Box<T> 变成 trait 对象,向 screen 增加 SelectBoxButton。他们可以在这个 Screen 实例上调用 run 方法,这又会调用每个组件的 draw 方法。 Listing 17-9 展示了实现:

    Filename: src/main.rs

    -
    use rust_gui::{Screen, Button};
    +
    use rust_gui::{Screen, Button};
     
     fn main() {
         let screen = Screen {
    @@ -9891,14 +9890,14 @@ fn main() {
         screen.run();
     }
     
    -

    Listing 17-9: 使用trait对象来存储实现了相同trait的不同类型 +

    Listing 17-9: 使用 trait 对象来存储实现了相同 trait 的不同类型

    -

    虽然我们不知道有些人可能有一天会增加SelectBox类型,但是我们的Screen 有能力操作SelectBox和绘制,因为SelectBox实现了Draw类型,这意味着它实现了draw方法。

    -

    只关心值响应的消息,而不关心值的具体类型,这类似于动态类型语言中的duck typing:如果它像鸭子一样走路,像鸭子一样叫,那么它肯定是只鸭子!在Listing 17-5的Screenrun方法的实现中,run不需要知道每个组件的具体类型。它也不检查是否一个组件是Button或者SelectBox的实例,只是调用组件的draw方法即可。通过指定Box<Draw>作为componentsvector中的值类型,我们定义了:Screen需要可以被调用其draw方法的值。

    -

    使用trait对象和支持duck typing的Rust类型系统的好处是,我们永远不需要在运行时检查一个值是否实现了一个特殊方法,或者担心因为调用了一个值没有实现方法而遇到错误。如果值没有实现trait对象需要的trait,Rust不会编译我们的代码。

    -

    比如,Listing 17-10展示了当我们创建一个把String当做其成员的Screen时发生的情况:

    +

    虽然我们不知道哪一天会有人增加 SelectBox 类型,但是我们的 Screen 能够操作 SelectBox 并绘制它,因为 SelectBox 实现了 Draw 类型,这意味着它实现了 draw 方法。

    +

    只关心值的响应,而不关心其具体类型,这类似于动态类型语言中的 duck typing:如果它像鸭子一样走路,像鸭子一样叫,那么它就是只鸭子!在 Listing 17-5 Screenrun 方法实现中,run 不需要知道每个组件的具体类型。它也不检查组件是 Button 还是 SelectBox 的实例,只管调用组件的 draw 方法。通过指定 Box<Draw> 作为 components 列表中元素的类型,我们约束了 Screen 需要这些实现了 draw 方法的值。

    +

    Rust 类型系统使用 trait 对象来支持 duck typing 的好处是,我们无需在运行时检查一个值是否实现了特定方法,或是担心调用了一个值没有实现的方法。如果值没有实现 trait 对象需要的 trait(方法),Rust 不会编译。

    +

    比如,Listing 17-10 展示了当我们创建一个使用 String 做为其组件的 Screen 时发生的情况:

    Filename: src/main.rs

    -
    extern crate rust_gui;
    +
    extern crate rust_gui;
     use rust_gui::Draw;
     
     fn main() {
    @@ -9911,9 +9910,9 @@ fn main() {
         screen.run();
     }
     
    -

    Listing 17-10: 尝试使用一种没有实现trait对象的trait的类型

    +

    Listing 17-10: 尝试使用一种没有实现 trait 对象的类型

    -

    我们会遇到这个错误,因为String没有实现 Drawtrait:

    +

    我们会遇到这个错误,因为 String 没有实现 Draw trait:

    error[E0277]: the trait bound `std::string::String: Draw` is not satisfied
       -->
        |
    @@ -9923,10 +9922,10 @@ fn main() {
        |
        = note: required for the cast to the object type `Draw`
     
    -

    这个报错让我们知道,或者我们传入了本来不想传给Screen的东西,我们应该传入一个不同的类型,或者是我们应该在String上实现Draw,这样,Screen才能调用它的draw方法。

    -

    Trait对象执行动态分发

    -

    回忆一下第10章,我们讨论过当我们使用通用类型的trait绑定时,编译器执行单类型的处理过程:在我们需要使用通用类型参数的地方,编译器为每个实体类型产生了非通用的函数实现和方法。由于非单类型而产生的代码是 static dispatch:当方法被调用,代码会执行在编译阶段就决定的方法,这样寻找那段代码是非常快速的。

    -

    当我们使用trait对象,编译器不能执行单类型的,因为我们不知道可能被代码调用的类型。而,当方法被调用的时候,Rust跟踪可能被使用的代码,然后在运行时找出为了方法被调用时该使用哪些代码。这也是我们熟知的dynamic dispatch,当运行时的查找发生时是比较耗费资源的。动态分发也防止编译器选择内联函数的代码,这样防止了一些优化。虽然我们写代码时得到了额外的代码灵活性,不过,这是一个权衡考虑。

    +

    这个错误告诉我们,要么传入 Screen 需要的类型,要么在 String 上实现 Draw,以便 Screen 调用它的 draw 方法。

    +

    Trait 对象执行动态分发

    +

    回忆一下第10章我们讨论过的,当我们在泛型上使用 trait 约束时,编译器按单态类型处理:在需要使用范型参数的地方,编译器为每个具体类型生成非泛型的函数和方法实现。单态类型处理产生的代码实际就是做 static dispatch:方法的代码在编译阶段就已经决定了,当调用时,寻找那段代码非常快速。

    +

    当我们使用 trait 对象,编译器不能按单态类型处理,因为无法知道使用代码的所有可能类型。而是调用方法的时候,Rust 跟踪可能被使用的代码,在运行时找出调用该方法时应使用的代码。这也是我们熟知的 dynamic dispatch,查找过程会产生运行时开销。动态分发也会阻止编译器内联函数,失去一些优化途径。尽管获得了额外的灵活性,但仍然需要权衡取舍。

    Trait 对象需要对象安全

    + - - + + - - + @@ -10056,7 +10055,7 @@ to know about. /Carol --> - + diff --git a/src/ch02-00-guessing-game-tutorial.md b/src/ch02-00-guessing-game-tutorial.md index 682b64e..a7be52a 100644 --- a/src/ch02-00-guessing-game-tutorial.md +++ b/src/ch02-00-guessing-game-tutorial.md @@ -685,7 +685,7 @@ let guess: u32 = match guess.trim().parse() { }; ``` -从`expect`调用切换到`expect`语句是如何从遇到错误就崩溃到真正处理错误的常用手段。记住`parse`返回一个`Result`类型,而`Result`是一个拥有`Ok`或`Err`两个成员的枚举。在这里使用`match`表达式,就像之前处理`cmp`方法返回的`Ordering`一样。 +从`expect`调用切换到`match`语句是如何从遇到错误就崩溃到真正处理错误的常用手段。记住`parse`返回一个`Result`类型,而`Result`是一个拥有`Ok`或`Err`两个成员的枚举。在这里使用`match`表达式,就像之前处理`cmp`方法返回的`Ordering`一样。 如果`parse`能够成功的将字符串转换为一个数字,它会返回一个包含结果数字`Ok`值。这个`Ok`值会匹配第一个分支的模式,这时`match`表达式仅仅返回`parse`产生的`Ok`值之中的`num`值。这个数字会最终如期变成新创建的`guess`变量。 diff --git a/src/ch03-02-data-types.md b/src/ch03-02-data-types.md index e40a8c7..9c903a4 100644 --- a/src/ch03-02-data-types.md +++ b/src/ch03-02-data-types.md @@ -2,7 +2,7 @@ > [ch03-02-data-types.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch03-02-data-types.md) >
    -> commit 04aa3a45eb72855b34213703718f50a12a3eeec8 +> commit fe4833a8ef2853c55424e7747a4ef8dd64c35b32 Rust 中的任何值都有一个具体的**类型**(*type*),这告诉了 Rust 它被指定了何种数据,这样 Rust 就知道如何处理这些数据了。这一部分将讲到一些语言内建的类型。我们将这些类型分为两个子集:标量(scalar)和复合(compound)。 @@ -124,7 +124,7 @@ fn main() { } ``` -使用布尔值的主要场景是条件语句,例如`if`。在“控制流”(“Control Flow”)部分将讲到`if`语句在 Rust 中如何工作。 +使用布尔值的主要场景是条件表达式,例如`if`。在“控制流”(“Control Flow”)部分将讲到`if`表达式在 Rust 中如何工作。 #### 字符类型 diff --git a/src/ch04-01-what-is-ownership.md b/src/ch04-01-what-is-ownership.md index a2b1f21..8175fc7 100644 --- a/src/ch04-01-what-is-ownership.md +++ b/src/ch04-01-what-is-ownership.md @@ -2,7 +2,7 @@ > [ch04-01-what-is-ownership.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch04-01-what-is-ownership.md) >
    -> commit fae5fa82d728b5965ecbba84060689430345e509 +> commit 6d4ef020095a375483b2121d4fa2b1661062cc92 Rust 的核心功能(之一)是**所有权**(*ownership*)。虽然这个功能理解起来很直观,不过它对语言的其余部分有着更深层的含义。 @@ -193,8 +193,8 @@ error[E0382]: use of moved value: `s1` | 3 | let s2 = s1; | -- value moved here -4 | println!("{}, world!",s1); - | ^^ value used here after move +4 | println!("{}, world!", s1); + | ^^ value used here after move | = note: move occurs because `s1` has type `std::string::String`, which does not implement the `Copy` trait @@ -326,7 +326,7 @@ fn takes_and_gives_back(a_string: String) -> String { // a_string comes into } ``` -变量的所有权总是遵循相同的模式:将值赋值给另一个变量时移动它,并且当持有堆中数据值的变量离开作用域时,如果数据的所有权没有被移动到另外一个变量时,其值将通过`drop`被清理掉。 +变量的所有权总是遵循相同的模式:将值赋值给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过`drop`被清理掉,除非数据被移动为另一个变量所有。 在每一个函数中都获取并接着返回所有权是冗余乏味的。如果我们想要函数使用一个值但不获取所有权改怎么办呢?如果我们还要接着使用它的话,每次都传递出去再传回来就有点烦人了,另外我们也可能想要返回函数体产生的任何(不止一个)数据。 diff --git a/src/ch07-01-mod-and-the-filesystem.md b/src/ch07-01-mod-and-the-filesystem.md index 0a4f5be..d64a786 100644 --- a/src/ch07-01-mod-and-the-filesystem.md +++ b/src/ch07-01-mod-and-the-filesystem.md @@ -2,7 +2,7 @@ > [ch07-01-mod-and-the-filesystem.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch07-01-mod-and-the-filesystem.md) >
    -> commit 6fc32eabcd09f7a130094767abadb691dfcdddf7 +> commit b0481ac44ff2594c6c240baa36357737739db445 我们将通过使用 Cargo 创建一个新项目来开始我们的模块之旅,不过不再创建一个二进制 crate,而是创建一个库 crate:一个其他人可以作为依赖导入的项目。第二章我们见过的`rand`就是这样的 crate。 diff --git a/src/ch10-03-lifetime-syntax.md b/src/ch10-03-lifetime-syntax.md index f6705e6..51865ae 100644 --- a/src/ch10-03-lifetime-syntax.md +++ b/src/ch10-03-lifetime-syntax.md @@ -2,7 +2,7 @@ > [ch10-03-lifetime-syntax.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch10-03-lifetime-syntax.md) >
    -> commit c49e5ee8859f8eb8f8867cbeafbdf5b802aa5894 +> commit 9fbbfb23c2cd1686dbd3ce7950ae1eda300937f6 当在第四章讨论引用时,我们遗漏了一个重要的细节:Rust 中的每一个引用都有其**生命周期**,也就是引用保持有效的作用域。大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以多种不同方式相关联的情况,所以 Rust 需要我们使用泛型生命周期参数来注明他们的关系,这样就能确保运行时实际使用的引用绝对是有效的。 @@ -223,7 +223,7 @@ specifies all the references in the signature must have the same lifetime, 当具体的引用被传递给`longest`时,具体被`'a`所替代的生命周期是`x`的作用域与`y`的作用域相重叠的那一部分。因为作用域总是嵌套的,所以换一种说法就是泛型生命周期`'a`的具体生命周期等同于`x`和`y`的生命周期中较小的那一个。因为我们用相同的生命周期参数标注了返回的引用值,所以返回的引用值就能保证在`x`和`y`中较短的那个生命周期结束之前保持有效。 -让我们如何通过传递拥有不同具体生命周期的引用来观察他们是如何限制`longest`函数的使用的。列表 10-22 是一个应该在任何编程语言中都很直观的例子:`string1`直到外部作用域结束都是有效的,`string2`则在内部作用域中是有效的,而`result`则引用了一些直到外部作用域结束都是有效的值。借用检查器赞同这些代码;它能够编译和运行,并打印出`The longest string is long string is long`: +让我们如何通过传递拥有不同具体生命周期的引用来观察他们是如何限制`longest`函数的使用的。列表 10-22 是一个应该在任何编程语言中都很直观的例子:`string1`直到外部作用域结束都是有效的,`string2`则在内部作用域中是有效的,而`result`则引用了一些直到内部作用域结束都是有效的值。借用检查器赞同这些代码;它能够编译和运行,并打印出`The longest string is long string is long`: Filename: src/main.rs diff --git a/src/ch11-01-writing-tests.md b/src/ch11-01-writing-tests.md index a03b4b7..ee39472 100644 --- a/src/ch11-01-writing-tests.md +++ b/src/ch11-01-writing-tests.md @@ -2,7 +2,7 @@ > [ch11-01-writing-tests.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch11-01-writing-tests.md) >
    -> commit 55b294f20fc846a13a9be623bf322d8b364cee77 +> commit c6162d22288253b2f2a017cfe96cf1aa765c2955 测试用来验证非测试的代码按照期望的方式运行的 Rust 函数。测试函数体通常包括一些设置,运行需要测试的代码,接着断言其结果是我们所期望的。让我们看看 Rust 提供的具体用来编写测试的功能:`test`属性、一些宏和`should_panic`属性。 @@ -454,7 +454,7 @@ fn greeting_contains_name() { ``` ---- tests::greeting_contains_name stdout ---- - thread 'tests::greeting_contains_name' panicked at 'Result did not contain + thread 'tests::greeting_contains_name' panicked at 'Greeting did not contain name, value was `Hello`', src/lib.rs:12 note: Run with `RUST_BACKTRACE=1` for a backtrace. ``` diff --git a/src/ch13-02-iterators.md b/src/ch13-02-iterators.md index 83aab4d..d810770 100644 --- a/src/ch13-02-iterators.md +++ b/src/ch13-02-iterators.md @@ -2,7 +2,7 @@ > [ch13-02-iterators.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch13-02-iterators.md) >
    -> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56 +> commit 431116f5c696000b9fd6780e5fde90392cef6812 迭代器是 Rust 中的一个模式,它允许你对一个项的序列进行某些处理。例如。列表 13-5 中对 vecctor 中的每一个数加一: @@ -68,7 +68,7 @@ trait Iterator { } ``` -这里有一些还未讲到的新语法:`type Item`和`Self::Item`定义了这个 trait 的**关联类型**(*associated type*),第19章会讲到关联类型。现在所有你需要知道就是这些代码表示`Iterator` trait 要求你也定义一个`Item`类型,而这个`Item`类型用作`next`方法的返回值。换句话说,`Item`类型将是迭代器返回的元素的类型。 +这里有一些还未讲到的新语法:`type Item`和`Self::Item`定义了这个 trait 的**关联类型**(*associated type*),第十九章会讲到关联类型。现在所有你需要知道就是这些代码表示`Iterator` trait 要求你也定义一个`Item`类型,而这个`Item`类型用作`next`方法的返回值。换句话说,`Item`类型将是迭代器返回的元素的类型。 让我们使用`Iterator` trait 来创建一个从一数到五的迭代器`Counter`。首先,需要创建一个结构体来存放迭代器的当前状态,它有一个`u32`的字段`count`。我们也定义了一个`new`方法,当然这并不是必须的。因为我们希望`Counter`能从一数到五,所以它总是从零开始: @@ -152,7 +152,7 @@ println!("{:?}", x); 好吧,当讲到`Iterator`的定义时,我们故意省略一个小的细节。`Iterator`定义了一系列默认实现,他们会调用`next`方法。因为`next`是唯一一个`Iterator` trait 没有默认实现的方法,一旦实现之后,`Iterator`的所有其他的适配器就都可用了。这些适配器可不少! -例如,处于某种原因我们希望获取一个`Counter`实例产生的头五个值,与另一个`Counter`实例第一个之后的值相组合,将每组数相乘,并只保留能被三整除的相乘结果,最后将保留结果相加,我们可以这么做: +例如,处于某种原因我们希望获取一个`Counter`实例产生的值,与另一个`Counter`实例忽略第一个值之后的值相组合,将每组数相乘,并只保留能被三整除的相乘结果,最后将所有保留的结果相加,我们可以这么做: ```rust @@ -182,8 +182,7 @@ println!("{:?}", x); # } # } # } -let sum: u32 = Counter::new().take(5) - .zip(Counter::new().skip(1)) +let sum: u32 = Counter::new().zip(Counter::new().skip(1)) .map(|(a, b)| a * b) .filter(|x| x % 3 == 0) .sum(); diff --git a/src/ch13-03-improving-our-io-project.md b/src/ch13-03-improving-our-io-project.md index 82877d1..09ac7b2 100644 --- a/src/ch13-03-improving-our-io-project.md +++ b/src/ch13-03-improving-our-io-project.md @@ -2,9 +2,9 @@ > [ch13-03-improving-our-io-project.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch13-03-improving-our-io-project.md) >
    -> commit 4f2dc564851dc04b271a2260c834643dfd86c724 +> commit 0608e2d0743951d8e628b6e130c6b5744775a783 -在我们上一章实现的`grep` I/O 项目中,其中有一些地方的代码可以使用迭代器来变得更清楚简洁一些。让我们看看迭代器如何能够改进`Config::new`函数和`grep`函数的实现。 +在我们上一章实现的`grep` I/O 项目中,其中有一些地方的代码可以使用迭代器来变得更清楚简洁一些。让我们看看迭代器如何能够改进`Config::new`函数和`search`函数的实现。 ### 使用迭代器并去掉`clone` @@ -17,18 +17,18 @@ impl Config { return Err("not enough arguments"); } - let search = args[1].clone(); + let query = args[1].clone(); let filename = args[2].clone(); Ok(Config { - search: search, + query: query, filename: filename, }) } } ``` -当时我们说不必担心这里的`clone`调用,因为将来会移除他们。好吧,就是现在了!所以,为什么这里需要`clone`呢?这里的问题是参数`args`中有一个`String`元素的 slice,而`new`函数并不拥有`args`。为了能够返回`Config`实例的所有权,我们需要克隆`Config`中字段`search`和`filename`的值,这样`Config`就能拥有这些值了。 +当时我们说不必担心这里的`clone`调用,因为将来会移除他们。好吧,就是现在了!所以,为什么这里需要`clone`呢?这里的问题是参数`args`中有一个`String`元素的 slice,而`new`函数并不拥有`args`。为了能够返回`Config`实例的所有权,我们需要克隆`Config`中字段`query`和`filename`的值,这样`Config`就能拥有这些值了。 现在在认识了迭代器之后,我们可以将`new`函数改为获取一个有所有权的迭代器作为参数。可以使用迭代器来代替之前必要的 slice 长度检查和特定位置的索引。因为我们获取了迭代器的所有权,就不再需要借用所有权的索引操作了,我们可以直接将迭代器中的`String`值移动到`Config`中,而不用调用`clone`来创建一个新的实例。 @@ -56,7 +56,7 @@ impl Config { ```rust # struct Config { -# search: String, +# query: String, # filename: String, # } # @@ -64,9 +64,9 @@ impl Config { fn new(mut args: std::env::Args) -> Result { args.next(); - let search = match args.next() { + let query = match args.next() { Some(arg) => arg, - None => return Err("Didn't get a search string"), + None => return Err("Didn't get a query string"), }; let filename = match args.next() { @@ -75,7 +75,7 @@ impl Config { }; Ok(Config { - search: search, + query: query, filename: filename, }) } @@ -84,24 +84,24 @@ impl Config { -还记得`env::args`返回值的第一个值是程序的名称吗。我们希望忽略它,所以首先调用`next`并不处理其返回值。第二次调用`next`的返回值应该是希望放入`Config`中`search`字段的值。使用`match`来在`next`返回`Some`时提取值,而在因为没有足够的参数(这会造成`next`调用返回`None`)而提早返回`Err`值。 +还记得`env::args`返回值的第一个值是程序的名称吗。我们希望忽略它,所以首先调用`next`并不处理其返回值。第二次调用`next`的返回值应该是希望放入`Config`中`query`字段的值。使用`match`来在`next`返回`Some`时提取值,而在因为没有足够的参数(这会造成`next`调用返回`None`)而提早返回`Err`值。 -对`filename`值也进行相同处理。稍微有些可惜的是`search`和`filename`的`match`表达式是如此的相似。如果可以对`next`返回的`Option`使用`?`就好了,不过目前`?`只能用于`Result`值。即便我们可以像`Result`一样对`Option`使用`?`,得到的值也是借用的,而我们希望能够将迭代器中的`String`移动到`Config`中。 +对`filename`值也进行相同处理。稍微有些可惜的是`query`和`filename`的`match`表达式是如此的相似。如果可以对`next`返回的`Option`使用`?`就好了,不过目前`?`只能用于`Result`值。即便我们可以像`Result`一样对`Option`使用`?`,得到的值也是借用的,而我们希望能够将迭代器中的`String`移动到`Config`中。 ### 使用迭代器适配器来使代码更简明 -另一部分可以利用迭代器的代码位于列表 12-15 中实现的`grep`函数中: +另一部分可以利用迭代器的代码位于列表 12-15 中实现的`search`函数中: ```rust -fn grep<'a>(search: &str, contents: &'a str) -> Vec<&'a str> { +fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { let mut results = Vec::new(); for line in contents.lines() { - if line.contains(search) { + if line.contains(query) { results.push(line); } } @@ -113,27 +113,27 @@ fn grep<'a>(search: &str, contents: &'a str) -> Vec<&'a str> { 我们可以用一种更简短的方式来编写这些代码,并避免使用了一个作为可变中间值的`results` vector,像这样使用迭代器适配器方法来实现: ```rust -fn grep<'a>(search: &str, contents: &'a str) -> Vec<&'a str> { +fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { contents.lines() - .filter(|line| line.contains(search)) + .filter(|line| line.contains(query)) .collect() } ``` -这里使用了`filter`适配器来只保留`line.contains(search)`为真的那些行。接着使用`collect`将他们放入另一个 vector 中。这就简单多了! +这里使用了`filter`适配器来只保留`line.contains(query)`为真的那些行。接着使用`collect`将他们放入另一个 vector 中。这就简单多了! -也可以对列表 12-16 中定义的`grep_case_insensitive`函数使用如下同样的技术: +也可以对列表 12-16 中定义的`search_case_insensitive`函数使用如下同样的技术: ```rust -fn grep_case_insensitive<'a>(search: &str, contents: &'a str) -> Vec<&'a str> { - let search = search.to_lowercase(); +fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { + let query = query.to_lowercase(); contents.lines() .filter(|line| { - line.to_lowercase().contains(&search) + line.to_lowercase().contains(&query) }).collect() } ``` diff --git a/src/ch15-06-reference-cycles.md b/src/ch15-06-reference-cycles.md index e42e652..48ce5f5 100644 --- a/src/ch15-06-reference-cycles.md +++ b/src/ch15-06-reference-cycles.md @@ -2,7 +2,7 @@ > [ch15-06-reference-cycles.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch15-06-reference-cycles.md) >
    -> commit c49e5ee8859f8eb8f8867cbeafbdf5b802aa5894 +> commit 9430a3d28a2121a938d704ce48b15d21062f880e 我们讨论过 Rust 做出的一些保证,例如永远也不会遇到一个空值,而且数据竞争也会在编译时被阻止。Rust 的内存安全保证也使其更难以制造从不被清理的内存,这被称为**内存泄露**。然而 Rust 并不是**不可能**出现内存泄漏,避免内存泄露**并**不是 Rust 的保证之一。换句话说,内存泄露是安全的。 @@ -274,6 +274,6 @@ examining strong and weak reference counts of `leaf` and `branch` 如果本章内容引起了你的兴趣并希望现在就实现你自己的智能指针的话,请阅读 [The Nomicon] 来获取更多有用的信息。 -[The Nomicon]: https://doc.rust-lang.org/stable/nomicon/vec.html +[The Nomicon]: https://doc.rust-lang.org/stable/nomicon/ 接下来,让我们谈谈 Rust 的并发。我们还会学习到一些新的对并发有帮助的智能指针。 \ 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 60c07cf..ad04399 100644 --- a/src/ch16-04-extensible-concurrency-sync-and-send.md +++ b/src/ch16-04-extensible-concurrency-sync-and-send.md @@ -2,7 +2,7 @@ > [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 +> commit 9430a3d28a2121a938d704ce48b15d21062f880e Rust 的并发模型中一个有趣的方面是:语言本身对并发知之**甚少**。我们之前讨论的几乎所有内容,都属于标准库,而不是语言本身的内容。由于不需要语言提供并发相关的基础设施,并发方案不受标准库或语言所限:我们可以编写自己的或使用别人编写的。 @@ -28,7 +28,7 @@ Rust 的并发模型中一个有趣的方面是:语言本身对并发知之** 实现这些标记 trait 涉及到编写不安全的 Rust 代码,第十九章将会讲述具体的方法;当前重要的是,在创建新的由不是`Send`和`Sync`的部分构成的并发类型时需要多加小心,以确保维持其安全保证。[The Nomicon] 中有更多关于这些保证以及如何维持他们的信息。 -[The Nomicon]: https://doc.rust-lang.org/stable/nomicon/vec.html +[The Nomicon]: https://doc.rust-lang.org/stable/nomicon/ ## 总结 diff --git a/src/ch17-00-oop.md b/src/ch17-00-oop.md index 6ad8124..0bc0f41 100644 --- a/src/ch17-00-oop.md +++ b/src/ch17-00-oop.md @@ -4,4 +4,4 @@ >
    > commit 759801361bde74b47e81755fff545c66020e6e63 -面向对象编程(Object-Oriented Programming)是一种起源于 20 世纪 60 年代的 Simula 编程语言的模式化编程方式,然后在 90 年代随着 C++ 语言开始流行。为了描述 OOP 有很多种复杂的定义:在一些定义下,Rust 是面向对象的;在其他定义下,Rust 不是。在本章节中,我们会探索一些被普遍认为是面向对象的特性和这些特性是如何体现在 Rust 语言习惯中的。 \ No newline at end of file +面向对象编程(Object-Oriented Programming)是一种起源于 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 index 263beb9..1e0d687 100644 --- a/src/ch17-01-what-is-oo.md +++ b/src/ch17-01-what-is-oo.md @@ -4,9 +4,9 @@ >
    > commit 2a9b2a1b019ad6d4832ff3e56fbcba5be68b250e -关于一门语言是否需要是面向对象,在编程社区内并未达成一致意见。Rust 被很多不同的编程范式影响,我们探索了十三章提到的函数式编程的特性。面向对象编程语言的一些特性往往是对象、封装和继承。我们看一下这每一个概念的含义以及 Rust 是否支持他们。 +关于一个语言被称为面向对象所需的功能,在编程社区内并未达成一致意见。Rust 被很多不同的编程范式影响;我们探索了十三章提到的来自函数式编程的特性。面向对象编程语言所共享的一些特性往往是对象、封装和继承。让我们看一下这每一个概念的含义以及 Rust 是否支持他们。 -## 对象包含数据和行为 +### 对象包含数据和行为 `Design Patterns: Elements of Reusable Object-Oriented Software`这本书被俗称为`The Gang of Four book`,是面向对象编程模式的目录。它这样定义面向对象编程: @@ -16,11 +16,11 @@ > > 面向对象的程序是由对象组成的。一个**对象**包含数据和操作这些数据的过程。这些过程通常被称为**方法**或**操作**。 -在这个定义下,Rust 是面向对象的:结构体和枚举包含数据而 impl 块提供了在结构体和枚举之上的方法。虽然带有方法的结构体和枚举并不被**称为**对象,但是他们提供了与对象相同的功能,参考 Gang of Four 所定义的对象。 +在这个定义下,Rust 是面向对象的:结构体和枚举包含数据而 impl 块提供了在结构体和枚举之上的方法。虽然带有方法的结构体和枚举并不被**称为**对象,但是他们提供了与对象相同的功能,参考 Gang of Four 中对象的定义。 -## 隐藏了实现细节的封装 +### 隐藏了实现细节的封装 -另一个通常与面向对象编程相关的方面是**封装**的思想:对象的实现细节不能被使用对象的代码获取到。唯一与对象交互的方式是通过对象提供的公有 API;使用对象的代码无法深入到对象内部并直接改变数据或者行为。封装使得改变和重构对象的内部时无需改变使用对象的代码。 +另一个通常与面向对象编程相关的方面是**封装**(*encapsulation*)的思想:对象的实现细节不能被使用对象的代码获取到。唯一与对象交互的方式是通过对象提供的公有 API;使用对象的代码无法深入到对象内部并直接改变数据或者行为。封装使得改变和重构对象的内部时无需改变使用对象的代码。 就像我们在第七章讨论的那样,可以使用`pub`关键字来决定模块、类型函数和方法是公有的,而默认情况下一切都是私有的。比如,我们可以定义一个包含一个`i32`类型的 vector 的结构体`AveragedCollection `。结构体也可以有一个字段,该字段保存了 vector 中所有值的平均值。这样,希望知道结构体中的 vector 的平均值的人可以随时获取它,而无需自己计算。`AveragedCollection`会为我们缓存平均值结果。列表 17-1 有`AveragedCollection`结构体的定义: @@ -35,7 +35,7 @@ pub struct AveragedCollection { 列表 17-1: `AveragedCollection`结构体维护了一个整型列表和集合中所有元素的平均值。 -注意,结构体自身被标记为`pub`,这样其他代码可以使用这个结构体,但是在结构体内部的字段仍然是私有的。这是非常重要的,因为我们希望保证变量被增加到列表或者被从列表删除时,也会同时更新平均值。我们通过在结构体上实现`add`、`remove`和`average`方法来做到这一点,如列表 17-2 所示: +注意,结构体自身被标记为`pub`,这样其他代码可以使用这个结构体,但是在结构体内部的字段仍然是私有的。这是非常重要的,因为我们希望保证变量被增加到列表或者被从列表删除时,也会同时更新平均值。可以通过在结构体上实现`add`、`remove`和`average`方法来做到这一点,如列表 17-2 所示: 文件名: src/lib.rs @@ -84,7 +84,7 @@ impl AveragedCollection { **继承**(*Inheritance*)是一个很多编程语言都提供的机制,一个对象可以定义为继承另一个对象的定义,这使其可以获得父对象的数据和行为,而不用重新定义。一些人定义面向对象语言时,认为继承是一个特色。 -如果一个语言必须有继承才能被称为面向对象的语言的话,那么 Rust 就不是面向对象的。无法定义一个结构体继承自另外一个结构体,从而获得父结构体的成员和方法。然而,如果你过去常常在你的编程工具箱使用继承,根据你希望使用继承的原因,Rust 提供了其他的解决方案。 +如果一个语言必须有继承才能被称为面向对象语言的话,那么 Rust 就不是面向对象的。无法定义一个结构体继承自另外一个结构体,从而获得父结构体的成员和方法。然而,如果你过去常常在你的编程工具箱使用继承,根据你希望使用继承的原因,Rust 也提供了其他的解决方案。 使用继承有两个主要的原因。第一个是为了重用代码:一旦为一个类型实现了特定行为,继承可以对一个不同的类型重用这个实现。相反 Rust 代码可以使用默认 trait 方法实现来进行共享,在列表 10-14 中我们见过在`Summarizable` trait 上增加的`summary`方法的默认实现。任何实现了`Summarizable` trait 的类型都可以使用`summary`方法而无须进一步实现。这类似于父类有一个方法的实现,而通过继承子类也拥有这个方法的实现。当实现`Summarizable` trait 时也可以选择覆盖`summary`的默认实现,这类似于子类覆盖从父类继承的方法实现。 @@ -92,7 +92,7 @@ impl AveragedCollection { ->虽然很多人使用“多态”来描述继承,但是它实际上是一种特殊的多态,称为“子类型多态”。也有很多种其他形式的多态,在 Rust 中带有泛型参数的 trait bound 也是多态,更具体的说是“参数多态”。不同类型多态的确切细节在这里并不关键,所以不要过于担心细节,只需要知道 Rust 有多种多态相关的特色就好,不同于很多其他 OOP 语言。 +> 虽然很多人使用“多态”("polymorphism")来描述继承,但是它实际上是一种特殊的多态,称为“子类型多态”("sub-type polymorphism")。也有很多种其他形式的多态,在 Rust 中带有泛型参数的 trait bound 也是多态,更具体的说是“参数多态”("parametric polymorphism")。不同类型多态的确切细节在这里并不关键,所以不要过于担心细节,只需要知道 Rust 有多种多态相关的特色就好,不同于很多其他 OOP 语言。 diff --git a/src/ch17-02-trait-objects.md b/src/ch17-02-trait-objects.md index 1e1af87..6f947b2 100644 --- a/src/ch17-02-trait-objects.md +++ b/src/ch17-02-trait-objects.md @@ -10,7 +10,7 @@ the one with SpreadsheetCell. I will go back and add Listing 8-1 next time I get Chapter 8 for editing. /Carol --> -有时,我们需要可扩展的类型集合,能够被库的用户扩展。比如很多图形化接口工具有一个条目列表,迭代该列表并调用每个条目的 draw 方法。我们将创建一个库 crate,包含称为 `rust_gui` 的 GUI 库。库中有一些为用户准备的类型,比如 `Button` 或 `TextField`,`rust_gui`的用户还会创建更多,有的用户会增加`Image`,有的用户会增加`SelectBox`,然后用它们在屏幕上绘图。我们不会在本章节实现一个完善的GUI库,只是展示如何把各部分组合起来。 +有时,我们希望使用的类型的集合对于使用库的程序员来说是可扩展的。例如,很多图形用户接口(GUI)工具有一个条目列表的概念,它通过遍历列表并对每一个条目调用`draw`方法来绘制在屏幕上。我们将要创建一个叫做`rust_gui`的包含一个 GUI 库结构的库 crate。GUI 库可以包含一些供开发者使用的类型,比如`Button`或`TextField`。使用`rust_gui`的程序员会想要创建更多可以绘制在屏幕上的类型:一个程序员可能会增加一个`Image`,而另一个可能会增加一个`SelectBox`。我们不会在本章节实现一个功能完善的 GUI 库,不过会展示各个部分是如何结合在一起的。 当写 `rust_gui` 库时,我们不知道其他程序员需要什么类型,所以无法定义一个 `enum` 来包含所有的类型。然而 `rust_gui` 需要跟踪所有这些不同类型的值,需要有在每个值上调用 `draw` 方法能力。我们的 GUI 库不需要确切地知道调用 `draw` 方法会发生什么,只需要有可用的方法供我们调用。