Rust 学习笔记:面向对象语言的特点
- [Rust 学习笔记:面向对象语言的特点](#Rust 学习笔记:面向对象语言的特点)
Rust 学习笔记:面向对象语言的特点
在编程社区中,对于一门语言必须具备哪些特性才能被认为是面向对象的,并没有达成共识。
面向对象语言具有某些共同的特征,即对象、封装和继承。让我们看看这些特征的含义以及 Rust 是否支持它们。
对象包含数据和行为
《设计模式:可重用面向对象软件的元素》一书是这样定义 OOP 的:
面向对象程序是由对象组成的。对象将数据和对该数据进行操作的过程打包。这些过程通常称为方法或操作。
使用这个定义,Rust 是面向对象的:结构体和枚举具有数据,而 impl 块提供结构体和枚举的方法。尽管带方法的结构体和枚举不称为对象,但它们提供了相同的功能。
封装隐藏了实现细节
与 OOP 相关的另一个方面是封装的思想,这意味着使用该对象的代码无法访问对象的实现细节。因此,与对象交互的唯一方法是通过它的公共 API。使用对象的代码不应该能够进入对象的内部并直接更改数据或行为。这使程序员能够更改和重构对象的内部结构,而无需更改使用该对象的代码。
我们可以使用 pub 关键字来决定代码中的哪些模块、类型、函数和方法应该是公共的,而默认情况下,其他的都是私有的。
例如,我们可以定义一个结构体 AveragedCollection,它有一个包含 i32 值的数组的字段。该结构体还可以有一个字段,包含向量中值的平均值,这意味着当任何人需要它时,不必根据需要计算平均值。换句话说, AveragedCollection 将为我们缓存计算出的平均值。
AveragedCollection 结构体的定义:
rust
pub struct AveragedCollection {
list: Vec<i32>,
average: f64,
}
该结构体被标记为 pub,以便其他代码可以使用它,但结构体中的字段仍然是私有的。
我们希望确保每当从列表中添加或删除值时,平均值也会更新。我们通过在结构体上实现 add、remove 和 average 方法来实现这一点:
rust
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;
}
}
我们将 list 和 average 字段保留为私有,公共方法 add、remove 和 average 是访问或修改 AveragedCollection 实例中的数据的唯一方法。当使用 add 方法将项添加到列表中或使用 remove 方法删除项时,每个项的实现都会调用私有的 update_average 方法,该方法也会处理平均字段的更新。average 方法返回 average 字段中的值,允许外部代码读取平均值,但不能修改它。
如果封装是一种语言被认为是面向对象的必要方面,那么 Rust 就满足了这个要求。对代码的不同部分使用 pub 或不使用 pub 的选项支持对实现细节进行封装。
作为类型系统和代码共享的继承
继承是一种机制,对象可以从另一个对象的定义中继承元素,从而获得父对象的数据和行为,而不必重新定义它们。
如果一种语言必须具有继承才能是面向对象的,那么 Rust 就不是这样的语言。如果不使用宏,就无法定义继承父结构的字段和方法实现的结构。
选择继承有两个主要原因。
一种是代码重用:为一种类型实现特定的行为,继承使能够为另一种类型重用该实现。在 Rust 代码中,你可以使用默认的 trait 方法实现以有限的方式做到这一点。
rust
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
我们在 Summary trait 上添加了一个 summarize 方法的默认实现。任何实现 Summary trait 的类型都可以在其上使用 summarize 方法,而无需任何进一步的代码。这类似于父类具有方法的实现,继承子类也具有该方法的实现。当我们实现 Summary trait 时,我们也可以覆盖 summarize 方法的默认实现,这类似于子类覆盖从父类继承的方法的实现。
使用继承的另一个原因与类型系统有关:使子类型能够在与父类型相同的位置使用。这也称为多态性,这意味着如果多个对象共享某些特征,则可以在运行时相互替换。Rust 使用泛型来抽象不同可能的类型和 trait 约束,以对这些类型必须提供的内容施加约束。这有时被称为有界参数多态性。
在许多编程语言中,继承作为一种编程设计解决方案最近已经不受欢迎了,因为它经常冒着共享不必要的代码的风险。子类不应该总是共享父类的所有特征,但继承时会这样做。这可能会使程序设计的灵活性降低。它还引入了在子类上调用没有意义的方法或由于方法不适用于子类而导致错误的可能性。此外,一些语言只允许单继承(意味着子类只能继承一个类),这进一步限制了程序设计的灵活性。
由于这些原因,Rust 采用了使用 trait 对象而不是继承的不同方法。