简体   繁体   English

在 HashMap 中存储特征的不同实现

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

I've come up against an interesting issue.我遇到了一个有趣的问题。 Here is a playground link I'm trying to write a flexible task executor that is driven by providing JSON objects such as the following.这是一个操场链接,我正在尝试编写一个灵活的任务执行器,该执行器由提供 JSON 对象(如下所示)驱动。

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

The task executor would use the task_type to determine which executor to call and then pass the associated payload.任务执行器将使用task_type来确定调用哪个执行器,然后传递关联的有效负载。 Most of my types support generics because different task_type s would support different payload s.我的大多数类型都支持泛型,因为不同的task_type将支持不同的payload The way I've chosen to solve this problem is by creating a trait Task and a single implementor (for now) called RegisteredTask which essentially just calls a function pointer.我选择解决这个问题的方法是创建一个 trait Task和一个名为RegisteredTask的单一实现者(目前),它本质上只是调用一个函数指针。

My only compilation problem is the following which indicates that I would need to restrict my registry to supporting implementations of Task that take the same inputs and return the same outputs (bye bye generics).我唯一的编译问题是以下问题,这表明我需要将我的注册表限制为支持接受相同输入并返回相同输出的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>`

After watching this video I understand the the compiler needs to know the sizes of everything at compile time, but I thought that by putting the trait in a Box I would be able to avoid that.观看此视频后,我明白编译器需要在编译时知道所有内容的大小,但我认为通过将 trait 放在Box我可以避免这种情况。

What is the root cause of my problem, and is the idiomatic way around this?我的问题的根本原因是什么,解决这个问题的惯用方法是什么?

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("{}"));
}

The reason the compiler is upset with you is that generic types with different generic value are actually different types.编译器对你不满的原因是具有不同泛型值的泛型类型实际上是不同的类型。 This means that if you have a trait Foo<T> , and use it with T == Bar or T == Barcode , you have two different traits internally, not one.这意味着,如果您有一个trait Foo<T> ,并将其与T == BarT == Barcode ,则您在内部有两个不同的特征,而不是一个。 The same goes with associated types.关联类型也是如此。 A struct that implements Foo<InputType = Bar> and different struct that implements Foo<InputType = Barcode> implement different traits.实现Foo<InputType = Bar>结构和实现Foo<InputType = Barcode>不同结构实现不同的特征。

That is why writing dyn Task in your case does not make sense.这就是为什么在您的情况下编写dyn Task没有意义。 Compiler needs to know which exact variant of Task trait objects you want to store.编译器需要知道您要存储的Task trait 对象的确切变体。

Having generic parameters (or associated types) as method arguments can also prove to be a challenge later, as typically traits with such traits are not object safe, so you cannot use dyn Task for them.将泛型参数(或关联类型)作为方法参数也可能在以后被证明是一个挑战,因为具有此类特征的特征通常不是对象安全的,因此您不能对它们使用dyn Task

It's also not quite clear how you would use the result of tasks, as you don't know specific types that the trait objects from the registry would return.您将如何使用任务的结果也不太清楚,因为您不知道注册表中的 trait 对象将返回的特定类型。

Better design would be to have just one non-generic trait.更好的设计应该是只有一个非通用特征。 Then each struct implementing this trait could be constructed with Payload type as input and store it internally.然后可以使用Payload类型作为输入构造每个实现此特征的结构并将其存储在内部。 Then .execute() can be called without parameter.然后.execute()可以不带参数调用。

What type to return as the result of .execute() depends on the way you are going to use it.作为.execute()结果返回的类型取决于您将使用它的方式。 It could be an enum or serialized value.它可以是枚举或序列化值。

Probably there is a better design for what you are trying to achieve, but this is what comes to my mind.对于您想要实现的目标,可能有更好的设计,但这就是我想到的。

Thanks to @Maxim, I redesigned my implementation and instead of having a generic registered task type, I created non-generic task-specific types.感谢@Maxim,我重新设计了我的实现,而不是使用通用的注册任务类型,我创建了非通用的任务特定类型。 For example, here is a task that executes a shell command.例如,这里有一个执行 shell 命令的任务。

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

I went ahead and implemented the Task trait for this task, and because the payloads are now stored type specifically on the Task implementations, I don't have to worry about any generic implementations of the trait.我继续为这个任务实现了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