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