check to ch17-03

This commit is contained in:
KaiserY 2018-02-26 16:52:45 +08:00
parent 8db8b03185
commit befecd54a9
2 changed files with 200 additions and 152 deletions

View File

@ -2,28 +2,27 @@
> [ch17-02-trait-objects.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch17-02-trait-objects.md)
> <br>
> commit 67876e3ef5323ce9d394f3ea6b08cb3d173d9ba9
> commit ccdd9ca7aacea4cefeb6a96e7ffb9ea91a923abd
在第八章中,我们谈到了 vector 只能存储同种类型元素的局限。在列表 8-1 中有一个例子,其中定义了一个拥有分别存放整型、浮点型和文本型成员的枚举类型 `SpreadsheetCell`,使用这个枚举的 vector 可以在每一个单元格cell中储存不同类型的数据并使得 vector 整体仍然代表一行row单元格。这当编译代码时就知道希望可以交替使用的类型为固定集合的情况下是可行的。
在第八章中,我们谈到了 vector 只能存储同种类型元素的局限。示例 8-10 中提供了一个定义 `SpreadsheetCell` 枚举来储存整型,浮点型和文本成员的替代方案。这意味着可以在每个单元中储存不同类型的数据,并仍能拥有一个代表一排单元的 vector。这在当编译代码时就知道希望可以交替使用的类型为固定集合的情况下是完全可行的。
然而有时我们希望库用户在特定情况下能够扩展有效的类型集合。为了展示如何实现这一点这里将创建一个图形用户接口Graphical User Interface GUI工具的例子其它通过遍历列表并调用每一个项目的 `draw` 方法来将其绘制到屏幕上;此乃一个 GUI 工具的常见技术。我们将要创建一个叫做 `rust_gui` 的库 crate它含一个 GUI 库的结构。这个 GUI 库包含一些可供开发者使用的类型,比如 `Button``TextField`。在此之上,`rust_gui` 的用户希望创建自定义的可以绘制于屏幕上的类型:比如,一个程序员可能会增加 `Image`,另一个可能会增加 `SelectBox`
<!-- The code example I want to reference did not have a listing number; it's
the one with SpreadsheetCell. I will go back and add Listing 8-1 next time I
get Chapter 8 for editing. /Carol -->
这个例子中并不会实现一个功能完善的 GUI 库,不过会展示其中各个部分是如何结合在一起的。编写库的时候,我们不可能知晓并定义所有其他程序员希望创建的类型。我们所知晓的是 `rust_gui` 需要记录一系列不同类型的值,并需要能够对其中每一个值调用 `draw` 方法。这里无需知道调用 `draw` 方法时具体会发生什么,只需提供可供这些值调用的方法即可。
有时我们希望使用的类型的集合对于使用库的程序员来说是可扩展的。例如很多图形用户接口GUI工具有一个项目列表的概念它通过遍历列表并调用每一个项目的 `draw` 方法来将其绘制到屏幕上。我们将要创建一个叫做 `rust_gui` 的库 crate它含一个 GUI 库的结构。这个 GUI 库包含一些可供开发者使用的类型,比如 `Button``TextField`。使用 `rust_gui` 的程序员会想要创建更多可以绘制在屏幕上的类型:其中一些可能会增加一个 `Image`,而另一些可能会增加一个 `SelectBox`。本章节并不准备实现一个功能完善的 GUI 库,不过会展示其中各个部分是如何结合在一起的。
编写 `rust_gui` 库时,我们并不知道其他程序员想要创建的全部类型,所以无法定义一个 `enum` 来包含所有这些类型。我们所要做的是使 `rust_gui` 能够记录一系列不同类型的值,并能够对其中每一个值调用 `draw` 方法。 GUI 库不需要知道当调用 `draw` 方法时具体会发生什么,只需提供这些值可供调用的方法即可。
在拥有继承的语言中,我们可能定义一个名为 `Component` 的类,该类上有一个 `draw` 方法。其他的类比如 `Button`、`Image` 和 `SelectBox` 会从 `Component` 派生并因此继承 `draw` 方法。它们各自都可以覆盖 `draw` 方法来定义自己的行为,但是框架会把所有这些类型当作是 `Component` 的实例,并在其上调用 `draw`
在拥有继承的语言中,可以定义一个名为 `Component` 的类,该类上有一个 `draw` 方法。其他的类比如 `Button`、`Image` 和 `SelectBox` 会从 `Component` 派生并因此继承 `draw` 方法。它们各自都可以覆盖 `draw` 方法来定义自己的行为,但是框架会把所有这些类型当作是 `Component` 的实例,并在其上调用 `draw`。不过 Rust 并没有继承,我们得另寻出路。
### 定义通用行为的 trait
不过,在 Rust 中,我们可以定义一个 `Draw` trait包含名为 `draw` 的方法。接着可以定义一个存放**trait 对象***trait
object*)的 vectortrait 对象是一个位于某些指针,比如 `&` 引用或 `Box<T>` 智能指针,之后的 trait。第十九章会讲到为何 trait 对象必须位于指针之后的原因。
为了实现 `rust_gui` 所期望拥有的行为,定义一个 `Draw` trait其包含名为 `draw` 的方法。接着可以定义一个存放 **trait 对象***trait object* 的 vector。trait 对象指向一个实现了我们指定 trait 的类型实例。我们通过指定某些指针,比如 `&` 引用或 `Box<T>` 智能指针,接着指定相关的 trait第十九章动态大小类型部分会介绍 trait 对象必须使用指针的原因)。我们可以使用 trait 对象代替泛型或具体类型。任何使用 trait 对象的位置Rust 的类型系统会在编译时确保任何在此上下文中使用的值会实现其 trait 对象的 trait。如此便无需在编译时就知晓所有可能的类型。
之前提到过,我们并不将结构体与枚举称之为“对象”,以便与其他语言中的对象相区别。结构体与枚举和 `impl` 块中的行为是分开的不同于其他语言中将数据和行为组合进一个称为对象的概念中。trait 对象将由指向具体对象的指针构成的数据和定义于 trait 中方法的行为结合在一起,从这种意义上说它**则**更类似其他语言中的对象。不过 trait 对象与其他语言中的对象是不同的,因为不能向 trait 对象增加数据。trait 对象并不像其他语言中的对象那么通用他们trait 对象)的作用是允许对通用行为的抽象。
<!-- What will the trait object do in this case? I've taken this last part of
the line from below, but I'm not 100% on that -->
<!-- I've moved up more and reworded a bit, hope that clarifies /Carol -->
trait 对象定义了在给定情况下所需的行为。接着就可以在要使用具体类型或泛型的地方使用 trait 来作为 trait 对象。Rust 的类型系统会确保任何我们替换为 trait 对象的值都会实现了 trait 的方法。这样就无需在编译时就知道所有可能的类型,就能够用同样的方法处理所有的实例。列表 17-3 展示了如何定义一个带有 `draw` 方法的 trait `Draw`
之前提到过Rust 刻意不将结构体与枚举称为 “对象”,以便与其他语言中的对象相区别。在结构体或枚举中,结构体字段中的数据和 `impl` 块中的行为是分开的不同于其他语言中将数据和行为组合进一个称为对象的概念中。trait 对象将数据和行为两者相结合,从这种意义上说 **则** 其更类似其他语言中的对象。不过 trait 对象不同于传统的对象,因为不能向 trait 对象增加数据。trait 对象并不像其他语言中的对象那么通用trait 对象)具体的作用是允许对通用行为的抽象。
示例 17-3 展示了如何定义一个带有 `draw` 方法的 trait `Draw`
<span class="filename">文件名: src/lib.rs</span>
@ -33,13 +32,15 @@ pub trait Draw {
}
```
<span class="caption">列表 17-3:`Draw` trait 的定义</span>
<span class="caption">示例 17-3`Draw` trait 的定义</span>
<!-- NEXT PARAGRAPH WRAPPED WEIRD INTENTIONALLY SEE #199 -->
因为第十章已经讨论过如何定义 trait这看起来应该比较眼熟。接下来就是新内容了实例 17-4 定义了一个存放了名叫 `components` 的 vector 的结构体 `Screen`。这个 vector 的类型是 `Box<Draw>`,此为一个 trait 对象:它是 `Box` 中任何实现了 `Draw` trait 的类型的替身。
因为第十章已经讨论过如何定义 trait这看起来应该比较眼熟。接下来就是新内容了列表 17-4 有一个名为 `Screen` 的结构体定义,它存放了一个叫做 `components``Box<Draw>` 类型的 vector 。`Box<Draw>` 是一个 trait 对象:它是 `Box` 中任何实现了 `Draw` trait 的类型的替身。
<!-- Would it be useful to let the reader know why we need a box here, or will
that be clear at this point? -->
<!-- We get into this in chapter 19; I've added a reference to the start of
this section where we talk about needing a `&` or a `Box` to be a trait object.
/Carol -->
<span class="filename">文件名: src/lib.rs</span>
@ -53,9 +54,9 @@ pub struct Screen {
}
```
<span class="caption">列表 17-4: 一个 `Screen` 结构体的定义,它带有一个字段`components`,其包含实现了 `Draw` trait 的 trait 对象的 vector</span>
<span class="caption">示例 17-4: 一个 `Screen` 结构体的定义,它带有一个字段`components`,其包含实现了 `Draw` trait 的 trait 对象的 vector</span>
`Screen` 结构体上,我们将定义一个 `run` 方法,该方法会对其 `components` 上的每一个元素调用 `draw` 方法,如列表 17-5 所示:
`Screen` 结构体上,我们将定义一个 `run` 方法,该方法会对其 `components` 上的每一个组件调用 `draw` 方法,如示例 17-5 所示:
<span class="filename">文件名: src/lib.rs</span>
@ -77,9 +78,9 @@ impl Screen {
}
```
<span class="caption">列表 17-5:`Screen` 上实现一个 `run` 方法,该方法在每个 component 上调用 `draw` 方法</span>
<span class="caption">示例 17-5`Screen` 上实现一个 `run` 方法,该方法在每个 component 上调用 `draw` 方法</span>
这与定义使用了带有 trait bound 的泛型类型参数的结构体不同。泛型类型参数一次只能替代一个具体类型,而 trait 对象则允许在运行时替代多种具体类型。例如,可以像列表 17-6 那样定义使用泛型和 trait bound 的结构体 `Screen`
这与定义使用了带有 trait bound 的泛型类型参数的结构体不同。泛型类型参数一次只能替代一个具体类型,而 trait 对象则允许在运行时替代多种具体类型。例如,可以定义 `Screen` 结构体来使用泛型和 trait bound如示例 17-6 所示
<span class="filename">文件名: src/lib.rs</span>
@ -102,15 +103,15 @@ impl<T> Screen<T>
}
```
<span class="caption">列表 17-6: 一种 `Screen` 结构体的替代实现,它的 `run` 方法使用泛型和 trait bound</span>
<span class="caption">示例 17-6: 一种 `Screen` 结构体的替代实现,其 `run` 方法使用泛型和 trait bound</span>
只允许我们拥有一个包含全是 `Button` 类型或者全是 `TextField` 类型的 component 列表的 `Screen` 实例。如果只拥有相同类型的集合,那么使用泛型和 trait bound 是更好的因为在编译时使用具体类型其定义是单态monomorphized
限制了 `Screen` 实例必须拥有一个全是 `Button` 类型或者全是 `TextField` 类型的组件列表。如果只需要同质(相同类型)集合,则倾向于使用泛型和 trait bound因为其定义会在编译时采用具体类型进行单态化
相反对于存放了 `Vec<Box<Draw>>` trait 对象的 component 列表的 `Screen` 定义,一个 `Screen` 实例可以存放一个既可以包含 `Box<Button>`,也可以包含 `Box<TextField>``Vec`。让我们看看它是如何工作的,接着会讲到其运行时性能影响。
另一方面,通过使用 trait 对象的方法,一个 `Screen` 实例可以存放一个既能包含 `Box<Button>`,也能包含 `Box<TextField>``Vec`。让我们看看它是如何工作的,接着会讲到其运行时性能影响。
### 来自我们或者库使用者的 trait 实现
### 实现 trait
现在来增加一些实现了 `Draw` trait 的类型。我们将提供 `Button` 类型再一次重申,真正实现 GUI 库超出了本书的范畴,所以 `draw` 方法体中不会有任何有意义的实现。为了想象一下这个实现看起来像什么,一个 `Button` 结构体可能会拥有 `width`、`height`和`label`字段,如列表 17-7 所示:
现在来增加一些实现了 `Draw` trait 的类型。我们将提供 `Button` 类型再一次重申,真正实现 GUI 库超出了本书的范畴,所以 `draw` 方法体中不会有任何有意义的实现。为了想象一下这个实现看起来像什么,一个 `Button` 结构体可能会拥有 `width`、`height``label` 字段,如示例 17-7 所示:
<span class="filename">文件名: src/lib.rs</span>
@ -132,15 +133,15 @@ impl Draw for Button {
}
```
<span class="caption">列表 17-7: 一个实现了`Draw` trait 的 `Button` 结构体</span>
<span class="caption">示例 17-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示例 17-8 所示:
<span class="filename">文件名: src/main.rs</span>
```rust
```rust,ignore
extern crate rust_gui;
use rust_gui::Draw;
@ -157,13 +158,13 @@ impl Draw for SelectBox {
}
```
<span class="caption">列表 17-8: 在另一个使用 `rust_gui` 的 crate 中,在 `SelectBox` 结构体上实现 `Draw` trait</span>
<span class="caption">示例 17-8: 另一个使用 `rust_gui` 的 crate 中,在 `SelectBox` 结构体上实现 `Draw` trait</span>
库使用者现在可以在他们的 `main` 函数中创建一个 `Screen` 实例,并通过将 `SelectBox``Button` 放入 `Box<T>` 转变为 trait 对象来将它们放入屏幕实例。接着可以调用 `Screen``run` 方法,它会调用每个组件的 `draw` 方法。列表 17-9 展示了这个实现:
库使用者现在可以在他们的 `main` 函数中创建一个 `Screen` 实例。至此可以通过将 `SelectBox``Button` 放入 `Box<T>` 转变为 trait 对象来增加组件。接着可以调用 `Screen``run` 方法,它会调用每个组件的 `draw` 方法。示例 17-9 展示了这个实现:
<span class="filename">文件名: src/main.rs</span>
```rust
```rust,ignore
use rust_gui::{Screen, Button};
fn main() {
@ -190,21 +191,32 @@ fn main() {
}
```
<span class="caption">列表 17-9: 使用 trait 对象来存储实现了相同 trait 的不同类型的值</span>
<span class="caption">示例 17-9: 使用 trait 对象来存储实现了相同 trait 的不同类型的值</span>
即使我们不知道何时何人会增加 `SelectBox` 类型,`Screen` 的实现能够操作`SelectBox` 并绘制它,因为 `SelectBox` 实现了 `Draw` trait这意味着它实现了 `draw` 方法。
当编写库的时候,我们不知道何人会在何时增加 `SelectBox` 类型,不过 `Screen` 的实现能够操作并绘制这个新类型,因为 `SelectBox` 实现了 `Draw` trait这意味着它实现了 `draw` 方法。
只关心值所反映的信息而不是值的具体类型,这类似于动态类型语言中称为**鸭子类型***duck typing*)的概念:如果它走起来像一只鸭子,叫起来像一只鸭子,那么它就是一只鸭子!在列表 17-5 中 `Screen` 上的 `run` 实现中,`run` 并不需要知道各个组件的具体类型是什么。它并不检查组件实例是 `Button` 或者是`SelectBox`,它只是调用组件上的 `draw` 方法。通过指定 `Box<Draw>` 作为 `components` vector 中值的类型,我们就定义了 `Screen` 需要可以在其上调用 `draw` 方法的值。
这个概念 ———— 只关心值所反映的信息而不是其具体类型 ———— 类似于动态类型语言中称为 **鸭子类型***duck typing*)的概念:如果它走起来像一只鸭子,叫起来像一只鸭子,那么它就是一只鸭子!在示例 17-5 中 `Screen` 上的 `run` 实现中,`run` 并不需要知道各个组件的具体类型是什么。它并不检查组件`Button` 或者 `SelectBox` 的实例。通过指定 `Box<Draw>` 作为 `components` vector 中值的类型,我们就定义了 `Screen` 需要可以在其上调用 `draw` 方法的值。
使用 trait 对象和 Rust 类型系统来使用鸭子类型的优势是无需在运行时检查一个值是否实现了特定方法或者担心在调用时因为值没有实现方法而产生错误。如果值没有实现 trait 对象所需的 trait 则 Rust 不会编译这些代码。
<!-- I may be slow on the uptake here, but it seems like we're saying that
responsibility for how the type trait object behaves with the draw method is
called on it belongs to the trait object, and not to the draw method itself. Is
that an accurate summary? I want to make sure I'm clearly following the
argument! -->
<!-- Each type (like `Button` or `SelectBox`) that implements the `Draw` trait
can customize what happens in the body of the `draw` method. The trait object
is just responsible for making sure that the only things that are usable in
that context are things that implement the `Draw` trait. Does this clear it up
at all? Is there something we should clarify in the text? /Carol -->
例如,列表 17-10 展示了当创建一个使用 `String` 做为其组件的 `Screen` 时发生的情况:
使用 trait 对象和 Rust 类型系统来进行类似鸭子类型操作的优势是无需在运行时检查一个值是否实现了特定方法或者担心在调用时因为值没有实现方法而产生错误。如果值没有实现 trait 对象所需的 trait 则 Rust 不会编译这些代码。
例如,示例 17-10 展示了当创建一个使用 `String` 做为其组件的 `Screen` 时发生的情况:
<span class="filename">文件名: src/main.rs</span>
```rust
```rust,ignore
extern crate rust_gui;
use rust_gui::Draw;
use rust_gui::Screen;
fn main() {
let screen = Screen {
@ -217,28 +229,34 @@ fn main() {
}
```
<span class="caption">列表 17-10: 尝试使用一种没有实现 trait 对象的 trait 的类型</span>
<span class="caption">示例 17-10: 尝试使用一种没有实现 trait 对象的 trait 的类型</span>
我们会遇到这个错误,因为 `String` 没有实现 `Draw` trait
我们会遇到这个错误,因为 `String` 没有实现 `rust_gui::Draw` trait
```
error[E0277]: the trait bound `std::string::String: Draw` is not satisfied
```text
error[E0277]: the trait bound `std::string::String: rust_gui::Draw` is not satisfied
-->
|
4 | Box::new(String::from("Hi")),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Draw` is not
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `rust_gui::Draw` is not
implemented for `std::string::String`
|
= note: required for the cast to the object type `Draw`
= note: required for the cast to the object type `rust_gui::Draw`
```
这告诉了我们,要么是我们传递了并不希望传递给 `Screen` 的类型并应该提供其他类型,要么应该在 `String` 上实现 `Draw` 以便 `Screen` 可以调用其上的 `draw`
### trait 对象执行动态分发
回忆一下第十章讨论过的,当对泛型使用 trait bound 时编译器所进行单态化处理:编译器为每一个被泛型类型参数代替的具体类型生成了非泛型的函数和方法实现。单态化所产生的代码进行**静态分发***static dispatch*):当方法被调用时,伴随方法调用的代码在编译时就被确定了,同时寻找这些代码是非常快速的
回忆一下第十章讨论过的,当对泛型使用 trait bound 时编译器所进行单态化处理:编译器为每一个被泛型类型参数代替的具体类型生成了非泛型的函数和方法实现。单态化所产生的代码进行 **静态分发***static dispatch*)。静态分发发生于编译器在编译时就知晓调用了什么方法的时候。这与 **动态分发** *dynamic dispatch*)相对,这时编译器在编译时无法知晓调用了什么方法。在这种情况下,编译器会生成在运行时确定调用了什么方法的代码
当使用 trait 对象时编译器并不进行单态化因为并不知道所有可能会使用这些代码的类型。相反Rust 记录当方法被调用时可能会用到的代码,并在运行时计算出特定方法调用时所需的代码。这被称为**动态分发***dynamic dispatch*),进行这种代码搜寻是有运行时开销的。动态分发也阻止编译有选择的内联方法的代码,这会禁用一些优化。尽管在编写和支持代码的过程中确实获得了额外的灵活性,但仍然需要权衡取舍。
<!--I'm struggling to follow the static dispatch definition, can you expand
that a little? Which part of that is the static dispatch, pre-determining the
code called with a method and storing it? -->
<!-- Yes, in a way. We've expanded and moved the definitions of static and
dynamic dispatch together to better contrast, hopefully this helps? /Carol -->
当使用 trait 对象时Rust 必须使用动态分发。编译器无法知晓所有可能用于 trait 对象代码的类型所以它也不知道应该调用哪个类型的哪个方法实现。为此Rust 在运行时使用 trait 对象中的指针来知晓需要调用哪个方法。动态分发也阻止编译器有选择的内联方法代码,这会相应的禁用一些优化。尽管在编写和支持代码的过程中确实获得了额外的灵活性,但仍然需要权衡取舍。
### Trait 对象要求对象安全
@ -251,37 +269,18 @@ a quick caveat, that just says something like "Some traits can't be trait
objects. Clone is an example of one. You'll get errors that will let you know
if a trait can't be a trait object, look up object safety if you're interested
in the details"? Thanks! /Carol -->
<!-- That sounds like a good solution, since the compiler will warn them in any
case. I read through, editing a little, and I agree we could afford to cut it,
I'm not sure it brings practical skills to the user -->
<!-- Ok, I've cut section way down to the practical pieces, but still explained
a little bit /Carol -->
不是所有的 trait 都可以被放进 trait 对象中;只有**对象安全***object safe*)的 trait 才可以。 一个 trait 只有同时满足如下两点时才被认为是对象安全的:
只有 **对象安全***object safe*)的 trait 才可以组成 trait 对象。围绕所有使得 trait 对象安全的属性存在一些复杂的规则,不过在实践中,只涉及到两条规则。如果一个 trait 中所有的方法有如下属性时,则该 trait 是对象安全的:
* trait 不要求 `Self``Sized`
* 所有的 trait 方法都是对象安全的
- 返回值类型不为 `Self`
- 方法没有任何泛型类型参数
`Self` 关键字是我们要实现 trait 或方法的类型的别名。`Sized` 是一个类似第十六章中介绍的 `Send``Sync` 那样的标记 trait。`Sized` 会自动为在编译时有已知大小的类型实现,比如 `i32` 和引用。包括 slice `[T]`)和 trait 对象这样的没有已知大小的类型则没有。
`Sized` 是一个所有泛型参数类型默认的隐含 trait bound。Rust 中大部分实用的操作都要求类型是 `Sized` 的,所以将 `Sized` 作为默认 trait bound 要求,就可以不必在每一次使用泛型时编写 `T: Sized` 了。然而,如果想要使用在 slice 上使用 trait则需要去掉 `Sized` trait bound可以通过指定 `T: ?Sized` 作为 trait bound 来做到这一点。
trait 有一个默认的 bound `Self: ?Sized`,这意味着他们可以在是或者不是 `Sized` 的类型上实现。如果创建了一个去掉了 `Self: ?Sized` bound 的 trait `Foo`,它可能看起来像这样:
```rust
trait Foo: Sized {
fn some_method(&self);
}
```
trait `Sized` 现在就是 trait `Foo` 的**父 trait***supertrait*)了,也就意味着 trait `Foo` 要求实现 `Foo` 的类型(也就是 `Self`)是 `Sized` 的。我们将在第十九章中更详细的介绍父 trait。
`Foo` 这样要求 `Self``Sized` 的 trait 不被允许成为 trait 对象的原因是,不可能为 trait 对象实现 `Foo` traittrait 对象不是 `Sized` 的,但是 `Foo` 又要求 `Self``Sized` 的。一个类型不可能同时既是有确定大小的又是无确定大小的。
关于第二条对象安全要求说到 trait 的所有方法都必须是对象安全的,一个对象安全的方法满足下列条件之一:
* 要求 `Self``Sized` 的,或者
* 满足如下三点:
* 必须不包含任何泛型类型参数
* 其第一个参数必须是 `Self` 类型或者能解引用为 `Self` 的类型(也就是说它必须是一个方法而非关联函数,并且以 `self`、`&self` 或 `&mut self` 作为第一个参数)
* 必须不能在方法签名中除第一个参数之外的地方使用 `Self`
虽然这些规则有一点形式化, 但是换个角度想一下:如果方法在它的签名的其他什么地方要求使用具体的 `Self` 类型,而一个对象又忘记了它具体的类型,这时方法就无法使用它遗忘的原始的具体类型了。当使用 trait 的泛型类型参数被放入具体类型参数时也是如此:这个具体的类型就成了实现该 trait 的类型的一部分。一旦这个类型因使用 trait 对象而被擦除掉了之后,就无法知道放入泛型类型参数的类型是什么了。
`Self` 关键字是我们要实现 trait 或方法的类型的别名。对象安全对于 tarit 对象是必须的,因为一旦有了 trait 对象,就不再知晓实现该 trait 的具体类型是什么了。如果 trait 方法返回具体的 `Self` 类型,但是 trait 对象忘记了其真正的类型,那么方法不可能使用已经忘却的原始具体类型。同理对于泛型类型参数来说,当使用 trait 时其会放入具体的类型参数:此具体类型编程了实现改 trait 的类型的一部分。当使用 trait 对象时其具体类型被抹去了,故无从得知放入泛型参数类型的类型是什么。
一个 trait 的方法不是对象安全的例子是标准库中的 `Clone` trait。`Clone` trait 的 `clone` 方法的参数签名看起来像这样:
@ -293,9 +292,8 @@ pub trait Clone {
`String` 实现了 `Clone` trait当在 `String` 实例上调用 `clone` 方法时会得到一个 `String` 实例。类似的,当调用 `Vec` 实例的 `clone` 方法会得到一个 `Vec` 实例。`clone` 的签名需要知道什么类型会代替 `Self`,因为这是它的返回值。
如果尝试在像列表 17-3 中 `Draw` 那样的 trait 上实现 `Clone`,就无法知道 `Self` 将会是 `Button`、`SelectBox` 亦或是将来会实现 `Draw` trait 的其他什么类型。
如果尝试做一些违反有关 trait 对象但违反对象安全规则的事情,编译器会提示你。例如,如果尝试实现列表 17-4 中的 `Screen` 结构体来存放实现了 `Clone` trait 而不是 `Draw` trait 的类型,像这样:
如果尝试做一些违反有关 trait 对象的对象安全规则的事情,编译器会提示你。例如,如果尝试实现示例 17-4 中的 `Screen` 结构体来存放实现了 `Clone` trait 而不是 `Draw` trait 的类型,像这样:
```rust,ignore
pub struct Screen {
@ -305,7 +303,7 @@ pub struct Screen {
将会得到如下错误:
```
```text
error[E0038]: the trait `std::clone::Clone` cannot be made into an object
-->
|
@ -316,8 +314,6 @@ error[E0038]: the trait `std::clone::Clone` cannot be made into an object
= note: the trait cannot require that `Self : Sized`
```
<!-- If we are including this section, we would explain how to fix this
problem. It involves adding another trait and implementing Clone manually for
that trait. Because this section is getting long, I stopped because it feels
like we're off in the weeds with an esoteric detail that not everyone will need
to know about. /Carol -->
这意味着不能以这种方式使用此 trait 作为 trait 对象。如果你对对象安全的更多细节感兴趣,请查看 [Rust RFC 255]。
[Rust RFC 255]: https://github.com/rust-lang/rfcs/blob/master/text/0255-object-safety.md

View File

@ -2,11 +2,19 @@
> [ch17-03-oo-design-patterns.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch17-03-oo-design-patterns.md)
> <br>
> commit 67737ff868e3347588cc832eceb8fc237afc5895
> commit b18f90970ab7223ee8af18ef466a5ba6ff8482ef
**状态模式***state pattern*)是一个面向对象设计模式。改模式的关键在于一个值有某些内部状态,体现为一系列的 **状态对象**,同时值的行为随着其内部状态而改变。状态对象共享功能 ———— 当然,在 Rust 中使用结构体和 trait 而不是对象和继承。每一个状态对象代表负责其自身的行为和当需要改变为另一个状态时的规则的状态。持有任何一个这种状态对象的值对于不同状态的行为以及何时状态转移毫不知情。
<!-- Below -- requirements for what, for what we need the value for? -->
<!-- I've clarified /Carol -->
使用状态模式意味着当程序的业务需求改变时,无需改变值持有状态或者使用值的代码。我们只需更新某个状态对象中的代码来改变其规则,或者是增加更多的状态对象。让我们看看一个有关状态模式和如何在 Rust 中使用它的例子。
让我们看看一个状态设计模式的例子以及如何在 Rust 中使用他们。**状态模式***state pattern*)是指一个值有某些内部状态,而它的行为随着其内部状态而改变。内部状态由一系列继承了共享功能的对象表现(我们使用结构体和 trait 因为 Rust 没有对象和继承)。每一个状态对象负责它自身的行为和当需要改变为另一个状态时的规则。持有任何一个这种状态对象的值对于不同状态的行为以及何时状态转移毫不知情。当将来需求改变时,无需改变值持有状态或者使用值的代码。我们只需更新某个状态对象中的代码来改变它的规则,或者是增加更多的状态对象。
为了探索这个概念,我们将实现一个增量式的发布博文的工作流。这个我们希望发布博文时所应遵守的工作流,一旦完成了它的实现,将为如下:
为了探索这个概念,我们将实现一个增量式的发布博文的工作流。这个博客的最终功能看起来像这样
1. 博文从空白的草案开始。
2. 一旦草案完成,请求审核博文。
@ -15,7 +23,7 @@
任何其他对博文的修改尝试都是没有作用的。例如,如果尝试在请求审核之前通过一个草案博文,博文应该保持未发布的状态。
列表 17-11 展示这个工作流的代码形式。这是一个我们将要在一个叫做 `blog` 的库 crate 中实现的 API 的使用示例:
示例 17-11 展示这个工作流的代码形式。这是一个我们将要在一个叫做 `blog` 的库 crate 中实现的 API 的示例:
<span class="filename">文件名: src/main.rs</span>
@ -37,17 +45,26 @@ fn main() {
}
```
<span class="caption">列表 17-11: 展示了 `blog` crate 期望行为的代码</span>
<span class="caption">示例 17-11: 展示了 `blog` crate 期望行为的代码</span>
我们希望能够使用 `Post::new` 创建一个新的博文草案。接着希望能在草案阶段为博文编写一些文本。如果尝试立即打印出博文的内容,将不会得到任何文本,因为博文仍然是草案。这里增加的 `assert_eq!` 用于展示目的。断言草案博文的 `content` 方法返回空字符串将能作为库的一个非常好的单元测试,不过我们并不准备为这个例子编写单元测试。
我们希望允许用户使用 `Post::new` 创建一个新的博文草案。接着希望能在草案阶段为博文编写一些文本。如果尝试在审核之前立即打印出博文的内容,什么也不会发生因为博文仍然是草案。这里增加的 `assert_eq!` 出于演示目的。一个好的单元测试将是断言草案博文的 `content` 方法返回空字符串,不过我们并不准备为这个例子编写单元测试。
接下来,我们希望能够请求审核博文,而在等待审核的阶段 `content` 应该仍然返回空字符串,当博文审核通过,它应该被发表,这意味着当调用 `content` 时我们编写的文本将被返回。
接下来,我们希望能够请求审核博文,而在等待审核的阶段 `content` 应该仍然返回空字符串。最后当博文审核通过,它应该被发表,这意味着当调用 `content` 时博文的文本将被返回。
注意我们与 crate 交互的唯一的类型是 `Post`。博文可能处于的多种状态(草案,等待审核和发布)由 `Post` 内部管理。博文状态依我们在`Post`调用的方法而改变,但不必直接管理状态改变。这也意味着不会在状态上犯错,比如忘记了在发布前请求审核。
<!-- Below -- so this is where we'll implement the state pattern? If so, can
you make that explicit, just to be clear! I've added some text to the second
line, not sure if that's accurate though -->
<!-- Yes, the state pattern will be implemented within the `Post` type. I've
tweaked the wording a bit but you've pretty much got it! /Carol-->
注意我们与 crate 交互的唯一的类型是 `Post`。这个类型会使用状态模式并会存放处于三种博文所可能的状态之一的值 ———— 草案,等待审核和发布。状态上的改变由 `Post` 类型内部进行管理。状态依库用户对 `Post` 实例调用的方法而改变,但是不能直接管理状态变化。这也意味着用户不会在状态上犯错,比如在过审前发布博文。
### 定义 `Post` 并新建一个草案状态的实例
让我们开始实现这个库吧!我们知道需要一个公有 `Post` 结构体来存放一些文本,所以让我们从结构体的定义和一个创建 `Post` 实例的公有关联函数 `new` 开始,如列表 17-12 所示。我们还需定义一个私有 trait `State`。`Post` 将在私有字段 `state` 中存放一个 `Option` 中的 trait 对象 `Box<State>`。稍后将会看到为何 `Option` 是必须的。`State` trait 定义了所有不同状态的博文所共享的行为,同时 `Draft`、`PendingReview` 和 `Published` 状态都会实现`State` 状态。现在这个 trait 并没有任何方法,同时开始将只定义`Draft`状态因为这是我们希望开始的状态:
让我们开始实现这个库吧!我们知道需要一个公有 `Post` 结构体来存放一些文本,所以让我们从结构体的定义和一个创建 `Post` 实例的公有关联函数 `new` 开始,如示例 17-12 所示。还需定义一个私有 trait `State`。`Post` 将在私有字段 `state` 中存放一个 `Option` 中的 trait 对象 `Box<State>`。稍后将会看到为何 `Option` 是必须的。
`State` trait 定义了所有不同状态的博文所共享的行为,同时 `Draft`、`PendingReview` 和 `Published` 状态都会实现 `State` 状态。现在这个 trait 并没有任何方法,同时开始将只定义 `Draft` 状态因为这是我们希望博文的初始状态:
<span class="filename">文件名: src/lib.rs</span>
@ -73,14 +90,13 @@ struct Draft {}
impl State for Draft {}
```
<span class="caption">列表 17-12: `Post`结构体的定义和新建 `Post` 实例的 `new`函数,`State` trait 和实现了 `State`结构体 `Draft`</span>
<span class="caption">示例 17-12: `Post` 结构体的定义和新建 `Post` 实例的 `new` 函数,`State` trait 和结构体 `Draft`</span>
当创建新的 `Post` 时,我们将其 `state` 字段设置为一个 `Some` 值,它存放了指向一个 `Draft` 结构体新实例的 `Box`。这确保了无论何时新建一个 `Post` 实例,它会从草案开始。因为 `Post``state` 字段是私有的,也就无法创建任何其他状态的 `Post` 了!。
当创建新的 `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` 块中增加一个实现:
`Post::new` 函数中,我们设置 `content` 字段为新的空 `String`。在示例 17-11 中,展示了我们希望能够调用一个叫做 `add_text` 的方法并向其传递一个 `&str` 来将文本增加到博文的内容中。选择实现为一个方法而不是将 `content` 字段暴露为 `pub` 。这意味着之后可以实现一个方法来控制 `content` 字段如何被读取。`add_text` 方法是非常直观的,让我们在示例 17-13 的 `impl Post` 块中增加一个实现:
<span class="filename">文件名: src/lib.rs</span>
@ -90,20 +106,20 @@ impl State for Draft {}
# }
#
impl Post {
// ...snip...
// --snip--
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
}
```
<span class="caption">列表 17-13: 实现方法 `add_text` 来向博文的 `content` 增加文本</span>
<span class="caption">示例 17-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因为博文仍然处于草案状态示例 17-11 的第 8 行所示。现在让我们使用能满足要求的最简单的方式来实现 `content` 方法:总是返回一个空字符串 slice。当实现了将博文状态改为发布的能力之后将改变这一做法。但是目前博文只能是草案状态这意味着其内容应该总是空的。示例 17-14 展示了这个占位符实现:
<span class="filename">文件名: src/lib.rs</span>
@ -113,7 +129,7 @@ impl Post {
# }
#
impl Post {
// ...snip...
// --snip--
pub fn content(&self) -> &str {
""
}
@ -122,11 +138,17 @@ impl Post {
<span class="caption">列表 17-14: 增加一个 `Post``content` 方法的占位实现,它总是返回一个空字符串 slice</span>
通过增加这个 `content`方法,列表 17-11 中直到第 8 行的代码能如期运行。
通过增加这个 `content`方法,示例 17-11 中直到第 8 行的代码能如期运行。
### 请求审核博文来改变其状态
接下来是请求审核博文,这应当将其状态由 `Draft` 改为 `PendingReview`。我们希望 `post` 有一个获取 `self` 可变引用的公有方法 `request_review`。接着将调用内部存放的状态的 `request_review` 方法,而这第二个 `request_review` 方法会消费当前的状态并返回要一个状态。为了能够消费旧状态,第二个 `request_review` 方法需要能够获取状态值的所有权。这就是 `Option` 的作用:我们将 `take` 字段 `state` 中的 `Some` 值并留下一个 `None` 值,因为 Rust 并不允许结构体中有空字段。接着将博文的 `state` 设置为这个操作的结果。列表 17-15 展示了这些代码:
接下来需要增加请求审核博文的功能,这应当将其状态由 `Draft` 改为 `PendingReview`。我们希望为 `Post` 增加一个获取 `self` 可变引用的公有方法 `request_review`。接着将 `Post` 当前状态内部的 `request_review` 方法而这第二个 `request_review` 方法会消费当前的状态并返回一个新状态。示例 17-15 展示了这个代码:
<!-- NOTE TO DE/AU: We might want to move this explanation to after the code if
you want to add wingdings, we can see once we transfer it to Word -->
<!-- I decided to move some of this explanation after the code for this reason
and because we got some questions about this example that I wanted to expand
upon /Carol -->
<span class="filename">文件名: src/lib.rs</span>
@ -137,7 +159,7 @@ impl Post {
# }
#
impl Post {
// ...snip...
// --snip--
pub fn request_review(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.request_review())
@ -166,19 +188,27 @@ impl State for PendingReview {
}
```
<span class="caption">列表 17-15: 实现 `Post``State` trait 的 `request_review` 方法</span>
<span class="caption">示例 17-15: 实现 `Post``State` trait 的 `request_review` 方法</span>
这里给 `State` trait 增加了 `request_review` 方法;所有实现了这个 trait 的类型现在都需要实现 `request_review` 方法。注意不用于使用`self``&self` 或者 `&mut self` 作为方法的第一个参数,这里使用了 `self: Box<Self>`。这个语法意味着这个方法调用只对这个类型的 `Box` 有效。这个语法获取了 `Box<Self>` 的所有权,这是我们希望的,因为需要从老状态转换为新状态,同时希望老状态不再有效
这里给 `State` trait 增加了 `request_review` 方法;所有实现了这个 trait 的类型现在都需要实现 `request_review` 方法。注意不同于使用 `self``&self` 或者 `&mut self` 作为方法的第一个参数,这里使用了 `self: Box<Self>`。这个语法意味着这个方法调用只对这个类型的 `Box` 有效。这个语法获取了 `Box<Self>` 的所有权,使老状态无效化以便 `Post` 的状态值可以将自身转换为新状态
`Draft` 的方法 `request_review` 的实现返回一个新的,装箱的 `PendingReview` 结构体的实例,这是新引入的用来代表博文处于等待审核状态的类型。结构体 `PendingReview` 同样也实现了 `request_review` 方法,不过它不进行任何状态转换。它返回自身,因为请求审核已经处于 `PendingReview` 状态的博文应该保持 `PendingReview` 状态。
<!-- Above -- so Post can transform, or so Draft can transform? -->
<!-- Technically it's so the Draft value can transform into another value,
which changes the state of Post-- I've tried to clarify. /Carol -->
现在能够看出状态模式的优势了:`Post` 的 `request_review` 方法无论 `state` 是何值都是一样的。每个状态负责它自己的规则。
为了消费老状态,`request_review` 方法需要获取状态值的所有权。这也就是 `Post``state` 字段中 `Option` 的来历:调用 `take` 方法将 `state` 字段中的 `Some` 值取出并留下一个 `None`,因为 Rust 不允许在结构体中存在空的字段。这使得我们将 `state` 值移动出 `Post` 而不是借用它。接着将博文的 `state` 值设置为这个操作的结果
我们将继续保持 `Post``content` 方法不变,返回一个空字符串 slice。现在可以拥有 `PendingReview` 状态而不仅仅是 `Draft` 状态的 `Post` 了,不过我们希望在 `PendingReview` 状态下其也有相同的行为。现在列表 17-11 中直到 11 行的代码是可以执行的!
这里需要将 `state` 临时设置为 `None`,不同于像 `self.state = self.state.request_review();` 这样的代码直接设置 `state` 字段,来获取 `state` 值的所有权。这确保了当 `Post` 被转换为新状态后其不再能使用老的 `state` 值。
### 批准博文并改变 `content` 的行为
`Draft` 的方法 `request_review` 的实现返回一个新的,装箱的 `PendingReview` 结构体的实例,其用来代表博文处于等待审核状态。结构体 `PendingReview` 同样也实现了 `request_review` 方法,不过它不进行任何状态转换。相反它返回自身,因为请求审核已经处于 `PendingReview` 状态的博文应该保持 `PendingReview` 状态。
`Post``approve` 方法将与 `request_review` 方法类似:它会将 `state` 设置为审核通过时应处于的状态。我们需要为 `State` trait 增加 `approve` 方法,并需新增实现了 `State` 的结构体, `Published` 状态。列表 17-16 展示了新增的代码:
现在开始能够看出状态模式的优势了:`Post` 的 `request_review` 方法无论 `state` 是何值都是一样的。每个状态只负责它自己的规则。
我们将继续保持 `Post``content` 方法不变,返回一个空字符串 slice。现在可以拥有 `PendingReview` 状态而不仅仅是 `Draft` 状态的 `Post` 了,不过我们希望在 `PendingReview` 状态下其也有相同的行为。现在示例 17-11 中直到 11 行的代码是可以执行的!
### 增加改变 `content` 行为的 `approve` 方法
`approve` 方法将与 `request_review` 方法类似:它会将 `state` 设置为审核通过时应处于的状态,如示例 17-16 所示。
<span class="filename">文件名: src/lib.rs</span>
@ -189,7 +219,7 @@ impl State for PendingReview {
# }
#
impl Post {
// ...snip...
// --snip--
pub fn approve(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.approve())
@ -209,7 +239,7 @@ impl State for Draft {
# Box::new(PendingReview {})
# }
#
// ...snip...
// --snip--
fn approve(self: Box<Self>) -> Box<State> {
self
}
@ -219,10 +249,10 @@ struct PendingReview {}
impl State for PendingReview {
# fn request_review(self: Box<Self>) -> Box<State> {
# Box::new(PendingReview {})
# self
# }
#
// ...snip...
// --snip--
fn approve(self: Box<Self>) -> Box<State> {
Box::new(Published {})
}
@ -241,11 +271,13 @@ impl State for Published {
}
```
<span class="caption">列表 17-16: 为 `Post``State` trait 实现 `approve` 方法</span>
<span class="caption">示例 17-16: 为 `Post``State` trait 实现 `approve` 方法</span>
类似于 `request_review`,如果对 `Draft` 调用 `approve` 方法,并没有任何效果,因为它会返回 `self`。当对 `PendingReview` 调用 `approve` 时,它返回一个新的、装箱的 `Published` 结构体的实例。`Published` 结构体实现了 `State` trait同时对于 `request_review``approve` 方法来说,它返回自身,因为在这两种情况博文应该保持 `Published` 状态。
这里为 `State` trait 增加了 `approve` 方法,并新增了一个实现了 `State` 的结构体,`Published` 状态。
现在更新 `Post``content` 方法:我们希望当博文处于 `Published` 时返回 `content` 字段的值,否则返回空字符串 slice。因为目标是将所有像这样的规则保持在实现了 `State` 的结构体中,我们将调用 `state` 中的值的 `content` 方法并传递博文实例(也就是 `self`)作为参数。接着返回 `state` 值的 `content` 方法的返回值,如列表 17-17 所示:
类似于 `request_review`,如果对 `Draft` 调用 `approve` 方法,并没有任何效果,因为它会返回 `self`。当对 `PendingReview` 调用 `approve` 时,它返回一个新的、装箱的 `Published` 结构体的实例。`Published` 结构体实现了 `State` trait同时对于 `request_review``approve` 两方法来说,它返回自身,因为在这两种情况博文应该保持 `Published` 状态。
现在更新 `Post``content` 方法:如果状态为 `Published` 希望返回博文 `content` 字段的值;否则希望返回空字符串 slice如示例 17-17 所示:
<span class="filename">文件名: src/lib.rs</span>
@ -259,19 +291,25 @@ impl State for Published {
# }
#
impl Post {
// ...snip...
// --snip--
pub fn content(&self) -> &str {
self.state.as_ref().unwrap().content(&self)
}
// ...snip...
// --snip--
}
```
<span class="caption">列表 17-17: 更新 `Post``content` 方法来委托调用 `State` 的`content` 方法</span>
<span class="caption">示例 17-17: 更新 `Post``content` 方法来委托调用 `State` 的`content` 方法</span>
这里调用 `Option``as_ref`方法是因为需要 `Option` 中值的引用。接着调用 `unwrap` 方法,这里我们知道永远也不会 panic 因为 `Post` 的所有方法都确保在他们返回时 `state` 会有一个 `Some` 值。这就是一个第十二章讨论过的我们知道 `None` 是不可能的而编译器却不能理解的情况
因为目标是将所有像这样的规则保持在实现了 `State` 的结构体中,我们将调用 `state` 中的值的 `content` 方法并传递博文实例(也就是 `self`)作为参数。接着返回 `state` 值的 `content` 方法的返回值
`State` trait 的 `content` 方法是博文返回什么内容的逻辑所在之处。我们将增加一个 `content` 方法的默认实现来返回一个空字符串 slice。这样就无需为 `Draft``PendingReview` 结构体实现 `content` 了。`Published` 结构体会覆盖 `content` 方法并会返回 `post.content` 的值,如列表 17-18 所示:
这里调用 `Option``as_ref` 方法是因为需要 `Option` 中值的引用而不是获取其所有权。因为 `state` 是一个 `Option<Box<State>>`,调用 `as_ref` 会返回一个 `Option<&Box<State>>`。如果不调用 `as_ref`,怎会得到一个错误,因为不能将 `state` 移动出借用的 `&self` 函数参数。
接着调用 `unwrap` 方法,这里我们知道它永远也不会 panic因为 `Post` 的所有方法都确保在他们返回时 `state` 会有一个 `Some` 值。这就是一个第十二章讨论过的我们知道 `None` 是不可能的而编译器却不能理解的情况。
接着我们就有了一个 `&Box<State>`,当调用其 `content` 时,解引用强制多态会作用于 `&``Box` 这样最终会调用实现了 `State` trait 的类型的 `content` 方法。
这意味着需要为 `State` trait 定义增加 `content`,这也是放置根据所处状态返回什么内容的逻辑的地方,如示例 17-18 所示:
<span class="filename">文件名: src/lib.rs</span>
@ -280,56 +318,68 @@ impl Post {
# content: String
# }
trait State {
// ...snip...
// --snip--
fn content<'a>(&self, post: &'a Post) -> &'a str {
""
}
}
// ...snip...
// --snip--
struct Published {}
impl State for Published {
// ...snip...
// --snip--
fn content<'a>(&self, post: &'a Post) -> &'a str {
&post.content
}
}
```
<span class="caption">列表 17-18: 为 `State` trait 增加 `content` 方法</span>
<span class="caption">示例 17-18: 为 `State` trait 增加 `content` 方法</span>
这里增加了一个 `content` 方法的默认实现来返回一个空字符串 slice。这意味着无需为 `Draft``PendingReview` 结构体实现 `content` 了。`Published` 结构体会覆盖 `content` 方法并会返回 `post.content` 的值。
注意这个方法需要生命周期注解,如第十章所讨论的。这里获取 `post` 的引用作为参数,并返回 `post` 一部分的引用,所以返回的引用的生命周期与 `post` 参数相关。
<!-- Is this it finished, without the touch up we make to get rid of the empty
string? That's pretty awesome coding, maybe give it some ceremony here. Does
all of 17-11 now work? -->
<!-- Yep! Good point, so added! /Carol -->
现在示例完成了 ———— 现在示例 17-11 中所有的代码都能工作!我们通过发布博文工作流的规则实现了状态模式。围绕这些规则的逻辑都存在于状态对象中而不是分散在 `Post` 之中。
### 状态模式的权衡取舍
我们展示了 Rust 是能够实现面向对象的状态模式的,以便能根据博文所处的状态来封装不同类型的行为。`Post` 的方法并不知道这些不同类型的行为。这种组织代码的方式,为了找到所有已发布的博文不同行为只需查看一处代码:`Published` 的 `State` trait 的实现。
我们展示了 Rust 是能够实现面向对象的状态模式的,以便能根据博文所处的状态来封装不同类型的行为。`Post` 的方法并不知道这些不同类型的行为。通过这种组织代码的方式,为了找到所有已发布的博文不同行为只需查看一处代码:`Published` 的 `State` trait 的实现。
一个不使用状态模式的替代实现可能会在 `Post` 的方法中,甚至于在使用 `Post` 的代码中(在这里是 `main` 中)用到 `match` 语句,来检查博文状态并在这里改变其行为。这可能意味着需要查看很多位置来理解处于发布状态的博文的所有逻辑!这在增加更多状态时会变得更糟:每一个 `match` 语句都会需要另一个分支。对于状态模式来说,`Post` 的方法和使用 `Post` 的位置无需`match` 语句,同时增加新状态只涉及到增加一个新 `struct` 和为其实现 trait 的方法。
如果要创建一个不使用状态模式的替代实现,则可能会在 `Post` 的方法中,或者甚至于在 `main` 代码中用到 `match` 语句,来检查博文状态并在这里改变其行为。这意味着需要查看很多位置来理解处于发布状态的博文的所有逻辑!这在增加更多状态时会变得更糟:每一个 `match` 语句都会需要另一个分支。
这个实现易于增加更多功能。这里是一些你可以尝试对本部分代码做出的修改,来亲自体会一下使用状态模式随着时间的推移维护代码是什么感觉:
对于状态模式来说,`Post` 的方法和使用 `Post` 的位置无需 `match` 语句,同时增加新状态只涉及到增加一个新 `struct` 和为其实现 trait 的方法。
这个实现易于扩展增加更多功能。为了体会使用此模式维护代码的简洁性,请尝试如下一些建议:
- 只允许博文处于 `Draft` 状态时增加文本内容
- 增加 `reject` 方法将博文的状态从 `PendingReview` 变回 `Draft`
- 在将状态变为 `Published` 之前需要两次 `approve` 调用
状态模式的一个缺点是因为状态实现了状态之间的转换,一些状态会相互联系。如果在 `PendingReview``Published` 之间增加另一个状态,比如 `Scheduled`,则不得不修改 `PendingReview` 中的代码来转移到 `Scheduled`。如果 `PendingReview` 无需因为新增的状态而改变就更好了,不过这意味着切换到另一设计模式。
状态模式的一个缺点是因为状态实现了状态之间的转换,一些状态会相互联系。如果在 `PendingReview``Published` 之间增加另一个状态,比如 `Scheduled`,则不得不修改 `PendingReview` 中的代码来转移到 `Scheduled`。如果 `PendingReview` 无需因为新增的状态而改变就更好了,不过这意味着切换到另一设计模式。
这个 Rust 中的实现的缺点在于存在一些重复的逻辑。如果能够`State` trait 中返回 `self``request_review``approve` 方法增加默认实现就好了,不过这会违反对象安全性,因为 trait 不知道 `self` 具体是什么。我们希望能够将 `State` 作为一个 trait 对象,所以需要这个方法是对象安全的。
另一个缺点是我们会发现一些重复的逻辑。为了消除他们,可以尝试`State` trait 中返回 `self``request_review``approve` 方法增加默认实现,不过这会违反对象安全性,因为 trait 不知道 `self` 具体是什么。我们希望能够将 `State` 作为一个 trait 对象,所以需要方法是对象安全的。
另一个最好能去除的重复是 `Post``request_review``approve` 这两个类似的实现。他们都委托调用了 `state` 字段中 `Option` 值的同一方法,并在结果中为 `state` 字段设置了新值。如果 `Post` 中的很多方法都遵循这个模式,我们可能会考虑定义一个宏来消除重复(查看附录 E 以了解宏)。
另一个重复是 `Post``request_review``approve` 这两个类似的实现。他们都委托调用了 `state` 字段中 `Option` 值的同一方法,并在结果中为 `state` 字段设置了新值。如果 `Post` 中的很多方法都遵循这个模式,我们可能会考虑定义一个宏来消除重复(查看附录 D 以了解宏)。
这个完全按照面向对象语言的定义实现的面向对象模式的缺点在于没有尽可能的利用 Rust 的优势。让我们看看一些代码中可以做出的修改,来将无效的状态和状态转移变为编译时错误。
完全按照面向对象语言的定义实现这个模式并没有没有尽可能的利用 Rust 的优势。让我们看看一些代码中可以做出的修改,来将无效的状态和状态转移变为编译时错误。
#### 将状态和行为编码为类型
我们将展示如何稍微反思状态模式来进行一系列不同的权衡取舍。不同于完全封装状态和状态转移使得外部代码对其毫不知情,我们将状态编码进不同的类型。当状态是类型时Rust 的类型检查就会使任何在只能使用发布的博文的地方使用草案博文的尝试变为编译时错误。
我们将展示如何稍微反思状态模式来进行一系列不同的权衡取舍。不同于完全封装状态和状态转移使得外部代码对其毫不知情,我们将状态编码进不同的类型。如此Rust 的类型检查就会将任何在只能使用发布博文的地方使用草案博文的尝试变为编译时错误。
让我们考虑一下列表 17-11 中 `main` 的第一部分:
让我们考虑一下示例 17-11 中 `main` 的第一部分:
<span class="filename">文件名: src/main.rs</span>
```rust
```rust,ignore
fn main() {
let mut post = Post::new();
@ -338,7 +388,7 @@ fn main() {
}
```
我们仍然希望使用 `Post::new` 创建一个新的草案博文,并仍然希望能够增加博文的内容。不过不同于存在一个草案博文时返回空字符串的 `content` 方法,我们将使草案博文完全没有 `content` 方法。这样如果尝试获取草案博文的内容,将会得到一个方法不存在的编译错误。这使得我们不可能在生产环境意外显示出草案博文的内容,因为这样的代码甚至就不能编译。列表 17-19 展示了 `Post` 结构体、`DraftPost` 结构体以及各自的方法的定义:
我们仍然希望能够使用 `Post::new` 创建一个新的草案博文,并能够增加博文的内容。不过不同于存在一个草案博文时返回空字符串的 `content` 方法,我们将使草案博文完全没有 `content` 方法。这样如果尝试获取草案博文的内容,将会得到一个方法不存在的编译错误。这使得我们不可能在生产环境意外显示出草案博文的内容,因为这样的代码甚至就不能编译。示例 17-19 展示了 `Post` 结构体、`DraftPost` 结构体以及各自的方法的定义:
<span class="filename">文件名: src/lib.rs</span>
@ -370,15 +420,17 @@ impl DraftPost {
}
```
<span class="caption">列表 17-19: 带有 `content` 方法的 `Post` 和没有 `content` 方法的 `DraftPost`</span>
<span class="caption">示例 17-19: 带有 `content` 方法的 `Post` 和没有 `content` 方法的 `DraftPost`</span>
`Post``DraftPost` 结构体都有一个私有的 `content` 字段来储存博文的文本。这些结构体不再有 `state` 字段因为我们将类型编码为结构体的类型。`Post` 将代表发布的博文,它有一个返回 `content``content` 方法。
仍然有一个 `Post::new` 函数,不过不同于返回 `Post` 实例,它返回 `DraftPost` 的实例。现在不可能创建一个 `Post` 实例,因为 `content` 是私有的同时没有任何函数返回 `Post``DraftPost` 上定义了一个 `add_text` 方法,这样就可以像之前那样向 `content` 增加文本,不过注意 `DraftPost` 并没有定义 `content` 方法!所以所有博文都强制从草案开始,同时草案博文没有任何可供展示的内容。任何绕过这些限制的尝试都会产生编译错误。
仍然有一个 `Post::new` 函数,不过不同于返回 `Post` 实例,它返回 `DraftPost` 的实例。现在不可能创建一个 `Post` 实例,因为 `content` 是私有的同时没有任何函数返回 `Post`
#### 实现状态转移为不同类型的转移
`DraftPost` 上定义了一个 `add_text` 方法,这样就可以像之前那样向 `content` 增加文本,不过注意 `DraftPost` 并没有定义 `content` 方法!如此现在程序确保了所有博文都从草案开始,同时草案博文没有任何可供展示的内容。任何绕过这些限制的尝试都会产生编译错误。
那么如何得到发布的博文呢?我们希望强制的规则是草案博文在可以发布之前必须被审核通过。等待审核状态的博文应该仍然不会显示任何内容。让我们通过增加另一个结构体 `PendingReviewPost` 来实现这个限制,在 `DraftPost` 上定义 `request_review` 方法来返回 `PendingReviewPost`,并在 `PendingReviewPost` 上定义 `approve` 方法来返回 `Post`,如列表 17-20 所示:
#### 实现状态转移为不同类型的转换
那么如何得到发布的博文呢?我们希望强制执行的规则是草案博文在可以发布之前必须被审核通过。等待审核状态的博文应该仍然不会显示任何内容。让我们通过增加另一个结构体 `PendingReviewPost` 来实现这个限制,在 `DraftPost` 上定义 `request_review` 方法来返回 `PendingReviewPost`,并在 `PendingReviewPost` 上定义 `approve` 方法来返回 `Post`,如示例 17-20 所示:
<span class="filename">文件名: src/lib.rs</span>
@ -392,7 +444,7 @@ impl DraftPost {
# }
#
impl DraftPost {
// ...snip...
// --snip--
pub fn request_review(self) -> PendingReviewPost {
PendingReviewPost {
@ -416,11 +468,11 @@ impl PendingReviewPost {
<span class="caption">列表 17-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` 方法,现在我们就将发博文的工作流编码进了类型系统。
`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` 的代码如列表 18-21 所示:
这也意味着不得不对 `main` 做出一些小的修改。因为 `request_review``approve` 返回新实例而不是修改被调用的结构体,所以我们需要增加更多的 `let post = ` 覆盖赋值来保存返回的实例。也不再断言草案和等待审核的博文的内容为空字符串了,我们也不再需要他们:不能编译尝试使用这些状态下博文内容的代码。更新后的 `main` 的代码如示例 17-21 所示:
<span class="filename">Filename: src/main.rs</span>
<span class="filename">文件名: src/main.rs</span>
```rust,ignore
extern crate blog;
@ -439,16 +491,16 @@ fn main() {
}
```
<span class="caption">列表 17-21: `main` 中使用新的博文工作流实现的修改</span>
<span class="caption">示例 17-21: `main` 中使用新的博文工作流实现的修改</span>
不得不修改 `main` 来重新赋值 `post` 使得这个实现不再完全遵守面向对象的状态模式:状态间的转换不再完全封装在 `Post` 实现中。然而,得益于类型系统和编译时类型检查我们得到了不可能拥有无效状态的属性!这确保了特定的 bug比如显示未发布博文的内容将在部署到生产环境之前被发现。
尝试在这一部分开始所建议的增加额外需求的任务来体会使用这个版本的代码是何感觉。
即便 Rust 能够实现面向对象设计模式,也有其他像将状态编码进类型这样的模式存在。这些模式有着不同于面向对象模式的权衡取舍。虽然你可能非常熟悉面向对象模式,重新思考这些问题来利用 Rust 提供的像在编译时避免一些 bug 这样有益功能。在 Rust 中面向对象模式并不总是最好的解决方案,因为 Rust 拥有像所有权这样的面向对象语言所没有的功能。
即便 Rust 能够实现面向对象设计模式,也有其他像将状态编码进类型这样的模式存在。这些模式有着不同的权衡取舍。虽然你可能非常熟悉面向对象模式,重新思考这些问题来利用 Rust 提供的像在编译时避免一些 bug 这样有益功能。在 Rust 中面向对象模式并不总是最好的解决方案,因为 Rust 拥有像所有权这样的面向对象语言所没有的功能。
## 总结
阅读本章后,不管你是否认为 Rust 是一个面向对象语言,现在你都见识了 trait 对象是一个 Rust 中获取部分面向对象功能的方法。动态分发可以通过牺牲一些运行时性能来为你的代码提供一些灵活性。这些灵活性可以用来实现有助于代码可维护性的面向对象模式。Rust 也有像所有权这样不同于面向对象语言的功能。面向对象模式并不总是利用 Rust 实力的最好方式。
阅读本章后,不管你是否认为 Rust 是一个面向对象语言,现在你都见识了 trait 对象是一个 Rust 中获取部分面向对象功能的方法。动态分发可以通过牺牲少量运行时性能来为你的代码提供一些灵活性。这些灵活性可以用来实现有助于代码可维护性的面向对象模式。Rust 也有像所有权这样不同于面向对象语言的功能。面向对象模式并不总是利用 Rust 实力的最好方式,但也是可用的选项
接下来让我们看看另一个提供了多样灵活性的Rust功能模式。贯穿全书的模式, 我们已经和它们打过照面了,但并没有见识过它们的全部本领。让我们开始探索吧!
接下来,让我们看看另一个提供了多样灵活性的 Rust 功能:模式。贯穿全书的模式, 我们已经和它们打过照面了,但并没有见识过它们的全部本领。让我们开始探索吧!