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,换句话说,它不是对象安全的。

相关推荐
lyc2333333 分钟前
鸿蒙多子类型输入法:3步实现输入模式自由切换🔤
前端
Danta3 分钟前
从 0 开始学习 Three.js(2)😁
前端·javascript·three.js
凌辰揽月4 分钟前
Web后端基础(基础知识)
java·开发语言·前端·数据库·学习·算法
Dignity_呱5 分钟前
vue3对组件通信做了哪些升级?
前端·vue.js·面试
植物系青年7 分钟前
基于 Lowcode Engine 的低码平台“编码效率”提升实践
前端·低代码
就是我8 分钟前
开发“业务组件库”,该从哪里入手?
前端·javascript·面试
Mintopia10 分钟前
在数字画布上雕刻曲线:NURBS 的奇幻冒险之旅
前端·javascript·计算机图形学
Hacker_seagull14 分钟前
Chrome安装代理插件ZeroOmega(保姆级别)
前端·chrome
石小石Orz18 分钟前
因为没有使用路由懒加载,产生了一个难以寻找的bug
前端
Mintopia18 分钟前
Three.js 力导向图:让数据跳起优雅的华尔兹
前端·javascript·three.js