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

相关推荐
excel10 小时前
为什么相同卷积代码在不同层学到的特征完全不同——基于 tfjs-node 猫图像识别示例的逐层解析
前端
知识分享小能手10 小时前
React学习教程,从入门到精通,React 使用属性(Props)创建组件语法知识点与案例详解(15)
前端·javascript·vue.js·学习·react.js·前端框架·vue
用户214118326360210 小时前
dify案例分享-免费玩转即梦 4.0 多图生成!Dify 工作流从搭建到使用全攻略,附案例效果
前端
CodeSheep10 小时前
稚晖君又开始摇人了,有点猛啊!
前端·后端·程序员
JarvanMo11 小时前
Flutter Web vs Mobile:主要区别以及如何调整你的UI
前端
IT_陈寒11 小时前
Java性能优化:从这8个关键指标开始,让你的应用提速50%
前端·人工智能·后端
天生我材必有用_吴用11 小时前
Vue3+Node.js 实现大文件上传:断点续传、秒传、分片上传完整教程(含源码)
前端
摸鱼的春哥11 小时前
前端程序员最讨厌的10件事
前端·javascript·后端
牧羊狼的狼15 小时前
React 中的 HOC 和 Hooks
前端·javascript·react.js·hooks·高阶组件·hoc
知识分享小能手17 小时前
React学习教程,从入门到精通, React 属性(Props)语法知识点与案例详解(14)
前端·javascript·vue.js·学习·react.js·vue·react