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


x-i18n: generated_at: “2026-03-01T14:51:59Z” model: gemini-3-flash-preview provider: google-gemini-cli source_hash: be159095c388554e8bdd41791f73f86cb4ecad440ee78f6226bbf556bff865ba source_path: ch18-02-trait-objects.md workflow: 16

使用特征对象实现不同类型间的抽象行为

Using Trait Objects to Abstract over Shared Behavior

在第 8 章中,我们提到向量的一个局限是它们只能存储单一类型的元素。我们在示例 8-9 中创建了一个变通方法,定义了一个 SpreadsheetCell 枚举,它具有持有整数、浮点数和文本的变体。这意味着我们可以在每个单元格中存储不同类型的数据,并且仍然拥有一个代表一单元格行的向量。当我们的互换项是一组我们在编译代码时已知的固定类型时,这是一个非常好的解决方案。

In Chapter 8, we mentioned that one limitation of vectors is that they can store elements of only one type. We created a workaround in Listing 8-9 where we defined a SpreadsheetCell enum that had variants to hold integers, floats, and text. This meant we could store different types of data in each cell and still have a vector that represented a row of cells. This is a perfectly good solution when our interchangeable items are a fixed set of types that we know when our code is compiled.

然而,有时我们希望我们的库用户能够扩展在特定情况下有效的类型集合。为了展示如何实现这一点,我们将创建一个图形用户界面 (GUI) 工具的示例,它遍历一个项列表,对每个项调用 draw 方法将其绘制到屏幕上——这是 GUI 工具的一种常用技术。我们将创建一个名为 gui 的库 crate,它包含一个 GUI 库的结构。这个 crate 可能包含一些供人们使用的类型,如 ButtonTextField 。此外, gui 的用户会想要创建他们自己可以被绘制的类型:例如,一个程序员可能会添加一个 Image ,而另一个可能会添加一个 SelectBox

However, sometimes we want our library user to be able to extend the set of types that are valid in a particular situation. To show how we might achieve this, we’ll create an example graphical user interface (GUI) tool that iterates through a list of items, calling a draw method on each one to draw it to the screen—a common technique for GUI tools. We’ll create a library crate called gui that contains the structure of a GUI library. This crate might include some types for people to use, such as Button or TextField. In addition, gui users will want to create their own types that can be drawn: For instance, one programmer might add an Image, and another might add a SelectBox.

在编写库的时候,我们无法知道并定义其他程序员可能想要创建的所有类型。但我们知道 gui 需要跟踪许多不同类型的值,并且它需要对这些不同类型的值中的每一个调用 draw 方法。它不需要确切知道调用 draw 方法时会发生什么,只需要知道该值将具有可供我们调用的该方法。

At the time of writing the library, we can’t know and define all the types other programmers might want to create. But we do know that gui needs to keep track of many values of different types, and it needs to call a draw method on each of these differently typed values. It doesn’t need to know exactly what will happen when we call the draw method, just that the value will have that method available for us to call.

要在具有继承性质的语言中做到这一点,我们可能会定义一个名为 Component 的类,它上面有一个名为 draw 的方法。其他类,如 ButtonImageSelectBox ,将继承自 Component 从而继承 draw 方法。它们可以各自覆盖 draw 方法以定义其自定义行为,但框架可以将所有类型都视为 Component 实例并对它们调用 draw 。但因为 Rust 没有继承,我们需要另一种方式来构建 gui 库,以允许用户创建与库兼容的新类型。

To do this in a language with inheritance, we might define a class named Component that has a method named draw on it. The other classes, such as Button, Image, and SelectBox, would inherit from Component and thus inherit the draw method. They could each override the draw method to define their custom behavior, but the framework could treat all of the types as if they were Component instances and call draw on them. But because Rust doesn’t have inheritance, we need another way to structure the gui library to allow users to create new types compatible with the library.

为共同行为定义特征

Defining a Trait for Common Behavior

为了实现我们希望 gui 具有的行为,我们将定义一个名为 Draw 的特征,它将有一个名为 draw 的方法。然后,我们可以定义一个接收特征对象的向量。一个“特征对象 (trait object)”同时指向一个实现了我们指定特征的类型的实例,以及一个在运行时用于查找该类型上特征方法的表。我们通过指定某种类型的指针(如引用或 Box<T> 智能指针),然后是 dyn 关键字,再指定相关的特征,来创建一个特征对象。(我们将在第 20 章的“动态大小类型与 Sized 特征”中讨论特征对象必须使用指针的原因。)我们可以使用特征对象来替代泛型或具体类型。在任何我们使用特征对象的地方,Rust 的类型系统都会在编译时确保在该上下文中使用的任何值都将实现该特征对象的特征。因此,我们不需要在编译时知道所有可能的类型。

To implement the behavior that we want gui to have, we’ll define a trait named Draw that will have one method named draw. Then, we can define a vector that takes a trait object. A trait object points to both an instance of a type implementing our specified trait and a table used to look up trait methods on that type at runtime. We create a trait object by specifying some sort of pointer, such as a reference or a Box<T> smart pointer, then the dyn keyword, and then specifying the relevant trait. (We’ll talk about the reason trait objects must use a pointer in “Dynamically Sized Types and the Sized Trait” in Chapter 20.) We can use trait objects in place of a generic or concrete type. Wherever we use a trait object, Rust’s type system will ensure at compile time that any value used in that context will implement the trait object’s trait. Consequently, we don’t need to know all the possible types at compile time.

我们已经提到过,在 Rust 中,我们避免将结构体和枚举称为“对象”,以便将它们与其他语言的对象区分开来。在结构体或枚举中,结构体字段中的数据和 impl 块中的行为是分开的,而在其他语言中,将数据和行为结合在一起的一个概念通常被标记为对象。特征对象与显式对象不同之处在于,我们不能向特征对象添加数据。特征对象不像其他语言中的对象那样具有普遍的用途:它们的特定目的是允许对共同行为进行抽象。

We’ve mentioned that, in Rust, we refrain from calling structs and enums “objects” to distinguish them from other languages’ objects. In a struct or enum, the data in the struct fields and the behavior in impl blocks are separated, whereas in other languages, the data and behavior combined into one concept is often labeled an object. Trait objects differ from objects in other languages in that we can’t add data to a trait object. Trait objects aren’t as generally useful as objects in other languages: Their specific purpose is to allow abstraction across common behavior.

示例 18-3 展示了如何定义一个名为 Draw 的特征,其中包含一个名为 draw 的方法。

Listing 18-3 shows how to define a trait named Draw with one method named draw.

{{#rustdoc_include ../listings/ch18-oop/listing-18-03/src/lib.rs}}

这种语法对于我们第 10 章中关于如何定义特征的讨论来说应该是熟悉的。接下来是一些新语法:示例 18-4 定义了一个名为 Screen 的结构体,它持有一个名为 components 的向量。这个向量的类型是 Box<dyn Draw> ,这是一个特征对象;它是 Box 内任何实现了 Draw 特征的类型的替身。

This syntax should look familiar from our discussions on how to define traits in Chapter 10. Next comes some new syntax: Listing 18-4 defines a struct named Screen that holds a vector named components. This vector is of type Box<dyn Draw>, which is a trait object; it’s a stand-in for any type inside a Box that implements the Draw trait.

{{#rustdoc_include ../listings/ch18-oop/listing-18-04/src/lib.rs:here}}

Screen 结构体上,我们将定义一个名为 run 的方法,该方法将对其每个 components 调用 draw 方法,如示例 18-5 所示。

On the Screen struct, we’ll define a method named run that will call the draw method on each of its components, as shown in Listing 18-5.

{{#rustdoc_include ../listings/ch18-oop/listing-18-05/src/lib.rs:here}}

这与定义使用带有特征约束的泛型类型参数的结构体不同。泛型类型参数一次只能替换为一个具体类型,而特征对象允许在运行时填充多个具体类型。例如,我们可以使用泛型类型和特征约束来定义 Screen 结构体,如示例 18-6 所示。

This works differently from defining a struct that uses a generic type parameter with trait bounds. A generic type parameter can be substituted with only one concrete type at a time, whereas trait objects allow for multiple concrete types to fill in for the trait object at runtime. For example, we could have defined the Screen struct using a generic type and a trait bound, as in Listing 18-6.

{{#rustdoc_include ../listings/ch18-oop/listing-18-06/src/lib.rs:here}}

这限制了我们使用的 Screen 实例只能拥有一组全部为 Button 类型或全部为 TextField 类型的组件列表。如果你永远只会有同质集合,那么使用泛型和特征约束是更佳的选择,因为定义将在编译时被单态化以使用具体类型。

This restricts us to a Screen instance that has a list of components all of type Button or all of type TextField. If you’ll only ever have homogeneous collections, using generics and trait bounds is preferable because the definitions will be monomorphized at compile time to use the concrete types.

另一方面,使用特征对象的方法,一个 Screen 实例可以持有包含 Box<Button> 以及 Box<TextField>Vec<T> 。让我们来看看这是如何工作的,然后我们将讨论运行时的性能影响。

On the other hand, with the method using trait objects, one Screen instance can hold a Vec<T> that contains a Box<Button> as well as a Box<TextField>. Let’s look at how this works, and then we’ll talk about the runtime performance implications.

实现特征

Implementing the Trait

现在我们将添加一些实现 Draw 特征的类型。我们将提供 Button 类型。同样,实际实现一个 GUI 库超出了本书的范围,所以 draw 方法体内不会有任何有用的实现。为了想象实现可能的样子, Button 结构体可能具有 widthheightlabel 字段,如示例 18-7 所示。

Now we’ll add some types that implement the Draw trait. We’ll provide the Button type. Again, actually implementing a GUI library is beyond the scope of this book, so the draw method won’t have any useful implementation in its body. To imagine what the implementation might look like, a Button struct might have fields for width, height, and label, as shown in Listing 18-7.

{{#rustdoc_include ../listings/ch18-oop/listing-18-07/src/lib.rs:here}}

Button 上的 widthheightlabel 字段将与其他组件上的字段不同;例如, TextField 类型可能具有这些相同的字段外加一个 placeholder 字段。我们要绘制在屏幕上的每个类型都将实现 Draw 特征,但会在 draw 方法中使用不同的代码来定义如何绘制该特定类型,就像这里的 Button 一样(如前所述,没有实际的 GUI 代码)。例如, Button 类型可能有一个额外的 impl 块,包含与用户点击按钮时发生的事情相关的方法。这类方法并不适用于像 TextField 这样的类型。

The width, height, and label fields on Button will differ from the fields on other components; for example, a TextField type might have those same fields plus a placeholder field. Each of the types we want to draw on the screen will implement the Draw trait but will use different code in the draw method to define how to draw that particular type, as Button has here (without the actual GUI code, as mentioned). The Button type, for instance, might have an additional impl block containing methods related to what happens when a user clicks the button. These kinds of methods won’t apply to types like TextField.

如果使用我们库的人决定实现一个具有 widthheightoptions 字段的 SelectBox 结构体,他们也会在 SelectBox 类型上实现 Draw 特征,如示例 18-8 所示。

If someone using our library decides to implement a SelectBox struct that has width, height, and options fields, they would implement the Draw trait on the SelectBox type as well, as shown in Listing 18-8.

{{#rustdoc_include ../listings/ch18-oop/listing-18-08/src/main.rs:here}}

我们库的用户现在可以编写他们的 main 函数来创建一个 Screen 实例。对于这个 Screen 实例,他们可以通过将 SelectBoxButton 分别放入 Box<T> 中使其成为特征对象来添加它们。然后他们可以在 Screen 实例上调用 run 方法,这将对每个组件调用 draw 。示例 18-9 展示了这一实现。

Our library’s user can now write their main function to create a Screen instance. To the Screen instance, they can add a SelectBox and a Button by putting each in a Box<T> to become a trait object. They can then call the run method on the Screen instance, which will call draw on each of the components. Listing 18-9 shows this implementation.

{{#rustdoc_include ../listings/ch18-oop/listing-18-09/src/main.rs:here}}

当我们编写库时,我们并不知道有人会添加 SelectBox 类型,但我们的 Screen 实现能够操作新类型并绘制它,因为 SelectBox 实现了 Draw 特征,这意味着它实现了 draw 方法。

When we wrote the library, we didn’t know that someone might add the SelectBox type, but our Screen implementation was able to operate on the new type and draw it because SelectBox implements the Draw trait, which means it implements the draw method.

这种概念——即只关注一个值响应的消息,而不是该值的具体类型——类似于动态类型语言中的“鸭子类型 (duck typing)”概念:如果它走起来像鸭子,叫起来像鸭子,那么它就一定是鸭子!在示例 18-5 中 Screen 上的 run 实现中, run 不需要知道每个组件的具体类型。它不检查一个组件是 Button 还是 SelectBox 的实例,它只是调用该组件上的 draw 方法。通过将 Box<dyn Draw> 指定为 components 向量中值的类型,我们已经定义了 Screen 需要的是我们可以调用 draw 方法的值。

This concept—of being concerned only with the messages a value responds to rather than the value’s concrete type—is similar to the concept of duck typing in dynamically typed languages: If it walks like a duck and quacks like a duck, then it must be a duck! In the implementation of run on Screen in Listing 18-5, run doesn’t need to know what the concrete type of each component is. It doesn’t check whether a component is an instance of a Button or a SelectBox, it just calls the draw method on the component. By specifying Box<dyn Draw> as the type of the values in the components vector, we’ve defined Screen to need values that we can call the draw method on.

使用特征对象和 Rust 的类型系统来编写类似于使用鸭子类型的代码的优势在于,我们永远不需要在运行时检查一个值是否实现了特定的方法,或者担心如果我们调用了一个值没有实现的方法但我们还是调用了它而出现错误。如果值没有实现特征对象所需的特征,Rust 就不会编译我们的代码。

The advantage of using trait objects and Rust’s type system to write code similar to code using duck typing is that we never have to check whether a value implements a particular method at runtime or worry about getting errors if a value doesn’t implement a method but we call it anyway. Rust won’t compile our code if the values don’t implement the traits that the trait objects need.

例如,示例 18-10 展示了如果我们尝试创建一个以 String 为组件的 Screen 会发生什么。

{{#rustdoc_include ../listings/ch18-oop/listing-18-10/src/main.rs}}

我们将得到这个错误,因为 String 没有实现 Draw 特征:

We’ll get this error because String doesn’t implement the Draw trait:

{{#include ../listings/ch18-oop/listing-18-10/output.txt}}

这个错误让我们知道,我们要么是向 Screen 传递了我们本不打算传递的东西,因此应该传递不同的类型,要么是我们应该在 String 上实现 Draw ,以便 Screen 能够对其调用 draw

This error lets us know that either we’re passing something to Screen that we didn’t mean to pass and so should pass a different type, or we should implement Draw on String so that Screen is able to call draw on it.

执行动态分派 (Performing Dynamic Dispatch)

Performing Dynamic Dispatch

回想第 10 章“使用泛型的代码的性能”中关于编译器对泛型执行的单态化过程的讨论:编译器为我们用来替换泛型类型参数的每个具体类型生成函数和方法的非泛型实现。由单态化产生的代码正在执行“静态分派 (static dispatch)”,即编译器在编译时就知道你调用的是什么方法。这与“动态分派 (dynamic dispatch)”相对,动态分派是指编译器在编译时无法判断你调用的是哪个方法。在动态分派的情况下,编译器发出的代码将在运行时知道调用哪个方法。

Recall in “Performance of Code Using Generics” in Chapter 10 our discussion on the monomorphization process performed on generics by the compiler: The compiler generates nongeneric implementations of functions and methods for each concrete type that we use in place of a generic type parameter. The code that results from monomorphization is doing static dispatch, which is when the compiler knows what method you’re calling at compile time. This is opposed to dynamic dispatch, which is when the compiler can’t tell at compile time which method you’re calling. In dynamic dispatch cases, the compiler emits code that at runtime will know which method to call.

当我们使用特征对象时,Rust 必须使用动态分派。编译器并不知道所有可能与使用特征对象的代码一起使用的类型,因此它不知道该调用在哪个类型上实现的哪个方法。相反,在运行时,Rust 使用特征对象内部的指针来确定调用哪个方法。这种查找会产生静态分派中不会发生的运行时成本。动态分派还会阻止编译器选择内联方法代码,这反过来又阻止了一些优化,并且 Rust 关于在哪里可以使用和不能使用动态分派有一些规则,称为“dyn 安全性 (dyn compatibility)”。这些规则超出了本次讨论的范围,但你可以在 参考手册中阅读更多相关内容。然而,我们在示例 18-5 中编写的代码以及在示例 18-9 中能够支持的代码确实获得了额外的灵活性,所以这是一个需要考虑的权衡。

When we use trait objects, Rust must use dynamic dispatch. The compiler doesn’t know all the types that might be used with the code that’s using trait objects, so it doesn’t know which method implemented on which type to call. Instead, at runtime, Rust uses the pointers inside the trait object to know which method to call. This lookup incurs a runtime cost that doesn’t occur with static dispatch. Dynamic dispatch also prevents the compiler from choosing to inline a method’s code, which in turn prevents some optimizations, and Rust has some rules about where you can and cannot use dynamic dispatch, called dyn compatibility. Those rules are beyond the scope of this discussion, but you can read more about them in the reference. However, we did get extra flexibility in the code that we wrote in Listing 18-5 and were able to support in Listing 18-9, so it’s a trade-off to consider.