美文网首页
Rust for cpp dev - 使用 Trait 对象实现

Rust for cpp dev - 使用 Trait 对象实现

作者: 找不到工作 | 来源:发表于2021-05-23 16:50 被阅读0次

    假设我们希望设计一个 GUI 库,对于每一个组件,我们希望能调用 draw() 方法来显示。

    对于传统的有“继承”特性的语言,可以让所有组件都继承自一个 Component 类,这个类有一个纯虚函数 draw(),然后每个组件实现各自的 draw() 方法。

    然而 Rust 没有继承的概念,如果用泛型来实现:

    pub struct StaticScreen<T: Draw> {
        pub components: Vec<T>,
    }
    
    impl<T: Draw> StaticScreen<T> {
        pub fn show(&self) {
            for component in self.components.iter() {
                component.draw();
            }
        }
    }
    

    这样的话,StaticScreen 类型只能存放一种类型的组件。例如Button。不能满足需求。

    我们需要一个新的方式来实现,即 Trait 对象。

    定义 trait 对象

    我们需要定义一个 Draw trait,然后定义一个 vector,其中的对象都是实现了 Draw trait 的。每个 trait 对象指向两个东西:

    • 一个实现了 Draw trait 的类型的实例
    • (类似于 cpp 中的虚表)一个在 runtime 查找 trait 方法的表

    trait 对象不能拥有数据成员,它的作用仅是允许抽象出公共的行为。 其定义方法是:

    Box<dyn T>>
    

    例如,对于这里的需求,可以如下定义:

    pub struct DynamicScreen {
        pub components: Vec<Box<dyn Draw>>,
    }
    
    impl DynamicScreen {
        pub fn show(&self) {
            for component in self.components.iter() {
                component.draw();
            }
        }
    }
    

    这样,components 就是一个由实现了 Draw trait 的智能指针构成的 vector。可以如下使用:

    use gui::{DynamicScreen, Button, SelectBox};
    
    fn main() {
        let screen = DynamicScreen {
            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.show();
    }
    

    可以看出,screen 中保存了两个类的实例,分别是 ButtonSelectBox。他们的共同特点是都实现了 Draw trait。运行时会调用各自的 draw() 函数。

    // component definition
    
    pub struct Button {
        pub width: u32,
        pub height: u32,
        pub label: String,
    }
    
    impl Draw for Button {
        fn draw(&self) {
            println!("Draw a button");
        }
    }
    
    pub struct SelectBox {
        pub width: u32,
        pub height: u32,
        pub options: Vec<String>,
    }
    
    impl Draw for SelectBox {
        fn draw(&self) {
            println!("Draw a select box");
        }
    }
    

    结果是:

    Draw a select box
    Draw a button
    

    Rust 中,我们并不关心 ButtonSelectBox 之间有什么关系,只要他们都实现了 draw() 方法,就可以调用。这个思路和 golang 相似,都是基于 duck typing 的概念——如果它走路像样子,叫声也像鸭子,那它就是鸭子。

    这样做的优势是我们不需要在运行时去检查是否实现了一个方法(cpp 中的 dynamic_cast),如果没有实现 Draw trait,它不会通过编译。

    trait 对象的开销

    trait 对象实际上起到了 cpp 中的虚函数的作用,因此也有类似的额外开销。

    在上面的例子中,编译器无法在编译时知道要调用的对象的类型,因为这些对象是在一个动态数组中的,实际使用时,可能会根据用户输入变化。所以,它只能在运行时通过 trait 对象内部的指针来查找需要调用的方法。

    相关文章

      网友评论

          本文标题:Rust for cpp dev - 使用 Trait 对象实现

          本文链接:https://www.haomeiwen.com/subject/mdoajltx.html