Compare commits

...

4 Commits

Author SHA1 Message Date
KaiserY
d26b9d4299 wip: 2024 edition 2025-05-19 23:42:00 +08:00
KaiserY
ac907219e7
Merge pull request #864 from skr2005/main
同步翻译 ch16-04-extensible-concurrency-sync-and-send.md 到原文的 56ec353
2025-05-19 16:30:32 +08:00
kazeno
90ea091397 update ch10-03 2025-05-19 16:23:21 +08:00
skr2005
ac6cc36657
Update ch16-04-extensible-concurrency-sync-and-send.md
同步翻译到原文的提交56ec353。
2025-05-19 15:20:46 +08:00
6 changed files with 55 additions and 64 deletions

View File

@ -1,7 +1,6 @@
# 枚举和模式匹配
> [ch06-00-enums.md](https://github.com/rust-lang/book/blob/main/src/ch06-00-enums.md)
> <br>
> commit bb7e429ad6b59d9a0c37db7434976364cbb9c6da
<!-- https://github.com/rust-lang/book/blob/main/src/ch06-00-enums.md -->
<!-- commit 3a30e4c1fbe641afc066b3af9eb01dcdf5ed8b24 -->
本章介绍 **枚举***enumerations*),也被称作 *enums*。枚举允许你通过列举可能的 **成员***variants*)来定义一个类型。首先,我们会定义并使用一个枚举来展示它是如何连同数据一起编码信息的。接下来,我们会探索一个特别有用的枚举,叫做 `Option`,它代表一个值要么是某个值要么什么都不是。然后会讲到在 `match` 表达式中用模式匹配,针对不同的枚举值编写相应要执行的代码。最后会介绍 `if let`,另一个简洁方便处理代码中枚举的结构。
本章介绍**枚举***enumerations*),也被称作 *enums*。枚举允许你通过列举可能的 **变体***variants*)来定义一个类型。首先,我们会定义并使用一个枚举来展示它是如何连同数据一起编码信息的。接下来,我们会探索一个特别有用的枚举,叫做 `Option`,它代表一个值要么是某个值要么什么都不是。然后会讲到在 `match` 表达式中用模式匹配,针对不同的枚举值编写相应要执行的代码。最后会介绍 `if let`,另一个简洁方便处理代码中枚举的结构。

View File

@ -1,16 +1,15 @@
## 枚举的定义
> [ch06-01-defining-an-enum.md](https://github.com/rust-lang/book/blob/main/src/ch06-01-defining-an-enum.md)
> <br>
> commit bb7e429ad6b59d9a0c37db7434976364cbb9c6da
<!-- https://github.com/rust-lang/book/blob/main/src/ch06-01-defining-an-enum.md -->
<!-- commit 2a4c00c4d0c373ff9b416712b74ffb7ed56c77d4 -->
结构体给予你将字段和数据聚合在一起的方法,像 `Rectangle` 结构体有 `width``height` 两个字段。而枚举给予你一个途径去声明某个值是一个集合中的一员。比如,我们想让 `Rectangle` 是一些形状的集合,包含 `Circle``Triangle` 。为了做到这个Rust 提供了枚举类型。
结构体给予你将字段和数据聚合在一起的方法,像 `Rectangle` 结构体有 `width``height` 两个字段。而枚举给予你一个途径去声明某个值是一个集合中的一员。比如,我们想让 `Rectangle` 是一些形状的集合,包含 `Circle``Triangle` 。为Rust 允许我们将这些可能性编码为一个枚举类型。
让我们看看一个需要诉诸于代码的场景,来考虑为何此时使用枚举更为合适且实用。假设我们要处理 IP 地址。目前被广泛使用的两个主要 IP 标准IPv4version four和 IPv6version six。这是我们的程序可能会遇到的所有可能的 IP 地址类型:所以可以 **枚举** 出所有可能的值,这也正是此枚举名字的由来。
让我们看看一个需要诉诸于代码的场景,来考虑为何此时使用枚举更为合适且实用。假设我们要处理 IP 地址。目前被广泛使用的两个主要 IP 标准IPv4version four和 IPv6version six。这是我们的程序可能会遇到的所有可能的 IP 地址类型:所以可以**枚举**出所有可能的值,这也正是枚举一词的由来。
任何一个 IP 地址要么是 IPv4 的要么是 IPv6 的而且不能两者都是。IP 地址的这个特性使得枚举数据结构非常适合这个场景,因为枚举值只可能是其中一个成员。IPv4 和 IPv6 从根本上讲仍是 IP 地址,所以当代码在处理适用于任何类型的 IP 地址的场景时应该把它们当作相同的类型。
任何一个 IP 地址要么是 IPv4 的要么是 IPv6 的而且不能两者都是。IP 地址的这个特性使得枚举数据结构非常适合这个场景,因为枚举值只可能是其中一个变体。IPv4 和 IPv6 从根本上讲仍是 IP 地址,所以当代码在处理适用于任何类型的 IP 地址的场景时应该把它们当作相同的类型。
可以通过在代码中定义一个 `IpAddrKind` 枚举来表现这个概念并列出可能的 IP 地址类型,`V4` 和 `V6`。这被称为枚举的 **成员***variants*
可以通过在代码中定义一个 `IpAddrKind` 枚举来表现这个概念并列出可能的 IP 地址类型,`V4` 和 `V6`。这被称为枚举的**变体***variants*
```rust
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-01-defining-enums/src/main.rs:def}}
@ -20,49 +19,49 @@
### 枚举值
可以像这样创建 `IpAddrKind` 两个不同成员的实例:
可以像这样创建 `IpAddrKind` 两个不同变体的实例:
```rust
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-01-defining-enums/src/main.rs:instance}}
```
注意枚举的成员位于其标识符的命名空间中,并使用两个冒号分开。这么设计的益处是现在 `IpAddrKind::V4``IpAddrKind::V6` 都是 `IpAddrKind` 类型的。例如,接着可以定义一个函数来接收任何 `IpAddrKind`类型的参数:
注意枚举的变体位于其标识符的命名空间中,并使用两个冒号分开。这么设计的益处是现在 `IpAddrKind::V4``IpAddrKind::V6` 都是 `IpAddrKind` 类型的。例如,接着可以定义一个函数来接收任何 `IpAddrKind`类型的参数:
```rust
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-01-defining-enums/src/main.rs:fn}}
```
现在可以使用任一成员来调用这个函数:
现在可以使用任一变体来调用这个函数:
```rust
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-01-defining-enums/src/main.rs:fn_call}}
```
使用枚举甚至还有更多优势。进一步考虑一下我们的 IP 地址类型,目前没有一个存储实际 IP 地址 **数据** 的方法;只知道它是什么 **类型** 的。考虑到已经在第五章学习过结构体了,你可能会像示例 6-1 那样处理这个问题:
使用枚举甚至还有更多优势。进一步考虑一下我们的 IP 地址类型,目前没有一个存储实际 IP 地址**数据**的方法;只知道它是什么**类型**的。考虑到已经在第五章学习过结构体了,你可能会像示例 6-1 那样尝试用结构体来解决这个问题:
```rust
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/listing-06-01/src/main.rs:here}}
```
<span class="caption">示例 6-1将 IP 地址的数据和 `IpAddrKind` 成员存储在一个 `struct`</span>
<span class="caption">示例 6-1将 IP 地址的数据和 `IpAddrKind` 变体存储在一个 `struct`</span>
这里我们定义了一个有两个字段的结构体 `IpAddr``IpAddrKind`(之前定义的枚举)类型的 `kind` 字段和 `String` 类型 `address` 字段。我们有这个结构体的两个实例。第一个,`home`,它的 `kind` 的值是 `IpAddrKind::V4` 与之相关联的地址数据是 `127.0.0.1`。第二个实例,`loopback``kind` 的值是 `IpAddrKind` 的另一个成员`V6`,关联的地址是 `::1`。我们使用了一个结构体来将 `kind``address` 打包在一起,现在枚举成员就与值相关联了。
这里我们定义了一个有两个字段的结构体 `IpAddr``IpAddrKind`(之前定义的枚举)类型的 `kind` 字段和 `String` 类型 `address` 字段。我们有这个结构体的两个实例。第一个,`home`,它的 `kind` 的值是 `IpAddrKind::V4` 与之相关联的地址数据是 `127.0.0.1`。第二个实例,`loopback``kind` 的值是 `IpAddrKind` 的另一个变体`V6`,关联的地址是 `::1`。我们使用了一个结构体来将 `kind``address` 打包在一起,现在枚举变体就与值相关联了。
我们可以使用一种更简洁的方式来表达相同的概念,仅仅使用枚举并将数据直接放进每一个枚举成员而不是将枚举作为结构体的一部分。`IpAddr` 枚举的新定义表明了 `V4``V6` 成员都关联了 `String` 值:
我们可以使用一种更简洁的方式来表达相同的概念,仅仅使用枚举并将数据直接放进每一个枚举变体而不是将枚举作为结构体的一部分。`IpAddr` 枚举的新定义表明了 `V4``V6` 变体都关联了 `String` 值:
```rust
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-02-enum-with-data/src/main.rs:here}}
```
我们直接将数据附加到枚举的每个成员上,这样就不需要一个额外的结构体了。这里也很容易看出枚举工作的另一个细节:每一个我们定义的枚举成员的名字也变成了一个构建枚举的实例的函数。也就是说,`IpAddr::V4()` 是一个获取 `String` 参数并返回 `IpAddr` 类型实例的函数调用。作为定义枚举的结果,这些构造函数会自动被定义。
我们直接将数据附加到枚举的每个变体上,这样就不需要一个额外的结构体了。这里也很容易看出枚举工作的另一个细节:每一个我们定义的枚举变体的名字也变成了一个构建枚举的实例的函数。也就是说,`IpAddr::V4()` 是一个获取 `String` 参数并返回 `IpAddr` 类型实例的函数调用。作为定义枚举的结果,这些构造函数会自动被定义。
用枚举替代结构体还有另一个优势:每个成员可以处理不同类型和数量的数据。IPv4 版本的 IP 地址总是含有四个值在 0 和 255 之间的数字部分。如果我们想要将 `V4` 地址存储为四个 `u8` 值而 `V6` 地址仍然表现为一个 `String`,这就不能使用结构体了。枚举则可以轻易的处理这个情况:
用枚举替代结构体还有另一个优势:每个变体可以处理不同类型和数量的数据。IPv4 版本的 IP 地址总是含有四个值在 0 和 255 之间的数字部分。如果我们想要将 `V4` 地址存储为四个 `u8` 值而 `V6` 地址仍然表现为一个 `String`,这就不能使用结构体了。枚举则可以轻易的处理这个情况:
```rust
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-03-variants-with-different-data/src/main.rs:here}}
```
这些代码展示了使用枚举来存储两种不同 IP 地址的几种可能的选择。然而,事实证明存储和编码 IP 地址实在是太常见了[以致标准库提供了一个开箱即用的定义!][IpAddr]<!-- ignore -->让我们看看标准库是如何定义 `IpAddr` 的:它正有着跟我们定义和使用的一样的枚举和成员,不过它将成员中的地址数据嵌入到了两个不同形式的结构体中,它们对不同的成员的定义是不同的:
这些代码展示了使用枚举来存储两种不同 IP 地址的几种可能的选择。然而,事实证明存储和编码 IP 地址实在是太常见了[以致标准库提供了一个开箱即用的定义!][IpAddr]<!-- ignore -->让我们看看标准库是如何定义 `IpAddr` 的:它正有着跟我们定义和使用的一样的枚举和变体,不过它将变体中的地址数据嵌入到了两个不同形式的结构体中,它们对不同的变体的定义是不同的:
```rust
struct Ipv4Addr {
@ -79,26 +78,26 @@ enum IpAddr {
}
```
这些代码展示了可以将任意类型的数据放入枚举成员中:例如字符串、数字类型或者结构体。甚至可以包含另一个枚举!另外,标准库中的类型通常并不比你设想出来的要复杂多少。
这些代码展示了可以将任意类型的数据放入枚举变体中:例如字符串、数字类型或者结构体。甚至可以包含另一个枚举!另外,标准库中的类型通常并不比你设想出来的要复杂多少。
注意虽然标准库中包含一个 `IpAddr` 的定义,仍然可以创建和使用我们自己的定义而不会有冲突,因为我们并没有将标准库中的定义引入作用域。第七章会讲到如何导入类型。
来看看示例 6-2 中的另一个枚举的例子:它的成员中内嵌了多种多样的类型:
来看看示例 6-2 中的另一个枚举的例子:它的变体中内嵌了多种多样的类型:
```rust
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/listing-06-02/src/main.rs:here}}
```
<span class="caption">示例 6-2一个 `Message` 枚举,其每个成员都存储了不同数量和类型的值</span>
<span class="caption">示例 6-2一个 `Message` 枚举,其每个变体都存储了不同数量和类型的值</span>
这个枚举有四个含有不同类型的成员
这个枚举有四个含有不同类型的变体
* `Quit` 没有关联任何数据。
* `Move` 类似结构体包含命名字段。
* `Write` 包含单独一个 `String`
* `ChangeColor` 包含三个 `i32`
定义一个如示例 6-2 中所示那样的有关联值的枚举的方式和定义多个不同类型的结构体的方式很相像,除了枚举不使用 `struct` 关键字以及其所有成员都被组合在一起位于 `Message` 类型下。如下这些结构体可以包含与之前枚举成员中相同的数据:
定义一个如示例 6-2 中所示那样的有关联值的枚举的方式和定义多个不同类型的结构体的方式很相像,除了枚举不使用 `struct` 关键字以及其所有变体都被组合在一起位于 `Message` 类型下。如下这些结构体可以包含与之前枚举变体中相同的数据:
```rust
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-04-structs-similar-to-message-enum/src/main.rs:here}}
@ -116,7 +115,7 @@ enum IpAddr {
让我们看看标准库中的另一个非常常见且实用的枚举:`Option`。
### `Option` 枚举其相对于空值的优势
### `Option` 枚举其相对于空值的优势
这一部分会分析一个 `Option` 的案例,`Option` 是标准库定义的另一个枚举。`Option` 类型应用广泛因为它编码了一个非常普遍的场景,即一个值要么有值要么没值。
@ -135,7 +134,7 @@ Tony Hoarenull 的发明者,在他 2009 年的演讲 “Null References: Th
> crashes, which have probably caused a billion dollars of pain and damage in
> the last forty years.
>
> 我称之为我十亿美元的错误。当时,我在为一个面向对象语言设计第一个综合性的面向引用的类型系统。我的目标是通过编译器的自动检查来保证所有引用的使用都应该是绝对安全的。不过我未能抵抗住引入一个空引用的诱惑,仅仅是因为它是这么的容易实现。这引发了无数错误、漏洞和系统崩溃,在之后的四十多年中造成了数十亿美元的苦痛和伤害
> 我称之为我十亿美元的错误。当时,我在为一个面向对象语言设计第一个综合性的面向引用的类型系统。我的目标是通过编译器的自动检查来保证所有引用的使用都应该是绝对安全的。不过我未能抵抗住引入一个空引用的诱惑,仅仅是因为它是这么的容易实现。这引发了无数错误、漏洞和系统崩溃,在过去四十年里可能造成了价值十亿美元的痛苦和损失
空值的问题在于当你尝试像一个非空值那样使用一个空值,会出现某种形式的错误。因为空和非空的属性无处不在,非常容易出现这类错误。
@ -150,15 +149,15 @@ enum Option<T> {
}
```
`Option<T>` 枚举是如此有用以至于它甚至被包含在了 prelude 之中,你不需要将其显式引入作用域。另外,它的成员也是如此,可以不需要 `Option::` 前缀来直接使用 `Some``None`。即便如此 `Option<T>` 也仍是常规的枚举,`Some(T)` 和 `None` 仍是 `Option<T>`成员
`Option<T>` 枚举是如此有用以至于它甚至被包含在了 prelude 之中,无需将其显式引入作用域。另外,它的变体也是如此:可以不需要 `Option::` 前缀来直接使用 `Some``None`。即便如此 `Option<T>` 也仍是常规的枚举,`Some(T)` 和 `None` 仍是 `Option<T>`变体
`<T>` 语法是一个我们还未讲到的 Rust 功能。它是一个泛型类型参数,第十章会更详细的讲解泛型。目前,所有你需要知道的就是 `<T>` 意味着 `Option` 枚举的 `Some` 成员可以包含任意类型的数据,同时每一个用于 `T` 位置的具体类型使得 `Option<T>` 整体作为不同的类型。这里是一些包含数字类型和字符串类型 `Option` 值的例子:
`<T>` 语法是一个我们还未讲到的 Rust 功能。它是一个泛型类型参数,第十章会更详细的讲解泛型。目前,所有你需要知道的就是 `<T>` 意味着 `Option` 枚举的 `Some` 变体可以包含任意类型的数据,同时每一个用于 `T` 位置的具体类型使得 `Option<T>` 整体作为不同的类型。这里是一些包含数字类型和字符串类型 `Option` 值的例子:
```rust
{{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-06-option-examples/src/main.rs:here}}
```
`some_number` 的类型是 `Option<i32>`。`some_char` 的类型是 `Option<char>`,是不同于`some_number`的类型。因为我们在 `Some` 成员中指定了值Rust 可以推断其类型。对于 `absent_number`Rust 需要我们指定 `Option` 整体的类型,因为编译器只通过 `None` 值无法推断出 `Some` 成员保存的值的类型。这里我们告诉 Rust 希望 `absent_number``Option<i32>` 类型的。
`some_number` 的类型是 `Option<i32>`。`some_char` 的类型是 `Option<char>`,是不同于 `some_number` 的类型。因为我们在 `Some` 变体中指定了值Rust 可以推断其类型。对于 `absent_number`Rust 需要我们指定 `Option` 整体的类型,因为编译器只通过 `None` 值无法推断出 `Some` 变体保存的值的类型。这里我们告诉 Rust 希望 `absent_number``Option<i32>` 类型的。
当有一个 `Some` 值时,我们就知道存在一个值,而这个值保存在 `Some` 中。当有个 `None` 值时,在某种意义上,它跟空值具有相同的意义:并没有一个有效的值。那么,`Option<T>` 为什么就比空值要好呢?
@ -174,15 +173,15 @@ enum Option<T> {
{{#include ../listings/ch06-enums-and-pattern-matching/no-listing-07-cant-use-option-directly/output.txt}}
```
很好!事实上,错误信息意味着 Rust 不知道该如何将 `Option<i8>``i8` 相加,因为它们的类型不同。当在 Rust 中拥有一个像 `i8` 这样类型的值时,编译器确保它总是有一个有效的值。我们可以自信使用而无需做空值检查。只有当使用 `Option<i8>`(或者任何用到的类型)的时候需要担心可能没有值,而编译器会确保我们在使用值之前处理了为空的情况。
很好!事实上,错误信息意味着 Rust 不知道该如何将 `Option<i8>``i8` 相加,因为它们的类型不同。当在 Rust 中拥有一个像 `i8` 这样类型的值时,编译器确保它总是有一个有效的值。我们可以自信使用而无需做空值检查。只有当使用 `Option<i8>`(或者任何用到的类型)的时候需要担心可能没有值,而编译器会确保我们在使用值之前处理了为空的情况。
换句话说,在对 `Option<T>` 进行运算之前必须将其转换为 `T`。通常这能帮助我们捕获到空值最常见的问题之一:假设某值不为空但实际上为空的情况。
消除了错误地假设一个非空值的风险,会让你对代码更加有信心。为了拥有一个可能为空的值,你必须要显式的将其放入对应类型的 `Option<T>` 中。接着,当使用这个值时,必须明确的处理值为空的情况。只要一个值不是 `Option<T>` 类型,你就 **可以** 安全的认定它的值不为空。这是 Rust 的一个经过深思熟虑的设计决策,来限制空值的泛滥以增加 Rust 代码的安全性。
消除了错误地假设一个非空值的风险,会让你对代码更加有信心。为了拥有一个可能为空的值,你必须要显式的将其放入对应类型的 `Option<T>` 中。接着,当使用这个值时,必须明确的处理值为空的情况。只要一个值不是 `Option<T>` 类型,你就**可以**安全的认定它的值不为空。这是 Rust 的一个经过深思熟虑的设计决策,来限制空值的泛滥以增加 Rust 代码的安全性。
那么当有一个 `Option<T>` 的值时,如何从 `Some` 成员中取出 `T` 的值来使用它呢?`Option<T>` 枚举拥有大量用于各种情况的方法:你可以查看[它的文档][docs]<!-- ignore -->。熟悉 `Option<T>` 的方法将对你的 Rust 之旅非常有用。
那么当有一个 `Option<T>` 的值时,如何从 `Some` 变体中取出 `T` 的值来使用它呢?`Option<T>` 枚举拥有大量用于各种情况的方法:你可以查看[它的文档][docs]<!-- ignore -->。熟悉 `Option<T>` 的方法将对你的 Rust 之旅非常有用。
总的来说,为了使用 `Option<T>` 值,需要编写处理每个成员的代码。你想要一些代码只当拥有 `Some(T)` 值时运行,允许这些代码使用其中的 `T`。也希望一些代码只在值为 `None` 时运行,这些代码并没有一个可用的 `T` 值。`match` 表达式就是这么一个处理枚举的控制流结构:它会根据枚举的成员运行不同的代码,这些代码可以使用匹配到的值中的数据。
总的来说,为了使用 `Option<T>` 值,需要编写处理每个变体的代码。你想要一些代码只当拥有 `Some(T)` 值时运行,允许这些代码使用其中的 `T`。也希望一些代码只在值为 `None` 时运行,这些代码并没有一个可用的 `T` 值。`match` 表达式就是这么一个处理枚举的控制流结构:它会根据枚举的变体运行不同的代码,这些代码可以使用匹配到的值中的数据。
[IpAddr]: https://doc.rust-lang.org/std/net/enum.IpAddr.html
[option]: https://doc.rust-lang.org/std/option/enum.Option.html

View File

@ -1,8 +1,7 @@
## `match` 控制流结构
> [ch06-02-match.md](https://github.com/rust-lang/book/blob/main/src/ch06-02-match.md)
> <br>
> commit 3962c0224b274e2358e0acf06443af64df115359
<!-- https://github.com/rust-lang/book/blob/main/src/ch06-02-match.md -->
<!-- commit 5d22a358fb2380aa3f270d7b6269b67b8e44849e -->
Rust 有一个叫做 `match` 的极为强大的控制流运算符,它允许我们将一个值与一系列的模式相比较,并根据相匹配的模式执行相应代码。模式可由字面值、变量、通配符和许多其他内容构成;[第十九章][ch19-00-patterns]会涉及到所有不同种类的模式以及它们的作用。`match` 的力量来源于模式的表现力以及编译器检查,它确保了所有可能的情况都得到处理。

View File

@ -103,10 +103,6 @@ Rust 编译器有一个**借用检查器***borrow checker*),它比较作
单个的生命周期注解本身没有多少意义,因为生命周期注解告诉 Rust 多个引用的泛型生命周期参数如何相互联系的。让我们在 `longest` 函数的上下文中理解生命周期注解如何相互联系。
例如如果函数有一个生命周期 `'a``i32` 的引用的参数 `first`。还有另一个同样是生命周期 `'a``i32` 的引用的参数 `second`。这两个生命周期注解意味着引用 `first``second` 必须与这泛型生命周期存在得一样久。
### 函数签名中的生命周期注解
为了在函数签名中使用生命周期注解,需要在函数名和参数列表间的尖括号中声明泛型生命周期(*lifetime*)参数,就像泛型类型(*type*)参数一样。
@ -153,7 +149,7 @@ Rust 编译器有一个**借用检查器***borrow checker*),它比较作
<span class="caption">示例 10-23尝试在 `string2` 离开作用域之后使用 `result` </span>
如果尝试编译会出现如下错误:
如果尝试编译这段代码会出现如下错误:
```console
{{#include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-23/output.txt}}
@ -161,7 +157,7 @@ Rust 编译器有一个**借用检查器***borrow checker*),它比较作
错误表明为了保证 `println!` 中的 `result` 是有效的,`string2` 需要直到外部作用域结束都是有效的。Rust 知道这些是因为(`longest`)函数的参数和返回值都使用了相同的生命周期参数 `'a`
如果从人的角度读上述代码,我们可能会觉得这个代码是正确的。 `string1` 更长,因此 `result` 会包含指向 `string1` 的引用。因为 `string1` 尚未离开作用域,对于 `println!` 来说 `string1` 的引用仍然是有效的。然而,我们通过生命周期参数告诉 Rust 的是: `longest` 函数返回的引用的生命周期应该与传入参数的生命周期中较短那个保持一致。因此,借用检查器不允许示例 10-23 中的代码,因为它可能会存在无效的引用。
作为人类,我们可以直观地发现 `string1``string2` 更长,因此 `result` 会包含指向 `string1` 的引用。因为 `string1` 尚未离开作用域,对于 `println!` 来说 `string1` 的引用仍然是有效的。然而,编译器并不能识别出这种情况。我们通过生命周期参数告诉 Rust 的是: `longest` 函数返回的引用的生命周期应该与传入参数的生命周期中较短那个保持一致。因此,借用检查器不允许示例 10-23 中的代码,因为它可能会存在无效的引用。
请尝试更多采用不同的值和不同生命周期的引用作为 `longest` 函数的参数和返回值的实验。并在开始编译前猜想你的实验能否通过借用检查器,接着编译一下看看你的理解是否正确!
@ -177,7 +173,7 @@ Rust 编译器有一个**借用检查器***borrow checker*),它比较作
我们为参数 `x` 和返回值指定了生命周期参数 `'a`,不过没有为参数 `y` 指定,因为 `y` 的生命周期与参数 `x` 和返回值的生命周期没有任何关系。
当从函数返回一个引用,返回值的生命周期参数需要与一个参数的生命周期参数相匹配。如果返回的引用 **没有** 指向任何一个参数,那么唯一的可能就是它指向一个函数内部创建的值。然而它将会是一个悬垂引用,因为它将会在函数结束时离开作用域。尝试考虑这个并不能编译的 `longest` 函数实现:
当从函数返回一个引用,返回值的生命周期参数需要与一个参数的生命周期参数相匹配。如果返回的引用**没有**指向任何一个参数,那么唯一的可能就是它指向一个函数内部创建的值。然而它将会是一个悬垂引用,因为它将会在函数结束时离开作用域。尝试考虑这个并不能编译的 `longest` 函数实现:
<span class="filename">文件名src/main.rs</span>
@ -207,13 +203,13 @@ Rust 编译器有一个**借用检查器***borrow checker*),它比较作
<span class="caption">示例 10-24一个存放引用的结构体所以其定义需要生命周期注解</span>
这个结构体有唯一一个字段 `part`,它存放了一个字符串 slice这是一个引用。类似于泛型参数类型必须在结构体名称后面的尖括号中声明泛型生命周期参数以便在结构体定义中使用生命周期参数。这个注解意味着 `ImportantExcerpt` 的实例不能比其 `part` 字段中的引用存在的更久。
这个结构体有一个字段 `part`,它存放了一个字符串 slice这是一个引用。类似于泛型参数类型必须在结构体名称后面的尖括号中声明泛型生命周期参数以便在结构体定义中使用生命周期参数。这个注解意味着 `ImportantExcerpt` 的实例不能比其 `part` 字段中的引用存在的更久。
这里的 `main` 函数创建了一个 `ImportantExcerpt` 的实例,它存放了变量 `novel` 所拥有的 `String` 的第一个句子的引用。`novel` 的数据在 `ImportantExcerpt` 实例创建之前就存在。另外,直到 `ImportantExcerpt` 离开作用域之后 `novel` 都不会离开作用域,所以 `ImportantExcerpt` 实例中的引用是有效的。
### 生命周期省略Lifetime Elision
现在我们已经知道了每一个引用都有一个生命周期,而且我们需要为那些使用了引用的函数或结构体指定生命周期。然而,第四章的示例 4-9 中有一个函数,如示例 10-25 所示,它没有生命周期注解却能编译成功:
现在我们已经知道了每一个引用都有一个生命周期,而且我们需要为那些使用了引用的函数或结构体指定生命周期。然而,第四章的示例 4-9 中有一个函数,再次展示为如示例 10-25 所示,它没有生命周期注解却能编译成功:
<span class="filename">文件名src/lib.rs</span>
@ -231,11 +227,11 @@ fn first_word<'a>(s: &'a str) -> &'a str {
在编写了很多 Rust 代码后Rust 团队发现在特定情况下 Rust 程序员们总是重复地编写一模一样的生命周期注解。这些场景是可预测的并且遵循几个明确的模式。接着 Rust 团队就把这些模式编码进了 Rust 编译器中,如此借用检查器在这些情况下就能推断出生命周期而不再强制程序员显式的增加注解。
这里我们提到一些 Rust 的历史是因为更多的明确的模式被合并和添加到编译器中是完全可能的。未来只需要更少的生命周期注解。
这里我们提到一些 Rust 的历史是因为更多的明确的模式被合并和添加到编译器中是完全可能的。未来可能只需要更少的生命周期注解。
被编码进 Rust 引用分析的模式被称为 **生命周期省略规则***lifetime elision rules*)。这并不是需要程序员遵守的规则;这些规则是一系列特定的场景,此时编译器会考虑,如果代码符合这些场景,就无需明确指定生命周期。
省略规则并不提供完整的推断如果 Rust 在明确遵守这些规则的前提下变量的生命周期仍然是模棱两可的话,它不会猜测剩余引用的生命周期应该是什么。编译器会在可以通过增加生命周期注解来解决错误问题的地方给出一个错误提示,而不是进行推断或猜测。
省略规则并不提供完整的推断如果 Rust 在明确遵守这些规则的前提下变量的生命周期仍然是模棱两可的话,它不会猜测剩余引用的生命周期应该是什么。编译器会在可以通过增加生命周期注解来解决错误问题的地方给出一个错误提示,而不是进行推断或猜测。
函数或方法的参数的生命周期被称为 **输入生命周期***input lifetimes*),而返回值的生命周期被称为 **输出生命周期***output lifetimes*)。
@ -245,7 +241,7 @@ fn first_word<'a>(s: &'a str) -> &'a str {
第二条规则是如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数:`fn foo<'a>(x: &'a i32) -> &'a i32`。
第三条规则是如果方法有多个输入生命周期参数并且其中一个参数是 `&self``&mut self`,说明是个对象的方法 (method)(译者注:这里涉及 rust 的面向对象参见 17 章),那么所有输出生命周期参数被赋予 `self` 的生命周期。第三条规则使得方法更容易读写,因为只需更少的符号。
第三条规则是如果方法有多个输入生命周期参数并且其中一个参数是 `&self``&mut self`,说明是个方法,那么所有输出生命周期参数被赋予 `self` 的生命周期。第三条规则使得方法更容易读写,因为只需更少的符号。
假设我们自己就是编译器。并应用这些规则来计算示例 10-25 中 `first_word` 函数签名中的引用的生命周期。开始时签名中的引用并没有关联任何生命周期:

View File

@ -1,42 +1,40 @@
## 使用 `Sync` 和 `Send` trait 的可扩展并发
## 使用 `Send` 和 `Sync` trait 的可扩展并发
> [ch16-04-extensible-concurrency-sync-and-send.md](https://github.com/rust-lang/book/blob/main/src/ch16-04-extensible-concurrency-sync-and-send.md)
> <br>
> commit 7c7740a5ddef1458d74f1daf85fd49e03aaa97cf
> commit 56ec353290429e6547109e88afea4de027b0f1a9
Rust 的并发模型中一个有趣的方面是:语言本身对并发知之 **甚少**我们之前讨论的几乎所有内容,都属于标准库,而不是语言本身的内容。由于不需要语言提供并发相关的基础设施,并发方案不受标准库或语言所限:我们可以编写自己的或使用别人编写的并发功能。
Rust 的并发模型中一个有趣的方面是:我们之前讨论的几乎所有内容,都属于标准库,而不是语言本身的内容。由于不需要语言提供并发相关的基础设施,并发方案不受标准库或语言所限:我们可以编写自己的或使用别人编写的并发功能。
然而有两个并发概念是内嵌于语言中的:`std::marker` 中的 `Sync``Send` trait。
然而,在内嵌于语言而非标准库的关键并发概念中,有属于 `std::marker``Send``Sync` trait。
### 通过 `Send` 允许在线程间转移所有权
`Send` 标记 trait 表明实现了 `Send` 的类型值的所有权可以在线程间传送。几乎所有的 Rust 类型都是`Send` 的,不过有一些例外,包括 `Rc<T>`:这是不能 `Send` 的,因为如果克隆了 `Rc<T>` 的值并尝试将克隆的所有权转移到另一个线程,这两个线程都可能同时更新引用计数。为此,`Rc<T>` 被实现为用于单线程场景,这时不需要为拥有线程安全的引用计数而付出性能代价。
`Send` 标记 trait 表明实现了 `Send` 的类型值的所有权可以在线程间传送。几乎所有的 Rust 类型都是`Send` 的,不过有一些例外,包括 `Rc<T>`:这是不能实现 `Send` 的,因为如果克隆了 `Rc<T>` 的值并尝试将克隆的所有权转移到另一个线程,这两个线程都可能同时更新引用计数。为此,`Rc<T>` 被实现为用于单线程场景,这时不需要为拥有线程安全的引用计数而付出性能代价。
因此Rust 类型系统和 trait bound 确保永远也不会意外的将不安全的 `Rc<T>` 在线程间发送。当尝试在示例 16-14 中这么做的时候,会得到错误 `the trait Send is not implemented for Rc<Mutex<i32>>`。而使用标记为 `Send``Arc<T>` 时,就没有问题了。
因此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
### `Sync` 允许多线程访问
`Sync` 标记 trait 表明一个实现了 `Sync` 的类型可以安全的在多个线程中拥有其值的引用。换一种方式来说,对于任意类型 `T`,如果 `&T``T` 的不可变引用)`Send` 的话 `T` 就是 `Sync`,这意味着其引用就可以安全的发送到另一个线程。类似于 `Send` 的情况,基本类型`Sync` 的,完全由 `Sync` 的类型组成的类型也是 `Sync`
`Sync` 标记 trait 表明一个实现了 `Sync` 的类型可以安全的在多个线程中拥有其值的引用。换一种方式来说,对于任意类型 `T`,如果 `&T``T` 的不可变引用)实现了 `Send` 的话 `T` 就实现了 `Sync`,这意味着其引用就可以安全的发送到另一个线程。类似于 `Send` 的情况,基本类型都实现了 `Sync`,完全由实现了 `Sync` 的类型组成的类型也实现了 `Sync`
智能指针 `Rc<T>`不是 `Sync` 的,出于其不是 `Send` 相同的原因。`RefCell<T>`(第十五章讨论过)和 `Cell<T>` 系列类型不是 `Sync`。`RefCell<T>` 在运行时所进行的借用检查也不是线程安全的。`Mutex<T>` `Sync`,正如 [“在线程间共享 `Mutex<T>`”][sharing-a-mutext-between-multiple-threads] 部分所讲的它可以被用来在多线程中共享访问。
智能指针 `Rc<T>`没有实现 `Sync`,出于其没有实现 `Send` 相同的原因。`RefCell<T>`(第十五章讨论过)和 `Cell<T>` 系列类型没有实现 `Sync`。`RefCell<T>` 在运行时所进行的借用检查也不是线程安全的。`Mutex<T>` 实现了 `Sync`,正如 [“在线程间共享 `Mutex<T>`”][sharing-a-mutext-between-multiple-threads] 部分所讲的它可以被用来在多线程中共享访问。
### 手动实现 `Send``Sync` 是不安全的
通常并不需要手动实现 `Send``Sync` trait因为由 `Send``Sync` 的类型组成的类型,自动就是 `Send``Sync`。因为它们是标记 trait甚至都不需要实现任何方法。它们只是用来加强并发相关的不可变性的。
通常并不需要手动实现 `Send``Sync` trait因为完全实现了 `Send``Sync` 的类型组成的类型,自动实现了 `Send``Sync`。因为它们是标记 trait甚至都不需要实现任何方法。它们只是用来加强并发相关的不可变性的。
手动实现这些标记 trait 涉及到编写不安全的 Rust 代码,第十章将会讲述具体的方法;当前重要的是,在创建新的由不是 `Send``Sync` 的部分构成的并发类型时需要多加小心,以确保维持其安全保证。[“The Rustonomicon”][nomicon] 中有更多关于这些保证以及如何维持它们的信息。
手动实现这些标记 trait 涉及到编写不安全的 Rust 代码,第十章将会讲述具体的方法;当前重要的是,在创建新的由不是 `Send``Sync` 的部分构成的并发类型时需要多加小心,以确保维持其安全保证。[“The Rustonomicon”][nomicon] 中有更多关于这些保证以及如何维持它们的信息。
## 总结
这不会是本书最后一个出现并发的章节:第二十一章的项目会在更现实的场景中使用这些概念,而不像本章中讨论的这些小例子。
这不会是本书最后一个出现并发的章节:下一章会我们会专注于异步编程,并且第二十一章的项目会在更现实的场景中使用这些概念,而不像本章中讨论的这些小例子。
正如之前提到的,因为 Rust 本身很少有处理并发的部分内容,有很多的并发方案都由 crate 实现。它们比标准库要发展的更快;请在网上搜索当前最新的用于多线程场景的 crate。
Rust 提供了用于消息传递的信道,和像 `Mutex<T>``Arc<T>` 这样可以安全的用于并发上下文的智能指针。类型系统和借用检查器会确保这些场景中的代码,不会出现数据竞争和无效的引用。一旦代码可以编译了,我们就可以坚信这些代码可以正确的运行于多线程环境,而不会出现其他语言中经常出现的那些难以追踪的 bug。并发编程不再是什么可怕的概念无所畏惧地并发吧
接下来,让我们讨论一下当 Rust 程序变得更大时,有哪些符合语言习惯的问题建模方法和结构化解决方案,以及 Rust 的风格是如何与面向对象编程Object Oriented Programming中那些你所熟悉的概念相联系的。
[sharing-a-mutext-between-multiple-threads]: ch16-03-shared-state.html#在线程间共享-mutext
[nomicon]: https://doc.rust-lang.org/nomicon/index.html

View File

@ -282,7 +282,7 @@ Miri 并不能捕获编写不安全代码时可能出现的所有错误。Miri
[dangling-references]: ch04-02-references-and-borrowing.html#悬垂引用dangling-references
[ABI]: https://doc.rust-lang.org/reference/items/external-blocks.html#abi
[differences-between-variables-and-constants]: ch03-01-variables-and-mutability.html#常量
[extensible-concurrency-with-the-sync-and-send-traits]: ch16-04-extensible-concurrency-sync-and-send.html#使用-sync-和-send-trait-的可扩展并发
[extensible-concurrency-with-the-sync-and-send-traits]: ch16-04-extensible-concurrency-sync-and-send.html#使用-send-和-sync-trait-的可扩展并发
[the-slice-type]: ch04-03-slices.html#slice-类型
[unions]: https://doc.rust-lang.org/reference/items/unions.html
[miri]: https://github.com/rust-lang/miri