diff --git a/README.md b/README.md index c2d7540..bb6ebe3 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,11 @@ ## 状态 -目前正向 2018 edtion 过渡,目前官方仓库状已经不再分版本提供源码了,故本仓库准备与官方保持一致。 +目前正向 2018 edtion 过渡,目前官方仓库状已经不再分版本提供源码了,故本仓库准备与官方保持一致。已更新到第十七章。 PS: * 对照源码位置:https://github.com/rust-lang/book/tree/master/src -* 官方仓库用于阅读的网页仍是之前的版本,与新版区别不大,最大的区别在于 2018 edtion 对模块功能做出较大修改,第七章基本重写。 * 每章翻译开头都带有官方链接和 commit hash,若发现与官方不一致,欢迎 Issue 或 PR :) ## 静态页面构建与文档撰写 diff --git a/src/ch09-03-to-panic-or-not-to-panic.md b/src/ch09-03-to-panic-or-not-to-panic.md index b038c37..a703cc8 100644 --- a/src/ch09-03-to-panic-or-not-to-panic.md +++ b/src/ch09-03-to-panic-or-not-to-panic.md @@ -16,7 +16,7 @@ 如果方法调用在测试中失败了,我们希望这个测试都失败,即便这个方法并不是需要测试的功能。因为 `panic!` 是测试如何被标记为失败的,调用 `unwrap` 或 `expect` 就是应该发生的事情。 -### 当你比编译器知道更多的情况 +### 当我们比编译器知道更多的情况 当你有一些其他的逻辑来确保 `Result` 会是 `Ok` 值时,调用 `unwrap` 也是合适的,虽然编译器无法理解这种逻辑。你仍然需要处理一个 `Result` 值:即使在你的特定情况下逻辑上是不可能的,你所调用的任何操作仍然有可能失败。如果通过人工检查代码来确保永远也不会出现 `Err` 值,那么调用 `unwrap` 也是完全可以接受的,这里是一个例子: diff --git a/src/ch17-00-oop.md b/src/ch17-00-oop.md index edd137f..93c73cf 100644 --- a/src/ch17-00-oop.md +++ b/src/ch17-00-oop.md @@ -1,12 +1,7 @@ # Rust 的面向对象特性 -> [ch17-00-oop.md][ch17-00] +> [ch17-00-oop.md](https://github.com/rust-lang/book/blob/master/src/ch17-00-oop.md) >
-> commit 07b0ca8c829af09d60ab4eb9e69584b6f4a96f60 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -[ch17-00]: https://github.com/rust-lang/book/blob/master/second-edition/src/ch17-00-oop.md -[commit]: https://github.com/rust-lang/book/commit/07b0ca8c829af09d60ab4eb9e69584b6f4a96f60 - -面向对象编程(Object-Oriented Programming,OOP) - -面向对象编程(Object-Oriented Programming,OOP)是一种起源于 20 世纪 60 年代的 Simula 编程语言的模式化编程方式,然后在 90 年代随着 C++ 语言开始流行。关于 OOP 是什么有很多相互矛盾的定义,在一些定义下,Rust 是面向对象的;在其他定义下,Rust 不是。在本章节中,我们会探索一些被普遍认为是面向对象的特性和这些特性是如何体现在 Rust 语言习惯中的。接着会展示如何在 Rust 中实现面向对象设计模式,并讨论这么做与利用 Rust 自身的一些优势实现的方案相比有什么取舍。 +面向对象编程(Object-Oriented Programming,OOP)是一种模式化编程方式。对象(Object)来源于 20 世纪 60 年代的 Simula 编程语言。这些对象影响了 Alan Kay 的编程架构中对象之间的消息传递。他在 1967 年创造了 **面向对象编程** 这个术语来描述这种架构。关于 OOP 是什么有很多相互矛盾的定义;在一些定义下,Rust 是面向对象的;在其他定义下,Rust 不是。在本章节中,我们会探索一些被普遍认为是面向对象的特性和这些特性是如何体现在 Rust 语言习惯中的。接着会展示如何在 Rust 中实现面向对象设计模式,并讨论这么做与利用 Rust 自身的一些优势实现的方案相比有什么取舍。 diff --git a/src/ch17-01-what-is-oo.md b/src/ch17-01-what-is-oo.md index 79ac7c6..ec35577 100644 --- a/src/ch17-01-what-is-oo.md +++ b/src/ch17-01-what-is-oo.md @@ -1,30 +1,26 @@ -## 什么是面向对象? +## 面向对象语言的特征 -> [ch17-01-what-is-oo.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch17-01-what-is-oo.md) +> [ch17-01-what-is-oo.md](https://github.com/rust-lang/book/blob/master/src/ch17-01-what-is-oo.md) >
-> commit e7df3050309924827ff828ddc668a8667652d2fe +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 关于一个语言被称为面向对象所需的功能,在编程社区内并未达成一致意见。Rust 被很多不同的编程范式影响,包括面向对象编程;比如第十三章提到了来自函数式编程的特性。面向对象编程语言所共享的一些特性往往是对象、封装和继承。让我们看一下这每一个概念的含义以及 Rust 是否支持他们。 ### 对象包含数据和行为 - - - -`Design Patterns: Elements of Reusable Object-Oriented Software` 这本书被俗称为 `The Gang of Four book`,是面向对象编程模式的目录。它这样定义面向对象编程: +Erich Gamma、Richard Helm、Ralph Johnson和 John Vlissides(Addison-Wesley Professional, 1994)的 *Design Patterns: Elements of Reusable Object-Oriented Software* 被俗称为 `The Gang of Four book`,是面向对象编程模式的目录。它这样定义面向对象编程: +> 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 块提供了在结构体和枚举之上的方法。虽然带有方法的结构体和枚举并不被 **称为** 对象,但是他们提供了与对象相同的功能,参考 Gang of Four 中对象的定义。 +在这个定义下,Rust 是面向对象的:结构体和枚举包含数据而 `impl` 块提供了在结构体和枚举之上的方法。虽然带有方法的结构体和枚举并不被 **称为** 对象,但是他们提供了与对象相同的功能,参考 Gang of Four 中对象的定义。 ### 封装隐藏了实现细节 -另一个通常与面向对象编程相关的方面是 **封装**(*encapsulation*)的思想:对象的实现细节不能被使用对象的代码获取到。唯一与对象交互的方式是通过对象提供的公有 API;使用对象的代码无法深入到对象内部并直接改变数据或者行为。封装使得改变和重构对象的内部时无需改变使用对象的代码。 +另一个通常与面向对象编程相关的方面是 **封装**(*encapsulation*)的思想:对象的实现细节不能被使用对象的代码获取到。所以唯一与对象交互的方式是通过对象提供的公有 API;使用对象的代码无法深入到对象内部并直接改变数据或者行为。封装使得改变和重构对象的内部时无需改变使用对象的代码。 就像我们在第七章讨论的那样:可以使用 `pub` 关键字来决定模块、类型、函数和方法是公有的,而默认情况下其他一切都是私有的。比如,我们可以定义一个包含一个 `i32` 类型 vector 的结构体 `AveragedCollection `。结构体也可以有一个字段,该字段保存了 vector 中所有值的平均值。这样,希望知道结构体中的 vector 的平均值的人可以随时获取它,而无需自己计算。换句话说,`AveragedCollection` 会为我们缓存平均值结果。示例 17-1 有 `AveragedCollection` 结构体的定义: @@ -92,21 +88,14 @@ impl AveragedCollection { 如果一个语言必须有继承才能被称为面向对象语言的话,那么 Rust 就不是面向对象的。无法定义一个结构体继承父结构体的成员和方法。然而,如果你过去常常在你的编程工具箱使用继承,根据你最初考虑继承的原因,Rust 也提供了其他的解决方案。 -选择继承有两个主要的原因。第一个是为了重用代码:一旦为一个类型实现了特定行为,继承可以对一个不同的类型重用这个实现。相反 Rust 代码可以使用默认 trait 方法实现来进行共享,在示例 10-15 中我们见过在 `Summarizable` trait 上增加的 `summary` 方法的默认实现。任何实现了 `Summarizable` trait 的类型都可以使用 `summary` 方法而无须进一步实现。这类似于父类有一个方法的实现,而通过继承子类也拥有这个方法的实现。当实现 `Summarizable` trait 时也可以选择覆盖 `summary` 的默认实现,这类似于子类覆盖从父类继承的方法实现。 +选择继承有两个主要的原因。第一个是为了重用代码:一旦为一个类型实现了特定行为,继承可以对一个不同的类型重用这个实现。相反 Rust 代码可以使用默认 trait 方法实现来进行共享,在示例 10-14 中我们见过在 `Summary` trait 上增加的 `summarize` 方法的默认实现。任何实现了 `Summary` trait 的类型都可以使用 `summarize` 方法而无须进一步实现。这类似于父类有一个方法的实现,而通过继承子类也拥有这个方法的实现。当实现 `Summary` trait 时也可以选择覆盖 `summarize` 的默认实现,这类似于子类覆盖从父类继承的方法实现。 第二个使用继承的原因与类型系统有关:表现为子类型可以用于父类型被使用的地方。这也被称为 **多态**(*polymorphism*),这意味着如果多种对象共享特定的属性,则可以相互替代使用。 - - - - - > 多态(Polymorphism) > -> 很多人将多态描述为继承的同义词。不过它是一个有关可以用于多种类型的代码的更广泛的概念。对于继承来说,这些类型通常是子类。Rust 则通过泛型来使得对多个不同类型的抽象成为可能,并通过 trait bounds 加强对这些类型所必须提供的内容的限制。这有时被称为 *bounded parametric polymorphism*。 - - +> 很多人将多态描述为继承的同义词。不过它是一个有关可以用于多种类型的代码的更广泛的概念。对于继承来说,这些类型通常是子类。 +> Rust 则通过泛型来使得对多个不同类型的抽象成为可能,并通过 trait bounds 加强对这些类型所必须提供的内容的限制。这有时被称为 *bounded parametric polymorphism*。 近来继承作为一种语言设计的解决方案在很多语言中失宠了,因为其时常带有共享多于所需的代码的风险。子类不应总是共享其父类的多有特征,但是继承却始终如此。如此会使程序设计更为不灵活,并引入无意义的子类方法调用,或由于方法实际并不适用于子类而造成错误的可能性。某些语言还只允许子类继承一个父类,进一步限制了程序设计的灵活性。 diff --git a/src/ch17-02-trait-objects.md b/src/ch17-02-trait-objects.md index 8345ba8..4184449 100644 --- a/src/ch17-02-trait-objects.md +++ b/src/ch17-02-trait-objects.md @@ -1,24 +1,20 @@ ## 为使用不同类型的值而设计的 trait 对象 -> [ch17-02-trait-objects.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch17-02-trait-objects.md) +> [ch17-02-trait-objects.md](https://github.com/rust-lang/book/blob/master/src/ch17-02-trait-objects.md) >
-> commit ccdd9ca7aacea4cefeb6a96e7ffb9ea91a923abd +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 在第八章中,我们谈到了 vector 只能存储同种类型元素的局限。示例 8-10 中提供了一个定义 `SpreadsheetCell` 枚举来储存整型,浮点型和文本成员的替代方案。这意味着可以在每个单元中储存不同类型的数据,并仍能拥有一个代表一排单元的 vector。这在当编译代码时就知道希望可以交替使用的类型为固定集合的情况下是完全可行的。 -然而有时,我们希望库用户在特定情况下能够扩展有效的类型集合。为了展示如何实现这一点,这里将创建一个图形用户接口(Graphical User Interface, GUI)工具的例子,其它通过遍历列表并调用每一个项目的 `draw` 方法来将其绘制到屏幕上;此乃一个 GUI 工具的常见技术。我们将要创建一个叫做 `rust_gui` 的库 crate,它含一个 GUI 库的结构。这个 GUI 库包含一些可供开发者使用的类型,比如 `Button` 或 `TextField`。在此之上,`rust_gui` 的用户希望创建自定义的可以绘制于屏幕上的类型:比如,一个程序员可能会增加 `Image`,另一个可能会增加 `SelectBox`。 +然而有时,我们希望库用户在特定情况下能够扩展有效的类型集合。为了展示如何实现这一点,这里将创建一个图形用户接口(Graphical User Interface, GUI)工具的例子,其它通过遍历列表并调用每一个项目的 `draw` 方法来将其绘制到屏幕上 —— 此乃一个 GUI 工具的常见技术。我们将要创建一个叫做 `gui` 的库 crate,它含一个 GUI 库的结构。这个 GUI 库包含一些可供开发者使用的类型,比如 `Button` 或 `TextField`。在此之上,`gui` 的用户希望创建自定义的可以绘制于屏幕上的类型:比如,一个程序员可能会增加 `Image`,另一个可能会增加 `SelectBox`。 -这个例子中并不会实现一个功能完善的 GUI 库,不过会展示其中各个部分是如何结合在一起的。编写库的时候,我们不可能知晓并定义所有其他程序员希望创建的类型。我们所知晓的是 `rust_gui` 需要记录一系列不同类型的值,并需要能够对其中每一个值调用 `draw` 方法。这里无需知道调用 `draw` 方法时具体会发生什么,只需提供可供这些值调用的方法即可。 +这个例子中并不会实现一个功能完善的 GUI 库,不过会展示其中各个部分是如何结合在一起的。编写库的时候,我们不可能知晓并定义所有其他程序员希望创建的类型。我们所知晓的是 `gui` 需要记录一系列不同类型的值,并需要能够对其中每一个值调用 `draw` 方法。这里无需知道调用 `draw` 方法时具体会发生什么,只需提供可供这些值调用的方法即可。 在拥有继承的语言中,可以定义一个名为 `Component` 的类,该类上有一个 `draw` 方法。其他的类比如 `Button`、`Image` 和 `SelectBox` 会从 `Component` 派生并因此继承 `draw` 方法。它们各自都可以覆盖 `draw` 方法来定义自己的行为,但是框架会把所有这些类型当作是 `Component` 的实例,并在其上调用 `draw`。不过 Rust 并没有继承,我们得另寻出路。 ### 定义通用行为的 trait -为了实现 `rust_gui` 所期望拥有的行为,定义一个 `Draw` trait,其包含名为 `draw` 的方法。接着可以定义一个存放 **trait 对象**(*trait object*) 的 vector。trait 对象指向一个实现了我们指定 trait 的类型实例。我们通过指定某些指针,比如 `&` 引用或 `Box` 智能指针,接着指定相关的 trait(第十九章动态大小类型部分会介绍 trait 对象必须使用指针的原因)。我们可以使用 trait 对象代替泛型或具体类型。任何使用 trait 对象的位置,Rust 的类型系统会在编译时确保任何在此上下文中使用的值会实现其 trait 对象的 trait。如此便无需在编译时就知晓所有可能的类型。 - - - +为了实现 `gui` 所期望拥有的行为,定义一个 `Draw` trait,其包含名为 `draw` 的方法。接着可以定义一个存放 **trait 对象**(*trait object*) 的 vector。trait 对象指向一个实现了我们指定 trait 的类型实例。我们通过指定某些指针,比如 `&` 引用或 `Box` 智能指针,接着指定相关的 trait(第十九章 “动态大小类型” 部分会介绍 trait 对象必须使用指针的原因)。我们可以使用 trait 对象代替泛型或具体类型。任何使用 trait 对象的位置,Rust 的类型系统会在编译时确保任何在此上下文中使用的值会实现其 trait 对象的 trait。如此便无需在编译时就知晓所有可能的类型。 之前提到过,Rust 刻意不将结构体与枚举称为 “对象”,以便与其他语言中的对象相区别。在结构体或枚举中,结构体字段中的数据和 `impl` 块中的行为是分开的,不同于其他语言中将数据和行为组合进一个称为对象的概念中。trait 对象将数据和行为两者相结合,从这种意义上说 **则** 其更类似其他语言中的对象。不过 trait 对象不同于传统的对象,因为不能向 trait 对象增加数据。trait 对象并不像其他语言中的对象那么通用:其(trait 对象)具体的作用是允许对通用行为的抽象。 @@ -34,13 +30,7 @@ pub trait Draw { 示例 17-3:`Draw` trait 的定义 -因为第十章已经讨论过如何定义 trait,这看起来应该比较眼熟。接下来就是新内容了:实例 17-4 定义了一个存放了名叫 `components` 的 vector 的结构体 `Screen`。这个 vector 的类型是 `Box`,此为一个 trait 对象:它是 `Box` 中任何实现了 `Draw` trait 的类型的替身。 - - - +因为第十章已经讨论过如何定义 trait,其语法看起来应该比较眼熟。接下来就是新内容了:实例 17-4 定义了一个存放了名叫 `components` 的 vector 的结构体 `Screen`。这个 vector 的类型是 `Box`,此为一个 trait 对象:它是 `Box` 中任何实现了 `Draw` trait 的类型的替身。 文件名: src/lib.rs @@ -50,7 +40,7 @@ this section where we talk about needing a `&` or a `Box` to be a trait object. # } # pub struct Screen { - pub components: Vec>, + pub components: Vec>, } ``` @@ -66,7 +56,7 @@ pub struct Screen { # } # # pub struct Screen { -# pub components: Vec>, +# pub components: Vec>, # } # impl Screen { @@ -128,7 +118,7 @@ pub struct Button { impl Draw for Button { fn draw(&self) { - // Code to actually draw a button + // 实际绘制按钮的代码 } } ``` @@ -142,8 +132,7 @@ impl Draw for Button { 文件名: src/main.rs ```rust,ignore -extern crate rust_gui; -use rust_gui::Draw; +use gui::Draw; struct SelectBox { width: u32, @@ -153,19 +142,19 @@ struct SelectBox { impl Draw for SelectBox { fn draw(&self) { - // Code to actually draw a select box + // code to actually draw a select box } } ``` -示例 17-8: 另一个使用 `rust_gui` 的 crate 中,在 `SelectBox` 结构体上实现 `Draw` trait +示例 17-8: 另一个使用 `gui` 的 crate 中,在 `SelectBox` 结构体上实现 `Draw` trait 库使用者现在可以在他们的 `main` 函数中创建一个 `Screen` 实例。至此可以通过将 `SelectBox` 和 `Button` 放入 `Box` 转变为 trait 对象来增加组件。接着可以调用 `Screen` 的 `run` 方法,它会调用每个组件的 `draw` 方法。示例 17-9 展示了这个实现: 文件名: src/main.rs ```rust,ignore -use rust_gui::{Screen, Button}; +use gui::{Screen, Button}; fn main() { let screen = Screen { @@ -195,18 +184,7 @@ fn main() { 当编写库的时候,我们不知道何人会在何时增加 `SelectBox` 类型,不过 `Screen` 的实现能够操作并绘制这个新类型,因为 `SelectBox` 实现了 `Draw` trait,这意味着它实现了 `draw` 方法。 -这个概念 ———— 只关心值所反映的信息而不是其具体类型 ———— 类似于动态类型语言中称为 **鸭子类型**(*duck typing*)的概念:如果它走起来像一只鸭子,叫起来像一只鸭子,那么它就是一只鸭子!在示例 17-5 中 `Screen` 上的 `run` 实现中,`run` 并不需要知道各个组件的具体类型是什么。它并不检查组件是 `Button` 或者 `SelectBox` 的实例。通过指定 `Box` 作为 `components` vector 中值的类型,我们就定义了 `Screen` 需要可以在其上调用 `draw` 方法的值。 - - - +这个概念 —— 只关心值所反映的信息而不是其具体类型 —— 类似于动态类型语言中称为 **鸭子类型**(*duck typing*)的概念:如果它走起来像一只鸭子,叫起来像一只鸭子,那么它就是一只鸭子!在示例 17-5 中 `Screen` 上的 `run` 实现中,`run` 并不需要知道各个组件的具体类型是什么。它并不检查组件是 `Button` 或者 `SelectBox` 的实例。通过指定 `Box` 作为 `components` vector 中值的类型,我们就定义了 `Screen` 需要可以在其上调用 `draw` 方法的值。 使用 trait 对象和 Rust 类型系统来进行类似鸭子类型操作的优势是无需在运行时检查一个值是否实现了特定方法或者担心在调用时因为值没有实现方法而产生错误。如果值没有实现 trait 对象所需的 trait 则 Rust 不会编译这些代码。 @@ -214,9 +192,8 @@ at all? Is there something we should clarify in the text? /Carol --> 文件名: src/main.rs -```rust,ignore -extern crate rust_gui; -use rust_gui::Screen; +```rust,ignore,does_not_compile +use gui::Screen; fn main() { let screen = Screen { @@ -234,47 +211,26 @@ fn main() { 我们会遇到这个错误,因为 `String` 没有实现 `rust_gui::Draw` trait: ```text -error[E0277]: the trait bound `std::string::String: rust_gui::Draw` is not satisfied - --> +error[E0277]: the trait bound `std::string::String: gui::Draw` is not satisfied + --> src/main.rs:7:13 | - 4 | Box::new(String::from("Hi")), - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `rust_gui::Draw` is not + 7 | Box::new(String::from("Hi")), + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait gui::Draw is not implemented for `std::string::String` | - = note: required for the cast to the object type `rust_gui::Draw` + = note: required for the cast to the object type `gui::Draw` ``` 这告诉了我们,要么是我们传递了并不希望传递给 `Screen` 的类型并应该提供其他类型,要么应该在 `String` 上实现 `Draw` 以便 `Screen` 可以调用其上的 `draw`。 ### trait 对象执行动态分发 -回忆一下第十章讨论过的,当对泛型使用 trait bound 时编译器所进行单态化处理:编译器为每一个被泛型类型参数代替的具体类型生成了非泛型的函数和方法实现。单态化所产生的代码进行 **静态分发**(*static dispatch*)。静态分发发生于编译器在编译时就知晓调用了什么方法的时候。这与 **动态分发** (*dynamic dispatch*)相对,这时编译器在编译时无法知晓调用了什么方法。在这种情况下,编译器会生成在运行时确定调用了什么方法的代码。 +回忆一下第十章讨论过的,当对泛型使用 trait bound 时编译器所进行单态化处理:编译器为每一个被泛型类型参数代替的具体类型生成了非泛型的函数和方法实现。单态化所产生的代码进行 **静态分发**(*static dispatch*)。静态分发发生于编译器在编译时就知晓调用了什么方法的时候。这与 **动态分发** (*dynamic dispatch*)相对,这时编译器在编译时无法知晓调用了什么方法。在动态分发的情况下,编译器会生成在运行时确定调用了什么方法的代码。 - - - -当使用 trait 对象时,Rust 必须使用动态分发。编译器无法知晓所有可能用于 trait 对象代码的类型,所以它也不知道应该调用哪个类型的哪个方法实现。为此,Rust 在运行时使用 trait 对象中的指针来知晓需要调用哪个方法。动态分发也阻止编译器有选择的内联方法代码,这会相应的禁用一些优化。尽管在编写和支持代码的过程中确实获得了额外的灵活性,但仍然需要权衡取舍。 +当使用 trait 对象时,Rust 必须使用动态分发。编译器无法知晓所有可能用于 trait 对象代码的类型,所以它也不知道应该调用哪个类型的哪个方法实现。为此,Rust 在运行时使用 trait 对象中的指针来知晓需要调用哪个方法。动态分发也阻止编译器有选择的内联方法代码,这会相应的禁用一些优化。尽管在编写示例 17-5和可以支持示例 17-9 中的代码的过程中确实获得了额外的灵活性,但仍然需要权衡取舍。 ### Trait 对象要求对象安全 - - - - 只有 **对象安全**(*object safe*)的 trait 才可以组成 trait 对象。围绕所有使得 trait 对象安全的属性存在一些复杂的规则,不过在实践中,只涉及到两条规则。如果一个 trait 中所有的方法有如下属性时,则该 trait 是对象安全的: - 返回值类型不为 `Self` @@ -292,12 +248,11 @@ pub trait Clone { `String` 实现了 `Clone` trait,当在 `String` 实例上调用 `clone` 方法时会得到一个 `String` 实例。类似的,当调用 `Vec` 实例的 `clone` 方法会得到一个 `Vec` 实例。`clone` 的签名需要知道什么类型会代替 `Self`,因为这是它的返回值。 - 如果尝试做一些违反有关 trait 对象的对象安全规则的事情,编译器会提示你。例如,如果尝试实现示例 17-4 中的 `Screen` 结构体来存放实现了 `Clone` trait 而不是 `Draw` trait 的类型,像这样: -```rust,ignore +```rust,ignore,does_not_compile pub struct Screen { - pub components: Vec>, + pub components: Vec>, } ``` @@ -305,11 +260,11 @@ pub struct Screen { ```text error[E0038]: the trait `std::clone::Clone` cannot be made into an object - --> + --> src/lib.rs:2:5 | -2 | pub components: Vec>, +2 | pub components: Vec>, | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::clone::Clone` cannot be - made into an object +made into an object | = note: the trait cannot require that `Self : Sized` ``` diff --git a/src/ch17-03-oo-design-patterns.md b/src/ch17-03-oo-design-patterns.md index be36961..c2ae9a9 100644 --- a/src/ch17-03-oo-design-patterns.md +++ b/src/ch17-03-oo-design-patterns.md @@ -1,19 +1,13 @@ ## 面向对象设计模式的实现 -> [ch17-03-oo-design-patterns.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch17-03-oo-design-patterns.md) +> [ch17-03-oo-design-patterns.md](https://github.com/rust-lang/book/blob/master/src/ch17-03-oo-design-patterns.md) >
-> commit b18f90970ab7223ee8af18ef466a5ba6ff8482ef +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -**状态模式**(*state pattern*)是一个面向对象设计模式。该模式的关键在于一个值有某些内部状态,体现为一系列的 **状态对象**,同时值的行为随着其内部状态而改变。状态对象共享功能 —— 当然,在 Rust 中使用结构体和 trait 而不是对象和继承。每一个状态对象代表负责其自身的行为和当需要改变为另一个状态时的规则的状态。持有任何一个这种状态对象的值对于不同状态的行为以及何时状态转移毫不知情。 - - - +**状态模式**(*state pattern*)是一个面向对象设计模式。该模式的关键在于一个值有某些内部状态,体现为一系列的 **状态对象**,同时值的行为随着其内部状态而改变。状态对象共享功能:当然,在 Rust 中使用结构体和 trait 而不是对象和继承。每一个状态对象代表负责其自身的行为和当需要改变为另一个状态时的规则的状态。持有任何一个这种状态对象的值对于不同状态的行为以及何时状态转移毫不知情。 使用状态模式意味着当程序的业务需求改变时,无需改变值持有状态或者使用值的代码。我们只需更新某个状态对象中的代码来改变其规则,或者是增加更多的状态对象。让我们看看一个有关状态模式和如何在 Rust 中使用它的例子。 - -让我们看看一个状态设计模式的例子以及如何在 Rust 中使用他们。**状态模式**(*state pattern*)是指一个值有某些内部状态,而它的行为随着其内部状态而改变。内部状态由一系列继承了共享功能的对象表现(我们使用结构体和 trait 因为 Rust 没有对象和继承)。每一个状态对象负责它自身的行为和当需要改变为另一个状态时的规则。持有任何一个这种状态对象的值对于不同状态的行为以及何时状态转移毫不知情。当将来需求改变时,无需改变值持有状态或者使用值的代码。我们只需更新某个状态对象中的代码来改变它的规则,或者是增加更多的状态对象。 - 为了探索这个概念,我们将实现一个增量式的发布博文的工作流。这个博客的最终功能看起来像这样: 1. 博文从空白的草案开始。 @@ -23,12 +17,11 @@ 任何其他对博文的修改尝试都是没有作用的。例如,如果尝试在请求审核之前通过一个草案博文,博文应该保持未发布的状态。 -示例 17-11 展示这个工作流的代码形式。这是一个我们将要在一个叫做 `blog` 的库 crate 中实现的 API 的示例: +示例 17-11 展示这个工作流的代码形式:这是一个我们将要在一个叫做 `blog` 的库 crate 中实现的 API 的示例。这段代码还不能编译,因为还未实现 `blog`。 文件名: src/main.rs ```rust,ignore -extern crate blog; use blog::Post; fn main() { @@ -51,26 +44,17 @@ fn main() { 接下来,我们希望能够请求审核博文,而在等待审核的阶段 `content` 应该仍然返回空字符串。最后当博文审核通过,它应该被发表,这意味着当调用 `content` 时博文的文本将被返回。 - - - - 注意我们与 crate 交互的唯一的类型是 `Post`。这个类型会使用状态模式并会存放处于三种博文所可能的状态之一的值 —— 草案,等待审核和发布。状态上的改变由 `Post` 类型内部进行管理。状态依库用户对 `Post` 实例调用的方法而改变,但是不能直接管理状态变化。这也意味着用户不会在状态上犯错,比如在过审前发布博文。 ### 定义 `Post` 并新建一个草案状态的实例 让我们开始实现这个库吧!我们知道需要一个公有 `Post` 结构体来存放一些文本,所以让我们从结构体的定义和一个创建 `Post` 实例的公有关联函数 `new` 开始,如示例 17-12 所示。还需定义一个私有 trait `State`。`Post` 将在私有字段 `state` 中存放一个 `Option` 类型的 trait 对象 `Box`。稍后将会看到为何 `Option` 是必须的。 -`State` trait 定义了所有不同状态的博文所共享的行为,同时 `Draft`、`PendingReview` 和 `Published` 状态都会实现 `State` 状态。现在这个 trait 并没有任何方法,同时开始将只定义 `Draft` 状态因为这是我们希望博文的初始状态: - 文件名: src/lib.rs ```rust pub struct Post { - state: Option>, + state: Option>, content: String, } @@ -90,13 +74,16 @@ struct Draft {} impl State for Draft {} ``` -示例 17-12: `Post` 结构体的定义和新建 `Post` 实例的 `new` 函数,`State` trait 和结构体 `Draft` -当创建新的 `Post` 时,我们将其 `state` 字段设置为一个存放了 `Box` 的 `Some` 值。这个 `Box` 指向一个 `Draft` 结构体新实例。这确保了无论何时新建一个 `Post` 实例,它都会从草案开始。因为 `Post` 的 `state` 字段是私有的,也就无法创建任何其他状态的 `Post` 了!。 +示例 17-12: `Post` 结构体的定义和新建 `Post` 实例的 `new` 函数,`State` trait 和结构体 `Draft` + +`State` trait 定义了所有不同状态的博文所共享的行为,同时 `Draft`、`PendingReview` 和 `Published` 状态都会实现 `State` 状态。现在这个 trait 并没有任何方法,同时开始将只定义 `Draft` 状态因为这是我们希望博文的初始状态。 + +当创建新的 `Post` 时,我们将其 `state` 字段设置为一个存放了 `Box` 的 `Some` 值。这个 `Box` 指向一个 `Draft` 结构体新实例。这确保了无论何时新建一个 `Post` 实例,它都会从草案开始。因为 `Post` 的 `state` 字段是私有的,也就无法创建任何其他状态的 `Post` 了!。`Post::new` 函数中将 `content` 设置为新建的空 `String`。 ### 存放博文内容的文本 -在 `Post::new` 函数中,我们设置 `content` 字段为新的空 `String`。在示例 17-11 中,展示了我们希望能够调用一个叫做 `add_text` 的方法并向其传递一个 `&str` 来将文本增加到博文的内容中。选择实现为一个方法而不是将 `content` 字段暴露为 `pub` 。这意味着之后可以实现一个方法来控制 `content` 字段如何被读取。`add_text` 方法是非常直观的,让我们在示例 17-13 的 `impl Post` 块中增加一个实现: +在示例 17-11 中,展示了我们希望能够调用一个叫做 `add_text` 的方法并向其传递一个 `&str` 来将文本增加到博文的内容中。选择实现为一个方法而不是将 `content` 字段暴露为 `pub` 。这意味着之后可以实现一个方法来控制 `content` 字段如何被读取。`add_text` 方法是非常直观的,让我们在示例 17-13 的 `impl Post` 块中增加一个实现: 文件名: src/lib.rs @@ -117,7 +104,7 @@ impl Post { `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 展示了这个占位符实现: @@ -142,19 +129,13 @@ impl Post { ### 请求审核博文来改变其状态 -接下来需要增加请求审核博文的功能,这应当将其状态由 `Draft` 改为 `PendingReview`。我们希望为 `Post` 增加一个获取 `self` 可变引用的公有方法 `request_review`。接着将 `Post` 当前状态内部的 `request_review` 方法而这第二个 `request_review` 方法会消费当前的状态并返回一个新状态。示例 17-15 展示了这个代码: - - - +接下来需要增加请求审核博文的功能,这应当将其状态由 `Draft` 改为 `PendingReview`。示例 17-15 展示了这个代码: 文件名: src/lib.rs ```rust # pub struct Post { -# state: Option>, +# state: Option>, # content: String, # } # @@ -168,13 +149,13 @@ impl Post { } trait State { - fn request_review(self: Box) -> Box; + fn request_review(self: Box) -> Box; } struct Draft {} impl State for Draft { - fn request_review(self: Box) -> Box { + fn request_review(self: Box) -> Box { Box::new(PendingReview {}) } } @@ -182,7 +163,7 @@ impl State for Draft { struct PendingReview {} impl State for PendingReview { - fn request_review(self: Box) -> Box { + fn request_review(self: Box) -> Box { self } } @@ -190,11 +171,9 @@ impl State for PendingReview { 示例 17-15: 实现 `Post` 和 `State` trait 的 `request_review` 方法 -这里给 `State` trait 增加了 `request_review` 方法;所有实现了这个 trait 的类型现在都需要实现 `request_review` 方法。注意不同于使用 `self`、 `&self` 或者 `&mut self` 作为方法的第一个参数,这里使用了 `self: Box`。这个语法意味着这个方法调用只对这个类型的 `Box` 有效。这个语法获取了 `Box` 的所有权,使老状态无效化以便 `Post` 的状态值可以将自身转换为新状态。 +这里为 `Post` 增加一个获取 `self` 可变引用的公有方法 `request_review`。接着将 `Post` 当前状态内部的 `request_review` 方法而这第二个 `request_review` 方法会消费当前的状态并返回一个新状态。 - - +这里给 `State` trait 增加了 `request_review` 方法;所有实现了这个 trait 的类型现在都需要实现 `request_review` 方法。注意不同于使用 `self`、 `&self` 或者 `&mut self` 作为方法的第一个参数,这里使用了 `self: Box`。这个语法意味着这个方法调用只对这个类型的 `Box` 有效。这个语法获取了 `Box` 的所有权,使老状态无效化以便 `Post` 的状态值可以将自身转换为新状态。 为了消费老状态,`request_review` 方法需要获取状态值的所有权。这也就是 `Post` 的 `state` 字段中 `Option` 的来历:调用 `take` 方法将 `state` 字段中的 `Some` 值取出并留下一个 `None`,因为 Rust 不允许在结构体中存在空的字段。这使得我们将 `state` 值移动出 `Post` 而不是借用它。接着将博文的 `state` 值设置为这个操作的结果。 @@ -214,7 +193,7 @@ which changes the state of Post-- I've tried to clarify. /Carol --> ```rust # pub struct Post { -# state: Option>, +# state: Option>, # content: String, # } # @@ -228,19 +207,19 @@ impl Post { } trait State { - fn request_review(self: Box) -> Box; - fn approve(self: Box) -> Box; + fn request_review(self: Box) -> Box; + fn approve(self: Box) -> Box; } struct Draft {} impl State for Draft { -# fn request_review(self: Box) -> Box { +# fn request_review(self: Box) -> Box { # Box::new(PendingReview {}) # } # // --snip-- - fn approve(self: Box) -> Box { + fn approve(self: Box) -> Box { self } } @@ -248,12 +227,12 @@ impl State for Draft { struct PendingReview {} impl State for PendingReview { -# fn request_review(self: Box) -> Box { +# fn request_review(self: Box) -> Box { # self # } # // --snip-- - fn approve(self: Box) -> Box { + fn approve(self: Box) -> Box { Box::new(Published {}) } } @@ -261,11 +240,11 @@ impl State for PendingReview { struct Published {} impl State for Published { - fn request_review(self: Box) -> Box { + fn request_review(self: Box) -> Box { self } - fn approve(self: Box) -> Box { + fn approve(self: Box) -> Box { self } } @@ -286,7 +265,7 @@ impl State for Published { # fn content<'a>(&self, post: &'a Post) -> &'a str; # } # pub struct Post { -# state: Option>, +# state: Option>, # content: String, # } # @@ -305,11 +284,9 @@ impl Post { 这里调用 `Option` 的 `as_ref` 方法是因为需要 `Option` 中值的引用而不是获取其所有权。因为 `state` 是一个 `Option>`,调用 `as_ref` 会返回一个 `Option<&Box>`。如果不调用 `as_ref`,怎会得到一个错误,因为不能将 `state` 移动出借用的 `&self` 函数参数。 -接着调用 `unwrap` 方法,这里我们知道它永远也不会 panic,因为 `Post` 的所有方法都确保在他们返回时 `state` 会有一个 `Some` 值。这就是一个第十二章讨论过的我们知道 `None` 是不可能的而编译器却不能理解的情况。 - -接着我们就有了一个 `&Box`,当调用其 `content` 时,解引用强制多态会作用于 `&` 和 `Box` 这样最终会调用实现了 `State` trait 的类型的 `content` 方法。 +接着调用 `unwrap` 方法,这里我们知道它永远也不会 panic,因为 `Post` 的所有方法都确保在他们返回时 `state` 会有一个 `Some` 值。这就是一个第十二章 “当我们比编译器知道更多的情况” 部分讨论过的我们知道 `None` 是不可能的而编译器却不能理解的情况。 -这意味着需要为 `State` trait 定义增加 `content`,这也是放置根据所处状态返回什么内容的逻辑的地方,如示例 17-18 所示: +接着我们就有了一个 `&Box`,当调用其 `content` 时,解引用强制多态会作用于 `&` 和 `Box` 这样最终会调用实现了 `State` trait 的类型的 `content` 方法。这意味着需要为 `State` trait 定义增加 `content`,这也是放置根据所处状态返回什么内容的逻辑的地方,如示例 17-18 所示: 文件名: src/lib.rs @@ -339,15 +316,9 @@ impl State for Published { 这里增加了一个 `content` 方法的默认实现来返回一个空字符串 slice。这意味着无需为 `Draft` 和 `PendingReview` 结构体实现 `content` 了。`Published` 结构体会覆盖 `content` 方法并会返回 `post.content` 的值。 - 注意这个方法需要生命周期注解,如第十章所讨论的。这里获取 `post` 的引用作为参数,并返回 `post` 一部分的引用,所以返回的引用的生命周期与 `post` 参数相关。 - - - -现在示例完成了 ———— 现在示例 17-11 中所有的代码都能工作!我们通过发布博文工作流的规则实现了状态模式。围绕这些规则的逻辑都存在于状态对象中而不是分散在 `Post` 之中。 +现在示例完成了 —— 现在示例 17-11 中所有的代码都能工作!我们通过发布博文工作流的规则实现了状态模式。围绕这些规则的逻辑都存在于状态对象中而不是分散在 `Post` 之中。 ### 状态模式的权衡取舍 @@ -367,7 +338,7 @@ all of 17-11 now work? --> 另一个缺点是我们会发现一些重复的逻辑。为了消除他们,可以尝试为 `State` trait 中返回 `self` 的 `request_review` 和 `approve` 方法增加默认实现,不过这会违反对象安全性,因为 trait 不知道 `self` 具体是什么。我们希望能够将 `State` 作为一个 trait 对象,所以需要其方法是对象安全的。 -另一个重复是 `Post` 中 `request_review` 和 `approve` 这两个类似的实现。他们都委托调用了 `state` 字段中 `Option` 值的同一方法,并在结果中为 `state` 字段设置了新值。如果 `Post` 中的很多方法都遵循这个模式,我们可能会考虑定义一个宏来消除重复(查看附录 D 以了解宏)。 +另一个重复是 `Post` 中 `request_review` 和 `approve` 这两个类似的实现。他们都委托调用了 `state` 字段中 `Option` 值的同一方法,并在结果中为 `state` 字段设置了新值。如果 `Post` 中的很多方法都遵循这个模式,我们可能会考虑定义一个宏来消除重复(查看第十九章的 “宏” 部分)。 完全按照面向对象语言的定义实现这个模式并没有没有尽可能的利用 Rust 的优势。让我们看看一些代码中可以做出的修改,来将无效的状态和状态转移变为编译时错误。 @@ -380,6 +351,8 @@ all of 17-11 now work? --> 文件名: src/main.rs ```rust,ignore +# use blog::Post; + fn main() { let mut post = Post::new(); @@ -409,7 +382,7 @@ impl Post { } pub fn content(&self) -> &str { - &self.content + &self.content } } @@ -475,7 +448,6 @@ impl PendingReviewPost { 文件名: src/main.rs ```rust,ignore -extern crate blog; use blog::Post; fn main() { @@ -495,7 +467,7 @@ fn main() { 不得不修改 `main` 来重新赋值 `post` 使得这个实现不再完全遵守面向对象的状态模式:状态间的转换不再完全封装在 `Post` 实现中。然而,得益于类型系统和编译时类型检查我们得到了不可能拥有无效状态的属性!这确保了特定的 bug,比如显示未发布博文的内容,将在部署到生产环境之前被发现。 -尝试在这一部分开始所建议的增加额外需求的任务来体会使用这个版本的代码是何感觉。 +尝试为示例 17-20 之后的 `blog` crate 实现这一部分开始所建议的增加额外需求的任务来体会使用这个版本的代码是何感觉。注意在这个设计中一些需求可能已经完成了。 即便 Rust 能够实现面向对象设计模式,也有其他像将状态编码进类型这样的模式存在。这些模式有着不同的权衡取舍。虽然你可能非常熟悉面向对象模式,重新思考这些问题来利用 Rust 提供的像在编译时避免一些 bug 这样有益功能。在 Rust 中面向对象模式并不总是最好的解决方案,因为 Rust 拥有像所有权这样的面向对象语言所没有的功能。