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 代码行以;
结尾。
仅仅使用rustc
编译简单程序是没问题的,不过随着项目的增长,你将想要能够控制你项目拥有的所有选项,并使其易于分享你的代码给别人或别的项目。接下来,我们将介绍一个叫做 Cargo 的工具,它将帮助你编写现实生活中的 Rust 程序。
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
是一个拥有Ok
或Err
两个成员的枚举。在这里使用match
表达式,就像之前处理cmp
方法返回的Ordering
一样。
从expect
调用切换到match
语句是如何从遇到错误就崩溃到真正处理错误的常用手段。记住parse
返回一个Result
类型,而Result
是一个拥有Ok
或Err
两个成员的枚举。在这里使用match
表达式,就像之前处理cmp
方法返回的Ordering
一样。
如果parse
能够成功的将字符串转换为一个数字,它会返回一个包含结果数字Ok
值。这个Ok
值会匹配第一个分支的模式,这时match
表达式仅仅返回parse
产生的Ok
值之中的num
值。这个数字会最终如期变成新创建的guess
变量。
如果parse
不能将字符串转换为一个数字,它会返回一个包含更多错误信息的Err
值。Err
值不能匹配第一个match
分支的Ok(num)
模式,但是会匹配第二个分支的Err(_)
模式。_
是一个包罗万象的值;在这个例子中,我们想要匹配所有Err
值,不管其中有何种信息。所以程序会执行第二个分支的代码,continue
,这意味着进入loop
的下一次循环并请求另一个猜测。这样程序就有效地忽略了parse
可能遇到的所有错误!
现在万事俱备(只欠东风)了。运行cargo run
来尝试一下:
ch03-02-data-types.md
+commit fe4833a8ef2853c55424e7747a4ef8dd64c35b32
-commit 04aa3a45eb72855b34213703718f50a12a3eeec8
Rust 中的任何值都有一个具体的类型(type),这告诉了 Rust 它被指定了何种数据,这样 Rust 就知道如何处理这些数据了。这一部分将讲到一些语言内建的类型。我们将这些类型分为两个子集:标量(scalar)和复合(compound)。
贯穿整个部分,请记住 Rust 是一个静态类型(statically typed)语言,也就是说必须在编译时就知道所有变量的类型。编译器通常可以通过值以及如何使用他们来推断出我们想要用的类型。当多个类型都是可能的时候,比如第二章中parse
将String
转换为数字类型,必须增加类型注解,像这样:
使用布尔值的主要场景是条件语句,例如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 6d4ef020095a375483b2121d4fa2b1661062cc92
-commit fae5fa82d728b5965ecbba84060689430345e509
Rust 的核心功能(之一)是所有权(ownership)。虽然这个功能理解起来很直观,不过它对语言的其余部分有着更深层的含义。
所有程序都必须管理他们运行时使用计算机内存的方式。一些语言中使用垃圾回收在程序运行过程中来时刻寻找不再被使用的内存;在另一些语言中,程序员必须亲自分配和释放内存。Rust 则选择了第三种方式:内存被一个所有权系统管理,它拥有一系列的规则使编译器在编译时进行检查。任何所有权系统的功能都不会导致运行时开销。
@@ -91,17 +91,17 @@ commit fae5fa82d728b5965ecbba84060689430345e509首先,让我们看一下所有权的规则。请记住这些规则因为我们将讲解一些说明这些规则的例子:
+首先,让我们看一下所有权的规则。请记住它们,我们将讲解一些它们的例子:
-
- Rust 中的每一个值都有一个叫做它的所有者(owner)的变量。
-- 同时一次只能有一个所有者
-- 当所有者变量离开作用域,这个值将被丢弃。
+- 每一个值都被它的所有者(owner)变量拥有。
+- 值在任意时刻只能被一个所有者拥有。
+- 当所有者离开作用域,这个值将被丢弃。
我们在第二章已经完成过一个 Rust 程序的例子了。现在我们已经掌握了基本语法,所以不会在所有的例子中包含fn main() {
代码了,所以如果你是一路跟过来的,必须手动将之后例子的代码放入一个main
函数中。为此,例子将显得更加具体,使我们可以关注具体细节而不是样板代码。
作为所有权的第一个例子,我们看看一些变量的作用域(scope)。作用域是一个项在程序中有效的范围。假如有一个这样的变量:
+我们已经在第二章完成过一个 Rust 程序的例子。现在我们已经掌握了基本语法,所以不会在所有的例子中包含fn main() {
代码了,所以如果你是一路跟过来的,必须手动将之后例子的代码放入一个main
函数中。为此,例子将显得更加具体,使我们可以关注具体细节而不是样板代码。
作为所有权的第一个例子,我们看看一些变量的作用域(scope)。作用域是一个项(原文:item)在程序中有效的范围。假如有一个这样的变量:
let s = "hello";
变量s
绑定到了一个字符串字面值,这个字符串值是硬编码进我们程序代码中的。这个变量从声明的点开始直到当前作用域结束时都是有效的。列表 4-1 的注释标明了变量s
在哪里是有效的:
变量的所有权总是遵循相同的模式:将值赋值给另一个变量时移动它,并且当持有堆中数据值的变量离开作用域时,如果数据的所有权没有被移动到另外一个变量时,其值将通过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)。
数据竞争是一种特定类型的竞争状态,它可由这三个行为造成:
-- 两个或更多指针同时访问相同的数据。
-- 至少有一个指针被用来写数据。
-- 没有被用来同步数据访问的机制。
+- 两个或更多指针同时访问同一数据。
+- 至少有一个指针被写入。
+- 没有同步数据访问的机制。
-数据竞争会导致未定义行为并且当在运行时尝试追踪时可能会变得难以诊断和修复;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 地址类型,V4
和V6
。这被称为枚举的成员(variants):
enum IpAddrKind {
@@ -92,7 +92,7 @@ commit e6d6caab41471f7115a621029bd428a812c5260e
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
-注意枚举的成员位于其标识符的命名空间中,并使用两个冒号分开。这么设计的益处是现在IpAddrKind::V4
和IpAddrKind::V6
是相同类型的:IpAddrKind
。例如,接着我们可以顶一个函数来获取IpAddrKind
:
+注意枚举的成员位于其标识符的命名空间中,并使用两个冒号分开。这么设计的益处是现在IpAddrKind::V4
和IpAddrKind::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
强制要求的穷进行检查。match
和if let
之间的选择依赖特定的环境以及增加简洁度和失去穷尽性检查的权衡取舍。
使用if let
意味着编写更少代码,更少的缩进和更少的样板代码。然而,这样会失去match
强制要求的穷尽性检查。match
和if let
之间的选择依赖特定的环境以及增加简洁度和失去穷尽性检查的权衡取舍。
换句话说,可以认为if let
是match
的一个语法糖,它当值匹配某一模式时执行代码而忽略所有其他值。
可以在if let
中包含一个else
。else
块中的代码与match
表达式中的_
分支块中的代码相同,这样的match
表达式就等同于if let
和else
。回忆一下列表 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
-最后要介绍的常用集合类型是哈希 map(hash map)。HashMap<K, V>
类型储存了一个键类型K
对应一个值类型V
的映射。它通过一个哈希函数(hashing function)来实现映射,它决定了如何将键和值放入内存中。很多编程语言支持这种数据结构,不过通常有不同的名字:哈希、map、对象、哈希表或者关联数组,仅举几例。
+最后介绍的常用集合类型是 哈希 map(hash 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
所包含的类型。
对于像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 中的数据时,根据目标键是否有值以及值的更新策略分成多种情况,下面我们了解一下:
如果我们插入了一个键值对,接着用相同的键插入一个不同的值,与这个键相关联的旧值将被替换。即便下面的代码调用了两次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 会在你的程序需要储存、访问和修改数据时帮助你。这里有一些你应该能够解决的练习问题:
如此获取一个参数并只返回 1 到 100 之间数字的函数就可以声明为获取或返回一个Guess
,而不是u32
,同时其函数体中也无需进行任何额外的检查。
Rust 的错误处理功能被设计为帮助你编写更加健壮的代码。panic!
宏代表一个程序无法处理的状态,并停止执行而不是使用无效或不正确的值继续处理。Rust 类型系统的Result
枚举代表操作可能会在一种可以恢复的情况下失败。可以使用Result
来告诉代码调用者他需要处理潜在的成功或失败。在适当的场景使用panic!
和Result
将会使你的代码在面对无处不在的错误时显得更加可靠。
现在我们已经见识过了标准库中Option
和Result
泛型枚举的能力了,让我们聊聊泛型是如何工作的,以及如果在你的代码中利用他们。
现在我们已经见识过了标准库中Option
和Result
泛型枚举的能力了,让我们聊聊泛型是如何工作的,以及如何在你的代码中利用他们。
这里largest_i32
和largest_char
有着完全相同的函数体,所以能够将这两个函数变成一个来减少重复就太好了。所幸通过引入一个泛型参数就能实现!
为了参数化要定义的函数的签名中的类型,我们需要像给函数的值参数起名那样为这类型参数起一个名字。这里选择了名称T
。任何标识符都可以作为类型参数名,选择T
是因为 Rust 的类型命名规范是骆驼命名法(CamelCase)。另外泛型类型参数的规范也倾向于简短,经常仅仅是一个字母。T
作为“type”的缩写是大部分 Rust 程序员的首选。
当需要再函数体中使用一个参数时,必须在函数签名中声明这个参数以便编译器能知道函数体中这个名称的意义。同理,当在函数签名中使用一个类型参数时,必须在使用它之前就声明它。类型参数声明位于函数名称与参数列表中间的尖括号中。
+当需要在函数体中使用一个参数时,必须在函数签名中声明这个参数以便编译器能知道函数体中这个名称的意义。同理,当在函数签名中使用一个类型参数时,必须在使用它之前就声明它。类型参数声明位于函数名称与参数列表中间的尖括号中。
我们将要定义的泛型版本的largest
函数的签名看起来像这样:
fn largest<T>(list: &[T]) -> T {
@@ -161,7 +161,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> {
diff --git a/docs/ch11-01-writing-tests.html b/docs/ch11-01-writing-tests.html
index 9d62674..72e9a54 100644
--- a/docs/ch11-01-writing-tests.html
+++ b/docs/ch11-01-writing-tests.html
@@ -71,7 +71,7 @@
ch11-01-writing-tests.md
-commit 55b294f20fc846a13a9be623bf322d8b364cee77
+commit c6162d22288253b2f2a017cfe96cf1aa765c2955
测试用来验证非测试的代码按照期望的方式运行的 Rust 函数。测试函数体通常包括一些设置,运行需要测试的代码,接着断言其结果是我们所期望的。让我们看看 Rust 提供的具体用来编写测试的功能:test
属性、一些宏和should_panic
属性。
测试函数剖析
@@ -406,7 +406,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/docs/ch12-04-testing-the-librarys-functionality.html b/docs/ch12-04-testing-the-librarys-functionality.html
index 72e0839..a4c1743 100644
--- a/docs/ch12-04-testing-the-librarys-functionality.html
+++ b/docs/ch12-04-testing-the-librarys-functionality.html
@@ -185,7 +185,7 @@ error: test failed
-
lines
方法返回一个迭代器。第十三张会深入了解迭代器,不过我们已经在列表 3-6 中见过使用迭代器的方法,在那里使用了一个for
循环和迭代器在一个集合的每一项上运行一些代码。
lines
方法返回一个迭代器。第十三章会深入了解迭代器,不过我们已经在列表 3-6 中见过使用迭代器的方法,在那里使用了一个for
循环和迭代器在一个集合的每一项上运行一些代码。
之后我们将修复Config::new
的函数体。因为标准库文档也表明,std::env::Args
实现了Iterator
trait,所以我们知道可以调用其next
方法!如下就是新的代码:
# struct Config {
-# search: String,
+# query: String,
# filename: String,
# }
#
@@ -116,9 +116,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() {
@@ -127,25 +127,25 @@ impl Config {
};
Ok(Config {
- search: search,
+ query: query,
filename: filename,
})
}
}
-还记得env::args
返回值的第一个值是程序的名称吗。我们希望忽略它,所以首先调用next
并不处理其返回值。第二次调用next
的返回值应该是希望放入Config
中search
字段的值。使用match
来在next
返回Some
时提取值,而在因为没有足够的参数(这会造成next
调用返回None
)而提早返回Err
值。
对filename
值也进行相同处理。稍微有些可惜的是search
和filename
的match
表达式是如此的相似。如果可以对next
返回的Option
使用?
就好了,不过目前?
只能用于Result
值。即便我们可以像Result
一样对Option
使用?
,得到的值也是借用的,而我们希望能够将迭代器中的String
移动到Config
中。
还记得env::args
返回值的第一个值是程序的名称吗。我们希望忽略它,所以首先调用next
并不处理其返回值。第二次调用next
的返回值应该是希望放入Config
中query
字段的值。使用match
来在next
返回Some
时提取值,而在因为没有足够的参数(这会造成next
调用返回None
)而提早返回Err
值。
对filename
值也进行相同处理。稍微有些可惜的是query
和filename
的match
表达式是如此的相似。如果可以对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);
}
}
@@ -154,22 +154,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()
}
diff --git a/docs/ch14-04-installing-binaries.html b/docs/ch14-04-installing-binaries.html
index 612cc5d..822e9b9 100644
--- a/docs/ch14-04-installing-binaries.html
+++ b/docs/ch14-04-installing-binaries.html
@@ -73,7 +73,7 @@
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`
diff --git a/docs/ch15-00-smart-pointers.html b/docs/ch15-00-smart-pointers.html
index 6942c4c..91304f6 100644
--- a/docs/ch15-00-smart-pointers.html
+++ b/docs/ch15-00-smart-pointers.html
@@ -91,31 +91,31 @@ commit 4f2dc564851dc04b271a2260c834643dfd86c724
-
+
+
-
-
+
-
+
-
+
+
-
-
+
-
+
@@ -128,7 +128,7 @@ commit 4f2dc564851dc04b271a2260c834643dfd86c724
-
+
diff --git a/docs/ch15-01-box.html b/docs/ch15-01-box.html
index a15be48..66dd193 100644
--- a/docs/ch15-01-box.html
+++ b/docs/ch15-01-box.html
@@ -73,7 +73,7 @@
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);
@@ -92,7 +92,7 @@ box
-我们实现了一个只存放i32
值的 cons list。也可以选择实用第十章介绍的泛型来实现一个类型无关的 cons list。
+我们实现了一个只存放i32
值的 cons list。也可以选择使用第十章介绍的泛型来实现一个类型无关的 cons list。
cons list 的更多内容
cons list 是一个来源于 Lisp 编程语言及其方言的数据结构。在 Lisp 中,cons
函数("construct function"的缩写)利用两个参数来构造一个新的列表,他们通常是一个单独的值和另一个列表。
@@ -159,7 +159,7 @@ fn main() {
-这样编译器就能够计算出储存一个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
成员看起来像什么:
diff --git a/docs/ch15-02-deref.html b/docs/ch15-02-deref.html
index 744fee6..003409d 100644
--- a/docs/ch15-02-deref.html
+++ b/docs/ch15-02-deref.html
@@ -73,7 +73,7 @@
commit ecc3adfe0cfa0a4a15a178dc002702fd0ea74b3f
-第一个智能指针相关的重要 trait 是Deref
,它允许我们重载*
,解引用运算符(不同于乘法运算符和全局引用运算符)。重载智能指针的*
能使访问其后的数据更为方便,在这个部分的稍后介绍解引用强制多态时我们会讨论方便的意义。
+第一个智能指针相关的重要 trait 是Deref
,它允许我们重载*
,解引用运算符(不同于乘法运算符和全局引用运算符)。重载智能指针的*
能使访问其持有的数据更为方便,在本章结束前谈到解引用强制多态时我们会说明方便的意义。
第八章的哈希 map 的“根据旧值更新一个值”部分简要的提到了解引用运算符。当时有一个可变引用,而我们希望改变这个引用所指向的值。为此,首先我们必须解引用。这是另一个使用i32
值引用的例子:
let mut x = 5;
{
@@ -84,7 +84,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
@@ -122,7 +122,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 调用函数或方法时无需很多使用&
和*
的引用和解引用。
@@ -147,36 +147,36 @@ struct that holds mp3 file data and metadata
从&mut T
到&U
当T: Deref<Target=U>
。
头两个情况除了可变性之外是相同的:如果有一个&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 是Drop
。Drop
运行我们在值要离开作用域时执行一些代码。智能指针在被丢弃时会执行一些重要的清理工作,比如释放内存或减少引用计数。更一般的来讲,数据类型可以管理多于内存的资源,比如文件或网络连接,而使用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() {
-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
中。接着当创建b
和c
时,我们对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>
,我们使用borrow
和borrow_mut
方法,它是RefCell<T>
拥有的安全 API 的一部分。borrow
返回Ref
类型的智能指针,而borrow_mut
返回RefMut
类型的智能指针。这两个类型实现了Deref
所以可以被当作常规引用处理。Ref
和RefMut
动态的借用所有权,而他们的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_count
;Weak<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 会确保Some
和None
的情况都被处理并不会尝试使用一个无效的指针。
-不同于列表 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 Deref
和Drop
。同时探索了形成引用循环和造成内存泄漏的可能性,以及如何使用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),其中多个线程可以访问同一片数据。
-Sync
和Send
trait,他们允许 Rust 的并发保证能扩展到用户定义的和标准库中提供的类型中。
+Sync
和Send
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 线程,这里M
和N
不必相同。
-每一个模型都有其自己的优势和取舍。对于 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() {
-这些代码可以运行,而新建线程则可能直接就出错了并完全没有机会运行。新建线程内部有一个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”的缩写,也就是说,任意时间,它只允许一个线程访问某些数据。互斥器以难以使用著称,因为你不得不记住:
-- 必须记住在使用数据之前尝试获取锁。
-- 一旦处理完被互斥器所保护的数据之后,必须记得解锁数据这样其他线程才能够获取锁。
+- 在使用数据之前尝试获取锁。
+- 处理完被互斥器所保护的数据之后,必须解锁数据,这样其他线程才能够获取锁。
-对于一个现实中的互斥器的例子,想象一下在一个会议中的专门小组讨论会上,不过只有一个麦克风。在一个小组成员可能发言之前,他们必须请求或示意他们需要使用麦克风。一旦得到了麦克风,他们可以发言任意长的时间,接着将麦克风交给系一个希望讲话的小组成员。如果小组成员在没有麦克风的时候就开始叫喊或者在其他成员发言结束之前就取得麦克风将是很无理的。如果对这个共享的麦克风的管理因为任何这些原因出现问题,讨论会将无法如期进行。
-正确的管理互斥器是异常复杂的,这也就是为什么这么多人都热衷于通道。然而,在 Rust 中,得益于类型系统和所有权,我们不可能会在锁和解锁上出错。
+现实中也有互斥器的例子,想象一下在一个会议中,只有一个麦克风。如果一个成员要发言,他必须请求使用麦克风。一旦得到了麦克风,他可以畅所欲言,然后将麦克风交给下一个希望讲话的成员。如果成员在没有麦克风的时候就开始叫喊,或者在其他成员发言结束之前就拿走麦克风,是很不合适的。如果这个共享的麦克风因为此类原因而出现问题,会议将无法正常进行。
+正确的管理互斥器异常复杂,这也是许多人之所以热衷于通道的原因。然而,在 Rust 中,得益于类型系统和所有权,我们不会在锁和解锁上出错。
Mutex<T>
的 API
-让我们看看列表 16-12 中使用互斥器的例子,现在并不涉及到多线程:
+让我们看看列表 16-12 中使用互斥器的例子,现在不涉及多线程:
Filename: src/main.rs
use std::sync::Mutex;
@@ -107,12 +107,12 @@ fn main() {
-与很多类型一样,我们通过叫做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() {
-这里创建了一个counter
变量来存放内含i32
的Mutex<T>
,类似列表 16-12 那样。接下来使用 range 创建了 10 个线程。这里使用了thread::spawn
并对所有线程使用了相同的闭包:他们每一个都将调用lock
方法来获取Mutex<T>
上的锁并对接着互斥器中的值加一。当一个线程结束执行其闭包,num
会离开作用域并释放锁这样另一个线程就可以获取它了。
-在主线程中,我们像列表 16-2 那样收集了所有的 join 句柄,并接着每一个的join
方法来确保所有线程都会结束。那时,主线程会获取锁并打印出程序的结果。
+这里创建了一个 counter
变量来存放内含 i32
的 Mutex<T>
,类似列表 16-12 那样。接下来使用 range 创建了 10 个线程。使用了 thread::spawn
并对所有线程使用了相同的闭包:他们每一个都将调用 lock
方法来获取 Mutex<T>
上的锁,接着将互斥器中的值加一。当一个线程结束执行,num
会离开闭包作用域并释放锁,这样另一个线程就可以获取它了。
+在主线程中,我们像列表 16-2 那样收集了所有的 join 句柄,调用它们的 join
方法来确保所有线程都会结束。之后,主线程会获取锁并打印出程序的结果。
之前提示过这个例子不能编译,让我们看看为什么!
error[E0373]: closure may outlive the current function, but it borrows
`counter`, which is owned by the current function
@@ -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);
-这里创建了两个线程,并将用于第二个线程的变量名改为handle2
和num2
。现在我们简化了例子来看看是否能够理解错误信息。这一次编译给出如下信息:
+这里创建了两个线程,并将第二个线程所用的变量改名为 handle2
和 num2
。我们简化了例子,看是否能理解错误信息。此次编译给出如下信息:
error[E0382]: capture of moved value: `counter`
-->
|
8 | let handle = thread::spawn(move || {
| ------- value moved (into closure) here
...
-16 | let mut num2 = counter.lock().unwrap();
- | ^^^^^^^ value captured here after move
+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() {
-又一次,编译并...出现了不同的错误!编译器真是教会了我们很多东西!
+再一次编译并...出现了不同的错误!编译器真是教会了我们很多!
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 的类型系统和所有权规则,确保了线程在更新共享值时拥有独占的访问权限,所以线程不会以不可预测的方式覆盖彼此的操作。虽然为了使一切正确运行而在编译器上花了一些时间,但是我们节省了未来的时间,尤其是线程以特定顺序执行才会出现的诡异错误难以重现。
接下来,为了丰富本章的内容,让我们讨论一下Send
和Sync
trait 以及如何对自定义类型使用他们。
diff --git a/docs/ch16-04-extensible-concurrency-sync-and-send.html b/docs/ch16-04-extensible-concurrency-sync-and-send.html
index 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::marker
:Sync
和Send
。
+Rust 的并发模型中一个有趣的方面是:语言本身对并发知之甚少。我们之前讨论的几乎所有内容,都属于标准库,而不是语言本身的内容。由于不需要语言提供并发相关的基础设施,并发方案不受标准库或语言所限:我们可以编写自己的或使用别人编写的。
+我们说“几乎所有内容都不属于语言本身”,那么属于语言本身的是什么呢?是两个 trait,都位于std::marker
: Sync
和Send
。
Send
用于表明所有权可能被传送给其他线程
-Send
标记 trait 表明类型的所有权可能被在线程间传递。几乎所有的 Rust 类型都是Send
的,不过有一些例外。标准库中提供的一个不是Send
的类型是Rc<T>
:如果克隆Rc<T>
值并尝试将克隆的所有权传递给另一个线程,这两个线程可能会同时更新引用计数。正如上一部分提到的,Rc<T>
被实现为用于单线程场景,这时不需要为拥有线程安全的引用计数而付出性能代价。
-因为Rc<T>
没有标记为Send
,Rust 的类型系统和 trait bound 会确保我们永远也不会忘记或错误的把一个Rc<T>
值不安全的在线程间传递。列表 16-14 曾尝试这么做,不过得到了一个错误说the trait Send is not implemented for Rc<Mutex<i32>>
。当切换为标记为Send
的Arc<T>
时,代码就可以编译了。
-任何完全由Send
的类型组成的类型也会自动被标记为Send
。几乎所有基本类型都是Send
的,除了第十九章将会讨论的裸指针(raw pointer)之外。大部分标准库类型是Send
的,除了Rc<T>
之外。
+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>>
。当切换为标记为Send
的Arc<T>
时,就没有问题了。
+任何完全由Send
的类型组成的类型也会自动被标记为Send
。几乎所有基本类型都是Send
的,大部分标准库类型是Send
的,除了Rc<T>
,以及第十九章将会讨论的裸指针(raw pointer)。
Sync
表明多线程访问是安全的
-Sync
标记 trait 表明一个类型可以安全的在多个线程中拥有其值的引用。换一种方式来说就是对于任意类型T
,如果&T
(T
的引用)是Send
的话T
就是Sync
的,这样其引用就可以安全的发送到另一个线程。类似于Send
的情况,基本类型是Sync
的,完全由Sync
的类型组成的类型也是Sync
的。
+Sync
标记 trait 表明一个类型可以安全的在多个线程中拥有其值的引用。换一种方式来说就是,对于任意类型T
,如果&T
(T
的引用)是Send
的话T
就是Sync
的,这样其引用就可以安全的发送到另一个线程。类似于Send
的情况,基本类型是Sync
的,完全由Sync
的类型组成的类型也是Sync
的。
Rc<T>
也不是Sync
的,出于其不是Send
的相同的原因。RefCell<T>
(第十五章讨论过)和Cell<T>
系列类型不是Sync
的。RefCell<T>
在运行时所进行的借用检查也不是线程安全的。Mutex<T>
是Sync
的,正如上一部分所讲的它可以被用来在多线程中共享访问。
手动实现Send
和Sync
是不安全的
-通常并不需要实现Send
和Sync
trait,因为由是Send
和Sync
的类型组成的类型也自动就是Send
和Sync
的了。因为他们是标记 trait,甚至都不需要实现任何方法。他们只是用来加强并发相关的不可变行性的。
-实现这些标记 trait 涉及到实现不安全的 Rust 代码。第十九章将会讲到如何使用不安全 Rust 代码;现在,重要的是在创建新的由不是Send
和Sync
的部分构成的并发类型时需要多加小心,以确保维持其安全保证。The Nomicon 中由更多关于这些保证和如何维持他们的信息。
+通常并不需要实现Send
和Sync
trait,由属于Send
和Sync
的类型组成的类型,自动就是Send
和Sync
的。因为他们是标记 trait,甚至都不需要实现任何方法。他们只是用来加强并发相关的不可变性的。
+实现这些标记 trait 涉及到编写不安全的 Rust 代码,第十九章将会讲述具体的方法;当前重要的是,在创建新的由不是Send
和Sync
的部分构成的并发类型时需要多加小心,以确保维持其安全保证。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.
}
-注意,结构体自身被标记为pub
,这样其他代码可以使用这个结构体,但是在结构体内部的字段仍然是私有的。这是非常重要的,因为我们希望保证变量被增加到列表或者被从列表删除时,也会同时更新平均值。我们通过在结构体上实现add
、remove
和average
方法来做到这一点,如列表 17-2 所示:
+注意,结构体自身被标记为pub
,这样其他代码可以使用这个结构体,但是在结构体内部的字段仍然是私有的。这是非常重要的,因为我们希望保证变量被增加到列表或者被从列表删除时,也会同时更新平均值。可以通过在结构体上实现add
、remove
和average
方法来做到这一点,如列表 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
方法。其他的类比如Button
、Image
和SelectBox
会从Component
继承并继承draw
方法。它们会各自覆写draw
方法来自定义行为,但是框架会把所有的类型当作是Component
的实例,并在它们上调用draw
。
+有时,我们希望使用的类型的集合对于使用库的程序员来说是可扩展的。例如,很多图形用户接口(GUI)工具有一个条目列表的概念,它通过遍历列表并对每一个条目调用draw
方法来绘制在屏幕上。我们将要创建一个叫做rust_gui
的包含一个 GUI 库结构的库 crate。GUI 库可以包含一些供开发者使用的类型,比如Button
或TextField
。使用rust_gui
的程序员会想要创建更多可以绘制在屏幕上的类型:一个程序员可能会增加一个Image
,而另一个可能会增加一个SelectBox
。我们不会在本章节实现一个功能完善的 GUI 库,不过会展示各个部分是如何结合在一起的。
+当写 rust_gui
库时,我们不知道其他程序员需要什么类型,所以无法定义一个 enum
来包含所有的类型。然而 rust_gui
需要跟踪所有这些不同类型的值,需要有在每个值上调用 draw
方法能力。我们的 GUI 库不需要确切地知道调用 draw
方法会发生什么,只需要有可用的方法供我们调用。
+在可以继承的语言里,我们会定义一个名为 Component
的类,该类上有一个draw
方法。其他的类比如Button
、Image
和SelectBox
会从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 -->
-因为我们已经在第10章讨论过如何定义trait,你可能比较熟悉。下面是新的定义:Listing 17-4有一个名为Screen
的结构体,里面有一个名为components
的vector,components
的类型是Box。Box<Draw>
是一个trait对象:它是一个任何Box
内部的实现了Draw
trait的类型的替身。
+因为我们已经在第10章讨论过如何定义 trait,你可能比较熟悉。下面是新的定义:Listing 17-4有一个名为 Screen
的结构体,里面有一个名为 components
的 vector,components
的类型是Box。Box<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>>,
}
-
+
-在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 {
}
}
-
->
运算符到哪去了?
-像在 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 地址类型,V4
和V6
。这被称为枚举的成员(variants):
enum IpAddrKind {
@@ -2593,7 +2593,7 @@ commit e6d6caab41471f7115a621029bd428a812c5260e
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
-注意枚举的成员位于其标识符的命名空间中,并使用两个冒号分开。这么设计的益处是现在IpAddrKind::V4
和IpAddrKind::V6
是相同类型的:IpAddrKind
。例如,接着我们可以顶一个函数来获取IpAddrKind
:
+注意枚举的成员位于其标识符的命名空间中,并使用两个冒号分开。这么设计的益处是现在IpAddrKind::V4
和IpAddrKind::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
强制要求的穷进行检查。match
和if let
之间的选择依赖特定的环境以及增加简洁度和失去穷尽性检查的权衡取舍。
+使用if let
意味着编写更少代码,更少的缩进和更少的样板代码。然而,这样会失去match
强制要求的穷尽性检查。match
和if let
之间的选择依赖特定的环境以及增加简洁度和失去穷尽性检查的权衡取舍。
换句话说,可以认为if let
是match
的一个语法糖,它当值匹配某一模式时执行代码而忽略所有其他值。
可以在if let
中包含一个else
。else
块中的代码与match
表达式中的_
分支块中的代码相同,这样的match
表达式就等同于if let
和else
。回忆一下列表 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
-最后要介绍的常用集合类型是哈希 map(hash map)。HashMap<K, V>
类型储存了一个键类型K
对应一个值类型V
的映射。它通过一个哈希函数(hashing function)来实现映射,它决定了如何将键和值放入内存中。很多编程语言支持这种数据结构,不过通常有不同的名字:哈希、map、对象、哈希表或者关联数组,仅举几例。
+最后介绍的常用集合类型是 哈希 map(hash 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
将会使你的代码在面对无处不在的错误时显得更加可靠。
-现在我们已经见识过了标准库中Option
和Result
泛型枚举的能力了,让我们聊聊泛型是如何工作的,以及如果在你的代码中利用他们。
+现在我们已经见识过了标准库中Option
和Result
泛型枚举的能力了,让我们聊聊泛型是如何工作的,以及如何在你的代码中利用他们。
泛型、trait 和生命周期
ch10-00-generics.md
@@ -4562,7 +4562,7 @@ fn main() {
names and the types in their signatures
这里largest_i32
和largest_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
-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
的返回值应该是希望放入Config
中search
字段的值。使用match
来在next
返回Some
时提取值,而在因为没有足够的参数(这会造成next
调用返回None
)而提早返回Err
值。
-对filename
值也进行相同处理。稍微有些可惜的是search
和filename
的match
表达式是如此的相似。如果可以对next
返回的Option
使用?
就好了,不过目前?
只能用于Result
值。即便我们可以像Result
一样对Option
使用?
,得到的值也是借用的,而我们希望能够将迭代器中的String
移动到Config
中。
+还记得env::args
返回值的第一个值是程序的名称吗。我们希望忽略它,所以首先调用next
并不处理其返回值。第二次调用next
的返回值应该是希望放入Config
中query
字段的值。使用match
来在next
返回Some
时提取值,而在因为没有足够的参数(这会造成next
调用返回None
)而提早返回Err
值。
+对filename
值也进行相同处理。稍微有些可惜的是query
和filename
的match
表达式是如此的相似。如果可以对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
-我们实现了一个只存放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() {
-这样编译器就能够计算出储存一个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
成员看起来像什么:
@@ -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
到&U
当T: 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 是Drop
。Drop
运行我们在值要离开作用域时执行一些代码。智能指针在被丢弃时会执行一些重要的清理工作,比如释放内存或减少引用计数。更一般的来讲,数据类型可以管理多于内存的资源,比如文件或网络连接,而使用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() {
-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
中。接着当创建b
和c
时,我们对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>
,我们使用borrow
和borrow_mut
方法,它是RefCell<T>
拥有的安全 API 的一部分。borrow
返回Ref
类型的智能指针,而borrow_mut
返回RefMut
类型的智能指针。这两个类型实现了Deref
所以可以被当作常规引用处理。Ref
和RefMut
动态的借用所有权,而他们的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_count
;Weak<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 会确保Some
和None
的情况都被处理并不会尝试使用一个无效的指针。
-不同于列表 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 Deref
和Drop
。同时探索了形成引用循环和造成内存泄漏的可能性,以及如何使用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),其中多个线程可以访问同一片数据。
-Sync
和Send
trait,他们允许 Rust 的并发保证能扩展到用户定义的和标准库中提供的类型中。
+Sync
和Send
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 线程,这里M
和N
不必相同。
-每一个模型都有其自己的优势和取舍。对于 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() {
-这些代码可以运行,而新建线程则可能直接就出错了并完全没有机会运行。新建线程内部有一个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”的缩写,也就是说,任意时间,它只允许一个线程访问某些数据。互斥器以难以使用著称,因为你不得不记住:
-- 必须记住在使用数据之前尝试获取锁。
-- 一旦处理完被互斥器所保护的数据之后,必须记得解锁数据这样其他线程才能够获取锁。
+- 在使用数据之前尝试获取锁。
+- 处理完被互斥器所保护的数据之后,必须解锁数据,这样其他线程才能够获取锁。
-对于一个现实中的互斥器的例子,想象一下在一个会议中的专门小组讨论会上,不过只有一个麦克风。在一个小组成员可能发言之前,他们必须请求或示意他们需要使用麦克风。一旦得到了麦克风,他们可以发言任意长的时间,接着将麦克风交给系一个希望讲话的小组成员。如果小组成员在没有麦克风的时候就开始叫喊或者在其他成员发言结束之前就取得麦克风将是很无理的。如果对这个共享的麦克风的管理因为任何这些原因出现问题,讨论会将无法如期进行。
-正确的管理互斥器是异常复杂的,这也就是为什么这么多人都热衷于通道。然而,在 Rust 中,得益于类型系统和所有权,我们不可能会在锁和解锁上出错。
+现实中也有互斥器的例子,想象一下在一个会议中,只有一个麦克风。如果一个成员要发言,他必须请求使用麦克风。一旦得到了麦克风,他可以畅所欲言,然后将麦克风交给下一个希望讲话的成员。如果成员在没有麦克风的时候就开始叫喊,或者在其他成员发言结束之前就拿走麦克风,是很不合适的。如果这个共享的麦克风因为此类原因而出现问题,会议将无法正常进行。
+正确的管理互斥器异常复杂,这也是许多人之所以热衷于通道的原因。然而,在 Rust 中,得益于类型系统和所有权,我们不会在锁和解锁上出错。
Mutex<T>
的 API
-让我们看看列表 16-12 中使用互斥器的例子,现在并不涉及到多线程:
+让我们看看列表 16-12 中使用互斥器的例子,现在不涉及多线程:
Filename: src/main.rs
use std::sync::Mutex;
@@ -9430,12 +9429,12 @@ fn main() {
-与很多类型一样,我们通过叫做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() {
-这里创建了一个counter
变量来存放内含i32
的Mutex<T>
,类似列表 16-12 那样。接下来使用 range 创建了 10 个线程。这里使用了thread::spawn
并对所有线程使用了相同的闭包:他们每一个都将调用lock
方法来获取Mutex<T>
上的锁并对接着互斥器中的值加一。当一个线程结束执行其闭包,num
会离开作用域并释放锁这样另一个线程就可以获取它了。
-在主线程中,我们像列表 16-2 那样收集了所有的 join 句柄,并接着每一个的join
方法来确保所有线程都会结束。那时,主线程会获取锁并打印出程序的结果。
+这里创建了一个 counter
变量来存放内含 i32
的 Mutex<T>
,类似列表 16-12 那样。接下来使用 range 创建了 10 个线程。使用了 thread::spawn
并对所有线程使用了相同的闭包:他们每一个都将调用 lock
方法来获取 Mutex<T>
上的锁,接着将互斥器中的值加一。当一个线程结束执行,num
会离开闭包作用域并释放锁,这样另一个线程就可以获取它了。
+在主线程中,我们像列表 16-2 那样收集了所有的 join 句柄,调用它们的 join
方法来确保所有线程都会结束。之后,主线程会获取锁并打印出程序的结果。
之前提示过这个例子不能编译,让我们看看为什么!
error[E0373]: closure may outlive the current function, but it borrows
`counter`, which is owned by the current function
@@ -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);
-这里创建了两个线程,并将用于第二个线程的变量名改为handle2
和num2
。现在我们简化了例子来看看是否能够理解错误信息。这一次编译给出如下信息:
+这里创建了两个线程,并将第二个线程所用的变量改名为 handle2
和 num2
。我们简化了例子,看是否能理解错误信息。此次编译给出如下信息:
error[E0382]: capture of moved value: `counter`
-->
|
8 | let handle = thread::spawn(move || {
| ------- value moved (into closure) here
...
-16 | let mut num2 = counter.lock().unwrap();
- | ^^^^^^^ value captured here after move
+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() {
-又一次,编译并...出现了不同的错误!编译器真是教会了我们很多东西!
+再一次编译并...出现了不同的错误!编译器真是教会了我们很多!
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 的类型系统和所有权规则,确保了线程在更新共享值时拥有独占的访问权限,所以线程不会以不可预测的方式覆盖彼此的操作。虽然为了使一切正确运行而在编译器上花了一些时间,但是我们节省了未来的时间,尤其是线程以特定顺序执行才会出现的诡异错误难以重现。
接下来,为了丰富本章的内容,让我们讨论一下Send
和Sync
trait 以及如何对自定义类型使用他们。
使用Sync
和Send
trait 的可扩展并发
ch16-04-extensible-concurrency-sync-and-send.md
-commit 55b294f20fc846a13a9be623bf322d8b364cee77
+commit 9430a3d28a2121a938d704ce48b15d21062f880e
-Rust 的并发模型中一个有趣的方面是语言本身对并发知道的很少。我们讨论过的几乎所有内容都是标准库的一部分,而不是语言本身的内容。因为并不需要语言提供任何用于并发上下文中的内容,并发选择也不仅限于标准库或语言所提供的:我们可以编写自己的或使用别人编写的内容。
-我们说了几乎所有内容都不在语言本身,那么位于语言本身的是什么呢?这是两个 trait,都位于std::marker
:Sync
和Send
。
+Rust 的并发模型中一个有趣的方面是:语言本身对并发知之甚少。我们之前讨论的几乎所有内容,都属于标准库,而不是语言本身的内容。由于不需要语言提供并发相关的基础设施,并发方案不受标准库或语言所限:我们可以编写自己的或使用别人编写的。
+我们说“几乎所有内容都不属于语言本身”,那么属于语言本身的是什么呢?是两个 trait,都位于std::marker
: Sync
和Send
。
Send
用于表明所有权可能被传送给其他线程
-Send
标记 trait 表明类型的所有权可能被在线程间传递。几乎所有的 Rust 类型都是Send
的,不过有一些例外。标准库中提供的一个不是Send
的类型是Rc<T>
:如果克隆Rc<T>
值并尝试将克隆的所有权传递给另一个线程,这两个线程可能会同时更新引用计数。正如上一部分提到的,Rc<T>
被实现为用于单线程场景,这时不需要为拥有线程安全的引用计数而付出性能代价。
-因为Rc<T>
没有标记为Send
,Rust 的类型系统和 trait bound 会确保我们永远也不会忘记或错误的把一个Rc<T>
值不安全的在线程间传递。列表 16-14 曾尝试这么做,不过得到了一个错误说the trait Send is not implemented for Rc<Mutex<i32>>
。当切换为标记为Send
的Arc<T>
时,代码就可以编译了。
-任何完全由Send
的类型组成的类型也会自动被标记为Send
。几乎所有基本类型都是Send
的,除了第十九章将会讨论的裸指针(raw pointer)之外。大部分标准库类型是Send
的,除了Rc<T>
之外。
+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>>
。当切换为标记为Send
的Arc<T>
时,就没有问题了。
+任何完全由Send
的类型组成的类型也会自动被标记为Send
。几乎所有基本类型都是Send
的,大部分标准库类型是Send
的,除了Rc<T>
,以及第十九章将会讨论的裸指针(raw pointer)。
Sync
表明多线程访问是安全的
-Sync
标记 trait 表明一个类型可以安全的在多个线程中拥有其值的引用。换一种方式来说就是对于任意类型T
,如果&T
(T
的引用)是Send
的话T
就是Sync
的,这样其引用就可以安全的发送到另一个线程。类似于Send
的情况,基本类型是Sync
的,完全由Sync
的类型组成的类型也是Sync
的。
+Sync
标记 trait 表明一个类型可以安全的在多个线程中拥有其值的引用。换一种方式来说就是,对于任意类型T
,如果&T
(T
的引用)是Send
的话T
就是Sync
的,这样其引用就可以安全的发送到另一个线程。类似于Send
的情况,基本类型是Sync
的,完全由Sync
的类型组成的类型也是Sync
的。
Rc<T>
也不是Sync
的,出于其不是Send
的相同的原因。RefCell<T>
(第十五章讨论过)和Cell<T>
系列类型不是Sync
的。RefCell<T>
在运行时所进行的借用检查也不是线程安全的。Mutex<T>
是Sync
的,正如上一部分所讲的它可以被用来在多线程中共享访问。
手动实现Send
和Sync
是不安全的
-通常并不需要实现Send
和Sync
trait,因为由是Send
和Sync
的类型组成的类型也自动就是Send
和Sync
的了。因为他们是标记 trait,甚至都不需要实现任何方法。他们只是用来加强并发相关的不可变行性的。
-实现这些标记 trait 涉及到实现不安全的 Rust 代码。第十九章将会讲到如何使用不安全 Rust 代码;现在,重要的是在创建新的由不是Send
和Sync
的部分构成的并发类型时需要多加小心,以确保维持其安全保证。The Nomicon 中由更多关于这些保证和如何维持他们的信息。
+通常并不需要实现Send
和Sync
trait,由属于Send
和Sync
的类型组成的类型,自动就是Send
和Sync
的。因为他们是标记 trait,甚至都不需要实现任何方法。他们只是用来加强并发相关的不可变性的。
+实现这些标记 trait 涉及到编写不安全的 Rust 代码,第十九章将会讲述具体的方法;当前重要的是,在创建新的由不是Send
和Sync
的部分构成的并发类型时需要多加小心,以确保维持其安全保证。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.
}
-注意,结构体自身被标记为pub
,这样其他代码可以使用这个结构体,但是在结构体内部的字段仍然是私有的。这是非常重要的,因为我们希望保证变量被增加到列表或者被从列表删除时,也会同时更新平均值。我们通过在结构体上实现add
、remove
和average
方法来做到这一点,如列表 17-2 所示:
+注意,结构体自身被标记为pub
,这样其他代码可以使用这个结构体,但是在结构体内部的字段仍然是私有的。这是非常重要的,因为我们希望保证变量被增加到列表或者被从列表删除时,也会同时更新平均值。可以通过在结构体上实现add
、remove
和average
方法来做到这一点,如列表 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
方法。其他的类比如Button
、Image
和SelectBox
会从Component
继承并继承draw
方法。它们会各自覆写draw
方法来自定义行为,但是框架会把所有的类型当作是Component
的实例,并在它们上调用draw
。
+有时,我们希望使用的类型的集合对于使用库的程序员来说是可扩展的。例如,很多图形用户接口(GUI)工具有一个条目列表的概念,它通过遍历列表并对每一个条目调用draw
方法来绘制在屏幕上。我们将要创建一个叫做rust_gui
的包含一个 GUI 库结构的库 crate。GUI 库可以包含一些供开发者使用的类型,比如Button
或TextField
。使用rust_gui
的程序员会想要创建更多可以绘制在屏幕上的类型:一个程序员可能会增加一个Image
,而另一个可能会增加一个SelectBox
。我们不会在本章节实现一个功能完善的 GUI 库,不过会展示各个部分是如何结合在一起的。
+当写 rust_gui
库时,我们不知道其他程序员需要什么类型,所以无法定义一个 enum
来包含所有的类型。然而 rust_gui
需要跟踪所有这些不同类型的值,需要有在每个值上调用 draw
方法能力。我们的 GUI 库不需要确切地知道调用 draw
方法会发生什么,只需要有可用的方法供我们调用。
+在可以继承的语言里,我们会定义一个名为 Component
的类,该类上有一个draw
方法。其他的类比如Button
、Image
和SelectBox
会从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 -->
-因为我们已经在第10章讨论过如何定义trait,你可能比较熟悉。下面是新的定义:Listing 17-4有一个名为Screen
的结构体,里面有一个名为components
的vector,components
的类型是Box。Box<Draw>
是一个trait对象:它是一个任何Box
内部的实现了Draw
trait的类型的替身。
+因为我们已经在第10章讨论过如何定义 trait,你可能比较熟悉。下面是新的定义:Listing 17-4有一个名为 Screen
的结构体,里面有一个名为 components
的 vector,components
的类型是Box。Box<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>>,
}
-
+
-在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 {
}
}
-