trpl-zh-cn/src/ch17-02-trait-objects.md

371 lines
18 KiB
Markdown
Raw Normal View History

2017-04-24 23:03:13 +08:00
## 为使用不同类型的值而设计的 trait 对象
2017-04-10 13:09:34 +08:00
> [ch17-02-trait-objects.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch17-02-trait-objects.md)
> <br>
2017-04-24 23:03:13 +08:00
> commit 67876e3ef5323ce9d394f3ea6b08cb3d173d9ba9
2017-04-10 13:09:34 +08:00
在第八章,我们谈到了 vector 只能存储同种类型元素的局限。在列表 8-1 中有一个例子,其中定义了存放包含整型、浮点型和文本型成员的枚举类型`SpreadsheetCell`,这样就可以在每一个单元格储存不同类型的数据,并使得 vector 仍然代表一行单元格。当编译时就知道类型集合全部元素的情况下,这种方案是可行的。
2017-04-10 13:09:34 +08:00
<!-- The code example I want to reference did not have a listing number; it's
the one with SpreadsheetCell. I will go back and add Listing 8-1 next time I
get Chapter 8 for editing. /Carol -->
有时,我们需要可扩展的类型集合,能够被库的用户扩展。比如很多图形化接口工具有一个条目列表,迭代该列表并调用每个条目的 draw 方法。我们将创建一个库 crate包含称为 `rust_gui` 的 GUI 库。库中有一些为用户准备的类型,比如 `Button``TextField``rust_gui`的用户还会创建更多,有的用户会增加`Image`,有的用户会增加`SelectBox`然后用它们在屏幕上绘图。我们不会在本章节实现一个完善的GUI库只是展示如何把各部分组合起来。
2017-04-10 13:09:34 +08:00
当写 `rust_gui` 库时,我们不知道其他程序员需要什么类型,所以无法定义一个 `enum` 来包含所有的类型。然而 `rust_gui` 需要跟踪所有这些不同类型的值,需要有在每个值上调用 `draw` 方法能力。我们的 GUI 库不需要确切地知道调用 `draw` 方法会发生什么,只需要有可用的方法供我们调用。
2017-04-10 13:09:34 +08:00
在可以继承的语言里,我们会定义一个名为 `Component` 的类,该类上有一个`draw`方法。其他的类比如`Button`、`Image`和`SelectBox`会从`Component`继承并拥有`draw`方法。它们各自覆写`draw`方法以自定义行为,但是框架会把所有的类型当作是`Component`的实例,并在其上调用`draw`。
2017-04-10 13:09:34 +08:00
2017-04-10 16:06:17 +08:00
### 定义一个带有自定义行为的Trait
2017-04-10 13:09:34 +08:00
不过在Rust语言中我们可以定义一个名为`Draw`的trait其上有一个名为`draw`的方法。我们定义一个带有*trait对象*的vector绑定了一种指针的trait比如`&`引用或者一个`Box<T>`智能指针。
2017-04-27 15:54:12 +08:00
我们提到,我们不会称结构体和枚举为对象,这是为了区分于其他语言的结构体和枚举对象。结构体或者枚举成员中的数据和`impl`块中的行为是分开的而其他语言则是数据和行为被组合到一个被称作对象的概念里。Trait对象更像其他语言的对象之所以这样说是因为他们把由其指针所指向的具体对象作为数据把在trait中定义的方法作为行为组合在了一起。但是trait对象和其他语言是不同的因为我们不能向一个trait对象增加数据。trait对象不像其他语言那样有用它们的目的是允许从公有的行为上抽象。
2017-04-10 13:09:34 +08:00
2017-04-27 15:54:12 +08:00
trait定义了在给定情况下我们所需要的行为。在我们需要使用一个实体类型或者一个通用类型的地方我们可以把trait当作trait对象使用。Rust的类型系统会保证我们为trait对象带入的任何值会实现trait的方法。我们不需要在编译阶段知道所有可能的类型我们可以把所有的实例统一对待。Listing 17-03展示了如何定义一个名为`Draw`的带有`draw`方法的trait。
2017-04-10 13:09:34 +08:00
<span class="filename">Filename: src/lib.rs</span>
```rust
pub trait Draw {
fn draw(&self);
}
```
<span class="caption">Listing 17-3:`Draw` trait的定义</span>
<!-- NEXT PARAGRAPH WRAPPED WEIRD INTENTIONALLY SEE #199 -->
2017-04-27 15:54:12 +08:00
因为我们已经在第10章讨论过如何定义trait你可能比较熟悉。下面是新的定义Listing 17-4有一个名为`Screen`的结构体,里面有一个名为`components`的vector`components`的类型是Box<Draw>。`Box<Draw>`是一个trait对象它是`Box`内部任意一个实现了`Draw`trait的类型的替身。
2017-04-10 13:09:34 +08:00
<span class="filename">Filename: src/lib.rs</span>
```rust
# pub trait Draw {
# fn draw(&self);
# }
#
pub struct Screen {
pub components: Vec<Box<Draw>>,
}
```
<span class="caption">Listing 17-4: 定义一个`Screen`结构体,带有一个含有实现了`Draw`trait的`components` vector成员
</span>
在`Screen`结构体上,我们将要定义一个`run`方法,该方法会在它的`components`上调用`draw`方法如Listing 17-5所示
<span class="filename">Filename: src/lib.rs</span>
```rust
# pub trait Draw {
# fn draw(&self);
# }
#
# pub struct Screen {
# pub components: Vec<Box<Draw>>,
# }
#
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
```
<span class="caption">Listing 17-5:在`Screen`上实现一个`run`方法,该方法在每个组件上调用`draw`方法
</span>
2017-04-27 15:54:12 +08:00
这不同于定义一个使用带有trait限定的泛型参数的结构体。泛型参数一次只能被一个实体类型替代而trait对象可以在运行时允许多种实体类型填充trait对象。比如我们已经定义了`Screen`结构体使用泛型和一个trait限定如Listing 17-6所示
2017-04-10 13:09:34 +08:00
<span class="filename">Filename: src/lib.rs</span>
```rust
# pub trait Draw {
# fn draw(&self);
# }
#
pub struct Screen<T: Draw> {
pub components: Vec<T>,
}
impl<T> Screen<T>
where T: Draw {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
```
<span class="caption">Listing 17-6: 一种`Screen`结构体的替代实现,它的`run`方法使用通用类型和trait绑定
</span>
2017-04-27 15:54:12 +08:00
这个例子只能使我们的`Screen`实例的所有组件类型全是`Button`,或者全是`TextField`。如果你的组件集合是单一类型的那么可以优先使用泛型和trait限定这是因为其使用的具体类型在编译阶段可以被定意为是单一的。
2017-04-10 13:09:34 +08:00
而如果使用内部有`Vec<Box<Draw>>` trait对象的列表的`Screen`结构体,`Screen`实例可以同时包含`Box<Button>`和`Box<TextField>`的`Vec`。我们看它是怎么工作的,然后讨论运行时性能的实现。
2017-04-10 16:06:17 +08:00
### 来自我们或者库使用者的实现
2017-04-10 13:09:34 +08:00
2017-04-28 09:45:27 +08:00
现在,我们增加一些实现了`Draw`trait的类型。我们会再次提供`Button`实际上实现一个GUI库超出了本书的范围所以`draw`方法的内部不会有任何有用的实现。为了想象一下实现可能的样子,`Button`结构体可能有 `width`、`height`和`label`字段如Listing 17-7所示
2017-04-10 13:09:34 +08:00
<span class="filename">Filename: src/lib.rs</span>
```rust
# pub trait Draw {
# fn draw(&self);
# }
#
pub struct Button {
pub width: u32,
pub height: u32,
pub label: String,
}
impl Draw for Button {
fn draw(&self) {
// Code to actually draw a button
}
}
```
2017-04-10 16:06:17 +08:00
<span class="caption">Listing 17-7: 实现了`Draw` trait的`Button` 结构体</span>
2017-04-10 13:09:34 +08:00
在`Button`上的 `width`、`height`和`label`会和其他组件不同,比如`TextField`可能有`width`、`height`,
`label``placeholder`字段。每个我们可以在屏幕上绘制的类型会实现`Draw`trait在`draw`方法中使用不同的代码,定义了如何绘制`Button`GUI代码的具体实现超出了本章节的范围。除了`Draw` trait`Button`可能也有另一个`impl`块,包含了当按钮被点击的时候的响应方法。这类方法不适用于`TextField`这样的类型。
2017-04-28 09:45:27 +08:00
假定使用了我们的库的程序员决定实现一个包含`width`、`height`和`options`的`SelectBox`结构体。同时也在`SelectBox`类型上实现了`Draw`trait如 Listing 17-8所示
2017-04-10 13:09:34 +08:00
<span class="filename">Filename: src/main.rs</span>
```rust,ignore
extern crate rust_gui;
use rust_gui::Draw;
struct SelectBox {
width: u32,
height: u32,
options: Vec<String>,
}
impl Draw for SelectBox {
fn draw(&self) {
// Code to actually draw a select box
}
}
```
<span class="caption">Listing 17-8: 另外一个crate中在`SelectBox`结构体上使用`rust_gui`和实现了`Draw` trait
</span>
2017-04-28 09:45:27 +08:00
我们的库的使用者现在可以写他们的`main`函数来创建一个`Screen`实例,然后通过把自身放入`Box<T>`变成trait对象向screen增加`SelectBox` 和`Button`。他们可以在这个`Screen`实例上调用`run`方法,这又会调用每个组件的`draw`方法。 Listing 17-9展示了实现
2017-04-10 13:09:34 +08:00
<span class="filename">Filename: src/main.rs</span>
```rust,ignore
use rust_gui::{Screen, Button};
fn main() {
let screen = Screen {
components: vec![
Box::new(SelectBox {
width: 75,
height: 10,
options: vec![
String::from("Yes"),
String::from("Maybe"),
String::from("No")
],
}),
Box::new(Button {
width: 50,
height: 10,
label: String::from("OK"),
}),
],
};
screen.run();
}
```
2017-04-10 16:06:17 +08:00
<span class="caption">Listing 17-9: 使用trait对象来存储实现了相同trait的不同类型
</span>
2017-04-28 09:45:27 +08:00
虽然我们不知道有人会在哪一天增加这个`SelectBox`类型,但是我们的`Screen` 能够操作`SelectBox`并绘制它,因为`SelectBox`实现了`Draw`类型,这意味着它实现了`draw`方法。
2017-04-10 16:06:17 +08:00
2017-04-28 09:45:27 +08:00
只关心值响应的消息,而不关心值的具体类型,这类似于动态类型语言中的*duck typing*如果它像鸭子一样走路像鸭子一样叫那么它肯定是只鸭子在Listing 17-5 `Screen`的`run`方法实现中,`run`不需要知道每个组件的具体类型。它也不检查一个组件是`Button`或者`SelectBox`的实例,只是调用组件的`draw`方法即可。通过指定`Box<Draw>`作为`components`列表中的值类型,我们限定了 `Screen` 需要这些实现了`draw`方法的值。
2017-04-10 16:06:17 +08:00
2017-04-28 09:45:27 +08:00
使用trait对象和支持duck typing的Rust类型系统的好处是我们永远不需要在运行时检查一个值是否实现了一个特殊方法或者担心因为调用了一个值没有实现的方法而遇到错误。如果值没有实现trait对象需要的trait方法Rust不会编译我们的代码。
2017-04-10 16:06:17 +08:00
2017-04-28 09:45:27 +08:00
比如Listing 17-10展示了当我们创建一个使用`String`做为其组件的`Screen`时发生的情况:
2017-04-10 13:09:34 +08:00
<span class="filename">Filename: src/main.rs</span>
```rust,ignore
extern crate rust_gui;
use rust_gui::Draw;
fn main() {
let screen = Screen {
components: vec![
Box::new(String::from("Hi")),
],
};
screen.run();
}
```
2017-04-10 16:06:17 +08:00
<span class="caption">Listing 17-10: 尝试使用一种没有实现trait对象的trait的类型
2017-04-10 13:09:34 +08:00
2017-04-10 16:06:17 +08:00
</span>
我们会遇到这个错误,因为`String`没有实现 `Draw`trait
2017-04-10 13:09:34 +08:00
```text
error[E0277]: the trait bound `std::string::String: Draw` is not satisfied
-->
|
4 | Box::new(String::from("Hi")),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Draw` is not
implemented for `std::string::String`
|
= note: required for the cast to the object type `Draw`
```
2017-04-28 09:45:27 +08:00
这个报错让我们知道,要么我们传入了本来不想传给`Screen`的东西,而实际我们应该传入一个不同的类型,或者是我们应该在`String`上实现`Draw`,这样,`Screen`才能调用它的`draw`方法。
2017-04-10 16:06:17 +08:00
### Trait对象执行动态分发
2017-04-28 09:45:27 +08:00
回忆一下第10章我们讨论过当我们在泛型上使用trait限定时编译器执行单类型的处理过程在我们需要使用范型参数的地方编译器为每个实体类型产生了非泛型的函数实现和方法。这个单类型的处理过程产生的代码实际做的就是 *static dispatch*:当方法被调用时,因为方法的代码在编译阶段就已经决定了,所以寻找那段代码是非常快速的。
2017-04-10 16:06:17 +08:00
2017-04-28 09:45:27 +08:00
当我们使用trait对象编译器不能执行单类型的处理过程因为我们不知道代码使用的所有可能类型。另一方面当方法被调用的时候Rust跟踪可能被使用的代码然后在运行时找出该方法被调用时应该被使用那些代码。这也是我们熟知的*dynamic dispatch*,当查找发生时会产生运行时资源消耗。动态分发也会阻止编译器选择生成内联函数的代码,从而失去了一些优化。虽然我们写代码时得到了额外的灵活性,不过,这仍然是一个需要考虑的取舍问题。
2017-04-10 16:06:17 +08:00
### Trait 对象需要对象安全
2017-04-10 13:09:34 +08:00
<!-- Liz: we're conflicted on including this section. Not being able to use a
trait as a trait object because of object safety is something that
beginner/intermediate Rust developers run into sometimes, but explaining it
fully is long and complicated. Should we just cut this whole section? Leave it
(and finish the explanation of how to fix the error at the end)? Shorten it to
a quick caveat, that just says something like "Some traits can't be trait
objects. Clone is an example of one. You'll get errors that will let you know
if a trait can't be a trait object, look up object safety if you're interested
in the details"? Thanks! /Carol -->
Not all traits can be made into trait objects; only *object safe* traits can. A
trait is object safe as long as both of the following are true:
* The trait does not require `Self` to be `Sized`
* All of the trait's methods are object safe.
`Self` is a keyword that is an alias for the type that we're implementing
traits or methods on. `Sized` is a marker trait like the `Send` and `Sync`
traits that we talked about in Chapter 16. `Sized` is automatically implemented
on types that have a known size at compile time, such as `i32` and references.
Types that do not have a known size include slices (`[T]`) and trait objects.
`Sized` is an implicit trait bound on all generic type parameters by default.
Most useful operations in Rust require a type to be `Sized`, so making `Sized`
a default requirement on trait bounds means we don't have to write `T: Sized`
with most every use of generics. If we want to be able to use a trait on
slices, however, we need to opt out of the `Sized` trait bound, and we can do
that by specifying `T: ?Sized` as a trait bound.
Traits have a default bound of `Self: ?Sized`, which means that they can be
implemented on types that may or may not be `Sized`. If we create a trait `Foo`
that opts out of the `Self: ?Sized` bound, that would look like the following:
```rust
trait Foo: Sized {
fn some_method(&self);
}
```
The trait `Sized` is now a *super trait* of trait `Foo`, which means trait
`Foo` requires types that implement `Foo` (that is, `Self`) to be `Sized`.
We're going to talk about super traits in more detail in Chapter 19.
The reason a trait like `Foo` that requires `Self` to be `Sized` is not allowed
to be a trait object is that it would be impossible to implement the trait
`Foo` for the trait object `Foo`: trait objects aren't sized, but `Foo`
requires `Self` to be `Sized`. A type can't be both sized and unsized at the
same time!
For the second object safety requirement that says all of a trait's methods
must be object safe, a method is object safe if either:
* It requires `Self` to be `Sized` or
* It meets all three of the following:
* It must not have any generic type parameters
* Its first argument must be of type `Self` or a type that dereferences to
the Self type (that is, it must be a method rather than an associated
function and have `self`, `&self`, or `&mut self` as the first argument)
* It must not use `Self` anywhere else in the signature except for the
first argument
Those rules are a bit formal, but think of it this way: if your method requires
the concrete `Self` type somewhere in its signature, but an object forgets the
exact type that it is, there's no way that the method can use the original
concrete type that it's forgotten. Same with generic type parameters that are
filled in with concrete type parameters when the trait is used: the concrete
types become part of the type that implements the trait. When the type is
erased by the use of a trait object, there's no way to know what types to fill
in the generic type parameters with.
An example of a trait whose methods are not object safe is the standard
library's `Clone` trait. The signature for the `clone` method in the `Clone`
trait looks like this:
```rust
pub trait Clone {
fn clone(&self) -> Self;
}
```
`String` implements the `Clone` trait, and when we call the `clone` method on
an instance of `String` we get back an instance of `String`. Similarly, if we
call `clone` on an instance of `Vec`, we get back an instance of `Vec`. The
signature of `clone` needs to know what type will stand in for `Self`, since
that's the return type.
If we try to implement `Clone` on a trait like the `Draw` trait from Listing
17-3, we wouldn't know whether `Self` would end up being a `Button`, a
`SelectBox`, or some other type that will implement the `Draw` trait in the
future.
The compiler will tell you if you're trying to do something that violates the
rules of object safety in regards to trait objects. For example, if we had
tried to implement the `Screen` struct in Listing 17-4 to hold types that
implement the `Clone` trait instead of the `Draw` trait, like this:
```rust,ignore
pub struct Screen {
pub components: Vec<Box<Clone>>,
}
```
We'll get this error:
```text
error[E0038]: the trait `std::clone::Clone` cannot be made into an object
-->
|
2 | pub components: Vec<Box<Clone>>,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::clone::Clone` cannot be
made into an object
|
= note: the trait cannot require that `Self : Sized`
```
<!-- If we are including this section, we would explain how to fix this
problem. It involves adding another trait and implementing Clone manually for
that trait. Because this section is getting long, I stopped because it feels
like we're off in the weeds with an esoteric detail that not everyone will need
to know about. /Carol -->