wip: 2024 edition

pull/875/head
KaiserY 2 weeks ago
parent 941eed2f24
commit 2bb698268d

@ -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) <!-- https://github.com/rust-lang/book/blob/main/src/ch18-01-what-is-oo.md -->
> <br> <!-- commit 56ec353290429e6547109e88afea4de027b0f1a9 -->
> commit 398d6f48d2e6b7b15efd51c4541d446e89de3892
关于一门语言必须具备哪些特征才能被视为面向对象目前在编程社区中并没有共识。Rust 受到了许多编程范式的影响包括面向对象编程OOP例如在第 13 章中,我们探讨了来自函数式编程的特性。可以说,面向对象的语言共有一些共同的特征,即对象、封装和继承。我们将会讨论这些特征分别是什么,以及 Rust 是否支持它们。 关于一门语言必须具备哪些特征才能被视为面向对象目前在编程社区中并没有共识。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 > Object-oriented programs are made up of objects. An *object* packages both
> data and the procedures that operate on that data. The procedures are > data and the procedures that operate on that data. The procedures are
> typically called *methods* or *operations*. > 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> <span class="filename">文件名src/lib.rs</span>
@ -30,9 +29,9 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-01/src/lib.rs}} {{#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> <span class="filename">文件名src/lib.rs</span>
@ -40,17 +39,17 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-02/src/lib.rs:here}} {{#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` 而不能修改它。 `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` 的使用来封装实现细节。 如果封装被认为是面向对象语言所必要的特征,那么 Rust 满足这个要求。在代码中不同的部分控制 `pub` 的使用来封装实现细节。
## 继承作为类型系统与代码共享 ## 继承作为类型系统与代码共享
**继承***Inheritance*)是一种机制:一个对象可以从另一个对象的定义中继承元素,从而获得父对象的数据和行为,无需再次定义。 **继承***Inheritance*)是一种机制:一个对象可以从另一个对象的定义中继承元素,从而获得父对象的数据和行为,无需再次定义。
@ -60,13 +59,14 @@
选择继承有两个主要的原因。其一是代码复用:您可以为一种类型实现特定的行为,继承可将其复用到不同的类型上。在 Rust 代码中可以使用默认 trait 方法实现来进行有限的代码复用,就像示例 10-14 中在 `Summary` trait 上增加的 `summarize` 方法的默认实现。任何实现了 `Summary` trait 的类型都可以使用 `summarize` 方法而无须进一步实现。这类似于父类有一个方法的实现,继承的子类也拥有这个方法的实现。当实现 `Summary` trait 时也可以选择覆盖 `summarize` 的默认实现,这类似于子类覆盖从父类继承方法的实现。 选择继承有两个主要的原因。其一是代码复用:您可以为一种类型实现特定的行为,继承可将其复用到不同的类型上。在 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 对象实现多态性。 出于这些原因Rust 使用 trait 对象而非继承。接下来我们会讨论 Rust 如何使用 trait 对象实现多态性。

@ -1,24 +1,23 @@
## 顾及不同类型值的 trait 对象 ## 顾及不同类型值的 trait 对象
> [ch18-02-trait-objects.md](https://github.com/rust-lang/book/blob/main/src/ch18-02-trait-objects.md) <!-- https://github.com/rust-lang/book/blob/main/src/ch18-02-trait-objects.md -->
> <br> <!-- commit 56ec353290429e6547109e88afea4de027b0f1a9 -->
> commit 96d4b0ec1c5e019b85604c33ceee68b3e2669d40
在第八章中,我们谈到了 vector 只能存储同种类型元素的局限。示例 8-9 中提供了一个替代方案,通过定义 `SpreadsheetCell` 枚举,来储存整型、浮点型或文本类型的成员。这意味着,我们可以在每个单元中储存不同类型的数据,并仍能拥有一个代表一排单元的 vector。只要我们需存储的值由一组固定的类型组成并且在代码编译时就知道具体会有哪些类型那么这种使用枚举的办法是完全可行的。 在第八章中,我们谈到了 vector 只能存储同种类型元素的局限。示例 8-9 中提供了一个替代方案,通过定义 `SpreadsheetCell` 枚举,来储存整型、浮点型或文本类型的变体。这意味着,我们可以在每个单元中储存不同类型的数据,并仍能拥有一个代表一排单元的 vector。只要我们需存储的值由一组固定的类型组成并且在代码编译时就知道具体会有哪些类型那么这种使用枚举的办法是完全可行的。
然而有时我们希望库用户在特定情况下能够扩展有效的类型集合。为了展示如何实现这一点这里将创建一个图形用户接口Graphical User InterfaceGUI工具的例子它通过遍历列表并调用每一个项目的 `draw` 方法来将其绘制到屏幕上 —— 此乃一个 GUI 工具的常见技术。我们将要创建一个叫做 `gui` 的库 crate它含一个 GUI 库的结构。这个 GUI 库包含一些可供开发者使用的类型,比如 `Button``TextField`。在此之上,`gui` 的用户希望创建自定义的可以绘制于屏幕上的类型:比如,一个程序员可能会增加 `Image`,另一个可能会增加 `SelectBox` 然而有时我们希望库用户在特定情况下能够扩展有效的类型集合。为了展示如何实现这一点这里将创建一个图形用户接口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 ### 定义通用行为的 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> <span class="filename">文件名src/lib.rs</span>
@ -26,9 +25,9 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-03/src/lib.rs}} {{#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> <span class="filename">文件名src/lib.rs</span>
@ -36,9 +35,9 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-04/src/lib.rs:here}} {{#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> <span class="filename">文件名src/lib.rs</span>
@ -46,9 +45,9 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-05/src/lib.rs:here}} {{#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> <span class="filename">文件名src/lib.rs</span>
@ -56,15 +55,15 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-06/src/lib.rs:here}} {{#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 对象的方法,一个 `Screen` 实例可以存放一个既能包含 `Box<Button>`,也能包含 `Box<TextField>``Vec<T>`。让我们看看它是如何工作的,接着会讲到其运行时性能影响。
### 实现 trait ### 实现 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> <span class="filename">文件名src/lib.rs</span>
@ -72,11 +71,11 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-07/src/lib.rs:here}} {{#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> <span class="filename">文件名src/main.rs</span>
@ -84,9 +83,9 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-08/src/main.rs:here}} {{#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> <span class="filename">文件名src/main.rs</span>
@ -94,15 +93,15 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-09/src/main.rs:here}} {{#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` 方法。 当编写库的时候,我们不知道何人会在何时增加 `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 不会编译这些代码。 使用 trait 对象和 Rust 类型系统来进行类似鸭子类型操作的优势是无需在运行时检查一个值是否实现了特定方法或者担心在调用时因为值没有实现方法而产生错误。如果值没有实现 trait 对象所需的 trait 则 Rust 不会编译这些代码。
例如,示例 17-10 展示了当创建一个使用 `String` 做为其组件的 `Screen` 时发生的情况: 例如,示例 18-10 展示了当创建一个使用 `String` 做为其组件的 `Screen` 时发生的情况:
<span class="filename">文件名src/main.rs</span> <span class="filename">文件名src/main.rs</span>
@ -110,9 +109,9 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-10/src/main.rs}} {{#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 ```console
{{#include ../listings/ch18-oop/listing-18-10/output.txt}} {{#include ../listings/ch18-oop/listing-18-10/output.txt}}
@ -122,12 +121,11 @@
### trait 对象执行动态分发 ### 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]: [performance-of-code-using-generics]:
ch10-01-syntax.html#泛型代码的性能 ch10-01-syntax.html#泛型代码的性能
[dynamically-sized]: ch20-03-advanced-types.html#动态大小类型和-sized-trait [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 [dyn-compatibility]: https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility
[Rust Reference ref]: https://doc.rust-lang.org/reference/items/traits.html#object-safety

@ -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) <!-- https://github.com/rust-lang/book/blob/main/src/ch18-03-oo-design-patterns.md -->
> <br> <!-- commit 8904592b6f0980c278ee52371be9af7b62541787 -->
> commit 937784b8708c24314707378ad42faeb12a334bbd
**状态模式***state pattern*)是一个面向对象设计模式。该模式的关键在于定义一系列值的内含状态。这些状态体现为一系列的 **状态对象**,同时值的行为随着其内部状态而改变。我们将编写一个博客发布结构体的例子,它拥有一个包含其状态的字段,这是一个有着 "draft"、"review" 或 "published" 状态对象 **状态模式***state pattern*)是一个面向对象设计模式。该模式的关键在于定义值的一系列内含状态。这些状态体现为一系列的**状态对象**_state objects_,同时值的行为随着其内部状态而改变。我们将编写一个博客发布结构体的例子,它拥有一个包含其状态的字段,该字段可以是 "draft"、"review" 或 "published" 状态对象之一。
状态对象共享功能:当然,在 Rust 中使用结构体和 trait 而不是对象和继承。每一个状态对象负责其自身的行为,以及该状态何时应当转移至另一个状态。持有一个状态对象的值对于不同状态的行为以及何时状态转移毫不知情。 状态对象共享功能:当然,在 Rust 中使用结构体和 trait 而不是对象和继承。每一个状态对象负责其自身的行为,以及该状态何时应当转移至另一个状态。持有一个状态对象的值对于不同状态的行为以及何时状态转移毫不知情。
使用状态模式的优点在于,程序的业务需求改变时,无需改变值持有状态或者使用值的代码。我们只需更新某个状态对象中的代码来改变其规则,或者是增加更多的状态对象。 使用状态模式的优点在于,程序的业务需求改变时,无需改变值持有状态或者使用值的代码。我们只需更新某个状态对象中的代码来改变其规则,或者是增加更多的状态对象。
首先我们将以一种更加传统的面向对象的方式实现状态模式,接着使用一种更 Rust 一点的方式。让我们使用状态模式增量式地实现一个发布博文的工作流以探索这个概念。 首先我们将以一种更加传统的面向对象的方式实现状态模式,接着使用一种在 Rust 中更自然的方式。让我们使用状态模式来增量式地实现一个发布博文的工作流以探索这个概念。
这个博客的最终功能看起来像这样: 最终功能看起来像这样:
1. 博文从空白的草开始。 1. 博文从空白的草稿开始。
2. 一旦草完成,请求审核博文。 2. 一旦草稿完成,请求审核博文。
3. 一旦博文过审,它将被发表。 3. 一旦博文过审,它将被发表。
4. 只有被发表的博文的内容会被打印,这样就不会意外打印出没有被审核的博文的文本。 4. 只有被发表的博文的内容会被打印,这样就不会意外打印出没有被审核的博文的文本。
任何其他对博文的修改尝试都是没有作用的。例如,如果尝试在请求审核之前通过一个草案博文,博文应该保持未发布的状态。 任何其他对博文的修改尝试都不会生效。例如,如果尝试在请求审核之前通过一个草稿博文,博文应该保持未发布的状态。
示例 17-11 展示这个工作流的代码形式:这是一个我们将要在一个叫做 `blog` 的库 crate 中实现的 API 的示例。这段代码还不能编译,因为还未实现 `blog` 示例 18-11 展示这个工作流的代码形式:这是一个我们将要在一个叫做 `blog` 的库 crate 中实现的 API 的示例。这段代码还不能编译,因为还未实现 `blog` crate
<span class="filename">文件名src/main.rs</span> <span class="filename">文件名src/main.rs</span>
@ -29,17 +28,17 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-11/src/main.rs:all}} {{#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` 时博文的文本将被返回。 接下来,我们希望能够请求审核博文,而在等待审核的阶段 `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>` 是必须的。 `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}} {{#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> <span class="filename">文件名src/lib.rs</span>
@ -65,13 +64,13 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-13/src/lib.rs:here}} {{#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> <span class="filename">文件名src/lib.rs</span>
@ -79,13 +78,13 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-14/src/lib.rs:here}} {{#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> <span class="filename">文件名src/lib.rs</span>
@ -93,13 +92,13 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-15/src/lib.rs:here}} {{#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` 方法会消费当前的状态并返回一个新状态。 这里为 `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` 的状态值可转换为一个新状态。 这里给 `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` 值。 我们需要将 `state` 临时设置为 `None` 来获取 `state` 值,即老状态的所有权,而不是使用 `self.state = self.state.request_review();` 这样的代码直接更新状态值。这确保了当 `Post` 被转换为新状态后不能再使用老 `state` 值。
@ -107,11 +106,11 @@
现在我们能看出状态模式的优势了:无论 `state` 是何值,`Post` 的 `request_review` 方法都是一样的。每个状态只负责它自己的规则。 现在我们能看出状态模式的优势了:无论 `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> <span class="filename">文件名src/lib.rs</span>
@ -119,13 +118,13 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-16/src/lib.rs:here}} {{#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` 状态。 这里为 `State` trait 增加了 `approve` 方法,并新增了一个实现了 `State` 的结构体,`Published` 状态。
类似于 `PendingReview``request_review` 的工作方式,如果对 `Draft` 调用 `approve` 方法,并没有任何效果,因为它会返回 `self`。当对 `PendingReview` 调用 `approve` 时,它返回一个新的、装箱的 `Published` 结构体的实例。`Published` 结构体实现了 `State` trait同时对于 `request_review``approve` 两方法来说,它返回自身,因为在这两种情况博文应该保持 `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> <span class="filename">文件名src/lib.rs</span>
@ -133,15 +132,15 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-17/src/lib.rs:here}} {{#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` 方法的返回值。 因为目标是将所有像这样的规则保持在实现了 `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` 函数参数。 这里调用 `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> <span class="filename">文件名src/lib.rs</span>
@ -149,17 +148,17 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-18/src/lib.rs:here}} {{#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` 参数相关。 注意这个方法需要生命周期注解,如第十章所讨论的。这里获取 `post` 的引用作为参数,并返回 `post` 一部分的引用,所以返回的引用的生命周期与 `post` 参数相关。
现在示例完成了 —— 现在示例 17-11 中所有的代码都能工作!我们通过发布博文工作流的规则实现了状态模式。围绕这些规则的逻辑都存在于状态对象中而不是分散在 `Post` 之中。 现在示例完成了 —— 现在示例 18-11 中所有的代码都能工作!我们通过发布博文工作流的规则实现了状态模式。围绕这些规则的逻辑都存在于状态对象中而不是分散在 `Post` 之中。
> #### 为什么不用枚举? > #### 为什么不用枚举?
> >
> 你可能会好奇为什么不用包含不同可能的博文状态的 `enum` 作为变量。这确实是一个可能的方案尝试实现并对比最终结果来看看哪一种更适合你!使用枚举的一个缺点是每一个检查枚举值的地方都需要一个 `match` 表达式或类似的代码来处理所有可能的成员。这相比 trait 对象模式可能显得更重复。 > 你可能会好奇为什么不用包含不同可能的博文状态变体`enum` 作为变量。这确实是一个可能的方案尝试实现并对比最终结果来看看哪一种更适合你!使用枚举的一个缺点是每一个检查枚举值的地方都需要一个 `match` 表达式或类似的代码来处理所有可能的变体。这相比 trait 对象模式可能显得更重复。
### 状态模式的权衡取舍 ### 状态模式的权衡取舍
@ -171,23 +170,23 @@
这个实现易于扩展增加更多功能。为了体会使用此模式维护代码的简洁性,请尝试如下一些建议: 这个实现易于扩展增加更多功能。为了体会使用此模式维护代码的简洁性,请尝试如下一些建议:
- 增加 `reject` 方法将博文的状态从 `PendingReview` 变回 `Draft` - 增加 `reject` 方法将博文的状态从 `PendingReview` 变回 `Draft`
- 在将状态变为 `Published` 之前要两次 `approve` 调用 - 在将状态变为 `Published` 之前要两次 `approve` 调用
- 只允许博文处于 `Draft` 状态时增加文本内容。提示:让状态对象负责内容可能发生什么改变,但不负责修改 `Post` - 只允许博文处于 `Draft` 状态时增加文本内容。提示:让状态对象负责内容可能发生什么改变,但不负责修改 `Post`
状态模式的一个缺点是因为状态实现了状态之间的转换,一些状态会相互联系。如果在 `PendingReview``Published` 之间增加另一个状态,比如 `Scheduled`,则不得不修改 `PendingReview` 中的代码来转移到 `Scheduled`。如果 `PendingReview` 无需因为新增的状态而改变就更好了,不过这意味着切换到另一种设计模式。 状态模式的一个缺点是因为状态实现了状态之间的转换,一些状态会相互联系。如果在 `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 的类型检查就会将任何在只能使用发布博文的地方使用草博文的尝试变为编译时错误。 我们将展示如何稍微反思状态模式来进行一系列不同的权衡取舍。不同于完全封装状态和状态转移使得外部代码对其毫不知情我们将状态编码进不同的类型。如此Rust 的类型检查就会将任何在只能使用发布博文的地方使用草稿博文的尝试变为编译时错误。
让我们考虑一下示例 17-11 中 `main` 的第一部分: 让我们考虑一下示例 18-11 中 `main` 的第一部分:
<span class="filename">文件名src/main.rs</span> <span class="filename">文件名src/main.rs</span>
@ -195,7 +194,7 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-11/src/main.rs:here}} {{#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> <span class="filename">文件名src/lib.rs</span>
@ -203,17 +202,17 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-19/src/lib.rs}} {{#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` 仍然有一个 `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> <span class="filename">文件名src/lib.rs</span>
@ -221,11 +220,11 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-20/src/lib.rs:here}} {{#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` 方法,现在我们就将发博文的工作流编码进了类型系统。 `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> <span class="filename">文件名src/main.rs</span>
@ -233,19 +232,19 @@
{{#rustdoc_include ../listings/ch18-oop/listing-18-21/src/main.rs}} {{#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#当我们比编译器知道更多的情况 [more-info-than-rustc]: ch09-03-to-panic-or-not-to-panic.html#当我们比编译器知道更多的情况
[macros]: ch20-05-macros.html#宏 [macros]: ch20-05-macros.html#宏

Loading…
Cancel
Save