# 面向对象语言的特性
关于一门语言必须具备哪些特性才能被认为是面向对象的,编程界并没有达成共识。Rust 受到了许多编程范式的影响,包括 OOP;例如,我们在第 13 章中探讨了来自函数式编程的特性。可以说,OOP 语言共享某些共同的特性,即对象、封装和继承。让我们看看这些特性各自意味着什么,以及 Rust 是否支持它们。
# 对象包含数据和行为
由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著的《设计模式:可复用面向对象软件的元素》(Addison-Wesley, 1994),通俗地称为“四人帮”书,是面向对象设计模式的目录。它这样定义 OOP:
面向对象程序由对象组成。一个对象将数据和操作该数据的过程打包在一起。这些过程通常被称为方法或操作。
根据这个定义,Rust 是面向对象的:结构体和枚举拥有数据,而 impl
块为结构体和枚举提供了方法。尽管带有方法的结构体和枚举不被称为对象,但根据“四人帮”对对象的定义,它们提供了相同的功能。
# 隐藏实现细节的封装
OOP 另一个常见的相关概念是封装,这意味着对象的实现细节对于使用该对象的代码是不可访问的。因此,与对象交互的唯一方式是通过其公共 API;使用对象的代码不应该能够直接访问对象的内部并更改数据或行为。这使得程序员可以更改和重构对象的内部,而无需更改使用该对象的代码。
我们在第 7 章中讨论了如何控制封装:我们可以使用 pub
关键字来决定代码中的哪些模块、类型、函数和方法应该是公共的,默认情况下,其他所有内容都是私有的。例如,我们可以定义一个结构体 AveragedCollection
,它包含一个 i32
值向量的字段。该结构体还可以包含一个字段,其中包含向量中值的平均值,这意味着无论何时需要平均值,都不必按需计算。换句话说,AveragedCollection
将为我们缓存计算出的平均值。示例 18-1 是 AveragedCollection
结构体的定义:
文件名: src/lib.rs:
pub struct AveragedCollection {
list: Vec<i32>,
average: f64,
}
示例 18-1: 一个 AveragedCollection
结构体,维护一个整数列表和集合中项的平均值
该结构体被标记为 pub
,以便其他代码可以使用它,但结构体内的字段仍然是私有的。在这种情况下,这很重要,因为我们希望确保每当从列表中添加或删除值时,平均值也会更新。我们通过在结构体上实现 add
、remove
和 average
方法来做到这一点,如示例 18-2 所示:
文件名: src/lib.rs:
pub struct AveragedCollection {
list: Vec<i32>,
average: f64,
}
impl AveragedCollection {
pub fn add(&mut self, value: i32) {
self.list.push(value);
self.update_average();
}
pub fn remove(&mut self) -> Option<i32> {
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;
}
}
示例 18-2: AveragedCollection
上公共方法 add
、remove
和 average
的实现
公共方法 add
、remove
和 average
是访问或修改 AveragedCollection
实例中数据的唯一方式。当使用 add
方法向列表中添加项或使用 remove
方法删除项时,每个实现都会调用私有的 update_average
方法,该方法也负责更新 average
字段。
我们将 list
和 average
字段保留为私有,这样外部代码就无法直接向 list
字段添加或删除项;否则,当列表更改时,average
字段可能会不同步。average
方法返回 average
字段中的值,允许外部代码读取平均值但不能修改它。
由于我们已经封装了 AveragedCollection
结构体的实现细节,我们将来可以轻松更改方面,例如数据结构。例如,我们可以使用 HashSet<i32>
而不是 Vec<i32>
作为 list
字段。只要 add
、remove
和 average
公共方法的签名保持不变,使用 AveragedCollection
的代码就不需要更改。如果我们改为将 list
公开,情况就不一定如此:HashSet<i32>
和 Vec<i32>
有不同的添加和删除项的方法,因此如果外部代码直接修改 list
,则可能需要更改。
如果封装是语言被认为是面向对象所必需的方面,那么 Rust 满足了这一要求。对代码不同部分使用 pub
或不使用 pub
的选项可以实现实现细节的封装。
# 继承作为类型系统和代码共享
继承是一种机制,通过该机制,一个对象可以从另一个对象的定义中继承元素,从而获得父对象的数据和行为,而无需您再次定义它们。
如果一门语言必须具有继承才能面向对象,那么 Rust 就不是这样一门语言。没有办法定义一个结构体,使其在不使用宏的情况下继承父结构体的字段和方法实现。
但是,如果您习惯于在编程工具箱中使用继承,那么您可以根据您最初选择继承的原因,在 Rust 中使用其他解决方案。
您选择继承有两个主要原因。一个是为了代码重用:您可以为一种类型实现特定行为,而继承使您能够为不同类型重用该实现。您可以在 Rust 代码中通过默认的 trait 方法实现以有限的方式做到这一点,这在示例 10-14 中我们为 Summary
trait 上的 summarize
方法添加默认实现时已经看到。任何实现 Summary
trait 的类型都将拥有 summarize
方法,而无需任何额外的代码。这类似于父类具有方法的实现,并且继承的子类也具有方法的实现。我们还可以在实现 Summary
trait 时覆盖 summarize
方法的默认实现,这类似于子类覆盖从父类继承的方法的实现。
使用继承的另一个原因与类型系统有关:使子类型能够在与父类型相同的地方使用。这也被称为多态性,这意味着如果多个对象共享某些特性,您可以在运行时相互替换它们。
# 多态性
对许多人来说,多态性是继承的同义词。但它实际上是一个更通用的概念,指的是可以处理多种类型数据的代码。对于继承,这些类型通常是子类。
Rust 转而使用泛型来抽象不同的可能类型,并使用 trait 约束来对这些类型必须提供的内容施加限制。这有时被称为有界参数多态性。
继承作为一种编程设计解决方案,最近在许多编程语言中已经失宠,因为它经常面临共享不必要代码的风险。子类不应该总是共享其父类的所有特性,但继承会这样做。这会使程序的設計灵活性降低。它还引入了在子类上调用没有意义或导致错误的方法的可能性,因为这些方法不适用于子类。此外,一些语言只允许单继承(意味着子类只能从一个类继承),这进一步限制了程序设计的灵活性。
由于这些原因,Rust 采用了使用 trait 对象而不是继承的不同方法。让我们看看 trait 对象如何在 Rust 中实现多态性。