|
|
@ -18,7 +18,7 @@ get Chapter 8 for editing. /Carol -->
|
|
|
|
|
|
|
|
|
|
|
|
在有继承的语言里,我们可能会定义一个名为`Component`的类,该类上有一个`draw`方法。其他的类比如`Button`、`Image`和`SelectBox`会从`Component`继承并继承`draw`方法。它们会各自覆写`draw`方法来自定义行为,但是框架会把所有的类型当作是`Component`的实例,并在它们上调用`draw`。
|
|
|
|
在有继承的语言里,我们可能会定义一个名为`Component`的类,该类上有一个`draw`方法。其他的类比如`Button`、`Image`和`SelectBox`会从`Component`继承并继承`draw`方法。它们会各自覆写`draw`方法来自定义行为,但是框架会把所有的类型当作是`Component`的实例,并在它们上调用`draw`。
|
|
|
|
|
|
|
|
|
|
|
|
## 定义一个带有自定义行为的Trait
|
|
|
|
### 定义一个带有自定义行为的Trait
|
|
|
|
|
|
|
|
|
|
|
|
不过,在Rust语言中,我们可以定义一个名为`Draw`的trait,其上有一个名为`draw`的方法。我们定义一个带有*trait对象*的vector,绑定了一种指针的trait,比如`&`引用或者一个`Box<T>`智能指针。
|
|
|
|
不过,在Rust语言中,我们可以定义一个名为`Draw`的trait,其上有一个名为`draw`的方法。我们定义一个带有*trait对象*的vector,绑定了一种指针的trait,比如`&`引用或者一个`Box<T>`智能指针。
|
|
|
|
|
|
|
|
|
|
|
@ -111,7 +111,7 @@ impl<T> Screen<T>
|
|
|
|
|
|
|
|
|
|
|
|
而如果使用内部有`Vec<Box<Draw>>` trait对象的列表的`Screen`结构体,`Screen`实例可以同时包含`Box<Button>`和`Box<TextField>`的`Vec`。我们看它是怎么工作的,然后讨论运行时性能的实现。
|
|
|
|
而如果使用内部有`Vec<Box<Draw>>` trait对象的列表的`Screen`结构体,`Screen`实例可以同时包含`Box<Button>`和`Box<TextField>`的`Vec`。我们看它是怎么工作的,然后讨论运行时性能的实现。
|
|
|
|
|
|
|
|
|
|
|
|
### 来自我们或者库使用者的实现
|
|
|
|
### 来自我们或者库使用者的实现
|
|
|
|
|
|
|
|
|
|
|
|
现在,我们增加一些实现了`Draw`trait的类型。我们会再次提供`Button`,实际上实现一个GUI库超出了本书的范围,所以`draw`方法的内部不会有任何有用的实现。为了想象一下实现可能的样子,`Button`结构体可能有 width`、`height`和`label`字段,如Listing 17-7所示:
|
|
|
|
现在,我们增加一些实现了`Draw`trait的类型。我们会再次提供`Button`,实际上实现一个GUI库超出了本书的范围,所以`draw`方法的内部不会有任何有用的实现。为了想象一下实现可能的样子,`Button`结构体可能有 width`、`height`和`label`字段,如Listing 17-7所示:
|
|
|
|
|
|
|
|
|
|
|
@ -135,8 +135,7 @@ impl Draw for Button {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
<span class="caption">Listing 17-7: A `Button` struct that implements the
|
|
|
|
<span class="caption">Listing 17-7: 实现了`Draw` trait的`Button` 结构体</span>
|
|
|
|
`Draw` trait</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
在`Button`上的 `width`、`height`和`label`会和其他组件不同,比如`TextField`可能有`width`、`height`,
|
|
|
|
在`Button`上的 `width`、`height`和`label`会和其他组件不同,比如`TextField`可能有`width`、`height`,
|
|
|
|
`label`和 `placeholder`字段。每个我们可以在屏幕上绘制的类型会实现`Draw`trait,在`draw`方法中使用不同的代码,定义了如何绘制`Button`(GUI代码的具体实现超出了本章节的范围)。除了`Draw` trait,`Button`可能也有另一个`impl`块,包含了当按钮被点击的时候的响应方法。这类方法不适用于`TextField`这样的类型。
|
|
|
|
`label`和 `placeholder`字段。每个我们可以在屏幕上绘制的类型会实现`Draw`trait,在`draw`方法中使用不同的代码,定义了如何绘制`Button`(GUI代码的具体实现超出了本章节的范围)。除了`Draw` trait,`Button`可能也有另一个`impl`块,包含了当按钮被点击的时候的响应方法。这类方法不适用于`TextField`这样的类型。
|
|
|
@ -165,11 +164,7 @@ impl Draw for SelectBox {
|
|
|
|
<span class="caption">Listing 17-8: 另外一个crate中,在`SelectBox`结构体上使用`rust_gui`和实现了`Draw` trait
|
|
|
|
<span class="caption">Listing 17-8: 另外一个crate中,在`SelectBox`结构体上使用`rust_gui`和实现了`Draw` trait
|
|
|
|
</span>
|
|
|
|
</span>
|
|
|
|
|
|
|
|
|
|
|
|
The user of our library can now write their `main` function to create a
|
|
|
|
我们的库的使用者现在可以写他们的`main`函数来创建一个`Screen`实例,然后通过把自身放入`Box<T>`变成trait对象,向screen增加`SelectBox` 和`Button`。它们可以在每个`Screen`实例上调用`run`方法,这会调用每个组件的`draw`方法。 Listing 17-9展示了实现:
|
|
|
|
`Screen` instance and add a `SelectBox` and a `Button` to the screen 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 17-9 shows this implementation:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<span class="filename">Filename: src/main.rs</span>
|
|
|
|
<span class="filename">Filename: src/main.rs</span>
|
|
|
|
|
|
|
|
|
|
|
@ -200,32 +195,16 @@ fn main() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
<span class="caption">Listing 17-9: Using trait objects to store values of
|
|
|
|
<span class="caption">Listing 17-9: 使用trait对象来存储实现了相同trait的不同类型
|
|
|
|
different types that implement the same trait</span>
|
|
|
|
</span>
|
|
|
|
|
|
|
|
|
|
|
|
Even though we didn't know that someone would add the `SelectBox` type someday,
|
|
|
|
虽然我们不知道有些人可能有一天会增加`SelectBox`类型,但是我们的`Screen` 有能力操作`SelectBox`和绘制,因为`SelectBox`实现了`Draw`类型,这意味着它实现了`draw`方法。
|
|
|
|
our `Screen` implementation was able to operate on the `SelectBox` and draw it
|
|
|
|
|
|
|
|
because `SelectBox` implements the `Draw` type, which means it implements the
|
|
|
|
只关心值响应的消息,而不关心值的具体类型,这类似于动态类型语言中的*duck typing*:如果它像鸭子一样走路,像鸭子一样叫,那么它肯定是只鸭子!在Listing 17-5的`Screen`的`run`方法的实现中,`run`不需要知道每个组件的具体类型。它也不检查是否一个组件是`Button`或者`SelectBox`的实例,只是调用组件的`draw`方法即可。通过指定`Box<Draw>`作为`components`vector中的值类型,我们定义了:`Screen`需要可以被调用其`draw`方法的值。
|
|
|
|
`draw` method.
|
|
|
|
|
|
|
|
|
|
|
|
使用trait对象和支持duck typing的Rust类型系统的好处是,我们永远不需要在运行时检查一个值是否实现了一个特殊方法,或者担心因为调用了一个值没有实现方法而遇到错误。如果值没有实现trait对象需要的trait,Rust不会编译我们的代码。
|
|
|
|
Only being concerned with the messages a value responds to, rather than the
|
|
|
|
|
|
|
|
value's concrete type, is similar to a concept called *duck typing* in
|
|
|
|
比如,Listing 17-10展示了当我们创建一个把`String`当做其成员的`Screen`时发生的情况:
|
|
|
|
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
|
|
|
|
|
|
|
|
17-5, `run` doesn't need to know what the concrete type of each component is.
|
|
|
|
|
|
|
|
It doesn't check to see if a component is an instance of a `Button` or a
|
|
|
|
|
|
|
|
`SelectBox`, it just calls the `draw` method on the component. By specifying
|
|
|
|
|
|
|
|
`Box<Draw>` as the type of the values in the `components` vector, we've defined
|
|
|
|
|
|
|
|
that `Screen` needs values that we can call the `draw` method on.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The advantage with using trait objects and Rust's type system to do duck typing
|
|
|
|
|
|
|
|
is that we never have to check that 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. Rust won't compile our code if the values don't implement the
|
|
|
|
|
|
|
|
traits that the trait objects need.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
For example, Listing 17-10 shows what happens if we try to create a `Screen`
|
|
|
|
|
|
|
|
with a `String` as a component:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<span class="filename">Filename: src/main.rs</span>
|
|
|
|
<span class="filename">Filename: src/main.rs</span>
|
|
|
|
|
|
|
|
|
|
|
@ -244,10 +223,11 @@ fn main() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
<span class="caption">Listing 17-10: Attempting to use a type that doesn't
|
|
|
|
<span class="caption">Listing 17-10: 尝试使用一种没有实现trait对象的trait的类型
|
|
|
|
implement the trait object's trait</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
We'll get this error because `String` doesn't implement the `Draw` trait:
|
|
|
|
</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
我们会遇到这个错误,因为`String`没有实现 `Draw`trait:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
```text
|
|
|
|
error[E0277]: the trait bound `std::string::String: Draw` is not satisfied
|
|
|
|
error[E0277]: the trait bound `std::string::String: Draw` is not satisfied
|
|
|
@ -260,31 +240,15 @@ error[E0277]: the trait bound `std::string::String: Draw` is not satisfied
|
|
|
|
= note: required for the cast to the object type `Draw`
|
|
|
|
= note: required for the cast to the object type `Draw`
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
This lets us know that either we're passing something we didn't mean to pass to
|
|
|
|
这个报错让我们知道,或者我们传入了本来不想传给`Screen`的东西,我们应该传入一个不同的类型,或者是我们应该在`String`上实现`Draw`,这样,`Screen`才能调用它的`draw`方法。
|
|
|
|
`Screen` and we should pass a different type, or we should implement `Draw` on
|
|
|
|
|
|
|
|
`String` so that `Screen` is able to call `draw` on it.
|
|
|
|
### Trait对象执行动态分发
|
|
|
|
|
|
|
|
|
|
|
|
### Trait Objects Perform Dynamic Dispatch
|
|
|
|
回忆一下第10章,我们讨论过当我们使用通用类型的trait绑定时,编译器执行单类型的处理过程:在我们需要使用通用类型参数的地方,编译器为每个实体类型产生了非通用的函数实现和方法。由于非单类型而产生的代码是 *static dispatch*:当方法被调用,代码会执行在编译阶段就决定的方法,这样寻找那段代码是非常快速的。
|
|
|
|
|
|
|
|
|
|
|
|
Recall in Chapter 10 when we discussed the process of monomorphization that the
|
|
|
|
当我们使用trait对象,编译器不能执行单类型的,因为我们不知道可能被代码调用的类型。而,当方法被调用的时候,Rust跟踪可能被使用的代码,然后在运行时找出为了方法被调用时该使用哪些代码。这也是我们熟知的*dynamic dispatch*,当运行时的查找发生时是比较耗费资源的。动态分发也防止编译器选择内联函数的代码,这样防止了一些优化。虽然我们写代码时得到了额外的代码灵活性,不过,这是一个权衡考虑。
|
|
|
|
compiler performs when we use trait bounds on generics: the compiler generates
|
|
|
|
|
|
|
|
non-generic implementations of functions and methods for each concrete type
|
|
|
|
### Trait 对象需要对象安全
|
|
|
|
that we use in place of a generic type parameter. The code that results from
|
|
|
|
|
|
|
|
monomorphization is doing *static dispatch*: when the method is called, the
|
|
|
|
|
|
|
|
code that goes with that method call has been determined at compile time, and
|
|
|
|
|
|
|
|
looking up that code is very fast.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
When we use trait objects, the compiler can't perform monomorphization because
|
|
|
|
|
|
|
|
we don't know all the types that might be used with the code. Instead, Rust
|
|
|
|
|
|
|
|
keeps track of the code that might be used when a method is called and figures
|
|
|
|
|
|
|
|
out at runtime which code needs to be used for a particular method call. This
|
|
|
|
|
|
|
|
is known as *dynamic dispatch*, and there's a runtime cost when this lookup
|
|
|
|
|
|
|
|
happens. Dynamic dispatch also prevents the compiler from choosing to inline a
|
|
|
|
|
|
|
|
method's code, which prevents some optimizations. We did get extra flexibility
|
|
|
|
|
|
|
|
in the code that we wrote and were able to support, though, so it's a tradeoff
|
|
|
|
|
|
|
|
to consider.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### Object Safety is Required for Trait Objects
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Liz: we're conflicted on including this section. Not being able to use a
|
|
|
|
<!-- 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
|
|
|
|
trait as a trait object because of object safety is something that
|
|
|
|