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),它是面向对象编程设计模式的目录,在其中是这样定义面向对象编程的:
Object-oriented programs are made up of objects. An object packages both data and the procedures that operate on that data. The procedures are typically called methods or operations.
面向对象的程序是由对象组成的。一个对象包含数据和操作这些数据的程序(不是计算机程序,而是指做事情的程序)。这些程序通常被称为方法或操作。
根据这个定义,Rust 是面向对象的:结构体和枚举包含数据, impl 为结构体和枚举提供了方法。尽管带有方法的结构体和枚举并不被称为对象,但是根据上面的定义,他们提供的功能与对象一样。
通过封装隐藏实现细节
另一个 OOP 相关的概念是封装(encapsulation)思想:使用对象的代码无法访问其实现细节。因此,唯一与对象交互的方式是通过其提供的公开 API;使用对象的代码无法直接改变对象内部的数据或者行为。这让我们可以改变或重构对象内部代码实现,而使用者无需改变其代码。
我们前面的章节中如何进行封装:我们可以在代码中利用 pub 关键字来决定哪些模块、类型、函数和方法是公有的(而默认情况下它们都是私有的)。譬如,我们可以定义一个包含 Vec<i32> 类型列表的结构体 AveragedCollection ;结构体中还有 1 个字段,他保存列表中所有值的平均值,这样在需要获得列表的平均值是可以随时获取它,而不用重新计算。也就是说,AveragedCollection 会缓存列表的平均值计算结果。参考下面的例子:
在上面的例子中,结构体被标记为 pub,这样其他代码就可以使用它,但是在结构体内部的字段仍然是私有的。这一点非常重要,因为我们希望平均值在列表发生改变时,会同时被更新。我们可以通过在结构体上实现 add、remove 和 average 等方法来做到这一点,参考下面的例子:
公有方法 add、remove 和 average 是访问或修改 AveragedCollection 实例中数据的唯一方式。当使用 add 方法为列表增加元素或使用 remove 方法从列表删除元素时,这些方法会调用私有的 update_average 方法更新 average 字段。我们保持 list 和 average 字段是私有的,因此外部代码无法直接增加或者删除列表中的元素,否则当列表改变时, average 字段可能并未更新。
因为我们已经封装了 AveragedCollection 的实现细节,将来可以比较容易进行修改或重构,譬如,改变列表的数据结构:我们可以将列表的类型改为 HashSet<i32> 。只要 add、remove 和 average 方法的定义保持不变,使用 AveragedCollection 的外部代码就无需改变。相反,如果列表字段是公有的,并且外部代码直接使用了这个列表,那么使用者可能不得不做出修改。
综上, Rust 也满足封装的特性。我们可以通过在代码中选择是否使用 pub 关键字来管理封装。
继承
继承(Inheritance)是指一个对象可以继承另一个对象,这使其可以获得(继承)其父对象的数据和行为,而无需再重新定义。
如果面向对象语言必须要支持继承的话,那么 Rust 就不是面向对象的。在 Rust 中我们无法定义一个结构体继承另一个结构体(父结构体)的成员和方法。然而,Rust 也提供了其它解决方案作为替代。
我们选择继承有两个主要的原因。第一个是为了重用代码:我们可以通过继承重用另一个类型中实现的特定行为。在 Rust 中我们可以通过 trait 方法的默认实现来共享代码,譬如,在前面章节的例子中我们在 Summary trait 上增加的 summarize 方法的默认实现。任何实现了 Summary trait 的类型都可以直接使用 summarize 方法的默认实现。这和子类可以复用父类的方法实现类似。当实现 Summary trait 时我们也可以选择覆盖(override )默认实现,重新实现 summarize 方法,这类似于在子类中覆盖从父类继承的方法。
第二个使用继承的原因与类型系统有关:我们可以在使用父类型的地方使用其子类型,即多态(polymorphism),具备某些相同特性的多个对象可以在运行时互相替代。
To many people, polymorphism is synonymous with inheritance. But it’s actually a more general concept that refers to code that can work with data of multiple types. For inheritance, those types are generally subclasses.
Rust instead uses generics to abstract over different possible types and trait bounds to impose constraints on what those types must provide. This is sometimes called bounded parametric polymorphism.
很多人将多态等同于继承。不过它是一个更为通用的概念,指代码可以用于可能包含不同数据的多种类型。对于继承来说,这些类型通常是某个类型的子类。
Rust 则通过泛型来抽象不同的类型,并通过 trait bounds 约束类型所必须包含的行为。这有时被称为“有界参数多态”。
最近,在很多语言中继承不再受到青睐,因为其共享的内容超出所需,带来的便利多于风险。子类不应总是共享其父类的所有特性,如此导致程序设计缺少灵活性,并可能导致某些方法调用对于子类没有任何意义,或由于方法不适用于子类而造成错误。某些语言还限制了子类只能继承一个父类,这进一步限制了程序设计的灵活性。
出于这些考虑,Rust 选择了另一条路,即,使用 trait 对象(trait objects)而不是继承。在下面的章节中,我们将讨论在 Rust 中如何利用 trait 对象实现多态。
版权声明: 本文为 InfoQ 作者【山】的原创文章。
原文链接:【http://xie.infoq.cn/article/6fc8c7b3dbbc57bdbb1885511】。文章转载请联系作者。
评论