Rust中的异构集合

447次阅读  |  发布于1年以前

在某些情况下,当编写软件时,开发人员会遇到异构集合的需求——也就是说,可以存储不同类型对象的集合。在Rust中,开发人员可以通过不同的方式实现这一目标,并进行不同的权衡。

使用枚举

Rust枚举是实现这一目标的好方法,如果要存储的对象的所有实现在开发时都是已知的,那么开发人员可以创建一个枚举来封装每种可能的类型,然后为这些枚举创建一个集合。

然后,为了访问内部类的方法和属性,可以使用匹配表达式来检索内部对象。

enum ComponentType {
    FirstComponent(MyFirstComponent),
    SecondComponent(MySecondComponent),
}

struct MyFirstComponent {
}

impl MyFirstComponent {
    fn do_first_component_thing(&self) {
        println!("First Component");        
    }    
}

struct MySecondComponent {
}

impl MySecondComponent {
    fn do_second_component_thing(&self) {
        println!("Second Component");        
    }
}

fn main() {
    // 创建一个枚举集合
    let mut components: Vec<ComponentType> = Vec::new();

    // 将枚举添加到集合中,封装目标类型
    components.push(ComponentType::FirstComponent(MyFirstComponent {}));
    components.push(ComponentType::SecondComponent(MySecondComponent {}));

    // 使用匹配表达式从枚举中检索对象,并访问方法和属性
    if let ComponentType::FirstComponent(component) = &components[0] {
        component.do_first_component_thing();
    }
}

这种方法的一个缺点是,在编写代码时需要确切知道这些被存储的组件类型。

使用Trait

另一种方法是使用特征作为替代解决方案,其中:

trait Component {
    fn do_component_thing(&self);
}

struct MyFirstComponent {}

impl Component for MyFirstComponent {
    fn do_component_thing(&self) {
        println!("First Component");
    }
}

struct MySecondComponent {}

impl Component for MySecondComponent {
    fn do_component_thing(&self) {
        println!("Second Component");
    }
}

fn main() {
    let mut components: Vec<Box<dyn Component>> = Vec::new();
    components.push(Box::new(MyFirstComponent { }));
    components.push(Box::new(MySecondComponent { }));

    components[0].do_component_thing();
    components[1].do_component_thing();
}

当只需要访问所有特征中的公共方法时,这种方法工作得很好。这种方法的一个缺点是元素总是在堆上分配,另一个缺点是只能访问公共方法。

使用Any Trait

Rust文档将Any类型描述为一个模拟动态类型的Trait,它提供了一个向下转换方法,允许将一个类型转换为不同的类型。

use std::any::Any;

struct MyFirstComponent {
}

impl MyFirstComponent {
    fn do_first_component_thing(&self) {
        println!("First Component");        
    }    
}

struct MySecondComponent {
}

impl MySecondComponent {
    fn do_second_component_thing(&self) {
        println!("Second Component");        
    }
}

fn main() {
    let mut components: Vec<Box<dyn Any>> = Vec::new();
    components.push(Box::new(MyFirstComponent {}));
    components.push(Box::new(MySecondComponent {}));

    if let Some(component) =
            components[0].downcast_ref::<MyFirstComponent>() {
        component.do_first_component_thing();
    }

    if let Some(component) =
            components[1].downcast_ref::<MySecondComponent>() {
        component.do_second_component_thing();
    }
}

虽然这仍然会始终在堆上分配对象,但现在可以在数据结构中使用不同的组件类型,将它们转换为原始类型并访问组件特定的属性和方法。

不过,有一个小问题——没有限制哪些类型可以添加到结构中。

混合Any和Trait

Any可以与Trait一起使用来为对象创建边界。诀窍是在trait中添加一个方法,将对象转换为Any,然后将其向下转换为其他对象。然后,每个结构体都必须实现trait和转换方法:

use std::any::Any;

trait Component {
    fn as_any(&self) -> &dyn Any;    
}

struct MyFirstComponent {
}

impl MyFirstComponent {
    fn do_first_component_thing(&self) {
        println!("First Component");        
    }    
}

impl Component for MyFirstComponent {
    fn as_any(&self) -> &dyn Any {
        self
    }
}

struct MySecondComponent {
}

impl MySecondComponent {
    fn do_second_component_thing(&self) {
        println!("Second Component");        
    }
}

impl Component for MySecondComponent {
    fn as_any(&self) -> &dyn Any {
        self
    }
}

fn main() {
    let mut components: Vec<Box<dyn Component>> = Vec::new();
    components.push(Box::new(MyFirstComponent {}));
    components.push(Box::new(MySecondComponent {}));

    if let Some(component) = 
            components[0].as_any().downcast_ref::<MyFirstComponent>() {
        component.do_first_component_thing();
    }

    if let Some(component) =
            components[1].as_any().downcast_ref::<MySecondComponent>() {
        component.do_second_component_thing();
    }
}

虽然这仍然会在堆上分配对象,但对象特定的方法和属性可以与向下转换一起使用,并且集合绑定到实现该特征的对象。一个很大的缺点是必须为每个对象实现trait,这恰恰是样板代码。

使用过程宏来减少样板代码

使用过程性宏来实现减少样板代码的解决方案:

// 派生宏需要驻留在自己的crate中
#[proc_macro_derive(Component)]
pub fn component_macro_derive(input: TokenStream) -> TokenStream {
    let ast: DeriveInput = syn::parse(input).unwrap();
    let name = &ast.ident;
    let gen = quote! {
        impl Component for #name {
            fn as_any(&self) -> &dyn Any {
                self
            }
        }
    };
    gen.into()
}

// Component仍然存在于项目文件中.
#[derive(Component)]
struct MyFirstComponent {
}

impl MyFirstComponent {
    fn do_first_component_thing(&self) {
        println!("First Component");        
    }    
}

总结

在Rust中有不同的方法来实现异构集合,虽然使用枚举似乎被认为是最常用的方法,但并不适合所有情况。在某些特定情况下,可以使用Trait和Any。

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8