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

面向对象语言的特征

Characteristics of Object-Oriented Languages

编程界对于一种语言必须具备哪些特性才能被视为面向对象并没有达成共识。Rust 受到了许多编程范式的影响,包括 OOP;例如,我们在第 13 章探讨了来自函数式编程的特性。可以说,OOP 语言具有某些共同特征——即对象(objects)、封装(encapsulation)和继承(inheritance)。让我们看看这些特征各自意味着什么,以及 Rust 是否支持它们。

There is no consensus in the programming community about what features a language must have to be considered object oriented. Rust is influenced by many programming paradigms, including OOP; for example, we explored the features that came from functional programming in Chapter 13. Arguably, OOP languages share certain common characteristics—namely, objects, encapsulation, and inheritance. Let’s look at what each of those characteristics means and whether Rust supports it.

对象包含数据和行为

Objects Contain Data and Behavior

由埃里希·伽玛(Erich Gamma)、理查德·赫尔姆(Richard Helm)、拉尔夫·约翰逊(Ralph Johnson)和约翰·威利斯迪斯(John Vlissides)合著的《设计模式:可复用面向对象软件的基础》(Addison-Wesley, 1994)一书,通俗地被称为“四人帮”(Gang of Four)之书,是一本面向对象设计模式的目录。它对 OOP 这样定义:

The book Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides (Addison-Wesley, 1994), colloquially referred to as The Gang of Four book, is a catalog of object-oriented design patterns. It defines OOP in this way:

面向对象程序由对象组成。一个对象包装了数据以及对这些数据进行操作的过程。这些过程通常被称为方法操作

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 块为结构体和枚举提供方法。尽管带有方法的结构体和枚举不被 称为 对象,但根据“四人帮”对对象的定义,它们提供了相同的功能。

Using this definition, Rust is object oriented: Structs and enums have data, and impl blocks provide methods on structs and enums. Even though structs and enums with methods aren’t called objects, they provide the same functionality, according to the Gang of Four’s definition of objects.

隐藏实现细节的封装

Encapsulation That Hides Implementation Details

通常与 OOP 相关的另一个方面是 封装(encapsulation)的思想,这意味着对象的实现细节对于使用该对象的代码来说是不可访问的。因此,与对象交互的唯一方式是通过其公共 API;使用对象的代码不应该能够深入到对象的内部并直接更改数据或行为。这使程序员能够更改和重构对象的内部,而无需更改使用该对象的代码。

Another aspect commonly associated with OOP is the idea of encapsulation, which means that the implementation details of an object aren’t accessible to code using that object. Therefore, the only way to interact with an object is through its public API; code using the object shouldn’t be able to reach into the object’s internals and change data or behavior directly. This enables the programmer to change and refactor an object’s internals without needing to change the code that uses the object.

我们在第 7 章讨论了如何控制封装:我们可以使用 pub 关键字来决定代码中的哪些模块、类型、函数和方法应该是公开的,默认情况下,其他一切都是私有的。例如,我们可以定义一个结构体 AveragedCollection,它有一个包含 i32 值 vector 的字段。该结构体还可以有一个包含该 vector 中数值平均值的字段,这意味着平均值不必在任何人需要时才按需计算。换句话说,AveragedCollection 会为我们缓存计算好的平均值。示例 18-1 包含了 AveragedCollection 结构体的定义。

We discussed how to control encapsulation in Chapter 7: We can use the pub keyword to decide which modules, types, functions, and methods in our code should be public, and by default everything else is private. For example, we can define a struct AveragedCollection that has a field containing a vector of i32 values. The struct can also have a field that contains the average of the values in the vector, meaning the average doesn’t have to be computed on demand whenever anyone needs it. In other words, AveragedCollection will cache the calculated average for us. Listing 18-1 has the definition of the AveragedCollection struct.

pub struct AveragedCollection {
    list: Vec<i32>,
    average: f64,
}

该结构体被标记为 pub,以便其他代码可以使用它,但结构体内部的字段保持私有。在这种情况下,这很重要,因为我们要确保每当从列表中添加或删除值时,平均值也会更新。我们通过在结构体上实现 addremoveaverage 方法来做到这一点,如示例 18-2 所示。

The struct is marked pub so that other code can use it, but the fields within the struct remain private. This is important in this case because we want to ensure that whenever a value is added or removed from the list, the average is also updated. We do this by implementing add, remove, and average methods on the struct, as shown in Listing 18-2.

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;
    }
}

公共方法 addremoveaverage 是访问或修改 AveragedCollection 实例中数据的唯一途径。当使用 add 方法向 list 添加项,或使用 remove 方法删除项时,每个方法的实现都会调用私有的 update_average 方法,该方法也负责处理 average 字段的更新。

The public methods add, remove, and average are the only ways to access or modify data in an instance of AveragedCollection. When an item is added to list using the add method or removed using the remove method, the implementations of each call the private update_average method that handles updating the average field as well.

我们将 listaverage 字段保持私有,这样外部代码就没有办法直接向 list 字段添加或删除项;否则,当 list 发生变化时,average 字段可能会变得不同步。average 方法返回 average 字段中的值,允许外部代码读取平均值但不能修改它。

We leave the list and average fields private so that there is no way for external code to add or remove items to or from the list field directly; otherwise, the average field might become out of sync when the list changes. The average method returns the value in the average field, allowing external code to read the average but not modify it.

由于我们封装了结构体 AveragedCollection 的实现细节,我们可以很容易地在未来更改某些方面,例如数据结构。比如,我们可以使用 HashSet<i32> 代替 Vec<i32> 作为 list 字段。只要 addremoveaverage 公共方法的签名保持不变,使用 AveragedCollection 的代码就无需更改。如果我们将 list 改为公开,情况就不一定如此了:HashSet<i32>Vec<i32> 添加和删除项的方法不同,因此如果外部代码直接修改 list,则可能不得不进行更改。

Because we’ve encapsulated the implementation details of the struct AveragedCollection, we can easily change aspects, such as the data structure, in the future. For instance, we could use a HashSet<i32> instead of a Vec<i32> for the list field. As long as the signatures of the add, remove, and average public methods stayed the same, code using AveragedCollection wouldn’t need to change. If we made list public instead, this wouldn’t necessarily be the case: HashSet<i32> and Vec<i32> have different methods for adding and removing items, so the external code would likely have to change if it were modifying list directly.

如果封装是一种语言被视为面向对象所必需的一个方面,那么 Rust 满足了这一要求。对代码的不同部分选择使用或不使用 pub 使得封装实现细节成为可能。

If encapsulation is a required aspect for a language to be considered object oriented, then Rust meets that requirement. The option to use pub or not for different parts of code enables encapsulation of implementation details.

作为类型系统和代码共享的继承

Inheritance as a Type System and as Code Sharing

继承(Inheritance)是一种机制,对象可以借此继承另一个对象定义的元素,从而获得父对象的数据和行为,而无需你重新定义它们。

Inheritance is a mechanism whereby an object can inherit elements from another object’s definition, thus gaining the parent object’s data and behavior without you having to define them again.

如果一种语言必须具备继承特性才能被称为面向对象,那么 Rust 不是这样的语言。在不使用宏的情况下,没有办法定义一个能够继承父结构体字段和方法实现的结构体。

If a language must have inheritance to be object oriented, then Rust is not such a language. There is no way to define a struct that inherits the parent struct’s fields and method implementations without using a macro.

然而,如果你习惯于在编程工具箱中使用继承,在 Rust 中你可以根据最初寻求继承的原因来使用其他解决方案。

However, if you’re used to having inheritance in your programming toolbox, you can use other solutions in Rust, depending on your reason for reaching for inheritance in the first place.

你会选择继承通常有两个主要原因。一是代码复用:你可以为一个类型实现特定的行为,而继承使你能够为另一个不同的类型复用该实现。在 Rust 代码中,你可以利用 trait 方法的默认实现来有限地做到这一点,正如你在示例 10-14 中看到的,我们在 Summary trait 上添加了 summarize 方法的默认实现。任何实现 Summary trait 的类型都可以直接使用 summarize 方法而无需编写进一步代码。这类似于父类拥有方法的实现,而继承的子类也拥有该方法的实现。当实现 Summary trait 时,我们也可以重写 summarize 方法的默认实现,这类似于子类重写从父类继承的方法实现。

You would choose inheritance for two main reasons. One is for reuse of code: You can implement particular behavior for one type, and inheritance enables you to reuse that implementation for a different type. You can do this in a limited way in Rust code using default trait method implementations, which you saw in Listing 10-14 when we added a default implementation of the summarize method on the Summary trait. Any type implementing the Summary trait would have the summarize method available on it without any further code. This is similar to a parent class having an implementation of a method and an inheriting child class also having the implementation of the method. We can also override the default implementation of the summarize method when we implement the Summary trait, which is similar to a child class overriding the implementation of a method inherited from a parent class.

使用继承的另一个原因与类型系统有关:使子类型能够用于与父类型相同的地方。这也被称为 多态(polymorphism),意味着如果多个对象共享某些特征,你可以在运行时相互替换它们。

The other reason to use inheritance relates to the type system: to enable a child type to be used in the same places as the parent type. This is also called polymorphism, which means that you can substitute multiple objects for each other at runtime if they share certain characteristics.

多态

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 使用泛型来抽象出各种可能的类型,并使用 trait bound 来对这些类型必须提供的功能施加约束。这有时被称为 受限参数多态(bounded parametric polymorphism)。

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 通过不提供继承而选择了一套不同的权衡。继承往往面临着共享过多不必要代码的风险。子类不应总是共享其父类的所有特征,但继承会强制这样做。这会降低程序设计的灵活性。它还引入了在子类上调用毫无意义或因不适用于子类而导致错误的方法的可能性。此外,有些语言只允许 单继承(即子类只能从一个类继承),进一步限制了程序设计的灵活性。

Rust has chosen a different set of trade-offs by not offering inheritance. Inheritance is often at risk of sharing more code than necessary. Subclasses shouldn’t always share all characteristics of their parent class but will do so with inheritance. This can make a program’s design less flexible. It also introduces the possibility of calling methods on subclasses that don’t make sense or that cause errors because the methods don’t apply to the subclass. In addition, some languages will only allow single inheritance (meaning a subclass can only inherit from one class), further restricting the flexibility of a program’s design.

由于这些原因,Rust 采取了不同的方法,使用 trait 对象代替继承来实现运行时的多态。让我们来看看 trait 对象是如何工作的。

For these reasons, Rust takes the different approach of using trait objects instead of inheritance to achieve polymorphism at runtime. Let’s look at how trait objects work.