From d4266d4fb5d98508e0d7d7bdf8aad0fc96ba9053 Mon Sep 17 00:00:00 2001 From: KaiserY Date: Sun, 25 May 2025 23:24:51 +0800 Subject: [PATCH 1/3] wip: 2024 edition --- src/ch16-00-concurrency.md | 21 ++++++++++----------- src/ch16-01-threads.md | 31 +++++++++++++++---------------- 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/src/ch16-00-concurrency.md b/src/ch16-00-concurrency.md index 940ad38..3210fab 100644 --- a/src/ch16-00-concurrency.md +++ b/src/ch16-00-concurrency.md @@ -1,20 +1,19 @@ # 无畏并发 -> [ch16-00-concurrency.md](https://github.com/rust-lang/book/blob/main/src/ch16-00-concurrency.md) ->
-> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f + + -安全且高效地处理并发编程是 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 的并发保证扩展到用户定义的以及标准库提供的类型中。 diff --git a/src/ch16-01-threads.md b/src/ch16-01-threads.md index 1c77f46..715337b 100644 --- a/src/ch16-01-threads.md +++ b/src/ch16-01-threads.md @@ -1,10 +1,9 @@ ## 使用线程同时运行代码 -> [ch16-01-threads.md](https://github.com/rust-lang/book/blob/main/src/ch16-01-threads.md) ->
-> commit 8aecae3efe5ca8f79f055b70f05d9a3f990bce7b + + -在大部分现代操作系统中,已执行程序的代码在一个 **进程**(_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}} ``` -示例 16-1: 创建一个打印某些内容的新线程,但是主线程打印其它内容 +示例 16-1: 创建一个打印某些内容的新线程同时主线程打印其它内容 -注意当 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`。`JoinHandle` 是一个拥有所有权的值,当对其调用 `join` 方法时,它会等待其线程结束。示例 16-2 展示了如何使用示例 16-1 中创建的线程的 `JoinHandle` 并调用 `join` 来确保新建线程在 `main` 退出前结束运行。 文件名:src/main.rs @@ -58,9 +57,9 @@ hi number 5 from the spawned thread! {{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-02/src/main.rs}} ``` -示例 16-2: 从 `thread::spawn` 保存一个 `JoinHandle` 以确保该线程能够运行至结束 +示例 16-2: 从 `thread::spawn` 保存一个 `JoinHandle` 以确保该线程能够运行至结束 -通过调用 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 代码的修改,就能按预期编译并运行: 文件名:src/main.rs @@ -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#使用闭包捕获环境 From 941eed2f2466ec78dd367ffd1c2a1fafc026c147 Mon Sep 17 00:00:00 2001 From: kazeno Date: Mon, 26 May 2025 16:54:25 +0800 Subject: [PATCH 2/3] wip: 2024 edition --- src/SUMMARY.md | 2 +- src/ch16-02-message-passing.md | 45 ++++++++--------- src/ch16-03-shared-state.md | 50 +++++++++---------- ...04-extensible-concurrency-sync-and-send.md | 17 +++---- src/ch17-02-concurrency-with-async.md | 2 +- src/ch18-00-oop.md | 9 ++-- 6 files changed, 61 insertions(+), 64 deletions(-) diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 5793201..cc3616f 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -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) diff --git a/src/ch16-02-message-passing.md b/src/ch16-02-message-passing.md index 86aede2..f4b62dc 100644 --- a/src/ch16-02-message-passing.md +++ b/src/ch16-02-message-passing.md @@ -1,16 +1,15 @@ ## 使用消息传递在线程间传送数据 -> [ch16-02-message-passing.md](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 @@ 示例 16-6: 创建一个信道,并将其两端赋值给 `tx` 和 `rx` -这里使用 `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}} ``` -示例 16-7: 将 `tx` 移动到一个新建的线程中并发送 “hi” +示例 16-7: 将 `tx` 移动到一个新建的线程中并发送 `"hi"` -这里再次使用 `thread::spawn` 来创建一个新线程并使用 `move` 将 `tx` 移动到闭包中这样新建线程就拥有 `tx` 了。新建线程需要拥有信道的发送端以便能向信道发送消息。信道的发送端有一个 `send` 方法用来获取需要放入信道的值。`send` 方法返回一个 `Result` 类型,所以如果接收端已经被丢弃了,将没有发送值的目标,所以发送操作会返回错误。在这个例子中,出错的时候调用 `unwrap` 产生 panic。不过对于一个真实程序,需要合理地处理它:回到第九章复习正确处理错误的策略。 +这里再次使用 `thread::spawn` 来创建一个新线程并使用 `move` 将 `tx` 移动到闭包中这样新建线程就拥有 `tx` 了。新建线程需要拥有信道的发送端以便能向信道发送消息。信道的发送端有一个 `send` 方法用来获取需要放入信道的值。`send` 方法返回一个 `Result` 类型,所以如果接收端已经被丢弃了,将没有发送值的目标,所以发送操作会返回错误。在这个例子中,出错的时候调用 `unwrap` 产生 panic。不过在一个真实应用中,需要合理地处理它:回到第九章复习正确处理错误的策略。 -在示例 16-8 中,我们在主线程中从信道的接收者获取值。这类似于在河的下游捞起橡皮鸭或接收聊天信息: +在示例 16-8 中,我们在主线程中从信道的接收端获取值。这类似于在河的下游捞起橡皮鸭或接收聊天信息: 文件名:src/main.rs @@ -48,15 +47,15 @@ {{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-08/src/main.rs}} ``` -示例 16-8: 在主线程中接收并打印内容 “hi” +示例 16-8: 在主线程中接收并打印内容 `"hi"` -信道的接收者有两个有用的方法:`recv` 和 `try_recv`。这里,我们使用了 `recv`,它是 _receive_ 的缩写。这个方法会阻塞主线程执行直到从信道中接收一个值。一旦发送了一个值,`recv` 会在一个 `Result` 中返回它。当信道发送端关闭,`recv` 会返回一个错误表明不会再有新的值到来了。 +信道的接收端有两个有用的方法:`recv` 和 `try_recv`。这里,我们使用了 `recv`,它是 _receive_ 的缩写,这会阻塞主线程执行直到从信道中接收一个值。一旦发送了一个值,`recv` 会在一个 `Result` 中返回它。当信道发送端关闭,`recv` 会返回一个错误表明不会再有新的值到来了。 `try_recv` 不会阻塞,相反它立刻返回一个 `Result`:`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 中的代码并看看为何这是不允许的: 文件名:src/main.rs @@ -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 示例 16-10: 发送多个消息,并在每次发送后暂停一段时间 -这一次,在新建线程中有一个字符串 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 所示: 文件名:src/main.rs @@ -123,9 +122,9 @@ Got: thread 示例 16-11: 从多个生产者发送多个消息 -这一次,在创建新线程之前,我们对发送者调用了 `clone` 方法。这会给我们一个可以传递给第一个新建线程的发送端句柄。我们会将原始的信道发送端传递给第二个新建线程。这样就会有两个线程,每个线程将向信道的接收端发送不同的消息。 +这一次,在创建新线程之前,我们对发送端调用了 `clone` 方法。这会给我们一个可以传递给第一个新建线程的发送端句柄。我们会将原始的信道发送端传递给第二个新建线程。这样就会有两个线程,每个线程将向信道的接收端发送不同的消息。 -如果运行这些代码,你 **可能** 会看到这样的输出: +运行代码时,输出应该看起来类似如下: ```text Got: hi diff --git a/src/ch16-03-shared-state.md b/src/ch16-03-shared-state.md index 6d247be..2ad2122 100644 --- a/src/ch16-03-shared-state.md +++ b/src/ch16-03-shared-state.md @@ -1,27 +1,26 @@ ## 共享状态的并发 -> [ch16-03-shared-state.md](https://github.com/rust-lang/book/blob/main/src/ch16-03-shared-state.md) ->
-> commit 856d89c53a6d69470bb5669c773fdfe6aab6fcc9 + + -消息传递是一个很好的处理并发的方式,但并不是唯一一个。另一种方式是让多个线程访问同一块内存中的数据(共享状态)。再考虑一下 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`的 API @@ -39,15 +38,15 @@ 如果另一个线程拥有锁,并且那个线程 panic 了,则 `lock` 调用会失败。在这种情况下,没人能够再获取锁,所以我们调用 `unwrap`,使当前线程 panic。 -一旦获取了锁,就可以将返回值(命名为 `num`)视为一个其内部数据(`i32`)的可变引用了。类型系统确保了我们在使用 `m` 中的值之前获取锁。`m` 的类型是 `Mutex` 而不是 `i32`,所以 **必须** 获取锁才能使用这个 `i32` 值。我们是不会忘记这么做的,因为如果没有获取锁,类型系统就不允许访问内部的 `i32` 值。 +一旦获取了锁,就可以将返回值(命名为 `num`)视为一个其内部数据的可变引用了。类型系统确保了我们在使用 `m` 中的值之前获取锁。`m` 的类型是 `Mutex` 而不是 `i32`,所以**必须**调用 `lock` 才能使用这个 `i32` 值。我们不能忘记这么做的;因为如果没有获取锁,类型系统就不允许访问内部的 `i32` 值。 -正如你所猜想的,`Mutex` 是一个智能指针。更准确的说,`lock` 调用 **返回** 一个叫做 `MutexGuard` 的智能指针。这个智能指针实现了 `Deref` 来指向其内部数据;它也实现了 `Drop`,当 `MutexGuard` 离开作用域时,自动释放锁(发生在示例 16-12 内部作用域的结尾)。有了这个特性,就不会有忘记释放锁的潜在风险(忘记释放锁会使互斥器无法再被其它线程使用),因为锁的释放是自动发生的。 +正如你所猜想的,`Mutex` 是一个智能指针。更准确的说,`lock` 调用会**返回**一个叫做 `MutexGuard` 的智能指针。`MutexGuard` 智能指针实现了 `Deref` 来指向其内部数据;它也实现了 `Drop`,当 `MutexGuard` 离开作用域时,自动释放锁(发生在示例 16-12 内部作用域的结尾)。这样一来,就不会有忘记释放锁从而导致互斥器阻塞无法被其他线程使用的潜在风险,因为锁的释放是自动发生的。 释放锁之后,我们可以打印出互斥器内部的 `i32` 值,并发现我们刚刚已经将其值改为 6。 -#### 在线程间共享 `Mutex` +#### 在多个线程间共享 `Mutex` -现在让我们尝试使用 `Mutex` 在多个线程间共享同一个值。我们将启动 10 个线程,并在各个线程中对同一个计数器值加 1,这样计数器将从 0 变为 10。示例 16-13 中的例子会出现编译错误,而我们将通过这些错误来学习如何使用 `Mutex`,以及 Rust 又是如何帮助我们正确使用的。 +现在让我们尝试使用 `Mutex` 在多个线程间共享同一个值。我们将启动 10 个线程,并在各个线程中对同一个计数器值加 1,这样计数器将从 0 累加到 10。示例 16-13 中的例子会出现编译错误,而我们将通过这些错误来学习如何使用 `Mutex`,以及 Rust 又是如何帮助我们正确使用它的。 文件名:src/main.rs @@ -57,11 +56,11 @@ 示例 16-13: 程序启动了 10 个线程,每个线程都通过 `Mutex` 来增加计数器的值 -这里创建了一个 `counter` 变量来存放内含 `i32` 的 `Mutex`,类似示例 16-12 那样。接下来我们遍历整数区间,创建了 10 个线程。我们使用了 `thread::spawn`,并为所有线程传入了相同的闭包:它们每一个都将调用 `lock` 方法来获取 `Mutex` 上的锁,接着将互斥器中的值加一。当一个线程结束执行,`num` 会离开闭包作用域并释放锁,这样另一个线程就可以获取它了。 +这里创建了一个 `counter` 变量来存放内含 `i32` 的 `Mutex`,类似示例 16-12 那样。接下来我们遍历一个整数 range 来创建了 10 个线程。我们使用了 `thread::spawn`,并为所有线程传入了相同的闭包:它们每一个都将计数器移动进线程,调用 `lock` 方法来获取 `Mutex` 上的锁,接着将互斥器中的值加一。当一个线程结束执行,`num` 会离开闭包作用域并释放锁,这样另一个线程就可以获取它了。 在主线程中,我们像示例 16-2 那样收集了所有的 `JoinHandle`,并调用它们的 `join` 方法来等待所有线程结束。然后,主线程会获取锁,并打印出程序的结果。 -之前提示过,这个例子不能编译,让我们看看为什么! +之前提示过,这个例子不能编译。现在让我们看看为什么! ```console {{#include ../listings/ch16-fearless-concurrency/listing-16-13/output.txt}} @@ -71,7 +70,7 @@ #### 多线程和多所有权 -在第 15 章中,我们用智能指针 `Rc` 来创建引用计数,使得一个值有了多个所有者。让我们做同样的事,看看会发生什么。将示例 16-14 中的 `Mutex` 封装进 `Rc` 中,并在将所有权移入线程之前克隆(clone) `Rc`。 +在第 15 章中,我们用智能指针 `Rc` 来创建引用计数,使得一个值有了多个所有者。让我们做同样的事,看看会发生什么。将示例 16-14 中的 `Mutex` 封装进 `Rc` 中,并在将所有权移入线程之前克隆(clone)`Rc`。 文件名:src/main.rs @@ -87,17 +86,18 @@ {{#include ../listings/ch16-fearless-concurrency/listing-16-14/output.txt}} ``` -哇哦,错误信息太长不看!划重点:第一行错误表明 `Rc>` 不能在线程间安全传递(`` `Rc>` cannot be sent between threads safely ``);编译器也指出了原因:`Rc>` 没有实现 `Send` trait(`` the trait `Send` is not implemented for `Rc>` ``)。下一节我们会讲到 `Send`:这是一个确保所使用的类型可以用于并发环境的 trait。 +哇哦,错误信息太长不看!下面是重点要关注的内容:`` `Rc>` cannot be sent between threads safely ``。编译器也指出了原因:`` the trait `Send` is not implemented for `Rc>` ``。下一节我们会讲到 `Send`:这是一个确保所使用的类型可以用于并发环境的 trait。 不幸的是,`Rc` 并不能安全的在线程间共享。当 `Rc` 管理引用计数时,它必须在每一个 `clone` 调用时增加计数,并在每一个克隆体被丢弃时减少计数。`Rc` 并没有使用任何并发原语,无法确保改变计数的操作不会被其他线程打断。这可能使计数出错,并导致诡异的 bug,比如可能会造成内存泄漏,或在使用结束之前就丢弃一个值。我们所需要的是一个与 `Rc` 完全一致,又以线程安全的方式改变引用计数的类型。 #### 原子引用计数 `Arc` -所幸 `Arc` 正是这么一个类似 `Rc` 并可以安全的用于并发环境的类型。字母 “a” 代表 **原子性**(_atomic_),所以这是一个 **原子引用计数**(_atomically reference counted_)类型。**原子类型** (Atomics) 是另一类这里还未涉及到的并发原语:请查看标准库中 [`std::sync::atomic`][atomic] 的文档来获取更多细节。目前我们只需要知道:原子类型就像基本类型一样,可以安全地在线程间共享。 +所幸 `Arc` 正是这么一个类似 `Rc` 并可以安全地用于并发环境的类型。字母 _a_ 代表 **原子性**(_atomic_),所以这是一个**原子引用计数**(_atomically reference counted_)类型。**原子类型** (Atomics) 是另一类这里还未涉及到的并发原语:请查看标准库中 [`std::sync::atomic`][atomic] 的文档来获取更多细节。目前我们只需要知道:原子类型就像基本类型一样,可以安全地在线程间共享。 -你可能会好奇,为什么不是所有的基本类型都是原子性的?为什么标准库中的类型没有全部默认使用 `Arc` 实现?原因在于,线程安全会造成性能损失,我们希望只在必要时才为此买单。如果只是在单线程中对值进行操作,原子性提供的保证并无必要,而不加入原子性可以使代码运行得更快。 +你可能会好奇,为什么不是所有的基本类型都是原子性的?为什么标准库中的类型没有全部默认使用 `Arc` 实现?原因在于,线程安全会造成性能损失,我们希望只在必要时才为此买单。如果只是在单线程中对值进行操作,如果不必强制原子性所提供的保证可以使代码运行得更快。 -回到之前的例子:`Arc` 和 `Rc` 有着相同的 API,所以我们只需修改程序中的 `use` 行、`new` 调用和 `clone` 调用。示例 16-15 中的代码最终可以编译和运行: + +回到之前的例子:`Arc` 和 `Rc` 有着相同的 API,所以我们只需修改程序中的 `use` 行、`new` 调用和 `clone` 调用。示例 16-15 中的代码最终可以编译并运行。 文件名:src/main.rs @@ -107,13 +107,13 @@ 示例 16-15: 使用 `Arc` 包装一个 `Mutex` 能够实现在多线程之间共享所有权 -这会打印出: +这段代码会打印出如下: ```text Result: 10 ``` -成功了!我们从 0 数到了 10,这好像没啥大不了的,不过一路上我们确实学习了很多关于 `Mutex` 和线程安全的内容!这个例子中构建的结构可以用于比增加计数更为复杂的操作。使用这个策略,我们可将计算任务分成独立的部分,并分散到多个线程中,接着使用 `Mutex` 使用各自的运算结果来更新最终的结果。 +成功了!我们从 0 数到了 10,这好像没啥大不了的,不过一路上我们确实学习了很多关于 `Mutex` 和线程安全的知识!这个例子中构建的结构可以用于比增加计数更为复杂的操作。使用这个策略,我们可将计算任务分成独立的部分,并分散到多个线程中,接着使用 `Mutex` 使用各自的运算结果来更新最终的结果。 注意,对于简单的数值运算,[标准库中 `std::sync::atomic` 模块][atomic] 提供了比 `Mutex` 更简单的类型。针对基本类型,这些类型提供了安全、并发、原子的操作。在上面的例子中,为了专注于讲明白 `Mutex` 的用法,我们才选择在基本类型上使用 `Mutex`。(译注:对于上面例子中出现的 `i32` 加法操作,更好的做法是使用 `AtomicI32` 类型来完成。具体参考文档。) @@ -121,8 +121,8 @@ Result: 10 你可能注意到了,尽管 `counter` 是不可变的,我们仍然可以获取其内部值的可变引用;这意味着 `Mutex` 提供了内部可变性,就像 `Cell` 系列类型那样。使用 `RefCell` 可以改变 `Rc` 中内容(在 15 章中讲到过),同样地,使用 `Mutex` 我们也可以改变 `Arc` 中的内容。 -另一个值得注意的细节是,Rust 不能完全避免使用 `Mutex` 所带来的逻辑错误。回忆一下,第 15 章中讲过,使用 `Rc` 就有造成引用循环的风险:两个 `Rc` 值相互引用,造成内存泄漏。同理,`Mutex` 也有造成 **死锁**(_deadlock_)的风险:当某个操作需要锁住两个资源,而两个线程分别持有两个资源的其中一个锁时,它们会永远相互等待。如果你对这个话题感兴趣,尝试编写一个带有死锁的 Rust 程序,接着研究别的语言中使用互斥器的死锁规避策略,并尝试在 Rust 中实现它们。标准库中 `Mutex` 和 `MutexGuard` 的 API 文档会提供有用的信息。 +另一个值得注意的细节是,Rust 不能完全避免使用 `Mutex` 所带来的逻辑错误。回忆一下,第 15 章中讲过,使用 `Rc` 就有造成引用循环的风险:两个 `Rc` 值相互引用,造成内存泄漏。同理,`Mutex` 也有造成**死锁**(_deadlock_)的风险:当某个操作需要锁住两个资源,而两个线程分别持有两个资源的其中一个锁时,它们会永远相互等待。如果你对这个话题感兴趣,尝试编写一个带有死锁的 Rust 程序,接着研究其它语言中使用互斥器的死锁规避策略,并尝试在 Rust 中实现它们。标准库中 `Mutex` 和 `MutexGuard` 的 API 文档会提供有用的信息。 -接下来,为了丰富本章的内容,让我们讨论一下 `Send`和 `Sync` trait,以及如何对自定义类型使用它们。 +作为本章的最后,我们将讨论 `Send` 和 `Sync` trait,以及如何将它们用于自定义类型。 [atomic]: https://doc.rust-lang.org/std/sync/atomic/index.html diff --git a/src/ch16-04-extensible-concurrency-sync-and-send.md b/src/ch16-04-extensible-concurrency-sync-and-send.md index a814ef1..9cf4b46 100644 --- a/src/ch16-04-extensible-concurrency-sync-and-send.md +++ b/src/ch16-04-extensible-concurrency-sync-and-send.md @@ -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) ->
-> commit 56ec353290429e6547109e88afea4de027b0f1a9 + + -Rust 的并发模型中一个有趣的方面是:我们之前讨论的几乎所有内容,都属于标准库,而不是语言本身的内容。由于不需要语言提供并发相关的基础设施,并发方案不受标准库或语言所限:我们可以编写自己的或使用别人编写的并发功能。 +Rust 的并发模型中一个有趣的方面是:我们之前讨论的几乎所有内容,都属于标准库,而不是语言本身的内容。处理并发的方案并不受标准库或语言所限:我们可以编写自己的或使用他人编写的并发特性。 -然而,在内嵌于语言而非标准库的关键并发概念中,有属于 `std::marker` 的 `Send` 和 `Sync` trait。 +然而,有一些关键的并发概念是内嵌于语言本身而非标准库的,其中就包括 `std::marker` 的 `Send` 和 `Sync` trait。 ### 通过 `Send` 允许在线程间转移所有权 `Send` 标记 trait 表明实现了 `Send` 的类型值的所有权可以在线程间传送。几乎所有的 Rust 类型都是`Send` 的,不过有一些例外,包括 `Rc`:这是不能实现 `Send` 的,因为如果克隆了 `Rc` 的值并尝试将克隆的所有权转移到另一个线程,这两个线程都可能同时更新引用计数。为此,`Rc` 被实现为用于单线程场景,这时不需要为拥有线程安全的引用计数而付出性能代价。 -因此,Rust 类型系统和 trait bound 确保永远也不会意外的将不安全的 `Rc` 在线程间发送。当尝试在示例 16-14 中这么做的时候,会得到错误 `the trait Send is not implemented for Rc>`。而使用实现了 `Send` 的 `Arc` 时,就没有问题了。 +因此,Rust 类型系统和 trait bound 确保永远也不会意外的将不安全的 `Rc` 在线程间发送。当尝试在示例 16-14 中这么做的时候,会得到错误 `the trait Send is not implemented for Rc>`。而使用实现了 `Send` 的 `Arc` 时,代码就能成功编译。 任何完全由 `Send` 的类型组成的类型也会自动被标记为 `Send`。几乎所有基本类型都是 `Send` 的,除了第二十章将会讨论的裸指针(raw pointer)。 @@ -20,7 +19,7 @@ Rust 的并发模型中一个有趣的方面是:我们之前讨论的几乎所 `Sync` 标记 trait 表明一个实现了 `Sync` 的类型可以安全的在多个线程中拥有其值的引用。换一种方式来说,对于任意类型 `T`,如果 `&T`(`T` 的不可变引用)实现了 `Send` 的话 `T` 就实现了 `Sync`,这意味着其引用就可以安全的发送到另一个线程。类似于 `Send` 的情况,基本类型都实现了 `Sync`,完全由实现了 `Sync` 的类型组成的类型也实现了 `Sync`。 -智能指针 `Rc` 也没有实现 `Sync`,出于其没有实现 `Send` 相同的原因。`RefCell`(第十五章讨论过)和 `Cell` 系列类型没有实现 `Sync`。`RefCell` 在运行时所进行的借用检查也不是线程安全的。`Mutex` 实现了 `Sync`,正如 [“在线程间共享 `Mutex`”][sharing-a-mutext-between-multiple-threads] 部分所讲的它可以被用来在多线程中共享访问。 +智能指针 `Rc` 也没有实现 `Sync`,出于其没有实现 `Send` 相同的原因。`RefCell`(第十五章讨论过)和 `Cell` 系列类型没有实现 `Sync`。`RefCell` 在运行时所进行的借用检查也不是线程安全的。`Mutex` 实现了 `Sync`,正如 [“在多个线程间共享 `Mutex`”][sharing-a-mutext-between-multiple-threads] 部分所讲的它可以被用来在多线程中共享访问。 ### 手动实现 `Send` 和 `Sync` 是不安全的 @@ -34,7 +33,7 @@ Rust 的并发模型中一个有趣的方面是:我们之前讨论的几乎所 正如之前提到的,因为 Rust 本身很少有处理并发的部分内容,有很多的并发方案都由 crate 实现。它们比标准库要发展的更快;请在网上搜索当前最新的用于多线程场景的 crate。 -Rust 提供了用于消息传递的信道,和像 `Mutex` 和 `Arc` 这样可以安全的用于并发上下文的智能指针。类型系统和借用检查器会确保这些场景中的代码,不会出现数据竞争和无效的引用。一旦代码可以编译了,我们就可以坚信这些代码可以正确的运行于多线程环境,而不会出现其他语言中经常出现的那些难以追踪的 bug。并发编程不再是什么可怕的概念:无所畏惧地并发吧! +Rust 提供了用于消息传递的信道,和像 `Mutex` 和 `Arc` 这样可以安全的用于并发上下文的智能指针。类型系统和借用检查器会确保这些场景中的代码,不会出现数据竞争和无效的引用。一旦代码可以编译了,我们就可以坚信这些代码可以正确地运行于多线程环境,而不会出现其他语言中经常出现的那些难以追踪的 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 diff --git a/src/ch17-02-concurrency-with-async.md b/src/ch17-02-concurrency-with-async.md index c6ea220..44cde4a 100644 --- a/src/ch17-02-concurrency-with-async.md +++ b/src/ch17-02-concurrency-with-async.md @@ -243,7 +243,7 @@ hi number 9 from the first task! {{#rustdoc_include ../listings/ch17-async-await/listing-17-13/src/main.rs:here}} ``` -
示例 17-13:通过多个异步代码块使用多个发送者
+
示例 17-13:通过多个异步代码块使用多个发送端
diff --git a/src/ch18-00-oop.md b/src/ch18-00-oop.md index 604df6d..0fa401f 100644 --- a/src/ch18-00-oop.md +++ b/src/ch18-00-oop.md @@ -1,7 +1,6 @@ -# Rust 的面向对象特性 +# 面向对象编程特性 -> [ch18-00-oop.md](https://github.com/rust-lang/book/blob/main/src/ch18-00-oop.md) ->
-> commit 398d6f48d2e6b7b15efd51c4541d446e89de3892 + + -面向对象编程(Object-Oriented Programming,OOP)是一种对程序进行建模方式。对象(Object)作为一个编程概念来源于 20 世纪 60 年代的 Simula 编程语言。这些对象影响了 Alan Kay 的编程架构,该架构中对象之间互相传递消息。他在 1967 年创造了 **面向对象编程** (*object-oriented programming*)这个术语。关于 OOP 是什么有很多相互矛盾的定义;在一些定义下,Rust 是面向对象的;在其他定义下,Rust 不是。在本章节中,我们会探索一些被普遍认为是面向对象的特性和这些特性是如何体现在 Rust 语言习惯中的。接着会展示如何在 Rust 中实现面向对象设计模式,并讨论这么做与利用 Rust 自身的一些优势实现的方案相比有什么取舍。 +面向对象编程(Object-Oriented Programming,OOP)是一种对程序进行建模方式。对象(Object)作为一个编程概念来源于 20 世纪 60 年代的 Simula 编程语言。这些对象影响了 Alan Kay 的编程架构,该架构中对象之间互相传递消息。他于 1967 年创造了**面向对象编程**(*object-oriented programming*)这一术语。对于 OOP 的定义众说纷纭;在一些定义下,Rust 是面向对象的;在其他定义下,Rust 不是。在本章节中,我们会探索一些被普遍认为是面向对象的特性和这些特性是如何体现在 Rust 语言习惯中的。接着会展示如何在 Rust 中实现面向对象设计模式,并讨论这么做与利用 Rust 自身的一些优势实现的方案相比有什么取舍。 From 2bb698268d360849ad5fae8287725a802a97ed8f Mon Sep 17 00:00:00 2001 From: KaiserY Date: Mon, 26 May 2025 23:40:22 +0800 Subject: [PATCH 3/3] wip: 2024 edition --- src/ch18-01-what-is-oo.md | 36 ++++----- src/ch18-02-trait-objects.md | 62 ++++++++-------- src/ch18-03-oo-design-patterns.md | 119 +++++++++++++++--------------- 3 files changed, 107 insertions(+), 110 deletions(-) diff --git a/src/ch18-01-what-is-oo.md b/src/ch18-01-what-is-oo.md index 228fd34..1f0cdb1 100644 --- a/src/ch18-01-what-is-oo.md +++ b/src/ch18-01-what-is-oo.md @@ -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) ->
-> commit 398d6f48d2e6b7b15efd51c4541d446e89de3892 + + 关于一门语言必须具备哪些特征才能被视为面向对象,目前在编程社区中并没有共识。Rust 受到了许多编程范式的影响,包括面向对象编程(OOP);例如,在第 13 章中,我们探讨了来自函数式编程的特性。可以说,面向对象的语言共有一些共同的特征,即对象、封装和继承。我们将会讨论这些特征分别是什么,以及 Rust 是否支持它们。 ### 对象包含数据和行为 -由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides(Addison-Wesley Professional, 1994)编写的书 *Design Patterns: Elements of Reusable Object-Oriented Software* ,通称 *The Gang of Four* (“四人帮”),是一本面向对象设计模式的目录。它这样定义面向对象编程: +由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides(Addison-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` 的字段。该结构体还可以有一个字段存储其平均值,以便需要时取用。示例 17-1 给出了 `AveragedCollection` 结构体的定义: +我们在第七章讨论了如何控制封装:我们可以使用 `pub` 关键字来决定代码中的哪些模块、类型、函数和方法是公有的,而默认情况下其他所有内容都是私有的。例如,我们可以定义一个 `AveragedCollection` 结构体,其中有一个存有 `Vec` 的字段。该结构体还可以有一个字段存储向量中值的平均值,从而无需在每次需要时重新计算。换句话说,`AveragedCollection` 会为我们缓存已计算的平均值。示例 18-1 给出了 `AveragedCollection` 结构体的定义: 文件名:src/lib.rs @@ -30,9 +29,9 @@ {{#rustdoc_include ../listings/ch18-oop/listing-18-01/src/lib.rs}} ``` -示例 17-1: `AveragedCollection` 结构体维护了一个整型列表及其所有元素的平均值。 +示例 18-1: `AveragedCollection` 结构体维护了一个整型列表及其所有元素的平均值。 -该结构体被标记为 `pub`,这样其他代码就可以使用它,但结构体内的字段保持私有。这在这种情况下很重要,因为我们想确保每当列表中添加或删除值时,平均值也会更新。我们通过实现结构体上的 `add`、`remove` 和 `average` 方法来做到这一点,如示例 17-2 所示: +该结构体被标记为 `pub`,这样其他代码就可以使用它,但结构体内的字段仍保持私有。这在这种情况下很重要,因为我们想确保每当列表中添加或删除值时,平均值也会更新。我们通过实现结构体上的 `add`、`remove` 和 `average` 方法来做到这一点,如示例 18-2 所示: 文件名:src/lib.rs @@ -40,17 +39,17 @@ {{#rustdoc_include ../listings/ch18-oop/listing-18-02/src/lib.rs:here}} ``` -示例 17-2: 在 `AveragedCollection` 结构体上实现了 `add`、`remove` 和 `average` 公有方法 +示例 18-2: 在 `AveragedCollection` 结构体上实现了 `add`、`remove` 和 `average` 公有方法 -公有方法 `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` 代替 `Vec` 作为 `list` 字段的类型。只要 `add`、`remove` 和 `average` 这些公有方法的签名保持不变,使用 `AveragedCollection` 的代码就无需改变。如果我们将 `list` 设为公有,情况就未必如此: `HashSet` 和 `Vec` 使用不同的方法增加或移除项,所以如果外部代码直接修改 `list` ,很可能需要进行更改。 +因为我们已经封装了 `AveragedCollection` 的实现细节,改动数据结构等内部实现非常简单。例如,可以使用 `HashSet` 代替 `Vec` 作为 `list` 字段的类型。只要 `add`、`remove` 和 `average` 这些公有方法的签名保持不变,使用 `AveragedCollection` 的代码就无需改变。如果我们将 `list` 设为公有,情况就未必如此:`HashSet` 和 `Vec` 使用不同的方法增加或移除项,所以如果外部代码直接修改 `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 对象实现多态性。 diff --git a/src/ch18-02-trait-objects.md b/src/ch18-02-trait-objects.md index 06340b2..d82c44a 100644 --- a/src/ch18-02-trait-objects.md +++ b/src/ch18-02-trait-objects.md @@ -1,24 +1,23 @@ ## 顾及不同类型值的 trait 对象 -> [ch18-02-trait-objects.md](https://github.com/rust-lang/book/blob/main/src/ch18-02-trait-objects.md) ->
-> commit 96d4b0ec1c5e019b85604c33ceee68b3e2669d40 + + -在第八章中,我们谈到了 vector 只能存储同种类型元素的局限。示例 8-9 中提供了一个替代方案,通过定义 `SpreadsheetCell` 枚举,来储存整型、浮点型或文本类型的成员。这意味着,我们可以在每个单元中储存不同类型的数据,并仍能拥有一个代表一排单元的 vector。只要我们需存储的值由一组固定的类型组成,并且在代码编译时就知道具体会有哪些类型,那么这种使用枚举的办法是完全可行的。 +在第八章中,我们谈到了 vector 只能存储同种类型元素的局限性。示例 8-9 中提供了一个替代方案,通过定义 `SpreadsheetCell` 枚举,来储存整型、浮点型或文本类型的变体。这意味着,我们可以在每个单元中储存不同类型的数据,并仍能拥有一个代表一排单元的 vector。只要我们需存储的值由一组固定的类型组成,并且在代码编译时就知道具体会有哪些类型,那么这种使用枚举的办法是完全可行的。 然而有时我们希望库用户在特定情况下能够扩展有效的类型集合。为了展示如何实现这一点,这里将创建一个图形用户接口(Graphical User Interface,GUI)工具的例子,它通过遍历列表并调用每一个项目的 `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` 智能指针,还有 `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` 智能指针,还有 `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`: 文件名:src/lib.rs @@ -26,9 +25,9 @@ {{#rustdoc_include ../listings/ch18-oop/listing-18-03/src/lib.rs}} ``` -示例 17-3:`Draw` trait 的定义 +示例 18-3:`Draw` trait 的定义 -因为第十章已经讨论过如何定义 trait,其语法看起来应该比较眼熟。接下来就是新内容了:示例 17-4 定义了一个存放了名叫 `components` 的 vector 的结构体 `Screen`。这个 vector 的类型是 `Box`,此为一个 trait 对象:它是 `Box` 中任何实现了 `Draw` trait 的类型的替身。 +因为第十章已经讨论过如何定义 trait,其语法看起来应该比较眼熟。接下来就是一些新语法:示例 18-4 定义了一个存放了名叫 `components` 的 vector 的结构体 `Screen`。这个 vector 的类型是 `Box`,此为一个 trait 对象:它是 `Box` 中任何实现了 `Draw` trait 的类型的替身。 文件名:src/lib.rs @@ -36,9 +35,9 @@ {{#rustdoc_include ../listings/ch18-oop/listing-18-04/src/lib.rs:here}} ``` -示例 17-4: 一个 `Screen` 结构体的定义,它带有一个字段 `components`,其包含实现了 `Draw` trait 的 trait 对象的 vector +示例 18-4: 一个 `Screen` 结构体的定义,它带有一个字段 `components`,其包含实现了 `Draw` trait 的 trait 对象的 vector -在 `Screen` 结构体上,我们将定义一个 `run` 方法,该方法会对其 `components` 上的每一个组件调用 `draw` 方法,如示例 17-5 所示: +在 `Screen` 结构体上,我们将定义一个 `run` 方法,该方法会对其 `components` 上的每一个组件调用 `draw` 方法,如示例 18-5 所示: 文件名:src/lib.rs @@ -46,9 +45,9 @@ {{#rustdoc_include ../listings/ch18-oop/listing-18-05/src/lib.rs:here}} ``` -示例 17-5:在 `Screen` 上实现一个 `run` 方法,该方法在每个 component 上调用 `draw` 方法 +示例 18-5:在 `Screen` 上实现一个 `run` 方法,该方法在每个 component 上调用 `draw` 方法 -这与定义使用了带有 trait bound 的泛型类型参数的结构体不同。泛型类型参数一次只能替代一个具体类型,而 trait 对象则允许在运行时替代多种具体类型。例如,可以定义 `Screen` 结构体来使用泛型和 trait bound,如示例 17-6 所示: +这与定义使用了带有 trait bound 的泛型类型参数的结构体不同。泛型类型参数一次只能替代一个具体类型,而 trait 对象则允许在运行时替代多种具体类型。例如,可以定义 `Screen` 结构体来使用泛型和 trait bound,如示例 18-6 所示: 文件名:src/lib.rs @@ -56,15 +55,15 @@ {{#rustdoc_include ../listings/ch18-oop/listing-18-06/src/lib.rs:here}} ``` -示例 17-6: 一种 `Screen` 结构体的替代实现,其 `run` 方法使用泛型和 trait bound +示例 18-6: 一种 `Screen` 结构体的替代实现,其 `run` 方法使用泛型和 trait bound -这限制了 `Screen` 实例必须拥有一个全是 `Button` 类型或者全是 `TextField` 类型的组件列表。如果只需要同质(相同类型)集合,则倾向于使用泛型和 trait bound,因为其定义会在编译时采用具体类型进行单态化。 +这限制了 `Screen` 实例必须拥有一个全是 `Button` 类型或者全是 `TextField` 类型的组件列表。如果只需要同质(相同类型,homogeneous)集合,则倾向于使用泛型和 trait bound,因为其定义会在编译时采用具体类型进行单态化(monomorphized)。 另一方面,通过使用 trait 对象的方法,一个 `Screen` 实例可以存放一个既能包含 `Box