簡體   English   中英

在 HashMap 中存儲特征的不同實現

[英]Storing different implementations of a trait in a HashMap

我遇到了一個有趣的問題。 這是一個操場鏈接,我正在嘗試編寫一個靈活的任務執行器,該執行器由提供 JSON 對象(如下所示)驅動。

{
    "task_type": "foobar",
    "payload": {}
}

任務執行器將使用task_type來確定調用哪個執行器,然后傳遞關聯的有效負載。 我的大多數類型都支持泛型,因為不同的task_type將支持不同的payload 我選擇解決這個問題的方法是創建一個 trait Task和一個名為RegisteredTask的單一實現者(目前),它本質上只是調用一個函數指針。

我唯一的編譯問題是以下問題,這表明我需要將我的注冊表限制為支持接受相同輸入並返回相同輸出的Task實現(再見泛型)。

Compiling playground v0.0.1 (/playground)
error[E0191]: the value of the associated types `Input` (from trait `Task`), `Output` (from trait `Task`) must be specified
  --> src/lib.rs:30:36
   |
5  |     type Input;
   |     ----------- `Input` defined here
6  |     type Output;
   |     ------------ `Output` defined here
...
30 |     tasks: HashMap<String, Box<dyn Task>>,
   |                                    ^^^^ help: specify the associated types: `Task<Input = Type, Output = Type>`

觀看此視頻后,我明白編譯器需要在編譯時知道所有內容的大小,但我認為通過將 trait 放在Box我可以避免這種情況。

我的問題的根本原因是什么,解決這個問題的慣用方法是什么?

use std::collections::HashMap;

/// Task defines something that can be executed with an input and respond with an output
trait Task {
    type Input;
    type Output;
    fn execute(&self, input: Self::Input) -> Self::Output;
}

/// RegisteredTask is an implementation of Task that simply executes a predefined function and
/// returns it's output. It is generic so that there can be many different types of RegisteredTasks
/// that do different things.
struct RegisteredTask<T, K> {
    action: fn(T) -> K,
}

impl<T, K> Task for RegisteredTask<T, K> {
    type Input = T;
    type Output = K;

    /// Executes the registered task's action function and returns the output
    fn execute(&self, input: Self::Input) -> Self::Output {
        (self.action)(input)
    }
}

/// Maintains a collection of anything that implements Task and associates them with a name. This
/// allows us to easily get at a specific task by name.
struct Registry {
    tasks: HashMap<String, Box<dyn Task>>,
}

impl Registry {
    /// Registers an implementation of Task by name
    fn register<T, K>(&mut self, name: &str, task: Box<dyn Task<Input = T, Output = K>>) {
        self.tasks.insert(name.to_string(), task);
    }

    /// Gets an implementation of task by name if it exists, over a generic input and output
    fn get<T, K>(&self, name: &str) -> Option<&Box<dyn Task<Input = T, Output = K>>> {
        self.tasks.get(name)
    }
}

/// An example input for a registered task
#[derive(Debug)]
pub struct FooPayload {}

/// An example output for a registered task
#[derive(Debug)]
pub struct FooOutput {}

/// Executes the named task from the registry and decodes the required payload from a JSON string. The
/// decoding is not shown here for simplicity.
fn execute_task(registry: &Registry, name: &str, json_payload: String) {
    match name {
        "foobar" => {
            let task = registry.get::<FooPayload, FooOutput>(name).unwrap();
            let output = task.execute(FooPayload {});
            println!("{:?}", output)
        }
        _ => {}
    }
}

#[test]
fn test_execute_task() {
    // Create a new empty registry
    let mut registry = Registry {
        tasks: HashMap::new(),
    };

    // Register an implementation of Task (RegisteredTask) with the name "foobar"
    registry.register::<FooPayload, FooOutput>(
        "foobar",
        Box::new(RegisteredTask::<FooPayload, FooOutput> {
            // This is the function that should be called when this implementation of Task is invoked
            action: |payload| {
                println!("Got payload {:?}", payload);
                FooOutput {}
            },
        }),
    );

    // Attempt to invoke whatever implementation of Task (in this case a RegisteredTask) is named 'foobar'.
    execute_task(&registry, "foobar", String::from("{}"));
}

編譯器對你不滿的原因是具有不同泛型值的泛型類型實際上是不同的類型。 這意味着,如果您有一個trait Foo<T> ,並將其與T == BarT == Barcode ,則您在內部有兩個不同的特征,而不是一個。 關聯類型也是如此。 實現Foo<InputType = Bar>結構和實現Foo<InputType = Barcode>不同結構實現不同的特征。

這就是為什么在您的情況下編寫dyn Task沒有意義。 編譯器需要知道您要存儲的Task trait 對象的確切變體。

將泛型參數(或關聯類型)作為方法參數也可能在以后被證明是一個挑戰,因為具有此類特征的特征通常不是對象安全的,因此您不能對它們使用dyn Task

您將如何使用任務的結果也不太清楚,因為您不知道注冊表中的 trait 對象將返回的特定類型。

更好的設計應該是只有一個非通用特征。 然后可以使用Payload類型作為輸入構造每個實現此特征的結構並將其存儲在內部。 然后.execute()可以不帶參數調用。

作為.execute()結果返回的類型取決於您將使用它的方式。 它可以是枚舉或序列化值。

對於您想要實現的目標,可能有更好的設計,但這就是我想到的。

感謝@Maxim,我重新設計了我的實現,而不是使用通用的注冊任務類型,我創建了非通用的任務特定類型。 例如,這里有一個執行 shell 命令的任務。

pub struct Exec {
    command: String,
    args: Vec<String>,
}

我繼續為這個任務實現了Task trait,因為負載現在是專門存儲在Task實現上的類型,所以我不必擔心 trait 的任何通用實現。

impl Task for Exec {
    async fn execute(&self) -> Result<Value> {
        // ....
    }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM