Rust从0到1-面向对象编程-概念
面向对象编程(Object-Oriented Programming,OOP)是一种编程范式。对象(Object)的概念最早出现在 1960 年代的 Simula 语言中,其影响了 Alan Kay (Smalltalk语言发明者之一),他在 1967 年提出了术语“面向对象编程” 来描述其所发明的语言。对于 OOP 是什么,在社区并未达成一致。根据某些定义,Rust 是面向对象的;而在其它一些定义下,Rust 又不是。本章中,我们会讨论一些被普遍认同的面向对象特性以及 Rust 语言如何对这些特性提供支持的。接着,我们会展示如何在 Rust 中实现这些面向对象特性,并讨论其和利用 Rust 语言的优势实现的方案的利弊。下面我们先介绍这些普遍认同的面向对象编程特性。
对于面向对象必须包含哪些特性,在编程内并未达成一致意见。Rust 受很多不同的编程范式影响,包括面向对象编程,还有前面我们介绍过的函数式编程等等。面向对象编程语言被普遍认为包含的特性是对象、封装和继承。让我们看一下这些概念的含义以及 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),它是面向对象编程设计模式的目录,在其中是这样定义面向对象编程的:
根据这个定义,Rust 是面向对象的:结构体和枚举包含数据, impl 为结构体和枚举提供了方法。尽管带有方法的结构体和枚举并不被称为对象,但是根据上面的定义,他们提供的功能与对象一样。
通过封装隐藏实现细节
另一个 OOP 相关的概念是封装(encapsulation)思想:使用对象的代码无法访问其实现细节。因此,唯一与对象交互的方式是通过其提供的公开 API;使用对象的代码无法直接改变对象内部的数据或者行为。这让我们可以改变或重构对象内部代码实现,而使用者无需改变其代码。
我们前面的章节中如何进行封装:我们可以在代码中利用 pub 关键字来决定哪些模块、类型、函数和方法是公有的(而默认情况下它们都是私有的)。譬如,我们可以定义一个包含Vec
pub struct AveragedCollection {
list: Vec,
average: f64,
}
在上面的例子中,结构体被标记为 pub,这样其他代码就可以使用它,但是在结构体内部的字段仍然是私有的。这一点非常重要,因为我们希望平均值在列表发生改变时,会同时被更新。我们可以通过在结构体上实现 add、remove 和 average 等方法来做到这一点,参考下面的例子:
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;
}
}
公有方法 add、remove 和 average 是访问或修改 AveragedCollection 实例中数据的唯一方式。当使用 add 方法为列表增加元素或使用 remove 方法从列表删除元素时,这些方法会调用私有的 update_average 方法更新 average 字段。我们保持 list 和 average 字段是私有的,因此外部代码无法直接增加或者删除列表中的元素,否则当列表改变时, average 字段可能并未更新。
因为我们已经封装了 AveragedCollection 的实现细节,将来可以比较容易进行修改或重构,譬如,改变列表的数据结构:我们可以将列表的类型改为 HashSet
综上, Rust 也满足封装的特性。我们可以通过在代码中选择是否使用 pub 关键字来管理封装。
继承
继承(Inheritance)是指一个对象可以继承另一个对象,这使其可以获得(继承)其父对象的数据和行为,而无需再重新定义。
如果面向对象语言必须要支持继承的话,那么 Rust 就不是面向对象的。在 Rust 中我们无法定义一个结构体继承另一个结构体(父结构体)的成员和方法。然而,Rust 也提供了其它解决方案作为替代。
我们选择继承有两个主要的原因。第一个是为了重用代码:我们可以通过继承重用另一个类型中实现的特定行为。在 Rust 中我们可以通过 trait 方法的默认实现来共享代码,譬如,在前面章节的例子中我们在 Summary trait 上增加的 summarize 方法的默认实现。任何实现了 Summary trait 的类型都可以直接使用 summarize 方法的默认实现。这和子类可以复用父类的方法实现类似。当实现 Summary trait 时我们也可以选择覆盖(override )默认实现,重新实现 summarize 方法,这类似于在子类中覆盖从父类继承的方法。
第二个使用继承的原因与类型系统有关:我们可以在使用父类型的地方使用其子类型,即多态(polymorphism),具备某些相同特性的多个对象可以在运行时互相替代。
最近,在很多语言中继承不再受到青睐,因为其共享的内容超出所需,带来的便利多于风险。子类不应总是共享其父类的所有特性,如此导致程序设计缺少灵活性,并可能导致某些方法调用对于子类没有任何意义,或由于方法不适用于子类而造成错误。某些语言还限制了子类只能继承一个父类,这进一步限制了程序设计的灵活性。
出于这些考虑,Rust 选择了另一条路,即,使用 trait 对象(trait objects)而不是继承。在下面的章节中,我们将讨论在 Rust 中如何利用 trait 对象实现多态。