如何在结构体中存储异步函数并从结构体实例中调用它?

24

我试图使用新的async/await语法、std::future :: Future和最近版本的Tokio来实现这一目标。 我正在使用Tokio 0.2.0-alpha.4和Rust 1.39.0-nightly

我尝试过的不同方法包括:

  • 为我想要存储在结构体中的类型使用Box<dyn>
  • 在结构体定义中使用泛型

我没有完全得到最小工作版本,所以这里是我尝试实现的简化版本:

async fn foo(x: u8) -> u8 {
    2 * x
}

// type StorableAsyncFn = Fn(u8) -> dyn Future<Output = u8>;

struct S {
    f: StorableAsyncFn,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let s = S { f: foo };

    let out = (s.f)(1).await;

    Ok(())
}

当然,这段代码无法编译,并会产生以下错误:

error[E0412]: cannot find type `StorableAsyncFn` in this scope

StorableAsyncFn在此处未定义,它是我正在尝试定义的类型。

2个回答

28

我们将使用这个最小可重现示例作为我们的样例:

async fn foo(x: u8) -> u8 {
    2 * x
}

struct S {
    foo: (),
}

async fn example() {
    let s = S { foo };
}

它产生了错误:

error[E0308]: mismatched types
  --> src/main.rs:10:17
   |
10 |     let s = S { foo };
   |                 ^^^ expected (), found fn item
   |
   = note: expected type `()`
              found type `fn(u8) -> impl std::future::Future {foo}`

foo 的类型是一个函数指针,它接受一个 u8 并返回实现 std::future::Future 特质的某种类型。 async fn 实际上只是语法糖,将 -> Foo 转换为 -> impl Future<Output = Foo>

我们将结构体泛型化,并放置一个匹配的特质约束。在实际代码中,您可能希望对 Output 关联类型进行限制,但在此示例中不需要。然后,我们可以像调用其他可调用成员字段一样调用该函数:

async fn foo(x: u8) -> u8 {
    2 * x
}

struct S<F>
where
    F: std::future::Future,
{
    foo: fn(u8) -> F,
}

impl<F> S<F>
where
    F: std::future::Future,
{
    async fn do_thing(self) {
        (self.foo)(42).await;
    }
}

async fn example() {
    let s = S { foo };
    s.do_thing().await;
}

为了更加灵活,你可以使用另一个通用的泛型来存储闭包,而不是仅强制使用函数指针:

struct S<C, F>
where
    C: Fn(u8) -> F,
    F: std::future::Future,
{
    foo: C,
}

impl<C, F> S<C, F>
where
    C: Fn(u8) -> F,
    F: std::future::Future,
{
    async fn do_thing(self) {
        (self.foo)(42).await;
    }
}

另请参见:


1
感谢您提供如此清晰的答案。另外,很高兴知道您可以将通用参数设置为“Future”而不指定“Output”。 - Nicolas Marshall
感谢精彩的演示!^_^.. 当一个函数需要返回Vec<S>时,如何指定返回类型? Vec<S>无法工作,它会抱怨“输出...”。你能给出一个返回类型的例子吗? - Charlie 木匠

17

另一种存储异步函数的方法是使用特质对象。如果您想在运行时动态地交换函数或存储一组异步函数,则此方法非常有用。为此,我们可以存储返回一个装箱的 Future 的装箱的 Fn

use futures::future::BoxFuture; // Pin<Box<dyn Future<Output = T> + Send>>

struct S {
    foo: Box<dyn Fn(u8) -> BoxFuture<'static, u8>,
}

然而,如果我们尝试初始化S,我们会立即遇到一个问题:
async fn foo(x: u8) -> u8 {
    x * 2
}

let s = S { foo: Box::new(foo) };

error[E0271]: type mismatch resolving `<fn(u8) -> impl futures::Future {foo} as FnOnce<(u8,)>>::Output == Pin<Box<(dyn futures::Future<Output = u8> + 'static)>>`
  --> src/lib.rs:14:22
   |
5  | async fn foo(x: u8) -> u8 {
   |                        -- the `Output` of this `async fn`'s found opaque type
...
14 |     let s = S { foo: Box::new(foo) };
   |                      ^^^^^^^^^^^^^ expected struct `Pin`, found opaque type
   |
   = note: expected struct `Pin<Box<(dyn futures::Future<Output = u8> + 'static)>>`
           found opaque type `impl futures::Future`

错误信息很清晰。 S 期望拥有一个 Future,但是 async 函数返回的是 impl Future。我们需要更新函数签名以匹配存储的 trait 对象。
fn foo(x: u8) -> BoxFuture<'static, u8> {
    Box::pin(async { x * 2 })
}

这样做虽然可行,但在每个要存储的函数中使用Box::pin会很麻烦。如果我们想将其开放给用户怎么办?

我们可以通过将函数包装进闭包中来抽象出封箱过程:

async fn foo(x: u8) -> u8 {
    x * 2
}

let s = S { foo: Box::new(move |x| Box::pin(foo(x))) };
(s.foo)(12).await // => 24

这很好用,但我们可以通过编写自定义特质并自动执行转换来使其更好:
trait AsyncFn {
    fn call(&self, args: u8) -> BoxFuture<'static, u8>;
}

并将其实现为我们想要存储的函数类型:

impl<T, F> AsyncFn for T
where
    T: Fn(u8) -> F,
    F: Future<Output = u8> + 'static,
{
    fn call(&self, args: u8) -> BoxFuture<'static, u8> {
        Box::pin(self(args))
    }
}

现在我们可以存储自定义trait的特征对象!
struct S {
    foo: Box<dyn AsyncFn>,
}


let s = S { foo: Box::new(foo) };
s.foo.call(12).await // => 24

这个答案启发了我去查找BoxFuture,并解决了我手头的问题。谢谢! - user1783732
如果参数类型不是u8,而是&mut u8呢? - Kenneth

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接