“根据某些定义,Rust是面向对象的;而在其它一些定义下,Rust又不是。”
面向对象编程(Object-OrientedProgramming,OOP)是一种编程范式。对象(Object)的概念最早出现在年代的Simula语言中,其影响了AlanKay(Smalltalk语言发明者之一),他在年提出了术语“面向对象编程”来描述其所发明的语言。对于OOP是什么,在社区并未达成一致。根据某些定义,Rust是面向对象的;而在其它一些定义下,Rust又不是。本章中,我们会讨论一些被普遍认同的面向对象特性以及Rust语言如何对这些特性提供支持的。接着,我们会展示如何在Rust中实现这些面向对象特性,并讨论其和利用Rust语言的优势实现的方案的利弊。下面我们先介绍这些普遍认同的面向对象编程特性。对于面向对象必须包含哪些特性,在编程内并未达成一致意见。Rust受很多不同的编程范式影响,包括面向对象编程,还有前面我们介绍过的函数式编程等等。面向对象编程语言被普遍认为包含的特性是对象、封装和继承。让我们看一下这些概念的含义以及Rust是否支持。01
—
包含数据和行为的对象
由ErichGamma、RichardHelm、RalphJohnson和JohnVlissides(Addison-WesleyProfessional,)编写的书DesignPatterns:ElementsofReusableObject-OrientedSoftware俗称“四人帮”(TheGangofFour),它是面向对象编程设计模式的目录,在其中是这样定义面向对象编程的:Object-orientedprogramsaremadeupofobjects.Anobjectpackagesbothdataandtheproceduresthatoperateonthatdata.Theproceduresaretypicallycalledmethodsoroperations.
面向对象的程序是由对象组成的。一个对象包含数据和操作这些数据的程序(不是计算机程序,而是指做事情的程序)。这些程序通常被称为方法或操作。
根据这个定义,Rust是面向对象的:结构体和枚举包含数据,impl为结构体和枚举提供了方法。尽管带有方法的结构体和枚举并不被称为对象,但是根据上面的定义,他们提供的功能与对象一样。02
—
通过封装隐藏实现细节
另一个OOP相关的概念是封装(encapsulation)思想:使用对象的代码无法访问其实现细节。因此,唯一与对象交互的方式是通过其提供的公开API;使用对象的代码无法直接改变对象内部的数据或者行为。这让我们可以改变或重构对象内部代码实现,而使用者无需改变其代码。我们前面的章节中如何进行封装:我们可以在代码中利用pub关键字来决定哪些模块、类型、函数和方法是公有的(而默认情况下它们都是私有的)。譬如,我们可以定义一个包含Veci32类型列表的结构体AveragedCollection;结构体中还有1个字段,他保存列表中所有值的平均值,这样在需要获得列表的平均值是可以随时获取它,而不用重新计算。也就是说,AveragedCollection会缓存列表的平均值计算结果。参考下面的例子:pubstructAveragedCollection{list:Veci32,average:f64,}
在上面的例子中,结构体被标记为pub,这样其他代码就可以使用它,但是在结构体内部的字段仍然是私有的。这一点非常重要,因为我们希望平均值在列表发生改变时,会同时被更新。我们可以通过在结构体上实现add、remove和average等方法来做到这一点,参考下面的例子:
implAveragedCollection{pubfnadd(mutself,value:i32){self.list.push(value);self.update_average();}pubfnremove(mutself)-Optioni32{letresult=self.list.pop();matchresult{Some(value)={self.update_average();Some(value)}None=None,}}pubfnaverage(self)-f64{self.average}fnupdate_average(mutself){lettotal:i32=self.list.iter().sum();self.average=totalasf64/self.list.len()asf64;}}
公有方法add、remove和average是访问或修改AveragedCollection实例中数据的唯一方式。当使用add方法为列表增加元素或使用remove方法从列表删除元素时,这些方法会调用私有的update_average方法更新average字段。我们保持list和average字段是私有的,因此外部代码无法直接增加或者删除列表中的元素,否则当列表改变时,average字段可能并未更新。
因为我们已经封装了AveragedCollection的实现细节,将来可以比较容易进行修改或重构,譬如,改变列表的数据结构:我们可以将列表的类型改为HashSeti32。只要add、remove和average方法的定义保持不变,使用AveragedCollection的外部代码就无需改变。相反,如果列表字段是公有的,并且外部代码直接使用了这个列表,那么使用者可能不得不做出修改。综上,Rust也满足封装的特性。我们可以通过在代码中选择是否使用pub关键字来管理封装。03
—
继承
继承(Inheritance)是指一个对象可以继承另一个对象,这使其可以获得(继承)其父对象的数据和行为,而无需再重新定义。如果面向对象语言必须要支持继承的话,那么Rust就不是面向对象的。在Rust中我们无法定义一个结构体继承另一个结构体(父结构体)的成员和方法。然而,Rust也提供了其它解决方案作为替代。我们选择继承有两个主要的原因。第一个是为了重用代码:我们可以通过继承重用另一个类型中实现的特定行为。在Rust中我们可以通过trait方法的默认实现来共享代码,譬如,在前面章节的例子中我们在Summarytrait上增加的summarize方法的默认实现。任何实现了Summarytrait的类型都可以直接使用summarize方法的默认实现。这和子类可以复用父类的方法实现类似。当实现Summarytrait时我们也可以选择覆盖(override)默认实现,重新实现summarize方法,这类似于在子类中覆盖从父类继承的方法。第二个使用继承的原因与类型系统有关:我们可以在使用父类型的地方使用其子类型,即多态(polymorphism),具备某些相同特性的多个对象可以在运行时互相替代。Tomanypeople,polymorphismissynonymouswithinheritance.Butit’sactuallyamoregeneralconceptthatreferstocodethatcanworkwithdataofmultipletypes.Forinheritance,thosetypesaregenerallysubclasses.
Rustinsteadusesgenericstoabstractoverdifferentpossibletypesandtraitboundstoimposeconstraintsonwhatthosetypesmustprovide.Thisissometimescalledboundedparametricpolymorphism.
很多人将多态等同于继承。不过它是一个更为通用的概念,指代码可以用于可能包含不同数据的多种类型。对于继承来说,这些类型通常是某个类型的子类。
Rust则通过泛型来抽象不同的类型,并通过traitbounds约束类型所必须包含的行为。这有时被称为“有界参数多态”。
最近,在很多语言中继承不再受到青睐,因为其共享的内容超出所需,带来的便利多于风险。子类不应总是共享其父类的所有特性,如此导致程序设计缺少灵活性,并可能导致某些方法调用对于子类没有任何意义,或由于方法不适用于子类而造成错误。某些语言还限制了子类只能继承一个父类,这进一步限制了程序设计的灵活性。出于这些考虑,Rust选择了另一条路,即,使用trait对象(traitobjects)而不是继承。在下面的章节中,我们将讨论在Rust中如何利用trait对象实现多态。预览时标签不可点收录于话题#个上一篇下一篇