繁体   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