# Rust 的一些面向对象编程特性 **Object Oriented Programming Features of Rust** 面向对象编程方法,object-oriented programming, OOP, 是建模程序的一种方法。对象是在 20 世纪 60 年代,在编程语言 Simula 中所引入的一个程序化概念。正是那些对象,影响了 Alan Kay 的编程架构,其中对象会相互传递消息。为描述这种架构,他在 1967 年创造了面向对象编程这个术语。有许多互相竞争的定义,都描述了 OOP 是什么,而根据其中一些定义,Rust 属于面向对象的,但根据另一些,Rust 则不属于面向对象的。在本章中,咱们将探讨通常被看作是面向对象的一些特征,以及这些特征怎样被转译为 Rust 的习惯说法。随后咱们将给出在 Rust 怎样实现面向对象的设计模式,并讨论在这样做,与相反采用 Rust 的一些长处来实现解决方案,之间的权衡取舍。 ## 面向对象语言的特征 **Characteristics of Object-Oriented Languages** 在编程界,并无关于某门被视为面向对象的,而必须具有哪些特性的共识。Rust 受了许多编程范式,programming paradigms,的影响,其中就包括 OOP;比如在第 13 章中,咱们就曾探讨过,那些来自于函数式编程的特性。可以说,那些 OOP 的语言,确实是共用了一些确切的特征的,那即是对象、封装与继承等。下面就来看看这些特征各自指的是什么,以及 Rust 是否支持他们。 ### 对象包含了数据及行为 **Objects Contain Data and Behavior** Erich Gamma、Richard Helm、Ralph Johnson 及 John Vlissides 等的合著 *Design Patterns: Elements of Reusable Object-Oriented Software* (Addison-Wesley Professional, 1994),又被通俗地叫做 *The Gang of Four* 书,便是面向对象设计模式的一个目录。该书像下面这样定义了 OOP: > 面向对象程序是由对象所组成的。*对象,an object* 同时打包了数据与运行在那数据上的过程。这些过程一般就叫做 *方法,methods* 或 *操作,operations*。 运用这个定义,Rust 便是面向对象的:结构体与枚举均有着数据,而 `impl` 块则提供了结构体与枚举上的那些方法。即使有着方法的那些结构体与枚举未*被称作* 对象,根据 The Gang of Four 的对象定义,他们提供了同样的功能。 ### 隐藏了实现细节的封装 **Encapsulation that Hides Implementation Details** 通常与 OOP 相关的另一方面的 *封装,encapsulation*,是指对于用到该对象的代码,对象实现细节是不可访问的。由此,与对象交互的唯一方式,便是经由该对象的公开 API;运用对象的代码,不应具备到达该对象内部,而直接改变数据或行为的能力。这实现了程序员在无需修改用到对象的那些代码之下,修改或重构对象的那些内部代码。 在第 7 章中,咱们曾讨论过怎样控制封装:咱们可以使用 `pub` 关键字,来决定咱们代码中,哪些模组、类型、函数与方法等应为公开的,而默认其他所有项目都是私有的。比如,咱们就可以定义有着包含 `i32` 值矢量的一个字段的 `AveragedCollection` 结构体。这个字段也可以有包含着那个矢量中值的平均数的一个字段,表示在有人需要该平均值时,不必按需计算出该平均值。换句话说,`AveragedCollection` 将为咱们缓存这个计算出的平均值。下面清单 17-1 便有着这个 `AveragedCollection` 结构体的定义: 文件名:`src/lib.rs` ```rust pub struct AveragedCollection { list: Vec, average: f64, } ``` *清单 17-1:维护着一个整数清单及该集合中项目平均数的 `AveragedCollection` 结构体* 该结构体被标记为 `pub`,从而其他代码就可以使用他,而该结构体内部的那些字段保持着私有。由于咱们打算不管何时在有某个值被添加到清单,或从清单移除时,其中的平均数也要同时被更新,因此在这个示例中这样的封装就很重要。咱们是通过实现下面清单 17-2 中所给出的 `add`、`remove` 及 `average` 方法,做到这一点的。 文件名:`src/lib.rs` ```rust impl AveragedCollection { pub fn add(&mut self, value: i32) { self.list.push(value); self.update_average(); } pub fn remove(&mut self) -> Option { let result = self.list.pop(); match result { Some(value) => { self.update_average(); Some(value) } None => None, } } pub fn average(&self) -> f64 { self.average } fn update_average(&mut self) { let total: i32 = self.list.iter().sum(); self.average = total as f64 / self.list.len() as f64; } } ``` *清单 17-2:`AveragedCollection` 上公开方法 `add`、`remove` 与 `average` 的实现* 这些公开方法 `add`、`remove` 与 `average`,是仅有的访问或修改 `AveragedCollection` 实例中数据的方式。在使用 `add` 方法或 `remove` 方法,添加或移除某个条目时,其各自的实现,就同时会调用处理更新 `average` 字段的私有 `update_average` 方法。 咱们把 `list` 与 `average` 自动留着私有,从而外部代码就无法直接添加项目到那个 `list`,或直接从那个 `list` 移除项目;不然的话,在那个`list` 变化时,`average` 字段就可能失去同步。其中的 `average` 方法,返回的是 `average` 字段中的值,这实现了外部代码读取那个 `average` 而不会修改他。 由于咱们已封装了结构体 `AveragedCollection` 的实现细节,因此咱们就可以在将来轻易地修改各个方面,诸如数据结构等。比如,咱们可以对其中的 `list` 字段,使用 `HashSet` 而非 `Vec`。只要 `add`、`remove` 及 `average` 三个公开方法的签名保持不变,那些使用 `AveragedCollection` 的代码就无需改变。而相反若咱们把 `list` 构造为公开,就未必如此了:`HashSet` 与 `Vec` 有着添加和一处条目的不同方法,由此在外部代码直接修改 `list` 时,就大概率不得不修改了。 若封装是某门语言被视为面向对象的要件,你们 Rust 是满足那种要求的。对代码的不同部分,使用抑或不使用 `pub` 的选项,实现了实现细节的封装。 ### 以类型系统及以代码共用的继承 **Inheritance as a Type System and as Code Sharing** *继承,inheritance*,乃籍以实现对象从另一对象继承一些元素,从而在不必再度定义这些元素之下,获得父辈对象数据与行为的一种机制。 若某们语言务必要有着继承,方能成为一门面向对象语言,那么 Rust 就不算是面向对象语言。在不使用宏,a macro 之下,没有定义出继承父辈结构体字段与方法实现的结构体的方法。 然而,若在编程工具箱中惯于使用继承,那么依据咱们将继承作为头等大事的自身理由,是可以运用 Rust 中别的一些办法的。 之所以选用继承,大致有两种原因。一个是代码的重用:咱们可以对一个类型实现一些特定行为,而继承就让咱们可以对另一类型重用那些实现。咱们可以使用一些默认的特质方法实现,即咱们曾在清单 10-14 中,将 `summarize` 方法的一个默认实现,添加到 `Summary` 特质上时所见到的那样,在 Rust 代码中以一种受限方式做到这点。任何实现了这个 `Summary` 特质的类型,在无需更多代码之下,都将在其上有着这个 `summarize` 方法。这与父类有着某个方法的实现,同时集成的子类也会有着该方法的实现是类似的。在实现这个 `Summary` 特质时,咱们也可以重写 `summarize` 方法的默认实现,这与子类重新继承自父类的方法实现类似。 而使用与类型系统相关继承的另一原因:即为了实现在与父类型的同一地方,使用子类型。这又被成为 *多态,polymorphism*,是指在多个对象共用了一些确切特征时,咱们可以相互替换使用他们。 > **关于多态** > > 对许多人来讲,多态等同于继承。但他实际上指的是代码可工作于多个类型数据之下的一个宽泛概念。而对于继承,这些类型则是通用的一些子类。 > > Rust 则运用了泛型,来对各异的各种可能类型加以抽象,并使用特质边界来强化这些类型所必须提供的那些约束。有时这样的做法,又被叫做 *有边界的参数化多态,bounded parametric polymorphism*。 由于继承通常有着共用了超出必要代码的风险,时至今日,其已在许多编程语言中,作为编程设计模式而失宠了。子类本不应共用其父类的全部特征,但在继承父类时却会这样做。这就会造成程序的设计有较低的灵活性。由于子类从父类继承的一些方法并不适用于子类,因此继承还会引入调用子类上无意义或引发错误方法的可能。此外,一些语言还只将运行单一继承(即子类只能从一个类继承),这进一步限制了程序设计的灵活度。 由于这些原因,Rust 便采取了运用特质对象,而非继承的方法。接下来就要看看特质对象是如何实现 Rust 中的多态。 ## 使用允许不同类型值的特质对象 **Using Trait Objects That Allow for Values of Different Types** > **注**:这类似于 Java 语言中,解决死亡钻石问题(DDD)的 [接口](https://java.xfoss.com/Ch08_Interfaces_and_Abstract_Classes.html#%E4%BD%BF%E7%94%A8%E6%8E%A5%E5%8F%A3%E6%9D%A5%E6%8B%AF%E6%95%91)。 在第 8 章中,咱们就提到过矢量值的一个局限,便是他们只能存储一种类型的元素。在清单 8-9 中咱们创建出了一种变通方案,其中定义了有着分别保存整数、浮点数与文本变种的 `SpreadsheetCell` 枚举。这就意味着咱们可在各个单元格中存储不同类型的数据,而仍旧有了表示这些单元格所组成行的一个矢量值。这对于在咱们的代码被编译时,就已经清楚这些可交换项目,为固定类型集的情况,这确实是一种相当不错的解决办法。 然而,有时咱们会想要咱们库的用户,能够扩展这个于某种特定情形下有效的类型集。为展示咱们将怎样达成这个目的,接下来咱们将创建对一个条目清单加以迭代的示例性图形用户界面,graphical user interface,GUI 工具 -- 对于 GUI 工具来讲这可是一项常见技能。咱们将创建包含 GUI 库架构的名为 `gui` 的一个库代码箱。此代码箱会包含给人类使用的一些类型,比如 `Button` 或 `TextField`。此外,`gui` 的用户将希望创建出他们自己的能被绘制出来的类型:比如,某个程序员要添加一个 `Image`,而另一程序员则要添加一个 `SelectBox`。 对于这个示例,咱们不会实现一个完全成熟的 GUI 库,而是会给出这些部分将怎样一起配合起来。在编写这个库时,咱们没法了解而定义出其他那些程序员可能想要创建的全部类型。但咱们肯定清楚 `gui` 需要追踪各种不同类型的许多不同值,同时他还需要调用这些不同类型值上的 `draw` 方法。其无需明白在咱们调用该 `draw` 方法时,具体会发生什么,他只需知道那个值会让那个方法可被咱们调用。 在有着继承的某门语言中要做到这点,咱们可能会定义其上有着名为 `draw` 的方法的一个名为 `Component` 类。至于其他类,比如 `Button`、`Image` 与 `SelectBox` 等,将从 `Component` 基础并因此继承这个 `draw` 方法。他们可以分别重写这个 `draw` 方法,来定义他们的定制行为,而框架则可以将全部这些类型,当作 `Component` 的实例对待而调用他们之上的 `draw`。但由于 Rust 并无继承,因此咱们需要另一种方法,来架构这个 `gui` 库,来允许用户以新类型来扩展他。 ### 定义用于共同行为的特质 **Defining a Trait for Common Behavior** 为了实现咱们想要 `gui` 所拥有的行为,咱们将定义将有着一个名为 `draw` 方法的名为 `Draw` 特质。随后咱们就可以定义取 *特质对象,a trait object* 的一个矢量。特质对象会同时指向实现了这个指定特质的某个类型,以及用于在运行时查找那个类型上特质方法的一张表。咱们是通过指定某种指针,比如某个 `&` 的引用,或某个 `Box` 的灵巧指针,接着便是 `dyn` 关键字,以及随后指明相关特质,创建出特质对象。(在第 19 章的 [“动态大小类型与 `Sized` 特质”](Ch19_Advanced_Features.md#动态大小的类型与-sized-特质) 小节咱们将讲到特质对象必须使用指针的原因。)在泛型或具体类型处,咱们就可以使用特质对象。而不论在何处使用特质对象,Rust 的类型系统都会确保在编译时,在那样的上下文中的任何值,都将实现该特质对象的特质。于是,咱们就无需掌握编译时的所有可能类型了。 咱们已经提到过,在 Rust 中,咱们避免将结构体与枚举称为 “对象”,是为了将二者与其他语言中的对象区别开来。在结构体或枚举中,结构体字段中的数据,与 `impl` 代码块中的行为是分开的,而在其他语言中,数据与行为被结合为通常被标称为对象的这么一个概念。然而,特质对象由于其结合了数据与行为,而 *真的* 更像其他语言中的对象。但从无法添加数据到特质对象上看,特质对象是不同于传统的对象的。特质对象并不如其他语言中的对象那样普遍的有用:其特定用途为实现共用行为的抽象。 下面清单 17-3 给出了怎样定义有着一个名为 `draw` 方法的一个名为 `Draw` 的特质: 文件名:`src/lib.rs` ```rust pub trait Draw { fn draw(&self); } ``` *清单 17-3:`Draw` 特质的定义* 这种语法应与在第 10 章中关于定义特质的方式看起来类似。接下来便有了一种新的语法:下面清单 17-4 定义了保存着一个名为 `components` 矢量的一个名为 `Screen` 的结构体。该矢量为类型 `Box` 的,而 `Box` 便是一个特质对象;`Box` 是 `Box` 里头实现了 `Draw` 特质的全部类型的代名词。 文件名:`src/lib.rs` ```rust pub struct Screen { pub components: Vec>, } ``` *清单 17-4:带有保存着一个实现了 `Draw` 特质的特质对象矢量的 `components` 字段的 `Screen` 结构体的定义* 在这个 `Screen` 结构体上,咱们将定义将调用其 `components` 各条目上 `draw` 方法的一个名为 `run` 的方法,如下清单 17-5 中所示: 文件名:`src/lib.rs` ```rust impl Screen { pub fn run(&self) { for component in self.components.iter() { component.draw(); } } } ``` *清单 17-5:`Screen` 上会调用各组件上 `draw` 方法的一个 `run` 方法* 这与定义出用到带有特质边界泛型参数的结构体,原理是不同的。泛型参数在某个时间只能用一种具体类型替换,而特质对象则允许在运行时填入多种具体类型。比如,咱们本可以像在下面清单 17-6 中那样,将这个 `Screen` 结构体定义为使用泛型与特质边界: 文件名:`src/lib.rs` ```rust pub struct Screen { pub components: Vec, } impl Screen where T: Draw, { pub fn run(&self) { for component in self.components.iter() { component.draw(); } } } ``` *清单 17-6:其 `run` 方法用到泛型与特质边界的 `Screen` 结构体的一种替代实现* 这种写法就会将咱们限制到有着全是类型 `Button` 或全是类型 `TextField` 组件清单的某个 `Screen` 实例。在咱们将仅有着同质集合,homogeneous collections,时,由于那些定义在编译时,为使用具体类型而将被单一化,那么此时使用泛型与特质边界便是更可取的做法。 另一方面,有了使用特质对象的方法,一个 `Screen` 实例便可以保存包含着 `Box