57_使用trait对象来存储不同类型的值

1. 概述

假设我们有这样的需求:创建一个GUI工具,它会遍历某个元素(指的是GUI原属)的列表,依次调用元素的draw方法进行绘制。例如Button、TextField等元素。

对于以上的需求,在面向对象的语言里

  • 我们通常先定义一个Component父类,里面定义了draw方法
  • 接下来定义各个元素的类(Button、TextField等),它们都继承于Component类,再覆盖draw方法

而在rust里是没有继承功能的,所以说如果想用rust来构建这个GUI工具,我们就得使用其他的方法,即为共有的行为定义一个trait。

2. 为共有行为定义一个trait

在rust里:

  • 我们避免将struct或enum称为对象,虽然它们是持有数据,但是它们的方法实现是在impl块里,而struct和enum和impl块是分开的。
  • 而trait对象有些类似于其他语言中的对象,因为trait对象在某种程度上实际上是组合了数据和行为。
  • trait对象与传统的对象不同的地方在于,我们无法为trait对象添加数据。
  • trait对象被专门用于抽象共有行为的,它没有其他语言中的对象那么通用。

看一个示例代码: lib.rs

rust 复制代码
pub trait Draw {
    fn draw(&self);
}

pub struct Screen {
    // 下面一行代码表示Box里的元素都实现了Draw trait
    // 所以只要实现了Draw trait的数据,都可以防汛Vec中
    // 之所以不使用泛型,应为泛型里只能存放一种元素
    pub components: Vec<Box<dyn Draw>>
}

impl Screen {
    pub fn run(&self) {
        // 遍历所有组件,执行draw方法
        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) {
        // 绘制一个按钮
    }
}

main.rs

rust 复制代码
use trait_save_different_val::Draw;
use trait_save_different_val::{Button, Screen};

struct SelectBox {
    width: u32,
    height: u32,
    options: Vec<String>,
}

impl Draw for SelectBox {
    fn draw(&self) {
        // 绘制一个选择框
    }
}

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

3. trait对象执行的是动态派发

将trait约束作用于泛型时,rust编译器会执行单态化。编译器会为我们用来替换泛型类型参数的每一个具体类型生成对应函数和方法的非泛型实现。

通过单态化生成的代码会执行静态派发(static dispatch),在编译过程中确定调用的具体方法。

所谓的动态派发(dynamic dispatch),它无法在编译过程中确定你调用的究竟是哪个方法,编译时会产生额外的代码以便在运行时找出希望调用的方法。如果使用trait对象,就会执行动态派发,那么将会导致产生运行时开销,并且阻止编译器内联方法代码,使得部分优化无法进行。

4. Trait对象必须保证对象安全

只能把满足对象安全(object-safe)的trait转化为trait对象。rust采用了一系列规则来判定某个对象是否安全,我们只需要记住两条规则

  • 方法的返回类型不是Self
  • 方法中不包含任何泛型类型参数

我们来看一个示例,在标准库中,Clone trait就是不符合对象安全的例子,Clone trait的clone方法如下

rust 复制代码
pub trait Clone {
    fn clone(&self) -> Self;
}

如果我们在"2"中的示例代码使用Clone trait作为 Screen 结构体 components 的对象,如下代码

rust 复制代码
pub struct Screen {
    pub commponents: Vec<Box<dyn Clone>>
}

程序将会报错,因为Clone trait的clone方法返回Self,换句话说,它不是对象安全的。

相关推荐
光影少年几秒前
vue2与vue3的全局通信插件,如何实现自定义的插件
前端·javascript·vue.js
As977_2 分钟前
前端学习Day12 CSS盒子的定位(相对定位篇“附练习”)
前端·css·学习
susu10830189114 分钟前
vue3 css的样式如果background没有,如何覆盖有background的样式
前端·css
Ocean☾5 分钟前
前端基础-html-注册界面
前端·算法·html
Dragon Wu8 分钟前
前端 Canvas 绘画 总结
前端
CodeToGym12 分钟前
Webpack性能优化指南:从构建到部署的全方位策略
前端·webpack·性能优化
~甲壳虫13 分钟前
说说webpack中常见的Loader?解决了什么问题?
前端·webpack·node.js
~甲壳虫17 分钟前
说说webpack proxy工作原理?为什么能解决跨域
前端·webpack·node.js
Cwhat19 分钟前
前端性能优化2
前端
熊的猫1 小时前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js