使用(可变的)借用参数调用通用异步函数

Calling a generic async function with a (mutably) borrowed argument

我的问题的最小示例。

use std::future::Future;

async fn call_changer<'a, F, Fut>(changer: F)
where
    F: FnOnce(&'a mut i32) -> Fut,
    Fut: Future<Output = ()> + 'a,
{
    let mut i = 0;
    changer(&mut i).await; // error 1
    dbg!(i); // error 2
}

#[tokio::main]
async fn main() {
    call_changer(|i| async move {
        *i = 100;
    })
    .await;
}

这会导致两个相关错误,请参阅 rust playground 了解详细输出:

  1. 借用的时间不够长,因为 icall_changer 的主体末尾被丢弃。
  2. i 不能在 await 之后使用,因为它仍在被可变地借用。

我对两者都有点惊讶,我理解为什么 FFuture return 需要与其借用 ('a) 具有相同的生命周期 ('a) relevant async book section)。但是,根据同一参考资料,一旦我对 changer 的结果调用等待,借用就应该结束,这显然不会发生,否则我不会有这些错误。将此示例改写为类似本书的内容,其中 changer 函数未作为参数传入,而是直接调用,按预期工作。

这是怎么回事,我能做些什么吗?用 Rc<RefCell<_>> 构造替换 &mut 可以按预期工作,但如果可能的话,我想避免这种情况。

当您将 'a 指定为通用参数时,您的意思是“我允许调用者选择它想要的任何生命周期”。例如,呼叫者也可以选择 'static。那么你承诺通过&'a mut i32,即&'static mut i32。但是i不是为了'static而活!这就是第一个错误的原因。

第二个错误是因为您承诺要为 'a 可变借用 i。但同样,'a 也可能覆盖整个函数,即使在您丢弃结果之后也是如此!例如,调用者可以选择 'static,然后将引用存储在全局变量中。如果您在之后使用 i ,则在可变借用时使用它。砰!

你想要的是 不是 让调用者选择生命周期,而是说“我正在向你传递一个 some lifetime 'a,我要你还我一个有相同lifetime的未来”。我们用来实现“我给你一些生命,但让我选择哪一个”的效果叫做HRTB(Higher-Kinded Trait Bounds)。

如果您只想 return 特定类型,而不是通用类型,它看起来像:

async fn call_changer<'a, F, Fut>(changer: F)
where
    F: for<'a> FnOnce(&'a mut i32) -> &'a mut i32,
{ ... }

您也可以通过以下语法使用 Box<dyn Future>

use std::future::Future;
use std::pin::Pin;

async fn call_changer<F>(changer: F)
where
    F: for<'a> FnOnce(&'a mut i32) -> Pin<Box<dyn Future<Output = ()> + 'a>>,
{
    let mut i = 0;
    changer(&mut i).await;
    dbg!(i);
}

#[tokio::main]
async fn main() {
    call_changer(|i| {
        Box::pin(async move {
            *i = 100;
        })
    })
    .await;
}

Playground.

事实上,您甚至可以阅读显式 for 子句,因为 HRTB 是闭包中生命周期的默认脱糖:

where
    F: FnOnce(&mut i32) -> &mut i32,
where
    F: FnOnce(&mut i32) -> Pin<Box<dyn Future<Output = ()> + '_>>,

剩下的唯一问题是:我们如何用泛型表达这个 Fut

尝试将 for<'a> 应用于多个条件很诱人:

where
    for<'a>
        F: FnOnce(&'a mut i32) -> Fut,
        Fut: Future<Output = ()> + 'a,

或:

where
    for<'a> FnOnce(&'a mut i32) -> (Fut + 'a),
    Fut: Future<Output = ()>,

但不幸的是,两者都不起作用。

我们能做什么?

一个选择是继续 Pin<Box<dyn Future>>

另一种是使用自定义特征:

trait AsyncSingleArgFnOnce<Arg>: FnOnce(Arg) -> <Self as AsyncSingleArgFnOnce<Arg>>::Fut {
    type Fut: Future<Output = <Self as AsyncSingleArgFnOnce<Arg>>::Output>;
    type Output;
}

impl<Arg, F, Fut> AsyncSingleArgFnOnce<Arg> for F
where
    F: FnOnce(Arg) -> Fut,
    Fut: Future,
{
    type Fut = Fut;
    type Output = Fut::Output;
}

async fn call_changer<F>(changer: F)
where
    F: for<'a> AsyncSingleArgFnOnce<&'a mut i32, Output = ()>,
{
    let mut i = 0;
    changer(&mut i).await;
    dbg!(i);
}

不幸的是,这不适用于闭包。我不知道为什么。你必须放一个 fn:

#[tokio::main]
async fn main() {
    async fn callback(i: &mut i32) {
        *i += 100;
    }
    call_changer(callback).await;
}

Playground.

更多信息: