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

相关推荐
不在逃避q6 分钟前
Trae/Vs Code/Cursor命令行无法跑npm命令
前端·arcgis·npm
夏殇之殁20 分钟前
包中创建自定义列表项。 . 使用自定义列表项进行数据绑定 . 将天气预报数据保存到本地内存表,通过LiveBindings进行显示。 ...
服务器·前端·javascript
The Chosen One98525 分钟前
高进度算法模板速记(待完善)
java·前端·算法
陈随易31 分钟前
MoonBit抓包模块pcap,查看电脑的每一次联网通信
前端·后端·程序员
新中地GIS开发老师2 小时前
WebGIS开发学生作品|低空航天管理与航线规划系统
前端·javascript·webgis·三维gis开发
用户059540174462 小时前
大模型对话记忆持久化踩坑实录:用 Playwright 自动化测了 300 次,终于揪出会话丢失的真凶
前端·css
丙氨酸長鏈2 小时前
Web前端入门第 问:JavaScript 一个简单的 IndexedDB 数据库入门示例
前端·javascript·数据库
kyriewen3 小时前
面试官让我手写虚拟列表——AI生成的版本,快速滚动几下就白屏了
前端·javascript·面试
IT_陈寒3 小时前
Vite热更新失效?你可能漏了这个配置
前端·人工智能·后端
梅孔立4 小时前
两种免费的翻译API调用方式详解 - Edge官方API与个人缓存服务
前端·缓存·edge