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

相关推荐
万叶学编程2 小时前
Day02-JavaScript-Vue
前端·javascript·vue.js
前端李易安4 小时前
Web常见的攻击方式及防御方法
前端
PythonFun4 小时前
Python技巧:如何避免数据输入类型错误
前端·python
知否技术4 小时前
为什么nodejs成为后端开发者的新宠?
前端·后端·node.js
hakesashou4 小时前
python交互式命令时如何清除
java·前端·python
天涯学馆4 小时前
Next.js与NextAuth:身份验证实践
前端·javascript·next.js
HEX9CF5 小时前
【CTF Web】Pikachu xss之href输出 Writeup(GET请求+反射型XSS+javascript:伪协议绕过)
开发语言·前端·javascript·安全·网络安全·ecmascript·xss
ConardLi5 小时前
Chrome:新的滚动捕捉事件助你实现更丝滑的动画效果!
前端·javascript·浏览器
ConardLi5 小时前
安全赋值运算符,新的 JavaScript 提案让你告别 trycatch !
前端·javascript