面向对象语言的特征
Characteristics of Object-Oriented Languages
在编程界,并无关于某门被视为面向对象的,而必须具有哪些特性的共识。Rust 受了许多编程范式,programming paradigms,的影响,其中就包括 OOP;比如在第 13 章中,咱们就曾探讨过,那些来自于函数式编程的特性。可以说,那些 OOP 的语言,确实是共用了一些确切的特征的,那即是对象、封装与继承等。下面就来看看这些特征各自指的是什么,以及 Rust 是否支持他们。
对象包含了数据及行为
Objects Contain Data and Behavior
Erich Gamma、Richard Helm、Ralph Johnson 及 John Vlissides 等的合著 Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley Professional, 1994),又被通俗地叫做 The Gang of Four 书,便是面向对象设计模式的一个目录。该书像下面这样定义了 OOP:
面向对象程序是由对象所组成的。对象,an object 同时打包了数据与运行在那数据上的过程。这些过程一般就叫做 方法,methods 或 操作,operations。
运用这个定义,Rust 便是面向对象的:结构体与枚举均有着数据,而 impl
块则提供了结构体与枚举上的那些方法。即使有着方法的那些结构体与枚举未被称作 对象,根据 The Gang of Four 的对象定义,他们提供了同样的功能。
隐藏了实现细节的封装
Encapsulation that Hides Implementation Details
通常与 OOP 相关的另一方面的 封装,encapsulation,是指对于用到该对象的代码,对象实现细节是不可访问的。由此,与对象交互的唯一方式,便是经由该对象的公开 API;运用对象的代码,不应具备到达该对象内部,而直接改变数据或行为的能力。这实现了程序员在无需修改用到对象的那些代码之下,修改或重构对象的那些内部代码。
在第 7 章中,咱们曾讨论过怎样控制封装:咱们可以使用 pub
关键字,来决定咱们代码中,哪些模组、类型、函数与方法等应为公开的,而默认其他所有项目都是私有的。比如,咱们就可以定义有着包含 i32
值矢量的一个字段的 AveragedCollection
结构体。这个字段也可以有包含着那个矢量中值的平均数的一个字段,表示在有人需要该平均值时,不必按需计算出该平均值。换句话说,AveragedCollection
将为咱们缓存这个计算出的平均值。下面清单 17-1 便有着这个 AveragedCollection
结构体的定义:
文件名:src/lib.rs
#![allow(unused)] fn main() { pub struct AveragedCollection { list: Vec<i32>, average: f64, } }
清单 17-1:维护着一个整数清单及该集合中项目平均数的 AveragedCollection
结构体
该结构体被标记为 pub
,从而其他代码就可以使用他,而该结构体内部的那些字段保持着私有。由于咱们打算不管何时在有某个值被添加到清单,或从清单移除时,其中的平均数也要同时被更新,因此在这个示例中这样的封装就很重要。咱们是通过实现下面清单 17-2 中所给出的 add
、remove
及 average
方法,做到这一点的。
文件名:src/lib.rs
#![allow(unused)] fn main() { 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; } } }
清单 17-2:AveragedCollection
上公开方法 add
、remove
与 average
的实现
这些公开方法 add
、remove
与 average
,是仅有的访问或修改 AveragedCollection
实例中数据的方式。在使用 add
方法或 remove
方法,添加或移除某个条目时,其各自的实现,就同时会调用处理更新 average
字段的私有 update_average
方法。
咱们把 list
与 average
自动留着私有,从而外部代码就无法直接添加项目到那个 list
,或直接从那个 list
移除项目;不然的话,在那个list
变化时,average
字段就可能失去同步。其中的 average
方法,返回的是 average
字段中的值,这实现了外部代码读取那个 average
而不会修改他。
由于咱们已封装了结构体 AveragedCollection
的实现细节,因此咱们就可以在将来轻易地修改各个方面,诸如数据结构等。比如,咱们可以对其中的 list
字段,使用 HashSet<i32>
而非 Vec<i32>
。只要 add
、remove
及 average
三个公开方法的签名保持不变,那些使用 AveragedCollection
的代码就无需改变。而相反若咱们把 list
构造为公开,就未必如此了:HashSet<i32>
与 Vec<i32>
有着添加和一处条目的不同方法,由此在外部代码直接修改 list
时,就大概率不得不修改了。
若封装是某门语言被视为面向对象的要件,你们 Rust 是满足那种要求的。对代码的不同部分,使用抑或不使用 pub
的选项,实现了实现细节的封装。
以类型系统及以代码共用的继承
Inheritance as a Type System and as Code Sharing
继承,inheritance,乃籍以实现对象从另一对象继承一些元素,从而在不必再度定义这些元素之下,获得父辈对象数据与行为的一种机制。
若某们语言务必要有着继承,方能成为一门面向对象语言,那么 Rust 就不算是面向对象语言。在不使用宏,a macro 之下,没有定义出继承父辈结构体字段与方法实现的结构体的方法。
然而,若在编程工具箱中惯于使用继承,那么依据咱们将继承作为头等大事的自身理由,是可以运用 Rust 中别的一些办法的。
之所以选用继承,大致有两种原因。一个是代码的重用:咱们可以对一个类型实现一些特定行为,而继承就让咱们可以对另一类型重用那些实现。咱们可以使用一些默认的特质方法实现,即咱们曾在清单 10-14 中,将 summarize
方法的一个默认实现,添加到 Summary
特质上时所见到的那样,在 Rust 代码中以一种受限方式做到这点。任何实现了这个 Summary
特质的类型,在无需更多代码之下,都将在其上有着这个 summarize
方法。这与父类有着某个方法的实现,同时集成的子类也会有着该方法的实现是类似的。在实现这个 Summary
特质时,咱们也可以重写 summarize
方法的默认实现,这与子类重新继承自父类的方法实现类似。
而使用与类型系统相关继承的另一原因:即为了实现在与父类型的同一地方,使用子类型。这又被成为 多态,polymorphism,是指在多个对象共用了一些确切特征时,咱们可以相互替换使用他们。
关于多态
对许多人来讲,多态等同于继承。但他实际上指的是代码可工作于多个类型数据之下的一个宽泛概念。而对于继承,这些类型则是通用的一些子类。
Rust 则运用了泛型,来对各异的各种可能类型加以抽象,并使用特质边界来强化这些类型所必须提供的那些约束。有时这样的做法,又被叫做 有边界的参数化多态,bounded parametric polymorphism。
由于继承通常有着共用了超出必要代码的风险,时至今日,其已在许多编程语言中,作为编程设计模式而失宠了。子类本不应共用其父类的全部特征,但在继承父类时却会这样做。这就会造成程序的设计有较低的灵活性。由于子类从父类继承的一些方法并不适用于子类,因此继承还会引入调用子类上无意义或引发错误方法的可能。此外,一些语言还只将运行单一继承(即子类只能从一个类继承),这进一步限制了程序设计的灵活度。
由于这些原因,Rust 便采取了运用特质对象,而非继承的方法。接下来就要看看特质对象是如何实现 Rust 中的多态。