Updated 'src/oop/characteristics_oop'.

This commit is contained in:
Hector PENG
2026-04-17 12:27:05 +08:00
parent e380d9b0c3
commit db27cd9032
5 changed files with 52 additions and 50 deletions

View File

@@ -1,8 +1,6 @@
[package]
name = "encapsulation_demo"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
edition = "2024"
[dependencies]

View File

@@ -1,12 +1,16 @@
#![allow(dead_code)]
#![allow(unused_variables)]
pub struct AveragedCollection {
list: Vec<i32>,
average: f64,
}
impl AveragedCollection {
pub fn new () -> AveragedCollection {
AveragedCollection {
list: vec![],
average: 0.0,
}
}
pub fn add(&mut self, value: i32) {
self.list.push(value);
self.update_average();

View File

@@ -0,0 +1,12 @@
use encapsulation_demo::AveragedCollection;
fn main() {
let mut col = AveragedCollection::new();
col.add(10);
col.add(100);
col.add(100);
col.add(100);
println! ("{}", col.average());
}

View File

@@ -114,7 +114,7 @@
- [未来值、任务与线程](async/all_together.md)
- [Rust 的面向对象编程特性](Ch17_Object_Oriented_Programming_Features_of_Rust.md)
- [面向对象编程特性](Ch17_Object_Oriented_Programming_Features_of_Rust.md)
- [面向对象语言的特征](oop/characteristics_oop.md)
- [使用特质对象抽象共用行为](oop/trait_objects.md)
- [实现一种面向对象设计模式](oop/implementing.md)

View File

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