Rust 中的異構集合

在某些情況下,當編寫軟件時,開發人員會遇到異構集合的需求——也就是說,可以存儲不同類型對象的集合。在 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。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/MLZaC8nq6KoHenK73_W6JA