Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

17.1 Rust的面向对象的编程特性

17.1.0 什么是面向对象的编程特性?

面向对象编程(Object-oriented programming,简称 OOP)是一种程序建模方法。对象作为编程概念在编程语言 Simula 中引入。这些对象影响了 Alan Kay 的编程架构,其中对象相互传递消息。为了描述这种架构,他在 1967 年创造了面向对象编程这个术语。

核心概念

  1. 对象(Object)

    • 程序的基本单位,包含属性(状态)和行为(操作)。
  2. 类(Class)

    • 对象的模板,定义了属性和行为。
  3. 封装(Encapsulation)

    • 将数据和操作绑定在一起,隐藏内部细节,通过接口与外部交互。
  4. 继承(Inheritance)

    • 子类继承父类的属性和行为,提高代码重用性。
  5. 多态(Polymorphism)

    • 相同的接口表现出不同的行为,包括方法重载和重写。
  6. 抽象(Abstraction)

    • 只关注必要部分,忽略复杂实现,通过类或接口提供高层次设计。

面向对象编程的优势

  1. 模块化和可维护性:代码易于维护和扩展。
  2. 代码重用性:通过继承和抽象减少重复代码。
  3. 易于扩展:新功能可轻松添加。
  4. 现实建模:贴近现实世界概念。
  5. 数据安全性:通过封装保护数据,增强安全性。

17.1.1 Rust 的面向对象编程特点

对于一种语言必须具备哪些特性才能被视为面向对象,编程社区尚未达成共识。Rust 受到许多编程范式的影响,包括 OOP。OOP 通常包括对象、封装和继承等特性。

面向对象编程有很多种定义,而这些定义有很多是相互矛盾的。其中有一些定义能够把 Rust 划为面向对象编程的语言,而另外一部分定义则不这样认为。

在第 13 章中我们说过了 Rust 函数式编程的特性,但 Rust 既不是传统的面向对象编程语言,也不是纯函数式编程语言。它是一个多范式编程语言,结合了函数式编程和面向对象编程的一些特点。

对象包含数据和行为

Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides(这四人通俗地被称为“四人帮”)所著的 Design Patterns: Elements of Reusable Object-Oriented Software(《设计模式:可重用面向对象软件的元素》)是一本关于设计模式的经典著作。它这样定义 OOP:

面向对象的程序是由对象组成的。对象封装了数据和操作该数据的过程。这些过程通常称为方法操作

基于这个定义,Rust 是面向对象的:structenum 包含数据,impl 块为之提供了方法。但在 Rust 里带有方法的 structenum 并没有称之为对象。

封装

封装指的是调用对象外部的代码无法直接访问对象内部的实现细节,唯一可以与对象进行交互的方法就是通过它公开的 API。

Rust 通过 pub 关键字来决定代码中哪些模块、类型、函数或者是方法是公开的。而默认情况下它们都是私有的。

看个例子:

#![allow(unused)]
fn main() {
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;
    }
}
}

该结构体被标记为 pub 以便其他代码可以使用它,但该结构中的字段仍然是私有的。因为我们希望确保每当在列表中添加或删除值时,平均值也会更新。直接给字段改值做不到这一点,所以不能让用户能够修改其字段值。我们通过在结构体上实现 addremoveaverage 方法来实现。

继承

继承是指使对象可以沿用另外一个对象的数据与行为,且无需重复定义相关的代码。Rust 并不支持这个特性。

通常使用继承的原因是代码复用多态

  • 针对代码复用,Rust 提供了默认 trait 方法来进行代码共享。 在 trait 中某个方法有默认实现,那么任何实现了这个 trait 的类型就会自动拥有这个方法。这和面向对象很类似,在面向对象语言中,父类中实现的方法就可以被继承它的子类拥有。当实现某个 trait 时,还可以覆盖 trait 的默认实现,这类似于子类覆盖从父类继承的方法的实现。

  • 多态指期望某个类型能够被应用在需要父类型的地方,换句话说,就是如果一些对象拥有某些共同的特征,那么这些对象就可以在运行时相互替换。 Rust 通过泛型和 trait bound(trait 约束)实现了这一点:泛型使得逻辑可以更好地脱离于实际的数据类型,并使用 trait bound 来决定能使用此逻辑的类型必须提供的某些具体特性,这一技术也称为限定参数多态(bounded parametric polymorphism)

现在其实很多语言都不使用继承作为内置的程序设计方案了。 因为它经常面临共享过多代码的风险。子类不应该总是共享其父类的所有特征,但可以通过继承来实现。这会降低程序设计的灵活性。它还引入了在子类上调用没有意义或导致错误的方法的可能性,因为这些方法不适用于子类。此外,有些语言只允许单继承(即子类只能从一个类继承),这进一步限制了程序设计的灵活性。