diff --git a/src/ch17-01-what-is-oo.md b/src/ch17-01-what-is-oo.md index 576b40a..6859a47 100644 --- a/src/ch17-01-what-is-oo.md +++ b/src/ch17-01-what-is-oo.md @@ -4,25 +4,25 @@ >
> commit 398d6f48d2e6b7b15efd51c4541d446e89de3892 -关于一个语言被称为面向对象所需的功能,在编程社区内并未达成一致意见。Rust 被很多不同的编程范式影响,包括面向对象编程;比如第十三章提到了来自函数式编程的特性。面向对象编程语言所共享的一些特性往往是对象、封装和继承。让我们看一下这每一个概念的含义以及 Rust 是否支持它们。 +关于一门语言必须具备哪些特征才能被视为面向对象,目前在编程社区中并没有共识。Rust 受到了许多编程范式的影响,包括面向对象编程(OOP);例如,在第 13 章中,我们探讨了来自函数式编程的特性。可以说,面向对象的语言共有一些共同的特征,即对象、封装和继承。我们将会讨论这些特征分别是什么,以及 Rust 是否支持它们。 ### 对象包含数据和行为 -由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides(Addison-Wesley Professional, 1994)编写的书 *Design Patterns: Elements of Reusable Object-Oriented Software* 被俗称为 *The Gang of Four* (字面意思为 “四人帮”),它是面向对象编程模式的目录。它这样定义面向对象编程: +由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides(Addison-Wesley Professional, 1994)编写的书 *Design Patterns: Elements of Reusable Object-Oriented Software* ,通称 *The Gang of Four* (“四人帮”),是一本面向对象设计模式的目录。它这样定义面向对象编程: > Object-oriented programs are made up of objects. An *object* packages both > data and the procedures that operate on that data. The procedures are > typically called *methods* or *operations*. > -> 面向对象的程序是由对象组成的。一个 **对象** 包含数据和操作这些数据的过程。这些过程通常被称为 **方法** 或 **操作**。 +> 面向对象的程序由对象组成。一个 **对象** 包含数据和操作这些数据的过程。这些过程通常被称为 **方法** 或 **操作**。 在这个定义下,Rust 是面向对象的:结构体和枚举包含数据而 `impl` 块提供了在结构体和枚举之上的方法。虽然带有方法的结构体和枚举并不被 **称为** 对象,但是它们提供了与对象相同的功能,参考 *The Gang of Four* 中对象的定义。 ### 封装隐藏了实现细节 -另一个通常与面向对象编程相关的方面是 **封装**(*encapsulation*)的思想:对象的实现细节不能被使用对象的代码获取到。所以唯一与对象交互的方式是通过对象提供的公有 API;使用对象的代码无法深入到对象内部并直接改变数据或者行为。封装使得改变和重构对象的内部时无需改变使用对象的代码。 +另一个通常与面向对象编程关联的概念是 **封装**(*encapsulation*):一个对象的实现细节对使用该对象的代码不可访问。因此,对象交互的唯一方式是通过其公共 API;使用对象的代码不应能直接触及对象的内部并改变数据或行为。这使得程序员能够更改和重构一个对象的内部实现,而无需改变使用该对象的代码。 -就像我们在第七章讨论的那样:可以使用 `pub` 关键字来决定模块、类型、函数和方法是公有的,而默认情况下其他一切都是私有的。比如,我们可以定义一个包含一个 `i32` 类型 vector 的结构体 `AveragedCollection `。结构体也可以有一个字段,该字段保存了 vector 中所有值的平均值。这样,希望知道结构体中的 vector 的平均值的人可以随时获取它,而无需自己计算。换句话说,`AveragedCollection` 会为我们缓存平均值结果。示例 17-1 有 `AveragedCollection` 结构体的定义: +我们在第 7 章讨论了如何控制封装:我们可以使用 `pub` 关键字来决定代码中的哪些模块、类型、函数和方法是公有的,而默认情况下其他所有内容都是私有的。例如,我们可以定义一个 `AveragedCollection` 结构体,其中有一个存有 `Vec` 的字段。该结构体还可以有一个字段存储其平均值,以便需要时取用。示例 17-1 给出了 `AveragedCollection` 结构体的定义: 文件名:src/lib.rs @@ -30,9 +30,9 @@ {{#rustdoc_include ../listings/ch17-oop/listing-17-01/src/lib.rs}} ``` -示例 17-1: `AveragedCollection` 结构体维护了一个整型列表和集合中所有元素的平均值。 +示例 17-1: `AveragedCollection` 结构体维护了一个整型列表及其所有元素的平均值。 -注意,结构体自身被标记为 `pub`,这样其他代码就可以使用这个结构体,但是在结构体内部的字段仍然是私有的。这是非常重要的,因为我们希望保证变量被增加到列表或者被从列表删除时,也会同时更新平均值。可以通过在结构体上实现 `add`、`remove` 和 `average` 方法来做到这一点,如示例 17-2 所示: +该结构体被标记为 `pub`,这样其他代码就可以使用它,但结构体内的字段保持私有。这在这种情况下很重要,因为我们想确保每当列表中添加或删除值时,平均值也会更新。我们通过实现结构体上的 `add`、`remove` 和 `average` 方法来做到这一点,如示例 17-2 所示: 文件名:src/lib.rs @@ -46,29 +46,27 @@ `list` 和 `average` 是私有的,所以没有其他方式来使得外部的代码直接向 `list` 增加或者删除元素,否则 `list` 改变时可能会导致 `average` 字段不同步。`average` 方法返回 `average` 字段的值,这使得外部的代码只能读取 `average` 而不能修改它。 -因为我们已经封装好了 `AveragedCollection` 的实现细节,将来可以轻松改变类似数据结构这些方面的内容。例如,可以使用 `HashSet` 代替 `Vec` 作为 `list` 字段的类型。只要 `add`、`remove` 和 `average` 公有函数的签名保持不变,使用 `AveragedCollection` 的代码就无需改变。相反如果使得 `list` 为公有,就未必都会如此了: `HashSet` 和 `Vec` 使用不同的方法增加或移除项,所以如果要想直接修改 `list` 的话,外部的代码可能不得不做出修改。 +因为我们已经封装了 `AveragedCollection` 的实现细节,改动数据结构等内部实现非常简单。例如,可以使用 `HashSet` 代替 `Vec` 作为 `list` 字段的类型。只要 `add`、`remove` 和 `average` 这些公有方法的签名保持不变,使用 `AveragedCollection` 的代码就无需改变。如果我们将 `list` 设为公有,情况就未必如此: `HashSet` 和 `Vec` 使用不同的方法增加或移除项,所以如果外部代码直接修改 `list` ,很可能需要进行更改。 -如果封装是一个语言被认为是面向对象语言所必要的方面的话,那么 Rust 满足这个要求。在代码中不同的部分使用 `pub` 与否可以封装其实现细节。 +如果封装被认为是面向对象语言所必要的特征,那么 Rust 满足这个要求。在代码中不同的部分控制 `pub` 的使用来封装实现细节。 ## 继承,作为类型系统与代码共享 -**继承**(*Inheritance*)是一个很多编程语言都提供的机制,一个对象可以定义为继承另一个对象定义中的元素,这使其可以获得父对象的数据和行为,而无需重新定义。 +**继承**(*Inheritance*)是一种机制:一个对象可以从另一个对象的定义中继承元素,从而获得父对象的数据和行为,无需再次定义。 -如果一个语言必须有继承才能被称为面向对象语言的话,那么 Rust 就不是面向对象的。因为没有宏则无法定义一个结构体继承父结构体的成员和方法。 +如果一种语言必须具有继承才能被认为是面向对象语言,那么 Rust 不是其中之一。Rust 不支持定义一个结构体时继承父结构体的字段和方法,除非使用宏。 -然而,如果你过去常常在你的编程工具箱使用继承,根据你最初考虑继承的原因,Rust 也提供了其他的解决方案。 +然而,如果您习惯于在编程过程中使用继承,那么根据运用继承的原因,Rust 提供了其他解决方案。 -选择继承有两个主要的原因。第一个是为了重用代码:一旦为一个类型实现了特定行为,继承可以对一个不同的类型重用这个实现。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) > -> 很多人将多态描述为继承的同义词。不过它是一个有关可以用于多种类型的代码的更广泛的概念。对于继承来说,这些类型通常是子类。 -> Rust 则通过泛型来对不同的可能类型进行抽象,并通过 trait bounds 对这些类型所必须提供的内容施加约束。这有时被称为 *bounded parametric polymorphism*。 +> 对很多人来说,多态性与继承同义。但它实际上是一个更广义的概念,指的是可以处理多种类型数据的代码。对继承而言,这些类型通常是子类。 +> Rust 使用泛型来抽象不同可能的类型,并通过 trait bounds 来约束这些类型所必须提供的内容。这有时被称为 *bounded parametric polymorphism*。 -近来继承作为一种语言设计的解决方案在很多语言中失宠了,因为其时常带有共享多于所需的代码的风险。子类不应总是共享其父类的所有特征,但是继承却始终如此。如此会使程序设计更为不灵活,并引入无意义的子类方法调用,或由于方法实际并不适用于子类而造成错误的可能性。 +作为一种语言设计的解决方案,继承在许多新的编程语言中逐渐不被青睐,因为它经常有分享过多代码的风险。子类不应总是共享父类的所有特征,但是继承始终如此。它还引入了在子类上调用方法的可能性,这些方法可能没有意义,或因为方法不适用于子类而导致错误。此外,一些语言只允许单一继承(意味着子类只能从一个类继承),进一步限制了程序设计的灵活性。 -另外某些语言还只允许单继承(意味着子类只能继承一个父类),进一步限制了程序设计的灵活性。 - -因为这些原因,Rust 选择了一个不同的途径,使用 trait 对象而不是继承。让我们看一下 trait 对象如何使 Rust 得以实现多态的。 +出于这些原因,Rust 使用 trait 对象而非继承。接下来我们会讨论 Rust 如何使用 trait 对象实现多态性。