pull/869/head
须语 1 week ago
commit dace4ffb7d

@ -109,7 +109,7 @@
- [深入理解 async 相关的 traits](ch17-05-traits-for-async.md)
- [future、任务和线程](ch17-06-futures-tasks-threads.md)
- [Rust 的面向对象编程特性](ch18-00-oop.md)
- [面向对象编程特性](ch18-00-oop.md)
- [面向对象语言的特点](ch18-01-what-is-oo.md)
- [顾及不同类型值的 trait 对象](ch18-02-trait-objects.md)
- [面向对象设计模式的实现](ch18-03-oo-design-patterns.md)

@ -1,20 +1,19 @@
# 无畏并发
> [ch16-00-concurrency.md](https://github.com/rust-lang/book/blob/main/src/ch16-00-concurrency.md)
> <br>
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f
<!-- https://github.com/rust-lang/book/blob/main/src/ch16-00-concurrency.md -->
<!-- commit 56ec353290429e6547109e88afea4de027b0f1a9 -->
安全且高效地处理并发编程是 Rust 的另一个主要目标。**并发编程**_Concurrent programming_代表程序的不同部分相互独立地执行 **并行编程**_parallel programming_代表程序不同部分同时执行这两个概念随着计算机越来越多的利用多处理器的优势而显得愈发重要。由于历史原因在此类上下文中编程一直是困难且容易出错的Rust 希望能改变这一
安全且高效地处理并发编程是 Rust 的另一个主要目标。**并发编程**_Concurrent programming_代表程序的不同部分相互独立地执行而**并行编程**_parallel programming_代表程序不同部分同时执行这两个概念随着计算机越来越多的利用多处理器的优势而显得愈发重要。由于历史原因在此类上下文中编程一直是困难且容易出错的Rust 希望能改变这一现状
起初Rust 团队认为确保内存安全和防止并发问题是两个分别需要不同方法应对的挑战。随着时间的推移,团队发现所有权和类型系统是一系列解决内存安全 **和** 并发问题的强有力的工具!通过利用所有权和类型检查,在 Rust 中很多并发错误都是 **编译时** 错误,而非运行时错误。因此,相比花费大量时间尝试重现运行时并发 bug 出现的特定情况,Rust 会拒绝编译不正确的代码并提供解释问题的错误信息。因此,你可以在开发时修复代码,而不是在部署到生产环境后修复代码。我们给 Rust 的这一部分起了一个绰号 **无畏并发**_fearless concurrency_。无畏并发令你的代码免于出现诡异的 bug 并可以轻松重构且无需担心会引入新的 bug。
起初Rust 团队认为确保内存安全和防止并发问题是两个分别需要不同方法应对的挑战。随着时间的推移,团队发现所有权和类型系统是一系列解决内存安全**和**并发问题的强有力的工具!通过利用所有权和类型检查,在 Rust 中很多并发错误都是**编译时**错误,而非运行时错误。因此,相比花费大量时间尝试重现运行时并发 bug 出现的特定情况,不正确的代码会直接编译失败并提供解释问题的错误信息。因此,你可以在开发时修复代码,而不是在部署到生产环境后修复代码。我们给 Rust 的这一部分起了一个绰号**无畏并发**_fearless concurrency_。无畏并发令你的代码免于出现诡异的 bug 并可以轻松重构且无需担心会引入新的 bug。
> 注意:出于简洁的考虑,我们将很多问题归类为 **并发**,而不是更准确的区分 **并发和(或)并行**。如果这是一本专注于并发和/或并行的书,我们肯定会更加精确的。对于本章,当我们谈到 **并发** 时,请自行脑内替换为 **并发和(或)并行**
> 注意:出于简洁的考虑,我们将很多问题归类为**并发**,而不是更准确的区分**并发和和/或并行**。对于本章,当我们谈到**并发**时,请自行脑内替换为 **并发和和/或并行**。在下一章中当区分二者更为重要时,我们会使用更准确的表述
很多语言所提供的处理并发问题的解决方法都非常有特色。例如Erlang 有着优雅的消息传递并发功能但只有模糊不清的在线程间共享状态的方法。对于高级语言来说只实现可能解决方案的子集是一个合理的策略因为高级语言所许诺的价值来源于牺牲一些控制来换取抽象。然而对于底层语言则期望提供在任何给定的情况下有着最高的性能且对硬件有更少的抽象。因此Rust 提供了多种工具,以符合实际情况和需求的方式来为问题建模。
很多语言所提供的处理并发问题的解决方法都非常有。例如Erlang 有着优雅的消息传递message-passing并发功能但只有模糊不清的在线程间共享状态的方法。对于高级语言来说只实现可能解决方案的子集是一个合理的策略因为高级语言所许诺的价值来源于牺牲一些控制来换取抽象。然而对于底层语言则期望提供在任何给定的情况下有着最高的性能且对硬件有更少的抽象。因此Rust 提供了多种工具,以符合实际情况和需求的方式来为问题建模。
如下是本章将要涉及到的内容:
* 如何创建线程来同时运行多段代码。
* **消息传递**_Message passing_并发其中信道channel被用来在线程间传递消息。
* **共享状态**_Shared state_并发其中多个线程可以访问同一片数据。
* `Sync``Send` trait将 Rust 的并发保证扩展到用户定义的以及标准库提供的类型中。
- 如何创建线程来同时运行多段代码。
- **消息传递**_Message passing_并发其中信道channel被用来在线程间传递消息。
- **共享状态**_Shared state_并发其中多个线程可以访问同一片数据。
- `Sync``Send` trait将 Rust 的并发保证扩展到用户定义的以及标准库提供的类型中。

@ -1,10 +1,9 @@
## 使用线程同时运行代码
> [ch16-01-threads.md](https://github.com/rust-lang/book/blob/main/src/ch16-01-threads.md)
> <br>
> commit 8aecae3efe5ca8f79f055b70f05d9a3f990bce7b
<!-- https://github.com/rust-lang/book/blob/main/src/ch16-01-threads.md -->
<!-- commit 56ec353290429e6547109e88afea4de027b0f1a9 -->
在大部分现代操作系统中,已执行程序的代码在一个 **进程**_process_中运行操作系统则会负责管理多个进程。在程序内部也可以拥有多个同时运行的独立部分。这些运行这些独立部分的功能被称为 **线程**_threads_。例如web 服务器可以有多个线程以便可以同时响应多个请求。
在大部分现代操作系统中,已执行程序的代码在一个**进程**_process_中运行操作系统则会负责管理多个进程。在程序内部也可以拥有多个同时运行的独立部分。这些运行这些独立部分的功能被称为**线程**_threads_。例如web 服务端可以有多个线程以便可以同时响应多个请求。
将程序中的计算拆分进多个线程可以改善性能,因为程序可以同时进行多个任务,不过这也会增加复杂性。因为线程是同时运行的,所以无法预先保证不同线程中的代码的执行顺序。这会导致诸如此类的问题:
@ -14,7 +13,7 @@
Rust 尝试减轻使用线程的负面影响。不过在多线程上下文中编程仍需格外小心,同时其所要求的代码结构也不同于运行于单线程的程序。
编程语言有一些不同的方法来实现线程,而且很多操作系统提供了创建新线程的 API。Rust 标准库使用 *1:1* 线程实现,这代表程序的每一个语言级线程使用一个系统线程。有一些 crate 实现了其他有着不同于 1:1 模型取舍的线程模型。
编程语言实现线程的方式各不相同,许多操作系统都提供了供语言调用以创建新线程的 API。Rust 标准库使用 *1:1* 模型的线程实现,这代表程序的每一个语言级线程使用一个系统线程。有一些 crate 实现了其他有着不同于 1:1 模型取舍的线程模型。Rust 的 async 系统,我们将在下一章看到,也提供了另一种并发方式。)
### 使用 `spawn` 创建新线程
@ -26,9 +25,9 @@ Rust 尝试减轻使用线程的负面影响。不过在多线程上下文中编
{{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-01/src/main.rs}}
```
<span class="caption">示例 16-1: 创建一个打印某些内容的新线程,但是主线程打印其它内容</span>
<span class="caption">示例 16-1: 创建一个打印某些内容的新线程同时主线程打印其它内容</span>
注意当 Rust 程序的主线程结束时,新线程也会结束,而不管其是否执行完毕。这个程序的输出可能每次都略有不同,不过它大体上看起来像这样:
注意当 Rust 程序的主线程结束时,所有新线程也会结束,而不管其是否执行完毕。这个程序的输出可能每次都略有不同,不过它大体上看起来像这样:
```text
hi number 1 from the main thread!
@ -50,7 +49,7 @@ hi number 5 from the spawned thread!
由于主线程结束,示例 16-1 中的代码大部分时候不光会提早结束新建线程,因为无法保证线程运行的顺序,我们甚至不能实际保证新建线程会被执行!
可以通过将 `thread::spawn` 的返回值储存在变量中来修复新建线程部分没有执行或者完全没有执行的问题。`thread::spawn` 的返回值类型是 `JoinHandle`。`JoinHandle` 是一个拥有所有权的值,当对其调用 `join` 方法时,它会等待其线程结束。示例 16-2 展示了如何使用示例 16-1 中创建的线程的 `JoinHandle` 并调用 `join` 来确保新建线程在 `main` 退出前结束运行
可以通过将 `thread::spawn` 的返回值储存在变量中来修复新建线程部分没有执行或者完全没有执行的问题。`thread::spawn` 的返回值类型是 `JoinHandle<T>`。`JoinHandle<T>` 是一个拥有所有权的值,当对其调用 `join` 方法时,它会等待其线程结束。示例 16-2 展示了如何使用示例 16-1 中创建的线程的 `JoinHandle<T>` 并调用 `join` 来确保新建线程在 `main` 退出前结束运行
<span class="filename">文件名src/main.rs</span>
@ -58,9 +57,9 @@ hi number 5 from the spawned thread!
{{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-02/src/main.rs}}
```
<span class="caption">示例 16-2: 从 `thread::spawn` 保存一个 `JoinHandle` 以确保该线程能够运行至结束</span>
<span class="caption">示例 16-2: 从 `thread::spawn` 保存一个 `JoinHandle<T>` 以确保该线程能够运行至结束</span>
通过调用 handle 的 `join` 会阻塞当前线程直到 handle 所代表的线程结束。**阻塞**_Blocking_线程意味着阻止该线程执行工作或退出。因为我们将 `join` 调用放在了主线程的 `for` 循环之后,运行示例 16-2 应该会产生类似这样的输出:
通过调用句柄的 `join` 会阻塞当前线程直到句柄所代表的线程结束。**阻塞**_Blocking_线程意味着阻止该线程执行工作或退出。因为我们将 `join` 调用放在了主线程的 `for` 循环之后,运行示例 16-2 应该会产生类似这样的输出:
```text
hi number 1 from the main thread!
@ -78,7 +77,7 @@ hi number 8 from the spawned thread!
hi number 9 from the spawned thread!
```
这两个线程仍然会交替执行,不过主线程会由于 `handle.join()` 调用会等待直到新建线程执行完毕。
这两个线程仍然会交替执行,不过主线程会由于 `handle.join()` 调用而不会结束直到新建线程执行完毕。
不过让我们看看将 `handle.join()` 移动到 `main``for` 循环之前会发生什么,如下:
@ -110,7 +109,7 @@ hi number 4 from the main thread!
### 将 `move` 闭包与线程一同使用
`move` 关键字经常用于传递给 `thread::spawn` 的闭包,因为闭包会获取从环境中取得的值的所有权,因此会将这些值的所有权从一个线程传送到另一个线程。在第十三章 [“使用闭包捕获环境”][capture] 部分讨论了闭包上下文中的 `move`。现在我们会更专注于 `move``thread::spawn` 之间的交互。
`move` 关键字经常用于传递给 `thread::spawn` 的闭包,因为闭包会获取从环境中取得的值的所有权,因此会将这些值的所有权从一个线程传送到另一个线程。在第十三章[“使用闭包捕获环境”][capture]部分讨论了闭包上下文中的 `move`。现在我们会更专注于 `move``thread::spawn` 之间的交互。
在第十三章中,我们讲到可以在参数列表前使用 `move` 关键字强制闭包获取其使用的环境值的所有权。这个技巧在创建新线程将值的所有权从一个线程移动到另一个线程时最为实用。
@ -130,7 +129,7 @@ hi number 4 from the main thread!
{{#include ../listings/ch16-fearless-concurrency/listing-16-03/output.txt}}
```
Rust 会 **推断** 如何捕获 `v`,因为 `println!` 只需要 `v` 的引用,闭包尝试借用 `v`。然而这有一个问题Rust 不知道这个新建线程会执行多久,所以无法知晓对 `v` 的引用是否一直有效。
Rust 会**推断**如何捕获 `v`,因为 `println!` 只需要 `v` 的引用,闭包尝试借用 `v`。然而这有一个问题Rust 不知道这个新建线程会执行多久,所以无法知晓对 `v` 的引用是否一直有效。
示例 16-4 展示了一个 `v` 的引用很有可能不再有效的场景:
@ -153,7 +152,7 @@ help: to force the closure to take ownership of `v` (and any other referenced va
| ++++
```
通过在闭包之前增加 `move` 关键字,我们强制闭包获取其使用的值的所有权,而不是任由 Rust 推断它应该借用值。示例 16-5 中展示的对示例 16-3 代码的修改,可以按照我们的预期编译并运行:
通过在闭包之前增加 `move` 关键字,我们强制闭包获取其使用的值的所有权,而不是任由 Rust 推断它应该借用值。示例 16-5 中展示的对示例 16-3 代码的修改,就能按预期编译并运行:
<span class="filename">文件名src/main.rs</span>
@ -169,8 +168,8 @@ help: to force the closure to take ownership of `v` (and any other referenced va
{{#include ../listings/ch16-fearless-concurrency/output-only-01-move-drop/output.txt}}
```
Rust 的所有权规则又一次帮助了我们!示例 16-3 中的错误是因为 Rust 是保守的并只会为线程借用 `v`,这意味着主线程理论上可能使新建线程的引用无效。通过告诉 Rust 将 `v` 的所有权移动到新建线程,我们向 Rust 保证主线程不会再使用 `v`。如果对示例 16-4 也做出如此修改,那么当在主线程中使用 `v` 时就会违反所有权规则。 `move` 关键字覆盖了 Rust 默认保守的借用,但它不允许我们违反所有权规则。
Rust 的所有权规则又一次帮助了我们!示例 16-3 中的错误是因为 Rust 是保守的并只会为线程借用 `v`,这意味着主线程理论上可能使新建线程的引用无效。通过告诉 Rust 将 `v` 的所有权移动到新建线程,我们向 Rust 保证主线程不会再使用 `v`。如果对示例 16-4 也做出如此修改,那么当在主线程中使用 `v` 时就会违反所有权规则。`move` 关键字覆盖了 Rust 默认保守的借用,但它不允许我们违反所有权规则。
现在我们对线程和线程 API 有了基本的了解,让我们讨论一下使用线程实际可以 **做** 什么吧
现在我们已经了解了线程的概念以及线程 API 提供的方法,下面让我们看看在什么情况下可以使用线程
[capture]: ch13-01-closures.html#使用闭包捕获环境

@ -1,16 +1,15 @@
## 使用消息传递在线程间传送数据
> [ch16-02-message-passing.md](https://github.com/rust-lang/book/blob/main/src/ch16-02-message-passing.md)
> <br>
> commit 36383b4da21dbd0a0781473bc8ad7ef0ed1b6751
<!-- https://github.com/rust-lang/book/blob/main/src/ch16-02-message-passing.md -->
<!-- commit 36383b4da21dbd0a0781473bc8ad7ef0ed1b6751 -->
一个日益流行的确保安全并发的方式是 **消息传递**_message passing_这里线程或 actor 通过发送包含数据的消息来相互沟通。这个思想来源于 [Go 编程语言文档中](https://golang.org/doc/effective_go.html#concurrency) 的口号:“不要通过共享内存来通讯;而通过通讯来共享内存。”“Do not communicate by sharing memory; instead, share memory by communicating.”)
一个日益流行的确保安全并发的方式是**消息传递**_message passing_这里线程或 actor 通过发送包含数据的消息来相互沟通。这个思想来源于 [Go 编程语言文档中](https://golang.org/doc/effective_go.html#concurrency) 的口号:“不要通过共享内存来通讯;而通过通讯来共享内存。”“Do not communicate by sharing memory; instead, share memory by communicating.”)
为了实现消息传递并发Rust 标准库提供了一个 **信道**_channel_实现。信道是一个通用编程概念表示数据从一个线程发送到另一个线程。
为了实现消息传递并发Rust 标准库提供了一个**信道**_channel_实现。信道是一个通用编程概念表示数据从一个线程发送到另一个线程。
你可以将编程中的信道想象为一个水流的渠道,比如河流或小溪。如果你将诸如橡皮鸭或小船之类的东西放入其中,它们会顺流而下到达下游。
你可以将编程中的信道想象为一个水流的渠道,比如河流或小溪。如果你将诸如橡皮鸭之类的东西放入其中,它们会顺流而下到达下游。
编程中的信息渠道信道有两部分组成一个发送者transmitter和一个接收者receiver。发送者位于上游位置在这里可以将橡皮鸭放入河中接收者则位于下游橡皮鸭最终会漂流至此。代码中的一部分调用发送者的方法以及希望发送的数据另一部分则检查接收端收到的消息。当发送者或接收者任一被丢弃时可以认为信道被 **关闭**_closed_了。
信道由两个组成部分一个发送端transmitter和一个接收端receiver。发送端位于上游位置在这里可以将橡皮鸭放入河中接收端则位于下游橡皮鸭最终会漂流至此。代码中的一部分调用发送端的方法以及希望发送的数据另一部分则检查接收端收到的消息。当发送端或接收端任一被丢弃时可以认为信道被**关闭**_closed_了。
这里,我们将开发一个程序,它会在一个线程生成值向信道发送,而在另一个线程会接收值并打印出来。这里会通过信道在线程间发送简单值来演示这个功能。一旦你熟悉了这项技术,你就可以将信道用于任何相互通信的任何线程,例如一个聊天系统,或利用很多线程进行分布式计算并将部分计算结果发送给一个线程进行聚合。
@ -24,9 +23,9 @@
<span class="caption">示例 16-6: 创建一个信道,并将其两端赋值给 `tx``rx`</span>
这里使用 `mpsc::channel` 函数创建一个新的信道;`mpsc` 是 **生产者,单消费者**_multiple producer, single consumer_的缩写。简而言之Rust 标准库实现信道的方式意味着一个信道可以有多个产生值的 **发送**_sending_,但只能有一个消费这些值的 **接收**_receiving_。想象一下多条小河小溪最终汇聚成大河:所有通过这些小河发出的东西最后都会来到下游的大河。目前我们以单个生产者开始,但是当示例可以工作后会增加多个生产者。
这里使用 `mpsc::channel` 函数创建一个新的信道;`mpsc` 是 **多生产者,单消费者**_multiple producer, single consumer_的缩写。简而言之Rust 标准库实现信道的方式意味着一个信道可以有多个产生值的 **发送**_sending_但只能有一个消费这些值的**接收**_receiving_。想象一下多条小河小溪最终汇聚成大河所有通过这些小河发出的东西最后都会来到下游的大河。目前我们以单个生产者开始但是当示例可以工作后会增加多个生产者。
`mpsc::channel` 函数返回一个元组:第一个元素是发送端 -- 发送者,而第二个元素是接收端 -- 接收者。由于历史原因,`tx` 和 `rx` 通常作为 **发送者**_transmitter_**接收者**_receiver_缩写,所以这就是我们将用来绑定这两端变量的名字。这里使用了一个 `let` 语句和模式来解构了此元组;第十九章会讨论 `let` 语句中的模式和解构。现在只需知道使用 `let` 语句是一个方便提取 `mpsc::channel` 返回的元组中一部分的手段。
`mpsc::channel` 函数返回一个元组:第一个元素是发送侧 -- 发送端,而第二个元素是接收侧 -- 接收端。由于历史原因,`tx` 和 `rx` 通常作为**发送端**_transmitter_**接收端**_receiver_的传统缩写,所以这就是我们将用来绑定这两端变量的名字。这里使用了一个 `let` 语句和模式来解构了此元组;第十九章会讨论 `let` 语句中的模式和解构。现在只需知道使用 `let` 语句是一个方便提取 `mpsc::channel` 返回的元组中一部分的手段。
让我们将发送端移动到一个新建线程中并发送一个字符串,这样新建线程就可以和主线程通讯了,如示例 16-7 所示。这类似于在河的上游扔下一只橡皮鸭或从一个线程向另一个线程发送聊天信息:
@ -36,11 +35,11 @@
{{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-07/src/main.rs}}
```
<span class="caption">示例 16-7: 将 `tx` 移动到一个新建的线程中并发送 “hi”</span>
<span class="caption">示例 16-7: 将 `tx` 移动到一个新建的线程中并发送 `"hi"`</span>
这里再次使用 `thread::spawn` 来创建一个新线程并使用 `move``tx` 移动到闭包中这样新建线程就拥有 `tx` 了。新建线程需要拥有信道的发送端以便能向信道发送消息。信道的发送端有一个 `send` 方法用来获取需要放入信道的值。`send` 方法返回一个 `Result<T, E>` 类型,所以如果接收端已经被丢弃了,将没有发送值的目标,所以发送操作会返回错误。在这个例子中,出错的时候调用 `unwrap` 产生 panic。不过对于一个真实程序,需要合理地处理它:回到第九章复习正确处理错误的策略。
这里再次使用 `thread::spawn` 来创建一个新线程并使用 `move``tx` 移动到闭包中这样新建线程就拥有 `tx` 了。新建线程需要拥有信道的发送端以便能向信道发送消息。信道的发送端有一个 `send` 方法用来获取需要放入信道的值。`send` 方法返回一个 `Result<T, E>` 类型,所以如果接收端已经被丢弃了,将没有发送值的目标,所以发送操作会返回错误。在这个例子中,出错的时候调用 `unwrap` 产生 panic。不过在一个真实应用中,需要合理地处理它:回到第九章复习正确处理错误的策略。
在示例 16-8 中,我们在主线程中从信道的接收获取值。这类似于在河的下游捞起橡皮鸭或接收聊天信息:
在示例 16-8 中,我们在主线程中从信道的接收获取值。这类似于在河的下游捞起橡皮鸭或接收聊天信息:
<span class="filename">文件名src/main.rs</span>
@ -48,15 +47,15 @@
{{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-08/src/main.rs}}
```
<span class="caption">示例 16-8: 在主线程中接收并打印内容 “hi”</span>
<span class="caption">示例 16-8: 在主线程中接收并打印内容 `"hi"`</span>
信道的接收有两个有用的方法:`recv` 和 `try_recv`。这里,我们使用了 `recv`,它是 _receive_ 的缩写。这个方法会阻塞主线程执行直到从信道中接收一个值。一旦发送了一个值,`recv` 会在一个 `Result<T, E>` 中返回它。当信道发送端关闭,`recv` 会返回一个错误表明不会再有新的值到来了。
信道的接收有两个有用的方法:`recv` 和 `try_recv`。这里,我们使用了 `recv`,它是 _receive_ 的缩写,这会阻塞主线程执行直到从信道中接收一个值。一旦发送了一个值,`recv` 会在一个 `Result<T, E>` 中返回它。当信道发送端关闭,`recv` 会返回一个错误表明不会再有新的值到来了。
`try_recv` 不会阻塞,相反它立刻返回一个 `Result<T, E>``Ok` 值包含可用的信息,而 `Err` 值代表此时没有任何消息。如果线程在等待消息过程中还有其他工作时使用 `try_recv` 很有用:可以编写一个循环来频繁调用 `try_recv`,在有可用消息时进行处理,其余时候则处理一会其他工作直到再次检查。
出于简单的考虑,这个例子使用了 `recv`;主线程中除了等待消息之外没有任何其他工作,所以阻塞主线程是合适的。
如果运行示例 16-8 中的代码,我们将会看到主线程打印出这个值:
运行示例 16-8 中的代码,我们将会看到主线程打印出这个值:
```text
Got: hi
@ -66,7 +65,7 @@ Got: hi
### 信道与所有权转移
所有权规则在消息传递中扮演了重要角色,其有助于我们编写安全的并发代码。防止并发编程中的错误是在 Rust 程序中考虑所有权的一大优势。现在让我们做一个验来看看信道与所有权如何一同协作以避免产生问题:我们将尝试在新建线程中的信道中发送完 `val` **之后** 再使用它。尝试编译示例 16-9 中的代码并看看为何这是不允许的:
所有权规则在消息传递中扮演了重要角色,其有助于我们编写安全的并发代码。防止并发编程中的错误是在 Rust 程序中考虑所有权的一大优势。现在让我们做一个验来看看信道与所有权如何一同协作以避免产生问题:我们将尝试在新建线程中的信道中发送完 `val` 值**之后**再使用它。尝试编译示例 16-9 中的代码并看看为何这是不允许的:
<span class="filename">文件名src/main.rs</span>
@ -82,9 +81,9 @@ Got: hi
{{#include ../listings/ch16-fearless-concurrency/listing-16-09/output.txt}}
```
我们的并发错误会造成一个编译时错误。`send` 函数获取其参数的所有权并移动这个值归接收者所有。这可以防止在发送后再次意外地使用这个值;所有权系统检查一切是否合乎规则。
我们的并发错误会造成一个编译时错误。`send` 函数获取其参数的所有权并移动这个值归接收端所有。这可以防止在发送后意外地再次使用这个值;所有权系统检查一切是否合乎规则。
### 发送多个值并观察接收的等待
### 发送多个值并观察接收的等待
示例 16-8 中的代码可以编译和运行,不过它并没有明确的告诉我们两个独立的线程通过信道相互通讯。示例 16-10 则有一些改进会证明示例 16-8 中的代码是并发执行的:新建线程现在会发送多个消息并在每个消息之间暂停一秒钟。
@ -96,7 +95,7 @@ Got: hi
<span class="caption">示例 16-10: 发送多个消息,并在每次发送后暂停一段时间</span>
这一次,在新建线程中有一个字符串 vector 希望发送到主线程。我们遍历它们,单独发送每一个字符串并通过一个 `Duration` 值调用 `thread::sleep` 函数来暂停一秒。
这一次,在新建线程中有一个字符串 vector 希望发送到主线程。我们遍历它们,单独发送每一个字符串并通过一个 `Duration` 值调用 `thread::sleep` 函数来暂停一秒。
在主线程中,不再显式调用 `recv` 函数:而是将 `rx` 当作一个迭代器。对于每一个接收到的值,我们将其打印出来。当信道被关闭时,迭代器也将结束。
@ -111,9 +110,9 @@ Got: thread
因为主线程中的 `for` 循环里并没有任何暂停或等待的代码,所以可以说主线程是在等待从新建线程中接收值。
### 通过克隆发送来创建多个生产者
### 通过克隆发送来创建多个生产者
之前我们提到了`mpsc`是 _multiple producer, single consumer_ 的缩写。可以运用 `mpsc` 来扩展示例 16-10 中的代码来创建向同一接收者发送值的多个线程。这可以通过克隆发送者来做到,如示例 16-11 所示:
之前我们提到了`mpsc`是 _multiple producer, single consumer_ 的缩写。可以运用 `mpsc` 来扩展示例 16-10 中的代码来创建多个向同一接收端发送值的线程。这可以通过克隆发送端来做到,如示例 16-11 所示:
<span class="filename">文件名src/main.rs</span>
@ -123,9 +122,9 @@ Got: thread
<span class="caption">示例 16-11: 从多个生产者发送多个消息</span>
这一次,在创建新线程之前,我们对发送调用了 `clone` 方法。这会给我们一个可以传递给第一个新建线程的发送端句柄。我们会将原始的信道发送端传递给第二个新建线程。这样就会有两个线程,每个线程将向信道的接收端发送不同的消息。
这一次,在创建新线程之前,我们对发送调用了 `clone` 方法。这会给我们一个可以传递给第一个新建线程的发送端句柄。我们会将原始的信道发送端传递给第二个新建线程。这样就会有两个线程,每个线程将向信道的接收端发送不同的消息。
如果运行这些代码,**可能** 会看到这样的输出:
运行代码,输出应该看起来类似如下
```text
Got: hi

@ -1,27 +1,26 @@
## 共享状态的并发
> [ch16-03-shared-state.md](https://github.com/rust-lang/book/blob/main/src/ch16-03-shared-state.md)
> <br>
> commit 856d89c53a6d69470bb5669c773fdfe6aab6fcc9
<!-- https://github.com/rust-lang/book/blob/main/src/ch16-03-shared-state.md -->
<!-- commit 56ec353290429e6547109e88afea4de027b0f1a9 -->
消息传递是一个很好的处理并发的方式,但并不是唯一一个。另一种方式是让多个线程访问同一块内存中的数据(共享状态)。再考虑一下 Go 语言文档中的这句口号“不要通过共享内存来通讯”“do not communicate by sharing memory.”
消息传递是一个很好的处理并发的方式,但并不是唯一一个。另一种方式是让多个线程访问相同的共享数据。再考虑一下 Go 语言文档中的这句口号:“不要通过共享内存来通讯”“do not communicate by sharing memory.”
通过共享内存进行通信,会是什么样的代码?此外,为什么喜欢消息传递的人会警告谨慎使用内存共享?
通过共享内存进行通信,会是什么样的代码?此外,为什么喜欢消息传递的人会警告谨慎使用内存共享?
在某种程度上,任何编程语言中的信道都类似于单所有权,因为一旦将一个值传送到信道中,将无法再使用这个值。共享内存类似于多所有权:多个线程可以同时访问相同的内存位置。在 15 章中我们介绍了智能指针可以实现多所有权然而这会增加额外的复杂性因为需要管理多个所有者。Rust 的类型系统和所有权规则在正确管理这些问题上提供了极大的帮助:举个例子,让我们来看看 **互斥器**,一个较常见的共享内存并发原语
在某种程度上,任何编程语言中的信道都类似于单所有权,因为一旦将一个值传送到信道中,将无法再使用这个值。共享内存类似于多所有权:多个线程可以同时访问相同的内存位置。在 15 章中我们介绍了智能指针可以实现多所有权然而这会增加额外的复杂性因为需要管理多个所有者。Rust 的类型系统和所有权规则在正确管理这些问题上提供了极大的帮助:举个例子,让我们来看看互斥器mutexes较为常见的共享内存并发原语之一
### 使用互斥器实现同一时刻只允许一个线程访问数据
### 使用互斥器实现同一时刻只允许一个线程访问数据
**互斥器**_mutex_ 互相排斥_mutual exclusion_的缩写。在同一时刻其只允许一个线程对数据拥有访问权。为了访问互斥器中的数据,线程首先需要通过获取互斥器的 **锁**_lock_来表明其希望访问数据。锁是一个数据结构作为互斥器的一部分它记录谁有数据的专属访问权。因此我们讲互斥器通过锁系统 **保护**_guarding_其数据。
**互斥器**_mutex_互相排斥_mutual exclusion_的缩写因为在同一时刻它只允许一个线程访问数据。为了访问互斥器中的数据,线程首先需要通过获取互斥器的**锁**_lock_来表明其希望访问数据。锁是一个数据结构作为互斥器的一部分它记录谁有数据的专属访问权。因此我们讲互斥器通过锁系统**保护**_guarding_其数据。
互斥器以难以使用著称(译注:原文指互斥器在其他编程语言中难以使用),因为你必须记住:
互斥器以难以使用著称(译注:原文指互斥器在其他编程语言中难以使用),因为你必须记住两个规则
1. 在使用数据之前,必须获取锁。
2. 使用完被互斥器所保护的数据之后,必须解锁数据,这样其他线程才能够获取锁。
作为一个现实中互斥器的例子,想象一下在某个会议的一次小组座谈会中,只有一个麦克风。如果一位成员要发言,他必须请求或表示希望使用麦克风。得到了麦克风后,他可以畅所欲言,讲完后再将麦克风交给下一位希望讲话的成员。如果一位成员结束发言后忘记将麦克风交还,其他人将无法发言。如果对共享麦克风的管理出现了问题,座谈会将无法正常进行!
正确的管理互斥器异常复杂,这也是许多人之所以热衷于信道的原因。然而,在 Rust 中,得益于类型系统和所有权,我们不会在锁和解锁上出错。
正确的管理互斥器异常复杂,这也是许多人之所以热衷于信道的原因。然而,在 Rust 中,得益于类型系统和所有权,你不可能在锁和解锁上出错。
### `Mutex<T>`的 API
@ -39,15 +38,15 @@
如果另一个线程拥有锁,并且那个线程 panic 了,则 `lock` 调用会失败。在这种情况下,没人能够再获取锁,所以我们调用 `unwrap`,使当前线程 panic。
一旦获取了锁,就可以将返回值(命名为 `num`)视为一个其内部数据`i32`的可变引用了。类型系统确保了我们在使用 `m` 中的值之前获取锁。`m` 的类型是 `Mutex<i32>` 而不是 `i32`,所以 **必须** 获取锁才能使用这个 `i32` 值。我们是不会忘记这么做的,因为如果没有获取锁,类型系统就不允许访问内部的 `i32` 值。
一旦获取了锁,就可以将返回值(命名为 `num`)视为一个其内部数据的可变引用了。类型系统确保了我们在使用 `m` 中的值之前获取锁。`m` 的类型是 `Mutex<i32>` 而不是 `i32`,所以**必须**调用 `lock` 才能使用这个 `i32` 值。我们不能忘记这么做的;因为如果没有获取锁,类型系统就不允许访问内部的 `i32` 值。
正如你所猜想的,`Mutex<T>` 是一个智能指针。更准确的说,`lock` 调用 **返回** 一个叫做 `MutexGuard` 的智能指针。这个智能指针实现了 `Deref` 来指向其内部数据;它也实现了 `Drop`,当 `MutexGuard` 离开作用域时,自动释放锁(发生在示例 16-12 内部作用域的结尾)。有了这个特性,就不会有忘记释放锁的潜在风险(忘记释放锁会使互斥器无法再被其它线程使用),因为锁的释放是自动发生的。
正如你所猜想的,`Mutex<T>` 是一个智能指针。更准确的说,`lock` 调用会**返回**一个叫做 `MutexGuard` 的智能指针。`MutexGuard` 智能指针实现了 `Deref` 来指向其内部数据;它也实现了 `Drop`,当 `MutexGuard` 离开作用域时,自动释放锁(发生在示例 16-12 内部作用域的结尾)。这样一来,就不会有忘记释放锁从而导致互斥器阻塞无法被其他线程使用的潜在风险,因为锁的释放是自动发生的。
释放锁之后,我们可以打印出互斥器内部的 `i32` 值,并发现我们刚刚已经将其值改为 6。
#### 在线程间共享 `Mutex<T>`
#### 在多个线程间共享 `Mutex<T>`
现在让我们尝试使用 `Mutex<T>` 在多个线程间共享同一个值。我们将启动 10 个线程,并在各个线程中对同一个计数器值加 1这样计数器将从 0 变为 10。示例 16-13 中的例子会出现编译错误,而我们将通过这些错误来学习如何使用 `Mutex<T>`,以及 Rust 又是如何帮助我们正确使用的。
现在让我们尝试使用 `Mutex<T>` 在多个线程间共享同一个值。我们将启动 10 个线程,并在各个线程中对同一个计数器值加 1这样计数器将从 0 累加到 10。示例 16-13 中的例子会出现编译错误,而我们将通过这些错误来学习如何使用 `Mutex<T>`,以及 Rust 又是如何帮助我们正确使用的。
<span class="filename">文件名src/main.rs</span>
@ -57,11 +56,11 @@
<span class="caption">示例 16-13: 程序启动了 10 个线程,每个线程都通过 `Mutex<T>` 来增加计数器的值</span>
这里创建了一个 `counter` 变量来存放内含 `i32``Mutex<T>`,类似示例 16-12 那样。接下来我们遍历整数区间,创建了 10 个线程。我们使用了 `thread::spawn`,并为所有线程传入了相同的闭包:它们每一个都将调用 `lock` 方法来获取 `Mutex<T>` 上的锁,接着将互斥器中的值加一。当一个线程结束执行,`num` 会离开闭包作用域并释放锁,这样另一个线程就可以获取它了。
这里创建了一个 `counter` 变量来存放内含 `i32``Mutex<T>`,类似示例 16-12 那样。接下来我们遍历一个整数 range 来创建了 10 个线程。我们使用了 `thread::spawn`,并为所有线程传入了相同的闭包:它们每一个都将计数器移动进线程,调用 `lock` 方法来获取 `Mutex<T>` 上的锁,接着将互斥器中的值加一。当一个线程结束执行,`num` 会离开闭包作用域并释放锁,这样另一个线程就可以获取它了。
在主线程中,我们像示例 16-2 那样收集了所有的 `JoinHandle`,并调用它们的 `join` 方法来等待所有线程结束。然后,主线程会获取锁,并打印出程序的结果。
之前提示过,这个例子不能编译让我们看看为什么!
之前提示过,这个例子不能编译。现在让我们看看为什么!
```console
{{#include ../listings/ch16-fearless-concurrency/listing-16-13/output.txt}}
@ -71,7 +70,7 @@
#### 多线程和多所有权
在第 15 章中,我们用智能指针 `Rc<T>` 来创建引用计数,使得一个值有了多个所有者。让我们做同样的事,看看会发生什么。将示例 16-14 中的 `Mutex<T>` 封装进 `Rc<T>`并在将所有权移入线程之前克隆clone `Rc<T>`
在第 15 章中,我们用智能指针 `Rc<T>` 来创建引用计数,使得一个值有了多个所有者。让我们做同样的事,看看会发生什么。将示例 16-14 中的 `Mutex<T>` 封装进 `Rc<T>`并在将所有权移入线程之前克隆clone`Rc<T>`
<span class="filename">文件名src/main.rs</span>
@ -87,17 +86,18 @@
{{#include ../listings/ch16-fearless-concurrency/listing-16-14/output.txt}}
```
哇哦,错误信息太长不看!划重点:第一行错误表明 `Rc<Mutex<i32>>` 不能在线程间安全传递(`` `Rc<Mutex<i32>>` cannot be sent between threads safely ``);编译器也指出了原因:`Rc<Mutex<i32>>` 没有实现 `Send` trait`` the trait `Send` is not implemented for `Rc<Mutex<i32>>` ``。下一节我们会讲到 `Send`:这是一个确保所使用的类型可以用于并发环境的 trait。
哇哦,错误信息太长不看!下面是重点要关注的内容:`` `Rc<Mutex<i32>>` cannot be sent between threads safely ``。编译器也指出了原因:`` the trait `Send` is not implemented for `Rc<Mutex<i32>>` ``。下一节我们会讲到 `Send`:这是一个确保所使用的类型可以用于并发环境的 trait。
不幸的是,`Rc<T>` 并不能安全的在线程间共享。当 `Rc<T>` 管理引用计数时,它必须在每一个 `clone` 调用时增加计数,并在每一个克隆体被丢弃时减少计数。`Rc<T>` 并没有使用任何并发原语,无法确保改变计数的操作不会被其他线程打断。这可能使计数出错,并导致诡异的 bug比如可能会造成内存泄漏或在使用结束之前就丢弃一个值。我们所需要的是一个与 `Rc<T>` 完全一致,又以线程安全的方式改变引用计数的类型。
#### 原子引用计数 `Arc<T>`
所幸 `Arc<T>` 正是这么一个类似 `Rc<T>` 并可以安全的用于并发环境的类型。字母 “a” 代表 **原子性**_atomic_所以这是一个 **原子引用计数**_atomically reference counted_类型。**原子类型** (Atomics) 是另一类这里还未涉及到的并发原语:请查看标准库中 [`std::sync::atomic`][atomic] 的文档来获取更多细节。目前我们只需要知道:原子类型就像基本类型一样,可以安全地在线程间共享。
所幸 `Arc<T>` 正是这么一个类似 `Rc<T>` 并可以安全地用于并发环境的类型。字母 _a_ 代表 **原子性**_atomic_所以这是一个**原子引用计数**_atomically reference counted_类型。**原子类型** (Atomics) 是另一类这里还未涉及到的并发原语:请查看标准库中 [`std::sync::atomic`][atomic] 的文档来获取更多细节。目前我们只需要知道:原子类型就像基本类型一样,可以安全地在线程间共享。
你可能会好奇,为什么不是所有的基本类型都是原子性的?为什么标准库中的类型没有全部默认使用 `Arc<T>` 实现?原因在于,线程安全会造成性能损失,我们希望只在必要时才为此买单。如果只是在单线程中对值进行操作,原子性提供的保证并无必要,而不加入原子性可以使代码运行得更快。
你可能会好奇,为什么不是所有的基本类型都是原子性的?为什么标准库中的类型没有全部默认使用 `Arc<T>` 实现?原因在于,线程安全会造成性能损失,我们希望只在必要时才为此买单。如果只是在单线程中对值进行操作,如果不必强制原子性提供的保证可以使代码运行得更快。
回到之前的例子:`Arc<T>` 和 `Rc<T>` 有着相同的 API所以我们只需修改程序中的 `use` 行、`new` 调用和 `clone` 调用。示例 16-15 中的代码最终可以编译和运行:
回到之前的例子:`Arc<T>` 和 `Rc<T>` 有着相同的 API所以我们只需修改程序中的 `use` 行、`new` 调用和 `clone` 调用。示例 16-15 中的代码最终可以编译并运行。
<span class="filename">文件名src/main.rs</span>
@ -107,13 +107,13 @@
<span class="caption">示例 16-15: 使用 `Arc<T>` 包装一个 `Mutex<T>` 能够实现在多线程之间共享所有权</span>
这会打印出:
段代码会打印出如下
```text
Result: 10
```
成功了!我们从 0 数到了 10这好像没啥大不了的不过一路上我们确实学习了很多关于 `Mutex<T>` 和线程安全的内容!这个例子中构建的结构可以用于比增加计数更为复杂的操作。使用这个策略,我们可将计算任务分成独立的部分,并分散到多个线程中,接着使用 `Mutex<T>` 使用各自的运算结果来更新最终的结果。
成功了!我们从 0 数到了 10这好像没啥大不了的不过一路上我们确实学习了很多关于 `Mutex<T>` 和线程安全的知识!这个例子中构建的结构可以用于比增加计数更为复杂的操作。使用这个策略,我们可将计算任务分成独立的部分,并分散到多个线程中,接着使用 `Mutex<T>` 使用各自的运算结果来更新最终的结果。
注意,对于简单的数值运算,[标准库中 `std::sync::atomic` 模块][atomic] 提供了比 `Mutex<T>` 更简单的类型。针对基本类型,这些类型提供了安全、并发、原子的操作。在上面的例子中,为了专注于讲明白 `Mutex<T>` 的用法,我们才选择在基本类型上使用 `Mutex<T>`。(译注:对于上面例子中出现的 `i32` 加法操作,更好的做法是使用 `AtomicI32` 类型来完成。具体参考文档。)
@ -121,8 +121,8 @@ Result: 10
你可能注意到了,尽管 `counter` 是不可变的,我们仍然可以获取其内部值的可变引用;这意味着 `Mutex<T>` 提供了内部可变性,就像 `Cell` 系列类型那样。使用 `RefCell<T>` 可以改变 `Rc<T>` 中内容(在 15 章中讲到过),同样地,使用 `Mutex<T>` 我们也可以改变 `Arc<T>` 中的内容。
另一个值得注意的细节是Rust 不能完全避免使用 `Mutex<T>` 所带来的逻辑错误。回忆一下,第 15 章中讲过,使用 `Rc<T>` 就有造成引用循环的风险:两个 `Rc<T>` 值相互引用,造成内存泄漏。同理,`Mutex<T>` 也有造成 **死锁**_deadlock_的风险当某个操作需要锁住两个资源而两个线程分别持有两个资源的其中一个锁时它们会永远相互等待。如果你对这个话题感兴趣尝试编写一个带有死锁的 Rust 程序,接着研究别的语言中使用互斥器的死锁规避策略,并尝试在 Rust 中实现它们。标准库中 `Mutex<T>``MutexGuard` 的 API 文档会提供有用的信息。
另一个值得注意的细节是Rust 不能完全避免使用 `Mutex<T>` 所带来的逻辑错误。回忆一下,第 15 章中讲过,使用 `Rc<T>` 就有造成引用循环的风险:两个 `Rc<T>` 值相互引用,造成内存泄漏。同理,`Mutex<T>` 也有造成**死锁**_deadlock_的风险当某个操作需要锁住两个资源而两个线程分别持有两个资源的其中一个锁时它们会永远相互等待。如果你对这个话题感兴趣尝试编写一个带有死锁的 Rust 程序,接着研究其它语言中使用互斥器的死锁规避策略,并尝试在 Rust 中实现它们。标准库中 `Mutex<T>``MutexGuard` 的 API 文档会提供有用的信息。
接下来,为了丰富本章的内容,让我们讨论一下 `Send``Sync` trait以及如何对自定义类型使用它们
作为本章的最后,我们将讨论 `Send``Sync` trait以及如何将它们用于自定义类型
[atomic]: https://doc.rust-lang.org/std/sync/atomic/index.html

@ -1,18 +1,17 @@
## 使用 `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 56ec353290429e6547109e88afea4de027b0f1a9
<!-- https://github.com/rust-lang/book/blob/main/src/ch16-04-extensible-concurrency-sync-and-send.md -->
<!-- commit 56ec353290429e6547109e88afea4de027b0f1a9 -->
Rust 的并发模型中一个有趣的方面是:我们之前讨论的几乎所有内容,都属于标准库,而不是语言本身的内容。由于不需要语言提供并发相关的基础设施,并发方案不受标准库或语言所限:我们可以编写自己的或使用别人编写的并发功能
Rust 的并发模型中一个有趣的方面是:我们之前讨论的几乎所有内容,都属于标准库,而不是语言本身的内容。处理并发的方案并不受标准库或语言所限:我们可以编写自己的或使用他人编写的并发特性
然而,在内嵌于语言而非标准库的关键并发概念中,有属于 `std::marker``Send``Sync` trait。
然而,有一些关键的并发概念是内嵌于语言本身而非标准库的,其中就包括 `std::marker``Send``Sync` trait。
### 通过 `Send` 允许在线程间转移所有权
`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
@ -20,7 +19,7 @@ Rust 的并发模型中一个有趣的方面是:我们之前讨论的几乎所
`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` 是不安全的
@ -34,7 +33,7 @@ Rust 的并发模型中一个有趣的方面是:我们之前讨论的几乎所
正如之前提到的,因为 Rust 本身很少有处理并发的部分内容,有很多的并发方案都由 crate 实现。它们比标准库要发展的更快;请在网上搜索当前最新的用于多线程场景的 crate。
Rust 提供了用于消息传递的信道,和像 `Mutex<T>``Arc<T>` 这样可以安全的用于并发上下文的智能指针。类型系统和借用检查器会确保这些场景中的代码,不会出现数据竞争和无效的引用。一旦代码可以编译了,我们就可以坚信这些代码可以正确运行于多线程环境,而不会出现其他语言中经常出现的那些难以追踪的 bug。并发编程不再是什么可怕的概念无所畏惧地并发吧
Rust 提供了用于消息传递的信道,和像 `Mutex<T>``Arc<T>` 这样可以安全的用于并发上下文的智能指针。类型系统和借用检查器会确保这些场景中的代码,不会出现数据竞争和无效的引用。一旦代码可以编译了,我们就可以坚信这些代码可以正确运行于多线程环境,而不会出现其他语言中经常出现的那些难以追踪的 bug。并发编程不再是什么可怕的概念无所畏惧地并发起来吧!
[sharing-a-mutext-between-multiple-threads]: ch16-03-shared-state.html#在线程间共享-mutext
[sharing-a-mutext-between-multiple-threads]: ch16-03-shared-state.html#在多个线程间共享-mutext
[nomicon]: https://doc.rust-lang.org/nomicon/index.html

@ -243,7 +243,7 @@ hi number 9 from the first task!
{{#rustdoc_include ../listings/ch17-async-await/listing-17-13/src/main.rs:here}}
```
<figcaption>示例 17-13通过多个异步代码块使用多个发送</figcaption>
<figcaption>示例 17-13通过多个异步代码块使用多个发送</figcaption>
</figure>

@ -1,7 +1,6 @@
# Rust 的面向对象特性
# 面向对象编程特性
> [ch18-00-oop.md](https://github.com/rust-lang/book/blob/main/src/ch18-00-oop.md)
> <br>
> commit 398d6f48d2e6b7b15efd51c4541d446e89de3892
<!-- https://github.com/rust-lang/book/blob/main/src/ch18-00-oop.md -->
<!-- commit 56ec353290429e6547109e88afea4de027b0f1a9 -->
面向对象编程Object-Oriented ProgrammingOOP是一种对程序进行建模方式。对象Object作为一个编程概念来源于 20 世纪 60 年代的 Simula 编程语言。这些对象影响了 Alan Kay 的编程架构,该架构中对象之间互相传递消息。他在 1967 年创造了 **面向对象编程** *object-oriented programming*)这个术语。关于 OOP 是什么有很多相互矛盾的定义在一些定义下Rust 是面向对象的在其他定义下Rust 不是。在本章节中,我们会探索一些被普遍认为是面向对象的特性和这些特性是如何体现在 Rust 语言习惯中的。接着会展示如何在 Rust 中实现面向对象设计模式,并讨论这么做与利用 Rust 自身的一些优势实现的方案相比有什么取舍。
面向对象编程Object-Oriented ProgrammingOOP是一种对程序进行建模方式。对象Object作为一个编程概念来源于 20 世纪 60 年代的 Simula 编程语言。这些对象影响了 Alan Kay 的编程架构,该架构中对象之间互相传递消息。他于 1967 年创造了**面向对象编程***object-oriented programming*)这一术语。对于 OOP 的定义众说纷纭在一些定义下Rust 是面向对象的在其他定义下Rust 不是。在本章节中,我们会探索一些被普遍认为是面向对象的特性和这些特性是如何体现在 Rust 语言习惯中的。接着会展示如何在 Rust 中实现面向对象设计模式,并讨论这么做与利用 Rust 自身的一些优势实现的方案相比有什么取舍。

@ -1,28 +1,27 @@
## 面向对象语言的特征
> [ch18-01-what-is-oo.md](https://github.com/rust-lang/book/blob/main/src/ch18-01-what-is-oo.md)
> <br>
> commit 398d6f48d2e6b7b15efd51c4541d446e89de3892
<!-- https://github.com/rust-lang/book/blob/main/src/ch18-01-what-is-oo.md -->
<!-- commit 56ec353290429e6547109e88afea4de027b0f1a9 -->
关于一门语言必须具备哪些特征才能被视为面向对象目前在编程社区中并没有共识。Rust 受到了许多编程范式的影响包括面向对象编程OOP例如在第 13 章中,我们探讨了来自函数式编程的特性。可以说,面向对象的语言共有一些共同的特征,即对象、封装和继承。我们将会讨论这些特征分别是什么,以及 Rust 是否支持它们。
### 对象包含数据和行为
由 Erich Gamma、Richard Helm、Ralph Johnson 和 John VlissidesAddison-Wesley Professional, 1994编写的书 *Design Patterns: Elements of Reusable Object-Oriented Software* ,通称 *The Gang of Four* (“四人帮”),是一本面向对象设计模式的目录。它这样定义面向对象编程:
由 Erich Gamma、Richard Helm、Ralph Johnson 和 John VlissidesAddison-Wesley Professional, 1994编写的书 *Design Patterns: Elements of Reusable Object-Oriented Software* ,通称 *The Gang of Four*,是一本面向对象设计模式的目录。它这样定义面向对象编程:
> 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` 块提供了在结构体和枚举之上的方法。虽然带有方法的结构体和枚举并不被 **称为** 对象,但是它们提供了与对象相同的功能,参考 *The Gang of Four* 中对象的定义。
在这个定义下Rust 是面向对象的:结构体和枚举包含数据而 `impl` 块提供了在结构体和枚举之上的方法。虽然带有方法的结构体和枚举并不被**称为**对象,但是参考 *The Gang of Four* 中对象的定义,它们提供了与对象相同的功能
### 封装隐藏了实现细节
另一个通常与面向对象编程关联的概念是 **封装***encapsulation*):一个对象的实现细节对使用该对象的代码不可访问。因此,对象交互的唯一方式是通过其公共 API使用对象的代码不应能直接触及对象的内部并改变数据或行为。这使得程序员能够更改和重构一个对象的内部实现而无需改变使用该对象的代码。
另一个通常与面向对象编程关联的概念是 **封装***encapsulation*):一个对象的实现细节对使用该对象的代码不可见。因此,对象交互的唯一方式是通过其公有 API使用对象的代码不应能直接触及对象的内部并改变数据或行为。这使得程序员能够更改和重构一个对象的内部实现而无需改变使用该对象的代码。
我们在第 7 章讨论了如何控制封装:我们可以使用 `pub` 关键字来决定代码中的哪些模块、类型、函数和方法是公有的,而默认情况下其他所有内容都是私有的。例如,我们可以定义一个 `AveragedCollection` 结构体,其中有一个存有 `Vec<i32>` 的字段。该结构体还可以有一个字段存储其平均值,以便需要时取用。示例 17-1 给出了 `AveragedCollection` 结构体的定义:
我们在第章讨论了如何控制封装:我们可以使用 `pub` 关键字来决定代码中的哪些模块、类型、函数和方法是公有的,而默认情况下其他所有内容都是私有的。例如,我们可以定义一个 `AveragedCollection` 结构体,其中有一个存有 `Vec<i32>` 的字段。该结构体还可以有一个字段存储向量中值的平均值,从而无需在每次需要时重新计算。换句话说,`AveragedCollection` 会为我们缓存已计算的平均值。示例 18-1 给出了 `AveragedCollection` 结构体的定义:
<span class="filename">文件名src/lib.rs</span>
@ -30,9 +29,9 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-01/src/lib.rs}}
```
<span class="caption">示例 17-1: `AveragedCollection` 结构体维护了一个整型列表及其所有元素的平均值。</span>
<span class="caption">示例 18-1: `AveragedCollection` 结构体维护了一个整型列表及其所有元素的平均值。</span>
该结构体被标记为 `pub`,这样其他代码就可以使用它,但结构体内的字段保持私有。这在这种情况下很重要,因为我们想确保每当列表中添加或删除值时,平均值也会更新。我们通过实现结构体上的 `add`、`remove` 和 `average` 方法来做到这一点,如示例 17-2 所示:
该结构体被标记为 `pub`,这样其他代码就可以使用它,但结构体内的字段保持私有。这在这种情况下很重要,因为我们想确保每当列表中添加或删除值时,平均值也会更新。我们通过实现结构体上的 `add`、`remove` 和 `average` 方法来做到这一点,如示例 18-2 所示:
<span class="filename">文件名src/lib.rs</span>
@ -40,17 +39,17 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-02/src/lib.rs:here}}
```
<span class="caption">示例 17-2: 在 `AveragedCollection` 结构体上实现了 `add`、`remove` 和 `average` 公有方法</span>
<span class="caption">示例 18-2: 在 `AveragedCollection` 结构体上实现了 `add`、`remove` 和 `average` 公有方法</span>
公有方法 `add`、`remove` 和 `average`修改 `AveragedCollection` 实例的唯一方式。当使用 `add` 方法把一个元素加入到 `list` 或者使用 `remove` 方法来删除时,这些方法的实现同时会调用私有的 `update_average` 方法来更新 `average` 字段。
公有方法 `add`、`remove` 和 `average`访问或修改 `AveragedCollection` 实例中数据的唯一途径。当使用 `add` 方法把一个元素加入到 `list` 或者使用 `remove` 方法来删除时,这些方法的实现同时会调用私有的 `update_average` 方法来更新 `average` 字段。
`list``average` 是私有的,所以没有其他方式来使得外部的代码直接向 `list` 增加或者删除元素,否则 `list` 改变时可能会导致 `average` 字段不同步。`average` 方法返回 `average` 字段的值,这使得外部的代码只能读取 `average` 而不能修改它。
因为我们已经封装了 `AveragedCollection` 的实现细节,改动数据结构等内部实现非常简单。例如,可以使用 `HashSet<i32>` 代替 `Vec<i32>` 作为 `list` 字段的类型。只要 `add`、`remove` 和 `average` 这些公有方法的签名保持不变,使用 `AveragedCollection` 的代码就无需改变。如果我们将 `list` 设为公有,情况就未必如此: `HashSet<i32>``Vec<i32>` 使用不同的方法增加或移除项,所以如果外部代码直接修改 `list` ,很可能需要进行更改。
因为我们已经封装了 `AveragedCollection` 的实现细节,改动数据结构等内部实现非常简单。例如,可以使用 `HashSet<i32>` 代替 `Vec<i32>` 作为 `list` 字段的类型。只要 `add`、`remove` 和 `average` 这些公有方法的签名保持不变,使用 `AveragedCollection` 的代码就无需改变。如果我们将 `list` 设为公有,情况就未必如此:`HashSet<i32>``Vec<i32>` 使用不同的方法增加或移除项,所以如果外部代码直接修改 `list`,很可能需要进行更改。
如果封装被认为是面向对象语言所必要的特征,那么 Rust 满足这个要求。在代码中不同的部分控制 `pub` 的使用来封装实现细节。
## 继承作为类型系统与代码共享
## 继承作为类型系统与代码共享
**继承***Inheritance*)是一种机制:一个对象可以从另一个对象的定义中继承元素,从而获得父对象的数据和行为,无需再次定义。
@ -60,13 +59,14 @@
选择继承有两个主要的原因。其一是代码复用:您可以为一种类型实现特定的行为,继承可将其复用到不同的类型上。在 Rust 代码中可以使用默认 trait 方法实现来进行有限的代码复用,就像示例 10-14 中在 `Summary` trait 上增加的 `summarize` 方法的默认实现。任何实现了 `Summary` trait 的类型都可以使用 `summarize` 方法而无须进一步实现。这类似于父类有一个方法的实现,继承的子类也拥有这个方法的实现。当实现 `Summary` trait 时也可以选择覆盖 `summarize` 的默认实现,这类似于子类覆盖从父类继承方法的实现。
其二与类型系统有关:子类型可以用于父类型被使用的地方。这也被称为 **多态***polymorphism*):如果多个对象共享某些特征,可以在运行时将它们互相替代。
其二与类型系统有关:子类型可以用于父类型被使用的地方。这也被称为**多态***polymorphism*):如果多个对象共享某些特征,可以在运行时将它们互相替代。
> 多态Polymorphism
> ### 多态Polymorphism
>
> 对很多人来说,多态性与继承同义。但它实际上是一个更广义的概念,指的是可以处理多种类型数据的代码。对继承而言,这些类型通常是子类。
> Rust 使用泛型来抽象不同可能的类型,并通过 trait bounds 来约束这些类型所必须提供的内容。这有时被称为 *bounded parametric polymorphism*
>
> Rust 使用泛型来抽象不同可能的类型,并通过 trait bound 来约束这些类型所必须提供的内容。这有时被称为 *bounded parametric polymorphism*
作为一种语言设计的解决方案,继承在许多新的编程语言中逐渐不被青睐,因为它经常有分享过多代码的风险。子类不应总是共享父类的所有特征,但是继承始终如此。它还引入了在子类上调用方法的可能性,这些方法可能没有意义,或因为方法不适用于子类而导致错误。此外,一些语言只允许单一继承(意味着子类只能从一个类继承),进一步限制了程序设计的灵活性。
作为一种语言设计的解决方案,继承在许多新的编程语言中逐渐不被青睐,因为它经常有分享过多代码的风险。子类不应总是共享父类的所有特征,但是继承始终如此。这会降低程序设计的灵活性。它还引入了在子类上调用方法的可能性,这些方法可能没有意义,或因为方法不适用于子类而导致错误。此外,一些语言只允许单一继承(意味着子类只能从一个类继承),进一步限制了程序设计的灵活性。
出于这些原因Rust 使用 trait 对象而非继承。接下来我们会讨论 Rust 如何使用 trait 对象实现多态性。

@ -1,24 +1,23 @@
## 顾及不同类型值的 trait 对象
> [ch18-02-trait-objects.md](https://github.com/rust-lang/book/blob/main/src/ch18-02-trait-objects.md)
> <br>
> commit 96d4b0ec1c5e019b85604c33ceee68b3e2669d40
<!-- https://github.com/rust-lang/book/blob/main/src/ch18-02-trait-objects.md -->
<!-- commit 56ec353290429e6547109e88afea4de027b0f1a9 -->
在第八章中,我们谈到了 vector 只能存储同种类型元素的局限。示例 8-9 中提供了一个替代方案,通过定义 `SpreadsheetCell` 枚举,来储存整型、浮点型或文本类型的成员。这意味着,我们可以在每个单元中储存不同类型的数据,并仍能拥有一个代表一排单元的 vector。只要我们需存储的值由一组固定的类型组成并且在代码编译时就知道具体会有哪些类型那么这种使用枚举的办法是完全可行的。
在第八章中,我们谈到了 vector 只能存储同种类型元素的局限。示例 8-9 中提供了一个替代方案,通过定义 `SpreadsheetCell` 枚举,来储存整型、浮点型或文本类型的变体。这意味着,我们可以在每个单元中储存不同类型的数据,并仍能拥有一个代表一排单元的 vector。只要我们需存储的值由一组固定的类型组成并且在代码编译时就知道具体会有哪些类型那么这种使用枚举的办法是完全可行的。
然而有时我们希望库用户在特定情况下能够扩展有效的类型集合。为了展示如何实现这一点这里将创建一个图形用户接口Graphical User InterfaceGUI工具的例子它通过遍历列表并调用每一个项目的 `draw` 方法来将其绘制到屏幕上 —— 此乃一个 GUI 工具的常见技术。我们将要创建一个叫做 `gui` 的库 crate它含一个 GUI 库的结构。这个 GUI 库包含一些可供开发者使用的类型,比如 `Button``TextField`。在此之上,`gui` 的用户希望创建自定义的可以绘制于屏幕上的类型:比如,一个程序员可能会增加 `Image`,另一个可能会增加 `SelectBox`
这个例子中并不会实现一个功能完善的 GUI 库,不过会展示其中各个部分是如何结合在一起的。编写库的时候,我们不可能知晓并定义所有其他程序员希望创建的类型。我们所知晓的是 `gui` 需要记录一系列不同类型的值,并需要能够对其中每一个值调用 `draw` 方法。这里无需知道调用 `draw` 方法时具体会发生什么,只要该值会有那个方法可供我们调用。
这个例子中并不会实现一个功能完善的 GUI 库,不过会展示其中各个部分是如何结合在一起的。编写库的时候,我们不可能知晓并定义所有其他程序员希望创建的类型。我们所知晓的是 `gui` 需要记录一系列不同类型的值,并需要能够对其中每一个值调用 `draw` 方法。这里无需知道调用 `draw` 方法时具体会发生什么,只要该值会有那个方法可供我们调用即可
在拥有继承的语言中,可以定义一个名为 `Component` 的类,该类上有一个 `draw` 方法。其他的类比如 `Button`、`Image` 和 `SelectBox` 会从 `Component` 派生并因此继承 `draw` 方法。它们各自都可以覆盖 `draw` 方法来定义自己的行为,但是框架会把所有这些类型当作是 `Component` 的实例,并在其上调用 `draw`。不过 Rust 并没有继承,我们得另寻出路
在拥有继承的语言中,可以定义一个名为 `Component` 的类,该类上有一个 `draw` 方法。其他的类比如 `Button`、`Image` 和 `SelectBox` 会从 `Component` 派生并因此继承 `draw` 方法。它们各自都可以重写 `draw` 方法来定义自己的行为,但是框架会把所有这些类型当作是 `Component` 的实例,并在其上调用 `draw`。不过 Rust 并没有继承,我们需要寻找另一种方式来设计 `gui` 库,以便用户能够使用新类型进行扩展
### 定义通用行为的 trait
为了实现 `gui` 所期望的行为,让我们定义一个 `Draw` trait其中包含名为 `draw` 的方法。接着可以定义一个存放 **trait 对象***trait object*)的 vector。trait 对象指向一个实现了我们指定 trait 的类型的实例,以及一个用于在运行时查找该类型的 trait 方法的表。我们通过指定某种指针来创建 trait 对象,例如 `&` 引用或 `Box<T>` 智能指针,还有 `dyn` keyword,以及指定相关的 trait第二十章 [“动态大小类型和 `Sized` trait”][dynamically-sized] 部分会介绍 trait 对象必须使用指针的原因)。我们可以使用 trait 对象代替泛型或具体类型。任何使用 trait 对象的位置Rust 的类型系统会在编译时确保任何在此上下文中使用的值会实现其 trait 对象的 trait。如此便无需在编译时就知晓所有可能的类型。
为了实现 `gui` 所期望的行为,让我们定义一个 `Draw` trait其中包含名为 `draw` 的方法。接着可以定义一个存放**trait 对象***trait object*)的 vector。trait 对象指向一个实现了我们指定 trait 的类型的实例,以及一个用于在运行时查找该类型的 trait 方法的表。我们通过指定某种指针来创建 trait 对象,例如 `&` 引用或 `Box<T>` 智能指针,还有 `dyn` 关键字,以及指定相关的 trait第二十章 [“动态大小类型和 `Sized` trait”][dynamically-sized] 部分会介绍 trait 对象必须使用指针的原因)。我们可以使用 trait 对象代替泛型或具体类型。任何使用 trait 对象的位置Rust 的类型系统会在编译时确保任何在此上下文中使用的值会实现其 trait 对象的 trait。如此便无需在编译时就知晓所有可能的类型。
之前提到过Rust 刻意不将结构体与枚举称为 “对象”,以便与其他语言中的对象相区别。在结构体或枚举中,结构体字段中的数据和 `impl` 块中的行为是分开的不同于其他语言中将数据和行为组合进一个称为对象的概念中。trait 对象将数据和行为两者相结合,从这种意义上说 **则**更类似其他语言中的对象。不过 trait 对象不同于传统的对象,因为不能向 trait 对象加数据。trait 对象并不像其他语言中的对象那么通用:其trait 对象)具体的作用是允许对通用行为进行抽象。
之前提到过Rust 刻意不将结构体与枚举称为 “对象”,以便与其他语言中的对象相区别。在结构体或枚举中,结构体字段中的数据和 `impl` 块中的行为是分开的不同于其他语言中将数据和行为组合进一个称为对象的概念中。trait 对象将数据和行为两者相结合,从这种意义上说**则**更类似其他语言中的对象。不过 trait 对象不同于传统的对象,因为不能向 trait 对象加数据。trait 对象并不像其他语言中的对象那么通用:其具体的作用是允许对通用行为进行抽象。
示例 17-3 展示了如何定义一个带有 `draw` 方法的 trait `Draw`
示例 18-3 展示了如何定义一个带有 `draw` 方法的 trait `Draw`
<span class="filename">文件名src/lib.rs</span>
@ -26,9 +25,9 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-03/src/lib.rs}}
```
<span class="caption">示例 17-3`Draw` trait 的定义</span>
<span class="caption">示例 18-3`Draw` trait 的定义</span>
因为第十章已经讨论过如何定义 trait其语法看起来应该比较眼熟。接下来就是新内容了:示例 17-4 定义了一个存放了名叫 `components` 的 vector 的结构体 `Screen`。这个 vector 的类型是 `Box<dyn Draw>`,此为一个 trait 对象:它是 `Box` 中任何实现了 `Draw` trait 的类型的替身。
因为第十章已经讨论过如何定义 trait其语法看起来应该比较眼熟。接下来就是一些新语法:示例 18-4 定义了一个存放了名叫 `components` 的 vector 的结构体 `Screen`。这个 vector 的类型是 `Box<dyn Draw>`,此为一个 trait 对象:它是 `Box` 中任何实现了 `Draw` trait 的类型的替身。
<span class="filename">文件名src/lib.rs</span>
@ -36,9 +35,9 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-04/src/lib.rs:here}}
```
<span class="caption">示例 17-4: 一个 `Screen` 结构体的定义,它带有一个字段 `components`,其包含实现了 `Draw` trait 的 trait 对象的 vector</span>
<span class="caption">示例 18-4: 一个 `Screen` 结构体的定义,它带有一个字段 `components`,其包含实现了 `Draw` trait 的 trait 对象的 vector</span>
`Screen` 结构体上,我们将定义一个 `run` 方法,该方法会对其 `components` 上的每一个组件调用 `draw` 方法,如示例 17-5 所示:
`Screen` 结构体上,我们将定义一个 `run` 方法,该方法会对其 `components` 上的每一个组件调用 `draw` 方法,如示例 18-5 所示:
<span class="filename">文件名src/lib.rs</span>
@ -46,9 +45,9 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-05/src/lib.rs:here}}
```
<span class="caption">示例 17-5`Screen` 上实现一个 `run` 方法,该方法在每个 component 上调用 `draw` 方法</span>
<span class="caption">示例 18-5`Screen` 上实现一个 `run` 方法,该方法在每个 component 上调用 `draw` 方法</span>
这与定义使用了带有 trait bound 的泛型类型参数的结构体不同。泛型类型参数一次只能替代一个具体类型,而 trait 对象则允许在运行时替代多种具体类型。例如,可以定义 `Screen` 结构体来使用泛型和 trait bound如示例 17-6 所示:
这与定义使用了带有 trait bound 的泛型类型参数的结构体不同。泛型类型参数一次只能替代一个具体类型,而 trait 对象则允许在运行时替代多种具体类型。例如,可以定义 `Screen` 结构体来使用泛型和 trait bound如示例 18-6 所示:
<span class="filename">文件名src/lib.rs</span>
@ -56,15 +55,15 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-06/src/lib.rs:here}}
```
<span class="caption">示例 17-6: 一种 `Screen` 结构体的替代实现,其 `run` 方法使用泛型和 trait bound</span>
<span class="caption">示例 18-6: 一种 `Screen` 结构体的替代实现,其 `run` 方法使用泛型和 trait bound</span>
这限制了 `Screen` 实例必须拥有一个全是 `Button` 类型或者全是 `TextField` 类型的组件列表。如果只需要同质(相同类型)集合,则倾向于使用泛型和 trait bound因为其定义会在编译时采用具体类型进行单态化。
这限制了 `Screen` 实例必须拥有一个全是 `Button` 类型或者全是 `TextField` 类型的组件列表。如果只需要同质(相同类型homogeneous)集合,则倾向于使用泛型和 trait bound因为其定义会在编译时采用具体类型进行单态化monomorphized
另一方面,通过使用 trait 对象的方法,一个 `Screen` 实例可以存放一个既能包含 `Box<Button>`,也能包含 `Box<TextField>``Vec<T>`。让我们看看它是如何工作的,接着会讲到其运行时性能影响。
### 实现 trait
现在来增加一些实现了 `Draw` trait 的类型。我们将提供 `Button` 类型。再一次重申,真正实现 GUI 库超出了本书的范畴,所以 `draw` 方法体中不会有任何有意义的实现。为了想象一下这个实现看起来像什么,一个 `Button` 结构体可能会拥有 `width`、`height` 和 `label` 字段,如示例 17-7 所示:
现在来增加一些实现了 `Draw` trait 的类型。我们将提供 `Button` 类型。再一次重申,真正实现 GUI 库超出了本书的范畴,所以 `draw` 方法体中不会有任何有意义的实现。为了想象一下这个实现看起来像什么,一个 `Button` 结构体可能会拥有 `width`、`height` 和 `label` 字段,如示例 18-7 所示:
<span class="filename">文件名src/lib.rs</span>
@ -72,11 +71,11 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-07/src/lib.rs:here}}
```
<span class="caption">示例 17-7: 一个实现了 `Draw` trait 的 `Button` 结构体</span>
<span class="caption">示例 18-7: 一个实现了 `Draw` trait 的 `Button` 结构体</span>
`Button` 上的 `width`、`height` 和 `label` 字段会和其他组件不同比如 `TextField` 可能有 `width`、`height`、`label` 以及 `placeholder` 字段。每一个我们希望能在屏幕上绘制的类型都会使用不同的代码来实现 `Draw` trait 的 `draw` 方法来定义如何绘制特定的类型,像这里的 `Button` 类型(如上提到的并不包含任何实际的 GUI 代码)。除了实现 `Draw` trait 之外,比如 `Button` 还可能有另一个包含按钮点击如何响应的方法的 `impl` 块。这类方法并不适用于像 `TextField` 这样的类型。
`Button` 上的 `width`、`height` 和 `label` 字段会和其他组件不同比如 `TextField` 可能有 `width`、`height`、`label` 以及 `placeholder` 字段。每一个我们希望能在屏幕上绘制的类型都会使用不同的代码来实现 `Draw` trait 的 `draw` 方法来定义如何绘制特定的类型,像这里的 `Button` 类型(如上提到的并不包含任何实际的 GUI 代码)。除了实现 `Draw` trait 之外,比如 `Button` 还可能有另一个包含按钮点击如何响应的方法的 `impl` 块。这类方法并不适用于像 `TextField` 这样的类型。
如果一些库的使用者决定实现一个包含 `width`、`height` 和 `options` 字段的结构体 `SelectBox`,并且也为其实现了 `Draw` trait如示例 17-8 所示:
如果一些库的使用者决定实现一个包含 `width`、`height` 和 `options` 字段的结构体 `SelectBox`,并且也为其实现了 `Draw` trait如示例 18-8 所示:
<span class="filename">文件名src/main.rs</span>
@ -84,9 +83,9 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-08/src/main.rs:here}}
```
<span class="caption">示例 17-8: 另一个使用 `gui` 的 crate 中,`SelectBox` 结构体上实现 `Draw` trait</span>
<span class="caption">示例 18-8: 另一个使用 `gui` 的 crate`SelectBox` 结构体上实现 `Draw` trait</span>
库使用者现在可以在他们的 `main` 函数中创建一个 `Screen` 实例。至此可以通过将 `SelectBox``Button` 放入 `Box<T>` 转变为 trait 对象再放入 `Screen` 实例中。接着可以调用 `Screen``run` 方法,它会调用每个组件的 `draw` 方法。示例 17-9 展示了这个实现:
库使用者现在可以在他们的 `main` 函数中创建一个 `Screen` 实例。至此可以通过将 `SelectBox``Button` 放入 `Box<T>` 转变为 trait 对象再放入 `Screen` 实例中。接着可以调用 `Screen``run` 方法,它会调用每个组件的 `draw` 方法。示例 18-9 展示了这个实现:
<span class="filename">文件名src/main.rs</span>
@ -94,15 +93,15 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-09/src/main.rs:here}}
```
<span class="caption">示例 17-9: 使用 trait 对象来存储实现了相同 trait 的不同类型的值</span>
<span class="caption">示例 18-9: 使用 trait 对象来存储实现了相同 trait 的不同类型的值</span>
当编写库的时候,我们不知道何人会在何时增加 `SelectBox` 类型,不过 `Screen` 的实现能够操作并绘制这个新类型,因为 `SelectBox` 实现了 `Draw` trait这意味着它实现了 `draw` 方法。
这个概念 —— 只关心值所反映的信息而不是其具体类型 —— 类似于动态类型语言中称为 **鸭子类型***duck typing*)的概念:如果它走起来像一只鸭子,叫起来像一只鸭子,那么它就是一只鸭子!在示例 17-5 中 `Screen` 上的 `run` 实现中,`run` 并不需要知道各个组件的具体类型是什么。它并不检查组件是 `Button` 或者 `SelectBox` 的实例。通过指定 `Box<dyn Draw>` 作为 `components` vector 中值的类型,我们就定义了 `Screen` 为需要可以在其上调用 `draw` 方法的值。
这个概念 —— 只关心值所反映的信息而不是其具体类型 —— 类似于动态类型语言中称为**鸭子类型***duck typing*)的概念:如果它走起来像一只鸭子,叫起来像一只鸭子,那么它就是一只鸭子!在示例 18-5 中 `Screen` 上的 `run` 实现中,`run` 并不需要知道各个组件的具体类型是什么。它并不检查组件是 `Button` 或者 `SelectBox` 的实例,而是直接调用组件的 `draw` 方法。通过指定 `Box<dyn Draw>` 作为 `components` vector 中值的类型,我们就定义了 `Screen` 为需要可以在其上调用 `draw` 方法的值。
使用 trait 对象和 Rust 类型系统来进行类似鸭子类型操作的优势是无需在运行时检查一个值是否实现了特定方法或者担心在调用时因为值没有实现方法而产生错误。如果值没有实现 trait 对象所需的 trait 则 Rust 不会编译这些代码。
例如,示例 17-10 展示了当创建一个使用 `String` 做为其组件的 `Screen` 时发生的情况:
例如,示例 18-10 展示了当创建一个使用 `String` 做为其组件的 `Screen` 时发生的情况:
<span class="filename">文件名src/main.rs</span>
@ -110,9 +109,9 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-10/src/main.rs}}
```
<span class="caption">示例 17-10: 尝试使用一种没有实现 trait 对象的 trait 的类型</span>
<span class="caption">示例 18-10: 尝试使用一种没有实现 trait 对象的 trait 的类型</span>
我们会遇到这个错误因为 `String` 没有实现 `rust_gui::Draw` trait
我们会遇到这个错误因为 `String` 没有实现 `Draw` trait
```console
{{#include ../listings/ch18-oop/listing-18-10/output.txt}}
@ -122,12 +121,11 @@
### trait 对象执行动态分发
回忆一下第十章 [“泛型代码的性能”][performance-of-code-using-generics] 部分讨论过的,当对泛型使用 trait bound 时编译器所执行的单态化处理:编译器为每一个被泛型类型参数代替的具体类型生成了函数和方法的非泛型实现。单态化产生的代码在执行 **静态分发***static dispatch*)。静态分发发生于编译器在编译时就知晓调用了什么方法的时候。这与 **动态分发** *dynamic dispatch*)相对,这时编译器在编译时无法知晓调用了什么方法。在动态分发的场景下,编译器会生成负责在运行时确定该调用什么方法的代码。
回忆一下第十章 [“泛型代码的性能”][performance-of-code-using-generics] 部分讨论过的,当对泛型使用 trait bound 时编译器所执行的单态化处理:编译器为每一个被泛型类型参数代替的具体类型生成了函数和方法的非泛型实现。单态化产生的代码在执行**静态分发***static dispatch*),也就是说编译器在编译时就知晓要调用什么方法。这与**动态分发** *dynamic dispatch*)相对,这时编译器在编译时无法知晓要调用哪个方法。在动态分发的场景下,编译器会生成负责在运行时确定该调用什么方法的代码。
当使用 trait 对象时Rust 必须使用动态分发。编译器无法知晓所有可能用于 trait 对象代码的类型所以它也不知道应该调用哪个类型的哪个方法实现。为此Rust 在运行时使用 trait 对象中的指针来知晓需要调用哪个方法。动态分发也阻止编译器有选择的内联方法代码,这会相应的禁用一些优化。尽管在编写示例 17-5 和可以支持示例 17-9 中的代码的过程中确实获得了额外的灵活性,但仍然需要权衡取舍。
当使用 trait 对象时Rust 必须使用动态分发。编译器无法知晓所有可能用于 trait 对象代码的类型所以它也不知道应该调用哪个类型的哪个方法实现。为此Rust 在运行时使用 trait 对象中的指针来知晓需要调用哪个方法。这种查找会带来在静态分发中不会产生的运行时开销。动态分发也阻止编译器有选择地内联方法代码这会相应地禁用一些优化Rust 还定义了一些规则,称为**dyn 兼容性**_dyn compatibility_用于规定可以和不可以在哪些地方使用动态分发。这些规则超出了本讨论范围但你可以在[参考资料][dyn-compatibility]中详细了解。尽管在编写示例 18-5 和可以支持示例 18-9 中的代码的过程中确实获得了额外的灵活性,但仍然需要权衡取舍。
[performance-of-code-using-generics]:
ch10-01-syntax.html#泛型代码的性能
[dynamically-sized]: ch20-03-advanced-types.html#动态大小类型和-sized-trait
[Rust RFC 255 ref]: https://github.com/rust-lang/rfcs/blob/master/text/0255-object-safety.md
[Rust Reference ref]: https://doc.rust-lang.org/reference/items/traits.html#object-safety
[dyn-compatibility]: https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility

@ -1,27 +1,26 @@
## 面向对象设计模式的实现
> [ch18-03-oo-design-patterns.md](https://github.com/rust-lang/book/blob/main/src/ch18-03-oo-design-patterns.md)
> <br>
> commit 937784b8708c24314707378ad42faeb12a334bbd
<!-- https://github.com/rust-lang/book/blob/main/src/ch18-03-oo-design-patterns.md -->
<!-- commit 8904592b6f0980c278ee52371be9af7b62541787 -->
**状态模式***state pattern*)是一个面向对象设计模式。该模式的关键在于定义一系列值的内含状态。这些状态体现为一系列的 **状态对象**,同时值的行为随着其内部状态而改变。我们将编写一个博客发布结构体的例子,它拥有一个包含其状态的字段,这是一个有着 "draft"、"review" 或 "published" 状态对象
**状态模式***state pattern*)是一个面向对象设计模式。该模式的关键在于定义值的一系列内含状态。这些状态体现为一系列的**状态对象**_state objects_,同时值的行为随着其内部状态而改变。我们将编写一个博客发布结构体的例子,它拥有一个包含其状态的字段,该字段可以是 "draft"、"review" 或 "published" 状态对象之一。
状态对象共享功能:当然,在 Rust 中使用结构体和 trait 而不是对象和继承。每一个状态对象负责其自身的行为,以及该状态何时应当转移至另一个状态。持有一个状态对象的值对于不同状态的行为以及何时状态转移毫不知情。
使用状态模式的优点在于,程序的业务需求改变时,无需改变值持有状态或者使用值的代码。我们只需更新某个状态对象中的代码来改变其规则,或者是增加更多的状态对象。
首先我们将以一种更加传统的面向对象的方式实现状态模式,接着使用一种更 Rust 一点的方式。让我们使用状态模式增量式地实现一个发布博文的工作流以探索这个概念。
首先我们将以一种更加传统的面向对象的方式实现状态模式,接着使用一种在 Rust 中更自然的方式。让我们使用状态模式来增量式地实现一个发布博文的工作流以探索这个概念。
这个博客的最终功能看起来像这样:
最终功能看起来像这样:
1. 博文从空白的草开始。
2. 一旦草完成,请求审核博文。
1. 博文从空白的草稿开始。
2. 一旦草稿完成,请求审核博文。
3. 一旦博文过审,它将被发表。
4. 只有被发表的博文的内容会被打印,这样就不会意外打印出没有被审核的博文的文本。
任何其他对博文的修改尝试都是没有作用的。例如,如果尝试在请求审核之前通过一个草案博文,博文应该保持未发布的状态。
任何其他对博文的修改尝试都不会生效。例如,如果尝试在请求审核之前通过一个草稿博文,博文应该保持未发布的状态。
示例 17-11 展示这个工作流的代码形式:这是一个我们将要在一个叫做 `blog` 的库 crate 中实现的 API 的示例。这段代码还不能编译,因为还未实现 `blog`
示例 18-11 展示这个工作流的代码形式:这是一个我们将要在一个叫做 `blog` 的库 crate 中实现的 API 的示例。这段代码还不能编译,因为还未实现 `blog` crate
<span class="filename">文件名src/main.rs</span>
@ -29,17 +28,17 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-11/src/main.rs:all}}
```
<span class="caption">示例 17-11: 展示了 `blog` crate 期望行为的代码</span>
<span class="caption">示例 18-11: 展示了 `blog` crate 期望行为的代码</span>
我们希望允许用户使用 `Post::new` 创建一个新的博文草案。也希望能在草案阶段为博文编写一些文本。如果在审批之前尝试立刻获取博文的内容,不应该获取到任何文本因为博文仍然是草案。一个好的单元测试将是断言草案博文的 `content` 方法返回空字符串,不过我们并不准备为这个例子编写单元测试。
我们希望允许用户使用 `Post::new` 创建一个新的博文草稿。也希望能在草稿阶段为博文编写一些文本。如果在审批之前尝试立刻获取博文的内容,不应该获取到任何文本因为博文仍然是草稿。出于演示目的我们在代码中添加了 `assert_eq!`。一个好的单元测试将是断言草稿博文的 `content` 方法返回空字符串,不过我们并不准备为这个例子编写单元测试。
接下来,我们希望能够请求审核博文,而在等待审核的阶段 `content` 应该仍然返回空字符串。最后当博文审核通过,它应该被发表,这意味着当调用 `content` 时博文的文本将被返回。
注意我们与 crate 交互的唯一的类型是 `Post`。这个类型会使用状态模式并会存放处于三种博文所可能的状态之一的值 —— 草案,等待审核和发布。状态上的改变由 `Post` 类型内部进行管理。状态依库用户对 `Post` 实例调用的方法而改变,但是不能直接管理状态变化。这也意味着用户不会在状态上犯错,比如在过审前发布博文。
注意我们与 crate 交互的唯一的类型是 `Post`。这个类型会使用状态模式并会存放处于三种博文所可能的状态之一的值 —— 草稿,审核和发布。状态上的改变由 `Post` 类型内部进行管理。状态依库用户对 `Post` 实例调用的方法而改变,但是不能直接管理状态变化。这也意味着用户不会在状态上犯错,比如在过审前发布博文。
### 定义 `Post` 并新建一个草状态的实例
### 定义 `Post` 并新建一个草稿状态的实例
让我们开始实现这个库吧!我们知道需要一个公有 `Post` 结构体来存放一些文本,所以让我们从结构体的定义和一个创建 `Post` 实例的公有关联函数 `new` 开始,如示例 17-12 所示。还需定义一个私有 trait `State`
让我们开始实现这个库吧!我们知道需要一个公有 `Post` 结构体来存放一些文本,所以让我们从结构体的定义和一个创建 `Post` 实例的公有关联函数 `new` 开始,如示例 18-12 所示。还需定义一个私有 trait `State` 用于定义 `Post` 的状态对象所必须有的行为
`Post` 将在私有字段 `state` 中存放一个 `Option<T>` 类型的 trait 对象 `Box<dyn State>`。稍后将会看到为何 `Option<T>` 是必须的。
@ -49,15 +48,15 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-12/src/lib.rs}}
```
<span class="caption">示例 17-12: `Post` 结构体的定义和新建 `Post` 实例的 `new` 函数,`State` trait 和结构体 `Draft`</span>
<span class="caption">示例 18-12: `Post` 结构体的定义和新建 `Post` 实例的 `new` 函数,`State` trait 和结构体 `Draft`</span>
`State` trait 定义了所有不同状态的博文所共享的行为,这个状态对象是 `Draft`、`PendingReview` 和 `Published`,它们都会实现 `State` 状态。现在这个 trait 并没有任何方法,同时开始将只定义 `Draft` 状态因为这是我们希望博文的初始状态。
`State` trait 定义了所有不同状态的博文所共享的行为,这个状态对象是 `Draft`、`PendingReview` 和 `Published`,它们都会实现 `State` trait。现在这个 trait 并没有任何方法,同时开始将只定义 `Draft` 状态因为这是我们希望博文的初始状态。
当创建新的 `Post` 时,我们将其 `state` 字段设置为一个存放了 `Box``Some` 值。这个 `Box` 指向一个 `Draft` 结构体新实例。这确保了无论何时新建一个 `Post` 实例,它都会从草开始。因为 `Post``state` 字段是私有的,也就无法创建任何其他状态的 `Post` 了!`Post::new` 函数中将 `content` 设置为新建的空 `String`
当创建新的 `Post` 时,我们将其 `state` 字段设置为一个存放了 `Box``Some` 值。这个 `Box` 指向一个 `Draft` 结构体新实例。这确保了无论何时新建一个 `Post` 实例,它都会从草稿开始。因为 `Post``state` 字段是私有的,也就无法创建任何其他状态的 `Post` 了!`Post::new` 函数中将 `content` 设置为新建的空 `String`
### 存放博文内容的文本
在示例 17-11 中,展示了我们希望能够调用一个叫做 `add_text` 的方法并向其传递一个 `&str` 来将文本增加到博文的内容中。选择实现为一个方法而不是将 `content` 字段暴露为 `pub` 。这意味着之后可以实现一个方法来控制 `content` 字段如何被读取。`add_text` 方法是非常直观的,让我们在示例 17-13 的 `impl Post` 块中增加一个实现:
在示例 18-11 中,展示了我们希望能够调用一个叫做 `add_text` 的方法并向其传递一个 `&str` 来将文本增加到博文的内容中。选择实现为一个方法而不是将 `content` 字段暴露为 `pub` 。这意味着之后可以实现一个方法来控制 `content` 字段如何被读取。`add_text` 方法是非常直观的,让我们在示例 18-13 的 `impl Post` 块中增加一个实现:
<span class="filename">文件名src/lib.rs</span>
@ -65,13 +64,13 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-13/src/lib.rs:here}}
```
<span class="caption">示例 17-13: 实现方法 `add_text` 来向博文的 `content` 增加文本</span>
<span class="caption">示例 18-13: 实现方法 `add_text` 来向博文的 `content` 增加文本</span>
`add_text` 获取一个 `self` 的可变引用,因为需要改变调用 `add_text``Post` 实例。接着调用 `content` 中的 `String``push_str` 并传递 `text` 参数来保存到 `content` 中。这不是状态模式的一部分,因为它的行为并不依赖博文所处的状态。`add_text` 方法完全不与 `state` 状态交互,不过这是我们希望支持的行为的一部分。
`add_text` 获取一个 `self` 的可变引用,因为需要改变调用 `add_text``Post` 实例。接着调用 `content` 中的 `String``push_str` 并传递 `text` 参数来将其追加到已保存的 `content` 中。这不是状态模式的一部分,因为它的行为并不依赖博文所处的状态。`add_text` 方法完全不与 `state` 字段交互,不过这是我们希望支持的行为的一部分。
### 确保博文草的内容是空的
### 确保博文草稿的内容是空的
即使调用 `add_text` 并向博文增加一些内容之后,我们仍然希望 `content` 方法返回一个空字符串 slice因为博文仍然处于草案状态,如示例 17-11 的第 8 行所示。现在让我们使用能满足要求的最简单的方式来实现 `content` 方法:总是返回一个空字符串 slice。当实现了将博文状态改为发布的能力之后将改变这一做法。但是目前博文只能是草案状态,这意味着其内容应该总是空的。示例 17-14 展示了这个占位符实现:
即使调用 `add_text` 并向博文增加一些内容之后,我们仍然希望 `content` 方法返回一个空字符串 slice因为博文仍然处于草稿状态,如示例 18-11 的第 7 行所示。现在让我们使用能满足要求的最简单的方式来实现 `content` 方法:总是返回一个空字符串 slice。当实现了将博文状态改为发布的能力之后将改变这一做法。但是目前博文只能是草稿状态,这意味着其内容应该总是空的。示例 18-14 展示了这个占位符实现。
<span class="filename">文件名src/lib.rs</span>
@ -79,13 +78,13 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-14/src/lib.rs:here}}
```
<span class="caption">列表 17-14: 增加一个 `Post``content` 方法的占位实现,它总是返回一个空字符串 slice</span>
<span class="caption">列表 18-14: 增加一个 `Post``content` 方法的占位实现,它总是返回一个空字符串 slice</span>
通过增加这个 `content` 方法,示例 17-11 中直到第 8 行的代码能如期运行。
通过增加这个 `content` 方法,示例 18-11 中直到第 7 行的代码能如期运行。
### 请求审核博文来改变其状态
### 请求审核来改变博文的状态
接下来需要增加请求审核博文的功能,这应当将其状态由 `Draft` 改为 `PendingReview`。示例 17-15 展示了这个代码:
接下来需要增加请求审核博文的功能,这应当将其状态由 `Draft` 改为 `PendingReview`。示例 18-15 展示了这个代码:
<span class="filename">文件名src/lib.rs</span>
@ -93,13 +92,13 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-15/src/lib.rs:here}}
```
<span class="caption">示例 17-15: 实现 `Post``State` trait 的 `request_review` 方法</span>
<span class="caption">示例 18-15: 实现 `Post``State` trait 的 `request_review` 方法</span>
这里为 `Post` 增加一个获取 `self` 可变引用的公有方法 `request_review`。接着在 `Post` 的当前状态下调用内部的 `request_review` 方法,并且第二个 `request_review` 方法会消费当前的状态并返回一个新状态。
这里给 `State` trait 增加了 `request_review` 方法;所有实现了这个 trait 的类型现在都需要实现 `request_review` 方法。注意不同于使用 `self``&self` 或者 `&mut self` 作为方法的第一个参数,这里使用了 `self: Box<Self>`。这个语法意味着该方法只可在持有这个类型的 `Box` 上被调用。这个语法获取了 `Box<Self>` 的所有权使老状态无效化,以便 `Post` 的状态值可转换为一个新状态。
为了消费老状态,`request_review` 方法需要获取状态值的所有权。这就是 `Post``state` 字段中 `Option` 的来历:调用 `take` 方法将 `state` 字段中的 `Some` 值取出并留下一个 `None`,因为 Rust 不允许结构体实例中存在值为空的字段。这使得我们将 `state` 的值移出 `Post` 而不是借用它。接着我们将博文的 `state` 值设置为这个操作的结果。
为了消费老状态,`request_review` 方法需要获取状态值的所有权。这就是 `Post``state` 字段中 `Option` 的来历:调用 `take` 方法将 `state` 字段中的 `Some` 值取出并留下一个 `None`,因为 Rust 不允许结构体实例中存在未初始化的字段。这使得我们将 `state` 的值移出 `Post` 而不是借用它。接着我们将博文的 `state` 值设置为这个操作的结果。
我们需要将 `state` 临时设置为 `None` 来获取 `state` 值,即老状态的所有权,而不是使用 `self.state = self.state.request_review();` 这样的代码直接更新状态值。这确保了当 `Post` 被转换为新状态后不能再使用老 `state` 值。
@ -107,11 +106,11 @@
现在我们能看出状态模式的优势了:无论 `state` 是何值,`Post` 的 `request_review` 方法都是一样的。每个状态只负责它自己的规则。
我们将继续保持 `Post``content` 方法实现不变,返回一个空字符串 slice。现在我们可以拥有 `PendingReview` 状态和 `Draft` 状态的 `Post` 了,不过我们希望在 `PendingReview` 状态下 `Post` 也有相同的行为。现在示例 17-11 中直到 10 行的代码是可以执行的!
我们将继续保持 `Post``content` 方法实现不变,返回一个空字符串 slice。现在我们可以拥有 `PendingReview` 状态和 `Draft` 状态的 `Post` 了,不过我们希望在 `PendingReview` 状态下 `Post` 也有相同的行为。现在示例 18-11 中直到 10 行的代码是可以执行的!
### 增加改变 `content` 行为的 `approve` 方法
### 添加 `approve` 以改变 `content` 的行为
`approve` 方法将与 `request_review` 方法类似:它会将 `state` 设置为审核通过时应处于的状态,如示例 17-16 所示。
`approve` 方法将与 `request_review` 方法类似:它会将 `state` 设置为审核通过时应处于的状态,如示例 18-16 所示。
<span class="filename">文件名src/lib.rs</span>
@ -119,13 +118,13 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-16/src/lib.rs:here}}
```
<span class="caption">示例 17-16: 为 `Post``State` trait 实现 `approve` 方法</span>
<span class="caption">示例 18-16: 为 `Post``State` trait 实现 `approve` 方法</span>
这里为 `State` trait 增加了 `approve` 方法,并新增了一个实现了 `State` 的结构体,`Published` 状态。
类似于 `PendingReview``request_review` 的工作方式,如果对 `Draft` 调用 `approve` 方法,并没有任何效果,因为它会返回 `self`。当对 `PendingReview` 调用 `approve` 时,它返回一个新的、装箱的 `Published` 结构体的实例。`Published` 结构体实现了 `State` trait同时对于 `request_review``approve` 两方法来说,它返回自身,因为在这两种情况博文应该保持 `Published` 状态。
现在需要更新 `Post``content` 方法。我们希望 `content` 根据 `Post` 的当前状态返回值,所以需要 `Post` 代理一个定义于 `state` 上的 `content` 方法,如实例 17-17 所示:
现在需要更新 `Post``content` 方法。我们希望 `content` 根据 `Post` 的当前状态返回值,所以需要 `Post` 代理一个定义于 `state` 上的 `content` 方法,如示例 18-17 所示:
<span class="filename">文件名src/lib.rs</span>
@ -133,15 +132,15 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-17/src/lib.rs:here}}
```
<span class="caption">示例 17-17: 更新 `Post``content` 方法来委托调用 `State``content` 方法</span>
<span class="caption">示例 18-17: 更新 `Post``content` 方法来委托调用 `State``content` 方法</span>
因为目标是将所有像这样的规则保持在实现了 `State` 的结构体中,我们将调用 `state` 中的值的 `content` 方法并传递博文实例(也就是 `self`)作为参数。接着返回 `state` 值的 `content` 方法的返回值。
这里调用 `Option``as_ref` 方法是因为需要 `Option` 中值的引用而不是获取其所有权。因为 `state` 是一个 `Option<Box<dyn State>>`,调用 `as_ref` 会返回一个 `Option<&Box<dyn State>>`。如果不调用 `as_ref`,将会得到一个错误,因为不能将 `state` 移动出借用的 `&self` 函数参数。
接着调用 `unwrap` 方法,这里我们知道它永远也不会 panic因为 `Post` 的所有方法都确保在它们返回时 `state` 会有一个 `Some` 值。这就是一个第十二章 [“当我们比编译器知道更多的情况”][more-info-than-rustc] 部分讨论过的我们知道 `None` 是不可能的而编译器却不能理解的情况。
接着调用 `unwrap` 方法,这里我们知道它永远也不会 panic因为 `Post` 的所有方法都确保在它们返回时 `state` 会有一个 `Some` 值。这就是一个第十二章 [“当我们比编译器知道更多的情况”][more-info-than-rustc] 部分讨论过的我们知道 `None` 是不可能的而编译器却不能理解的情况之一
接着我们就有了一个 `&Box<dyn State>`,当调用其 `content` 时,Deref 强制转换会作用于 `&``Box` ,这样最终会调用实现了 `State` trait 的类型的 `content` 方法。这意味着需要为 `State` trait 定义增加 `content`,这也是放置根据所处状态返回什么内容的逻辑的地方,如示例 17-18 所示:
接着我们就有了一个 `&Box<dyn State>`,当调用其 `content` 时,解引用强制转换会作用于 `&``Box` ,这样最终会调用实现了 `State` trait 的类型的 `content` 方法。这意味着需要为 `State` trait 定义增加 `content`,这也是放置根据所处状态返回什么内容的逻辑的地方,如示例 18-18 所示:
<span class="filename">文件名src/lib.rs</span>
@ -149,17 +148,17 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-18/src/lib.rs:here}}
```
<span class="caption">示例 17-18: 为 `State` trait 增加 `content` 方法</span>
<span class="caption">示例 18-18: 为 `State` trait 增加 `content` 方法</span>
这里增加了一个 `content` 方法的默认实现来返回一个空字符串 slice。这意味着无需为 `Draft``PendingReview` 结构体实现 `content` 了。`Published` 结构体会覆盖 `content` 方法并会返回 `post.content` 的值。
这里增加了一个 `content` 方法的默认实现来返回一个空字符串 slice。这意味着无需为 `Draft``PendingReview` 结构体实现 `content` 了。`Published` 结构体会重写 `content` 方法并会返回 `post.content` 的值。
注意这个方法需要生命周期注解,如第十章所讨论的。这里获取 `post` 的引用作为参数,并返回 `post` 一部分的引用,所以返回的引用的生命周期与 `post` 参数相关。
现在示例完成了 —— 现在示例 17-11 中所有的代码都能工作!我们通过发布博文工作流的规则实现了状态模式。围绕这些规则的逻辑都存在于状态对象中而不是分散在 `Post` 之中。
现在示例完成了 —— 现在示例 18-11 中所有的代码都能工作!我们通过发布博文工作流的规则实现了状态模式。围绕这些规则的逻辑都存在于状态对象中而不是分散在 `Post` 之中。
> #### 为什么不用枚举?
>
> 你可能会好奇为什么不用包含不同可能的博文状态的 `enum` 作为变量。这确实是一个可能的方案尝试实现并对比最终结果来看看哪一种更适合你!使用枚举的一个缺点是每一个检查枚举值的地方都需要一个 `match` 表达式或类似的代码来处理所有可能的成员。这相比 trait 对象模式可能显得更重复。
> 你可能会好奇为什么不用包含不同可能的博文状态变体`enum` 作为变量。这确实是一个可能的方案尝试实现并对比最终结果来看看哪一种更适合你!使用枚举的一个缺点是每一个检查枚举值的地方都需要一个 `match` 表达式或类似的代码来处理所有可能的变体。这相比 trait 对象模式可能显得更重复。
### 状态模式的权衡取舍
@ -171,23 +170,23 @@
这个实现易于扩展增加更多功能。为了体会使用此模式维护代码的简洁性,请尝试如下一些建议:
- 增加 `reject` 方法将博文的状态从 `PendingReview` 变回 `Draft`
- 在将状态变为 `Published` 之前要两次 `approve` 调用
- 增加 `reject` 方法将博文的状态从 `PendingReview` 变回 `Draft`
- 在将状态变为 `Published` 之前要两次 `approve` 调用
- 只允许博文处于 `Draft` 状态时增加文本内容。提示:让状态对象负责内容可能发生什么改变,但不负责修改 `Post`
状态模式的一个缺点是因为状态实现了状态之间的转换,一些状态会相互联系。如果在 `PendingReview``Published` 之间增加另一个状态,比如 `Scheduled`,则不得不修改 `PendingReview` 中的代码来转移到 `Scheduled`。如果 `PendingReview` 无需因为新增的状态而改变就更好了,不过这意味着切换到另一种设计模式。
另一个缺点是我们会发现一些重复的逻辑。为了消除它们,可以尝试为 `State` trait 中返回 `self``request_review``approve` 方法增加默认实现,不过这会违反对象安全性,因为 trait 不知道 `self` 具体是什么。我们希望能够将 `State` 作为一个 trait 对象,所以需要其方法是对象安全的。
另一个缺点是我们会发现一些重复的逻辑。为了消除它们,可以尝试为 `State` trait 中返回 `self``request_review``approve` 方法增加默认实现;然而这样做行不通:当将 `State` 用作 trait 对象时trait 并不知道 `self` 具体是什么类型,因此无法在编译时确定返回类型。(这是前面提到的 dyn 兼容性规则之一。)
另一个重复是 `Post``request_review``approve` 这两个类似的实现。它们都委托调用了 `state` 字段中 `Option` 值的同一方法,并在结果中为 `state` 字段设置了新值。如果 `Post` 中的很多方法都遵循这个模式,我们可能会考虑定义一个宏来消除重复(查看第二十章的 [“宏”][macros] 部分)。
另一个重复是 `Post``request_review``approve` 这两个类似的实现。它们都会对 `Post``state` 字段调用 `Option::take`,如果 `state``Some`,就将调用委托给封装值的同名方法,并将返回结果重新赋值给 `state` 字段。如果 `Post` 中的很多方法都遵循这个模式,我们可能会考虑定义一个宏来消除重复(查看第二十章的 [“宏”][macros] 部分)。
完全按照面向对象语言的定义实现这个模式并没有尽可能地利用 Rust 的优势。让我们看看一些代码中可以做出的修改,来将无效的状态和状态转移变为编译时错误。
#### 将状态和行为编码为类型
我们将展示如何稍微反思状态模式来进行一系列不同的权衡取舍。不同于完全封装状态和状态转移使得外部代码对其毫不知情我们将状态编码进不同的类型。如此Rust 的类型检查就会将任何在只能使用发布博文的地方使用草博文的尝试变为编译时错误。
我们将展示如何稍微反思状态模式来进行一系列不同的权衡取舍。不同于完全封装状态和状态转移使得外部代码对其毫不知情我们将状态编码进不同的类型。如此Rust 的类型检查就会将任何在只能使用发布博文的地方使用草稿博文的尝试变为编译时错误。
让我们考虑一下示例 17-11 中 `main` 的第一部分:
让我们考虑一下示例 18-11 中 `main` 的第一部分:
<span class="filename">文件名src/main.rs</span>
@ -195,7 +194,7 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-11/src/main.rs:here}}
```
我们仍然希望能够使用 `Post::new` 创建一个新的草案博文,并能够增加博文的内容。不过不同于存在一个草案博文时返回空字符串的 `content` 方法,我们将使草案博文完全没有 `content` 方法。这样如果尝试获取草案博文的内容,将会得到一个方法不存在的编译错误。这使得我们不可能在生产环境意外显示出草案博文的内容,因为这样的代码甚至就不能编译。示例 17-19 展示了 `Post` 结构体、`DraftPost` 结构体以及各自的方法的定义:
我们仍然希望能够使用 `Post::new` 创建一个新的草稿博文,并能够增加博文的内容。不过不同于存在一个草稿博文时返回空字符串的 `content` 方法,我们将使草稿博文完全没有 `content` 方法。这样如果尝试获取草稿博文的内容,将会得到一个方法不存在的编译错误。这使得我们不可能在生产环境意外显示出草稿博文的内容,因为这样的代码甚至就不能编译。示例 18-19 展示了 `Post` 结构体、`DraftPost` 结构体以及各自的方法的定义:
<span class="filename">文件名src/lib.rs</span>
@ -203,17 +202,17 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-19/src/lib.rs}}
```
<span class="caption">示例 17-19: 带有 `content` 方法的 `Post` 和没有 `content` 方法的 `DraftPost`</span>
<span class="caption">示例 18-19: 带有 `content` 方法的 `Post` 和没有 `content` 方法的 `DraftPost`</span>
`Post``DraftPost` 结构体都有一个私有的 `content` 字段来储存博文的文本。这些结构体不再有 `state` 字段因为我们将状态编码改为结构体类型。`Post` 将代表发布的博文,它有一个返回 `content``content` 方法。
`Post``DraftPost` 结构体都有一个私有的 `content` 字段来储存博文的文本。这些结构体不再有 `state` 字段因为我们将状态编码改为结构体类型本身。`Post` 将代表发布的博文,它有一个返回 `content``content` 方法。
仍然有一个 `Post::new` 函数,不过不同于返回 `Post` 实例,它返回 `DraftPost` 的实例。现在不可能创建一个 `Post` 实例,因为 `content` 是私有的同时没有任何函数返回 `Post`
`DraftPost` 上定义了一个 `add_text` 方法,这样就可以像之前那样向 `content` 增加文本,不过注意 `DraftPost` 并没有定义 `content` 方法!如此现在程序确保了所有博文都从草案开始,同时草案博文没有任何可供展示的内容。任何绕过这些限制的尝试都会产生编译错误。
`DraftPost` 上定义了一个 `add_text` 方法,这样就可以像之前那样向 `content` 增加文本,不过注意 `DraftPost` 并没有定义 `content` 方法!如此现在程序确保了所有博文都从草稿开始,同时草稿博文没有任何可供展示的内容。任何绕过这些限制的尝试都会产生编译错误。
#### 实现状态转移为不同类型的转换
那么如何得到发布的博文呢?我们希望强制执行的规则是草博文在可以发布之前必须被审核通过。等待审核状态的博文应该仍然不会显示任何内容。让我们通过增加另一个结构体 `PendingReviewPost` 来实现这个限制,在 `DraftPost` 上定义 `request_review` 方法来返回 `PendingReviewPost`,并在 `PendingReviewPost` 上定义 `approve` 方法来返回 `Post`,如示例 17-20 所示:
那么如何得到发布的博文呢?我们希望强制执行的规则是草稿博文在可以发布之前必须被审核通过。等待审核状态的博文应该仍然不会显示任何内容。让我们通过增加另一个结构体 `PendingReviewPost` 来实现这个限制,在 `DraftPost` 上定义 `request_review` 方法来返回 `PendingReviewPost`,并在 `PendingReviewPost` 上定义 `approve` 方法来返回 `Post`,如示例 18-20 所示:
<span class="filename">文件名src/lib.rs</span>
@ -221,11 +220,11 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-20/src/lib.rs:here}}
```
<span class="caption">列表 17-20: `PendingReviewPost` 通过调用 `DraftPost``request_review` 创建,`approve` 方法将 `PendingReviewPost` 变为发布的 `Post`</span>
<span class="caption">列表 18-20: `PendingReviewPost` 通过调用 `DraftPost``request_review` 创建,`approve` 方法将 `PendingReviewPost` 变为发布的 `Post`</span>
`request_review``approve` 方法获取 `self` 的所有权,因此会消费 `DraftPost``PendingReviewPost` 实例,并分别转换为 `PendingReviewPost` 和发布的 `Post`。这样在调用 `request_review` 之后就不会遗留任何 `DraftPost` 实例,后者同理。`PendingReviewPost` 并没有定义 `content` 方法,所以尝试读取其内容会导致编译错误,`DraftPost` 同理。因为唯一得到定义了 `content` 方法的 `Post` 实例的途径是调用 `PendingReviewPost``approve` 方法,而得到 `PendingReviewPost` 的唯一办法是调用 `DraftPost``request_review` 方法,现在我们就将发博文的工作流编码进了类型系统。
这也意味着不得不对 `main` 做出一些小的修改。因为 `request_review``approve` 返回新实例而不是修改被调用的结构体,所以我们需要增加更多的 `let post = ` 覆盖赋值来保存返回的实例。也不再能断言草案和等待审核的博文的内容为空字符串了,我们也不再需要它们:不能编译尝试使用这些状态下博文内容的代码。更新后的 `main` 的代码如示例 17-21 所示:
这也意味着不得不对 `main` 做出一些小的修改。因为 `request_review``approve` 返回新实例而不是修改被调用的结构体,所以我们需要增加更多的 `let post = ` 遮蔽赋值来保存返回的实例。也不再能断言草稿和等待审核的博文的内容为空字符串了,我们也不再需要它们:不能编译尝试使用这些状态下博文内容的代码。更新后的 `main` 的代码如示例 18-21 所示。
<span class="filename">文件名src/main.rs</span>
@ -233,19 +232,19 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-21/src/main.rs}}
```
<span class="caption">示例 17-21: `main` 中使用新的博文工作流实现的修改</span>
<span class="caption">示例 18-21: `main` 中使用新的博文工作流实现的修改</span>
不得不修改 `main` 来重新赋值 `post` 使得这个实现不再完全遵守面向对象的状态模式:状态间的转换不再完全封装在 `Post` 实现中。然而,得益于类型系统和编译时类型检查,我们得到的是无效状态是不可能的!这确保了某些特定的 bug比如显示未发布博文的内容将在部署到生产环境之前被发现。
不得不修改 `main` 来重新赋值 `post` 使得这个实现不再完全遵守面向对象的状态模式:状态间的转换不再完全封装在 `Post` 实现中。然而,得益于类型系统和编译时类型检查,我们得到的收获是无效状态是不可能的!这确保了某些特定的 bug比如显示未发布博文的内容将在部署到生产环境之前被发现。
尝试为示例 17-21 之后的 `blog` crate 实现这一部分开始所建议的任务来体会使用这个版本的代码是何感觉。注意在这个设计中一些需求可能已经完成了。
尝试为示例 18-21 之后的 `blog` crate 实现这一部分开始所建议的任务来体会使用这个版本的代码是何感觉。注意在这个设计中一些需求可能已经完成了。
即便 Rust 能够实现面向对象设计模式,也有其他像将状态编码进类型这样的模式存在。这些模式有着不同的权衡取舍。虽然你可能非常熟悉面向对象模式,重新思考这些问题来利用 Rust 提供的像在编译时避免一些 bug 这样有益功能。在 Rust 中面向对象模式并不总是最好的解决方案,因为 Rust 拥有像所有权这样的面向对象语言所没有的功能
我们已经看到,虽然 Rust 能够实现面向对象设计模式,但 Rust 还提供了诸如将状态编码进类型系统之类的其他模式。这些模式有着不同的权衡取舍。虽然你可能非常熟悉面向对象模式,重新思考这些问题来利用 Rust 提供的像在编译时避免一些 bug 这样有益功能。在 Rust 中面向对象模式并不总是最好的解决方案,因为 Rust 拥有像所有权这样的面向对象语言所没有的特性
## 总结
阅读本章后,不管你是否认为 Rust 是一个面向对象语言,现在你都见识了 trait 对象是一个 Rust 中获取部分面向对象功能的方法。动态分发可以通过牺牲少量运行时性能来为你的代码提供一些灵活性。这些灵活性可以用来实现有助于代码可维护性的面向对象模式。Rust 也有像所有权这样不同于面向对象语言的功能。面向对象模式并不总是利用 Rust 优势的最好方式,但也是可用选项。
阅读本章后,不管你是否认为 Rust 是一个面向对象语言,现在你都见识了 trait 对象是一个 Rust 中获取部分面向对象功能的方法。动态分发可以通过牺牲少量运行时性能来为你的代码提供一些灵活性。这些灵活性可以用来实现有助于代码可维护性的面向对象模式。Rust 也有像所有权这样不同于面向对象语言的特性。面向对象模式并不总是利用 Rust 优势的最好方式,但也是一个可用选项。
接下来,让我们看看另一个提供了多样灵活性的 Rust 功能:模式。贯穿全书的模式,我们已经和它们打过照面了,但并没有见识过它们的全部本领。让我们开始探索吧!
接下来,让我们看看另一个提供了多样灵活性的 Rust 功能:模式。我们在全书中已多次简要提及它们,但尚未充分领略它们的全部威力。让我们开始探索吧!
[more-info-than-rustc]: ch09-03-to-panic-or-not-to-panic.html#当我们比编译器知道更多的情况
[macros]: ch20-05-macros.html#宏

Loading…
Cancel
Save