使用允许不同类型值的 trait 对象
在第 8 章中,我们提到了 vector 的一个限制是它们可以
仅存储一种类型的元素。我们在示例 8-9 中创建了一个解决方法,其中
我们定义了一个SpreadsheetCell
enum 具有保存整数、浮点数、
和文本。这意味着我们可以在每个单元格中存储不同类型的数据,并且
仍然具有表示一行单元格的向量。这是一个非常好的
当我们的可互换项目是我们已知的一组固定类型时的解决方案
当我们的代码被编译时。
但是,有时我们希望我们的库用户能够扩展
类型。展示我们如何实现
为此,我们将创建一个示例图形用户界面 (GUI) 工具,该工具迭代
通过项目列表,调用draw
方法将其绘制到
screen - GUI 工具的常用技术。我们将创建一个名为gui
,其中包含 GUI 库的结构。此 crate 可能包含
一些类型供人们使用,例如Button
或TextField
.另外gui
用户将希望创建自己的可以绘制的类型:For
实例中,一个程序员可能会添加一个Image
另一个可能会添加SelectBox
.
我们不会为此示例实现一个成熟的 GUI 库,但将显示
这些碎片如何组合在一起。在编写库时,我们不能
了解并定义其他程序员可能想要创建的所有类型的类型。但我们确实如此
知道gui
需要跟踪许多不同类型的值,并且
需要调用draw
方法。它
不需要确切知道当我们调用draw
方法
只是该值将具有可供我们调用的方法。
要在具有继承的语言中执行此作,我们可以定义一个名为Component
它有一个名为draw
在上面。其他类(例如Button
,Image
和SelectBox
,将继承自Component
因此
继承draw
方法。它们都可以覆盖draw
定义 method to define
它们的自定义行为,但框架可以将所有类型视为
他们是Component
实例并调用draw
在他们身上。但是因为 Rust
没有继承,我们需要另一种方法来构建gui
library 设置为
允许用户使用新类型扩展它。
定义常见行为的特征
为了实现我们想要的行为gui
为此,我们将定义一个名为Draw
它将有一个名为draw
.然后我们可以定义一个向量,该向量
接受一个 trait 对象。trait 对象同时指向一个类型的实例
实现我们指定的 trait 和一个用于查找 trait 方法的表
该类型。我们通过指定某种
指针,例如引用或&
Box<T>
智能指针,则dyn
keyword,然后指定相关特征。(我们将讨论原因
trait 对象必须使用第 19 章中“动态
Sized 类型,并且Sized
特质。) 我们可以
使用 trait 对象代替泛型或具体类型。无论我们在何处使用
trait 对象时,Rust 的类型系统将确保在编译时任何值
used 将实现 trait 对象的 trait。因此,我们
不需要在编译时知道所有可能的类型。
我们已经提到,在 Rust 中,我们避免调用结构和枚举
“objects” 来区分它们与其他语言的对象。在结构体或
enum 中,结构体字段中的数据以及impl
块是
分开的,而在其他语言中,数据和行为合二为一
概念通常被标记为对象。但是,trait 对象更像
对象,因为它们结合了数据和行为。
但是 trait 对象与传统对象的不同之处在于,我们不能向
trait 对象。trait 对象通常不如其他
languages:它们的具体目的是允许跨公共
行为。
示例 17-3 展示了如何定义一个名为Draw
使用一个名为draw
:
文件名: src/lib.rs
pub trait Draw {
fn draw(&self);
}
示例 17-3: 定义Draw
特性
从我们关于如何定义 trait 的讨论中,这种语法应该看起来很熟悉
在第 10 章中。接下来是一些新的语法:示例 17-4 定义了一个名为Screen
,其中包含一个名为components
.此向量的类型为Box<dyn Draw>
,它是一个 trait 对象;它是任何类型的内部替代品
一个Box
实现Draw
特性。
文件名: src/lib.rs
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
示例 17-4: 定义Screen
struct 替换为components
field 中,其中包含实现Draw
特性
在Screen
struct 中,我们将定义一个名为run
这将调用draw
method 添加到其每个components
,如示例 17-5 所示:
文件名: src/lib.rs
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
示例 17-5:一个run
method 开启Screen
这会调用draw
method 在每个组件上
这与定义使用泛型类型的结构不同
参数。泛型类型参数只能被替换
一次使用一个具体类型,而 trait 对象允许多个
具体类型来填充 trait 对象。例如,我们
可以定义Screen
struct 使用泛型类型和 trait 绑定
如示例 17-6:
文件名: src/lib.rs
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();
}
}
}
示例 17-6:Screen
struct 及其run
使用泛型和 trait 边界的方法
这限制了我们Screen
实例,该实例具有所有组件列表
类型Button
或所有类型TextField
.如果您只拥有同构
集合中,使用泛型和 trait bounds 是可取的,因为
定义将在编译时被单态化以使用具体类型。
另一方面,对于使用 trait 对象的方法,一个Screen
实例
可以容纳Vec<T>
,其中包含一个Box<Button>
以及Box<TextField>
.让我们看看它是如何工作的,然后我们将讨论
运行时性能影响。
实现 Trait
现在,我们将添加一些实现Draw
特性。我们将提供Button
类型。同样,实际实现 GUI 库超出了范围
的draw
method 中没有任何有用的实现
身体。为了想象实现可能是什么样子,一个Button
结构
可能有width
,height
和label
,如示例 17-7 所示:
文件名: src/lib.rs
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
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
}
}
示例 17-7:一个Button
结构体实现Draw
特性
这width
,height
和label
字段Button
将与
其他组件上的字段;例如,TextField
type 可能具有这些
相同的字段加上placeholder
田。我们想要利用的每种类型
该界面将实现Draw
trait 中,但在draw
方法来定义如何绘制该特定类型,如Button
在这里
(如前所述,没有实际的 GUI 代码)。这Button
type 等
可能有一个额外的impl
块中包含与 what 相关的方法
在用户单击按钮时发生。这些类型的方法不适用于
类型,如TextField
.
如果有人使用我们的库决定实施SelectBox
struct 中具有width
,height
和options
字段中,它们实现Draw
traitSelectBox
type 中也一样,如示例 17-8 所示:
文件名: src/main.rs
use 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
}
}
fn main() {}
示例 17-8:另一个使用gui
并实施
这Draw
traitSelectBox
结构
我们库的用户现在可以编写他们的main
函数创建Screen
实例。到Screen
实例中,他们可以添加SelectBox
以及Button
通过将每个 cookie 放入Box<T>
成为 trait 对象。然后,他们可以调用run
方法上的Screen
实例,它将调用draw
在每个
组件。示例 17-9 显示了这个实现:
文件名: src/main.rs
use 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
}
}
use gui::{Button, Screen};
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();
}
示例 17-9:使用 trait 对象存储 实现相同特征的不同类型
当我们编写库时,我们不知道有人可能会添加SelectBox
类型,但是我们的Screen
implementation 能够在
new 类型并绘制它,因为SelectBox
实现Draw
trait 的
表示它实现了draw
方法。
这个概念 — 只关心值响应的消息
而不是值的具体类型 - 类似于 Duck 的概念
使用动态类型语言键入:如果它像鸭子和嘎嘎声一样走路
像鸭子一样,那一定是鸭子!在run
上Screen
在示例 17-5 中,run
不需要知道每个的具体类型是什么
组件是。它不会检查组件是否是Button
或SelectBox
,它只是调用draw
方法。由
指定Box<dyn Draw>
作为components
vector 的Screen
需要我们可以调用draw
method 打开。
使用 trait objects 和 Rust 的类型系统编写代码的优势 与使用 duck 类型的代码类似,我们永远不必检查 value 在运行时实现特定方法,或者担心出错 如果一个值没有实现方法,但我们仍然调用它。Rust 无法编译 如果值没有实现 trait 对象需要的 trait,则我们的代码。
例如,示例 17-10 展示了如果我们尝试创建一个Screen
替换为String
作为组件:
文件名: src/main.rs
use gui::Screen;
fn main() {
let screen = Screen {
components: vec![Box::new(String::from("Hi"))],
};
screen.run();
}
示例 17-10:尝试使用不 实现 trait 对象的 trait
我们收到这个错误是因为String
未实现Draw
特性:
$ cargo run
Compiling gui v0.1.0 (file:///projects/gui)
error[E0277]: the trait bound `String: Draw` is not satisfied
--> src/main.rs:5:26
|
5 | components: vec![Box::new(String::from("Hi"))],
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Draw` is not implemented for `String`
|
= help: the trait `Draw` is implemented for `Button`
= note: required for the cast from `Box<String>` to `Box<dyn Draw>`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `gui` (bin "gui") due to 1 previous error
这个错误让我们知道,要么我们将某些东西传递给Screen
我们
没有传递的意思,所以应该传递一个不同的类型,或者我们应该实现Draw
上String
因此Screen
能够调用draw
在上面。
特征对象执行动态调度
回想一下 “Performance of Code Using Generics“ 部分 第 10 章 我们对 compiler:编译器会生成 每个具体类型的函数和方法的非泛型实现 我们使用 in 代替泛型类型参数。生成的代码 monomorphization 执行静态调度,这是编译器知道 在编译时调用的方法。这与动态相反 dispatch,即编译器在编译时无法判断是哪个方法 你在打电话。在动态调度情况下,编译器发出的代码在 runtime 将确定要调用的方法。
当我们使用 trait 对象时,Rust 必须使用动态 dispatch。编译器不会 了解可能与使用 trait 对象的代码一起使用的所有类型, 所以它不知道要调用哪个方法在哪个类型上实现。相反,在 runtime,Rust 使用 trait 对象内的指针来知道要 叫。此查找会产生 static 不会发生的运行时成本 遣。动态调度还会阻止编译器选择内联 方法的代码,这反过来又会阻止某些优化。但是,我们确实得到了 我们在示例 17-5 中编写的代码具有额外的灵活性,并且能够 support 在示例 17-9 中,所以这是一个需要考虑的权衡。
本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准