【Rust】——估计不同类型值的trait对象

💻博主现有专栏:

                C51单片机(STC89C516),c语言,c++,离散数学,算法设计与分析,数据结构,Python,Java基础,MySQL,linux,基于HTML5的网页设计及应用,Rust(官方文档重点总结),jQuery,前端vue.js,Javaweb开发,Python机器学习等
🥏主页链接:

                Y小夜-CSDN博客

目录

🎯定义通用行为的trait

🎯实现trait

🎯trait对象执行分发


        定义 SpreadsheetCell 枚举来储存整型,浮点型和文本成员的替代方案。这意味着可以在每个单元中储存不同类型的数据,并仍能拥有一个代表一排单元的 vector。这在当编译代码时就知道希望可以交替使用的类型为固定集合的情况下是完全可行的。

        然而有时我们希望库用户在特定情况下能够扩展有效的类型集合。为了展示如何实现这一点,这里将创建一个图形用户接口(Graphical User Interface,GUI)工具的例子,它通过遍历列表并调用每一个项目的 draw 方法来将其绘制到屏幕上 —— 此乃一个 GUI 工具的常见技术。我们将要创建一个叫做 gui 的库 crate,它含一个 GUI 库的结构。这个 GUI 库包含一些可供开发者使用的类型,比如 Button 或 TextField。在此之上,gui 的用户希望创建自定义的可以绘制于屏幕上的类型:比如,一个程序员可能会增加 Image,另一个可能会增加 SelectBox

        这个例子中并不会实现一个功能完善的 GUI 库,不过会展示其中各个部分是如何结合在一起的。编写库的时候,我们不可能知晓并定义所有其他程序员希望创建的类型。我们所知晓的是 gui 需要记录一系列不同类型的值,并需要能够对其中每一个值调用 draw 方法。这里无需知道调用 draw 方法时具体会发生什么,只要该值会有那个方法可供我们调用。

        在拥有继承的语言中,可以定义一个名为 Component 的类,该类上有一个 draw 方法。其他的类比如 ButtonImage 和 SelectBox 会从 Component 派生并因此继承 draw 方法。它们各自都可以覆盖 draw 方法来定义自己的行为,但是框架会把所有这些类型当作是 Component 的实例,并在其上调用 draw。不过 Rust 并没有继承,我们得另寻出路。

🎯定义通用行为的trait

        为了实现 gui 所期望的行为,让我们定义一个 Draw trait,其中包含名为 draw 的方法。接着可以定义一个存放 trait 对象trait object)的 vector。trait 对象指向一个实现了我们指定 trait 的类型的实例,以及一个用于在运行时查找该类型的 trait 方法的表。

        我们可以使用 trait 对象代替泛型或具体类型。任何使用 trait 对象的位置,Rust 的类型系统会在编译时确保任何在此上下文中使用的值会实现其 trait 对象的 trait。如此便无需在编译时就知晓所有可能的类型。

        之前提到过,Rust 刻意不将结构体与枚举称为 “对象”,以便与其他语言中的对象相区别。在结构体或枚举中,结构体字段中的数据和 impl 块中的行为是分开的,不同于其他语言中将数据和行为组合进一个称为对象的概念中。trait 对象将数据和行为两者相结合,从这种意义上说  其更类似其他语言中的对象。不过 trait 对象不同于传统的对象,因为不能向 trait 对象增加数据。trait 对象并不像其他语言中的对象那么通用:其(trait 对象)具体的作用是允许对通用行为进行抽象。

pub trait Draw {
    fn draw(&self);
}

        一个存放了名叫 components 的 vector 的结构体 Screen。这个 vector 的类型是 Box<dyn Draw>,此为一个 trait 对象:它是 Box 中任何实现了 Draw trait 的类型的替身。

pub struct Screen {
    pub components: Vec<Box<dyn Draw>>,
}

        在 Screen 结构体上,我们将定义一个 run 方法,该方法会对其 components 上的每一个组件调用 draw 方法

impl Screen {
    pub fn run(&self) {
        for component in self.components.iter() {
            component.draw();
        }
    }
}

        这与定义使用了带有 trait bound 的泛型类型参数的结构体不同。泛型类型参数一次只能替代一个具体类型,而 trait 对象则允许在运行时替代多种具体类型。

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();
        }
    }
}

        这限制了 Screen 实例必须拥有一个全是 Button 类型或者全是 TextField 类型的组件列表。如果只需要同质(相同类型)集合,则倾向于使用泛型和 trait bound,因为其定义会在编译时采用具体类型进行单态化。

        另一方面,通过使用 trait 对象的方法,一个 Screen 实例可以存放一个既能包含 Box<Button>,也能包含 Box<TextField> 的 Vec<T>。让我们看看它是如何工作的,接着会讲到其运行时性能影响。

🎯实现trait

        现在来增加一些实现了 Draw trait 的类型。我们将提供 Button 类型。再一次重申,真正实现 GUI 库超出了本书的范畴,所以 draw 方法体中不会有任何有意义的实现。为了想象一下这个实现看起来像什么,一个 Button 结构体可能会拥有 widthheight 和 label 字段:

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
    }
}

        在 Button 上的 widthheight 和 label 字段会和其他组件不同,比如 TextField 可能有 widthheightlabel 以及 placeholder 字段。每一个我们希望能在屏幕上绘制的类型都会使用不同的代码来实现 Draw trait 的 draw 方法来定义如何绘制特定的类型,像这里的 Button 类型(如上提到的并不包含任何实际的 GUI 代码)。除了实现 Draw trait 之外,比如 Button 还可能有另一个包含按钮点击如何响应的方法的 impl 块。这类方法并不适用于像 TextField 这样的类型。

        如果一些库的使用者决定实现一个包含 widthheight 和 options 字段的结构体 SelectBox,并且也为其实现了 Draw trait

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
    }
}

        库使用者现在可以在他们的 main 函数中创建一个 Screen 实例。至此可以通过将 SelectBox 和 Button 放入 Box<T> 转变为 trait 对象再放入 Screen 实例中。

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();
}

        当编写库的时候,我们不知道何人会在何时增加 SelectBox 类型,不过 Screen 的实现能够操作并绘制这个新类型,因为 SelectBox 实现了 Draw trait,这意味着它实现了 draw 方法。

        这个概念 —— 只关心值所反映的信息而不是其具体类型 —— 类似于动态类型语言中称为 鸭子类型duck typing)的概念:如果它走起来像一只鸭子,叫起来像一只鸭子,那么它就是一只鸭子!在示例 17-5 中 Screen 上的 run 实现中,run 并不需要知道各个组件的具体类型是什么。它并不检查组件是 Button 或者 SelectBox 的实例。通过指定 Box<dyn Draw> 作为 components vector 中值的类型,我们就定义了 Screen 为需要可以在其上调用 draw 方法的值。

🎯trait对象执行分发

        当对泛型使用 trait bound 时编译器所执行的单态化处理:编译器为每一个被泛型类型参数代替的具体类型生成了函数和方法的非泛型实现。单态化产生的代码在执行 静态分发static dispatch)。静态分发发生于编译器在编译时就知晓调用了什么方法的时候。这与 动态分发 (dynamic dispatch)相对,这时编译器在编译时无法知晓调用了什么方法。在动态分发的场景下,编译器会生成负责在运行时确定该调用什么方法的代码。

        当使用 trait 对象时,Rust 必须使用动态分发。编译器无法知晓所有可能用于 trait 对象代码的类型,所以它也不知道应该调用哪个类型的哪个方法实现。为此,Rust 在运行时使用 trait 对象中的指针来知晓需要调用哪个方法。动态分发也阻止编译器有选择的内联方法代码,这会相应的禁用一些优化。尽管在编写示例 17-5 和可以支持示例 17-9 中的代码的过程中确实获得了额外的灵活性,但仍然需要权衡取舍。

  • 27
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Y小夜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值