如何在容器中存储指向异步方法的指针?

How to store a pointer to an async method in a container?

我有一个定义多个 async 方法的结构,我想将每个方法的指针存储在 HashMap 中,这样我就可以在一个方法中调用任何方法行,只知道参数中给定的键。

这里的目的是尽可能避免有一个巨大的 match 子句,当我向我的结构中添加新方法时,它会膨胀得越来越多。

方法都具有相同的签名:

async fn handle_xxx(&self, c: Command) -> Result<String, ()>

我真的很想这样称呼他们:

pub async fn execute_command(&mut self, command: Command) -> Result<String, ()> {
    let handler = self.command_callbacks.get(&command);
    let return_message: String = match handler {
        Some(f) => f(self, command).await.unwrap(), // The call is made here.
        None => return Err(()),
    };
    Ok(return_message)
}

然而,显然,为了在 HashMap 中存储一些东西,你必须在声明 HashMap 时指定它的类型,这就是麻烦开始的时候。

我尝试了最明显的方法,即声明包装函数类型:

type CommandExecutionNotWorking = fn(&CommandExecutor, Command) -> Future<Output = Result<String, ()>>;

这是行不通的,因为 Future 是一个特征,而不是一个类型。

我试图声明一个泛型类型并在下面的某处指定它:

type CommandExecutionNotWorkingEither<Fut> = fn(&CommandExecutor, Command) -> Fut;

但是我遇到了同样的问题,因为我需要指定 Future 类型,并且有一个 HashMap 声明,如下所示:

let mut command_callbacks: HashMap<
    Command,
    CommandExecutionFn<dyn Future<Output = Result<String, ()>>>,
> = HashMap::new();

impl Future 显然不起作用,因为我们不在函数签名中,Future 要么因为它不是类型,而 dyn Future 会造成合法的类型不匹配。

因此我尝试使用 Pin 以便我可以操纵 dyn Futures,最后得到以下签名:

type CommandExecutionStillNotWorking = fn(
    &CommandExecutor,
    Command,
) -> Pin<Box<dyn Future<Output = Result<String, ()>>>>;

但我需要操作 return Pin<Box<dyn Future<...>>> 而不仅仅是 Future 的函数。因此,我尝试定义一个 lambda,它在参数中采用 async 函数,并且 returns 是一个将我的 async 方法的 return 值包装在 Pin<Box<...>> 中的函数:

let wrap = |f| {
    |executor, command| Box::pin(f(&executor, command))
};

但是编译器不高兴,因为它希望我定义 f 的类型,这是我在这里试图避免的,所以我回到原点。

因此我的问题是:您知道是否真的可以编写 async 函数的类型,以便可以像任何变量或任何其他函数指针一样轻松地操纵它们上的指针? 或者我应该寻求另一种可能不太优雅的解决方案,有一些重复代码或巨大的 match 结构?

TL;DR: 是的,这是可能的,但可能比你想象的要复杂。


首先,闭包不能是通用的,因此您需要一个函数:

fn wrap<Fut>(f: fn(&CommandExecutor, Command) -> Fut) -> CommandExecution
where
    Fut: Future<Output = Result<String, ()>>
{
    move |executor, command| Box::pin(f(executor, command))
}

但是你不能将 returned 闭包变成函数指针,因为它捕获 f.

现在技术上 应该 是可能的,因为我们只想处理函数项(即 non-capturing)及其类型(除非转换为函数指针) 是 zero-sized。因此,仅凭类型我们就应该能够构造一个实例。但是这样做需要不安全的代码:

fn wrap<Fut, F>(_f: F) -> CommandExecution
where
    Fut: Future<Output = Result<String, ()>>,
    F: Fn(&CommandExecutor, Command) -> Fut,
{
    assert_eq!(std::mem::size_of::<F>(), 0, "expected a fn item");
    move |executor, command| {
        // SAFETY: `F` is a ZST (checked above), any (aligned!) pointer, even crafted
        // out of the thin air, is valid for it.
        let f: &F = unsafe { std::ptr::NonNull::dangling().as_ref() };
        Box::pin(f(executor, command))
    }
}

(我们需要_f作为参数,因为我们无法指定函数类型;让推理自己找到它。)

然而,麻烦并没有就此结束。他们才刚刚开始。

现在我们得到以下错误:

error[E0310]: the parameter type `Fut` may not live long enough
  --> src/lib.rs:28:9
   |
18 | fn wrap<Fut, F>(_f: F) -> CommandExecution
   |         --- help: consider adding an explicit lifetime bound...: `Fut: 'static`
...
28 |         Box::pin(f(executor, command))
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ...so that the type `Fut` will meet its required lifetime bounds

好吧,它提出了一个解决方案。让我们试试...

编译成功!成功!!

...直到我们实际尝试使用它:

let mut _command_callbacks: Vec<CommandExecution> = vec![
    wrap(CommandExecutor::handle_xxx),
    wrap(CommandExecutor::handle_xxx2),
];

(一个HashMap会有同样的效果)。

error[E0308]: mismatched types
  --> src/lib.rs:34:9
   |
34 |         wrap(CommandExecutor::handle_xxx),
   |         ^^^^ lifetime mismatch
   |
   = note: expected associated type `<for<'_> fn(&CommandExecutor, Command) -> impl Future<Output = Result<String, ()>> {CommandExecutor::handle_xxx} as FnOnce<(&CommandExecutor, Command)>>::Output`
              found associated type `<for<'_> fn(&CommandExecutor, Command) -> impl Future<Output = Result<String, ()>> {CommandExecutor::handle_xxx} as FnOnce<(&CommandExecutor, Command)>>::Output`
   = note: the required lifetime does not necessarily outlive the static lifetime
note: the lifetime requirement is introduced here
  --> src/lib.rs:21:41
   |
21 |     F: Fn(&CommandExecutor, Command) -> Fut,
   |                                         ^^^

error[E0308]: mismatched types
  --> src/lib.rs:35:9
   |
35 |         wrap(CommandExecutor::handle_xxx2),
   |         ^^^^ lifetime mismatch
   |
   = note: expected associated type `<for<'_> fn(&CommandExecutor, Command) -> impl Future<Output = Result<String, ()>> {CommandExecutor::handle_xxx2} as FnOnce<(&CommandExecutor, Command)>>::Output`
              found associated type `<for<'_> fn(&CommandExecutor, Command) -> impl Future<Output = Result<String, ()>> {CommandExecutor::handle_xxx2} as FnOnce<(&CommandExecutor, Command)>>::Output`
   = note: the required lifetime does not necessarily outlive the static lifetime
note: the lifetime requirement is introduced here
  --> src/lib.rs:21:41
   |
21 |     F: Fn(&CommandExecutor, Command) -> Fut,
   |                                         ^^^

中描述了该问题。解决方案是使用特征来解决问题:

type CommandExecution = for<'a> fn(
    &'a CommandExecutor,
    Command,
) -> Pin<Box<dyn Future<Output = Result<String, ()>> + 'a>>;

trait CommandExecutionAsyncFn<CommandExecutor>:
    Fn(CommandExecutor, Command) -> <Self as CommandExecutionAsyncFn<CommandExecutor>>::Fut
{
    type Fut: Future<Output = Result<String, ()>>;
}

impl<CommandExecutor, F, Fut> CommandExecutionAsyncFn<CommandExecutor> for F
where
    F: Fn(CommandExecutor, Command) -> Fut,
    Fut: Future<Output = Result<String, ()>>,
{
    type Fut = Fut;
}

fn wrap<F>(_f: F) -> CommandExecution
where
    F: 'static + for<'a> CommandExecutionAsyncFn<&'a CommandExecutor>,
{
    assert_eq!(std::mem::size_of::<F>(), 0, "expected a fn item");
    move |executor, command| {
        // SAFETY: `F` is a ZST (checked above), any (aligned!) pointer, even crafted
        // out of the thin air, is valid for it.
        let f: &F = unsafe { std::ptr::NonNull::dangling().as_ref() };
        Box::pin(f(executor, command))
    }
}

我不会详述为什么需要这个东西或者它是如何解决问题的。您可以在链接的问题和其中链接的问题中找到解释。

现在我们的代码可以工作了。喜欢,真的。

但是,如果您真的想要所有这些东西,请仔细考虑:将功能更改为 return 盒装未来可能更容易。