使用允许不同类型值的 trait 对象

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

但是,有时我们希望我们的库用户能够扩展 类型。展示我们如何实现 为此,我们将创建一个示例图形用户界面 (GUI) 工具,该工具迭代 通过项目列表,调用draw方法将其绘制到 screen - GUI 工具的常用技术。我们将创建一个名为gui,其中包含 GUI 库的结构。此 crate 可能包含 一些类型供人们使用,例如ButtonTextField.另外gui用户将希望创建自己的可以绘制的类型:For 实例中,一个程序员可能会添加一个Image另一个可能会添加SelectBox.

我们不会为此示例实现一个成熟的 GUI 库,但将显示 这些碎片如何组合在一起。在编写库时,我们不能 了解并定义其他程序员可能想要创建的所有类型的类型。但我们确实如此 知道gui需要跟踪许多不同类型的值,并且 需要调用draw方法。它 不需要确切知道当我们调用draw方法 只是该值将具有可供我们调用的方法。

要在具有继承的语言中执行此作,我们可以定义一个名为Component它有一个名为draw在上面。其他类(例如Button,ImageSelectBox,将继承自Component因此 继承draw方法。它们都可以覆盖draw定义 method to define 它们的自定义行为,但框架可以将所有类型视为 他们是Component实例并调用draw在他们身上。但是因为 Rust 没有继承,我们需要另一种方法来构建guilibrary 设置为 允许用户使用新类型扩展它。

定义常见行为的特征

为了实现我们想要的行为gui为此,我们将定义一个名为Draw它将有一个名为draw.然后我们可以定义一个向量,该向量 接受一个 trait 对象。trait 对象同时指向一个类型的实例 实现我们指定的 trait 和一个用于查找 trait 方法的表 该类型。我们通过指定某种 指针,例如引用或&Box<T>智能指针,则dynkeyword,然后指定相关特征。(我们将讨论原因 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: 定义Screenstruct 替换为componentsfield 中,其中包含实现Draw特性

Screenstruct 中,我们将定义一个名为run这将调用drawmethod 添加到其每个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:一个runmethod 开启Screen这会调用drawmethod 在每个组件上

这与定义使用泛型类型的结构不同 参数。泛型类型参数只能被替换 一次使用一个具体类型,而 trait 对象允许多个 具体类型来填充 trait 对象。例如,我们 可以定义Screenstruct 使用泛型类型和 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:Screenstruct 及其run使用泛型和 trait 边界的方法

这限制了我们Screen实例,该实例具有所有组件列表 类型Button或所有类型TextField.如果您只拥有同构 集合中,使用泛型和 trait bounds 是可取的,因为 定义将在编译时被单态化以使用具体类型。

另一方面,对于使用 trait 对象的方法,一个Screen实例 可以容纳Vec<T>,其中包含一个Box<Button>以及Box<TextField>.让我们看看它是如何工作的,然后我们将讨论 运行时性能影响。

实现 Trait

现在,我们将添加一些实现Draw特性。我们将提供Button类型。同样,实际实现 GUI 库超出了范围 的drawmethod 中没有任何有用的实现 身体。为了想象实现可能是什么样子,一个Button结构 可能有width,heightlabel,如示例 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,heightlabel字段Button将与 其他组件上的字段;例如,TextFieldtype 可能具有这些 相同的字段加上placeholder田。我们想要利用的每种类型 该界面将实现Drawtrait 中,但在draw方法来定义如何绘制该特定类型,如Button在这里 (如前所述,没有实际的 GUI 代码)。这Buttontype 等 可能有一个额外的impl块中包含与 what 相关的方法 在用户单击按钮时发生。这些类型的方法不适用于 类型,如TextField.

如果有人使用我们的库决定实施SelectBoxstruct 中具有width,heightoptions字段中,它们实现DrawtraitSelectBoxtype 中也一样,如示例 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并实施 这DrawtraitSelectBox结构

我们库的用户现在可以编写他们的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类型,但是我们的Screenimplementation 能够在 new 类型并绘制它,因为SelectBox实现Drawtrait 的 表示它实现了draw方法。

这个概念 — 只关心值响应的消息 而不是值的具体类型 - 类似于 Duck 的概念 使用动态类型语言键入:如果它像鸭子和嘎嘎声一样走路 像鸭子一样,那一定是鸭子!在runScreen在示例 17-5 中,run不需要知道每个的具体类型是什么 组件是。它不会检查组件是否是ButtonSelectBox,它只是调用draw方法。由 指定Box<dyn Draw>作为componentsvector 的Screen需要我们可以调用drawmethod 打开。

使用 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我们 没有传递的意思,所以应该传递一个不同的类型,或者我们应该实现DrawString因此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/)为准