用可变引用参数调用通用异步函数

11

我的问题的最小示例。

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 不能在等待之后使用,因为它仍然被可变借用。
我对这两者都有些惊讶,我理解为什么 FFuture 返回需要与其借用('a) 相同的生命周期(相关异步书籍部分)。然而,根据同一参考文献,只要我在 changer 的结果上调用 await,借用就应该结束,但这显然没有发生,否则我就不会遇到这些错误。将此示例重新设计为像书中那样,即不将 changer 函数作为参数传递,而是直接调用,结果符合预期。

这里发生了什么,我能做些什么吗?使用 &mut 替换为 Rc<RefCell<_>> 结构会按预期工作,但如果可能,我想避免这样做。

1个回答

20
当您将 'a 作为通用参数指定时,意味着“我允许调用者选择任何它想要的生命周期”。调用者也可以选择 'static。然后,您承诺传递 &'a mut i32,即 &'static mut i32。但是,i 并没有存在于 'static 生命周期中!这就是第一个错误的原因。
第二个错误是因为您承诺您将可变地借用 i 作为 'a。但同样,'a 也可以覆盖整个函数,即使在丢弃结果后仍然如此!调用者也可以选择 'static,然后将引用存储在全局变量中。如果在之后使用 i,则会在其被可变借用时使用。炸裂!
您想要的不是让调用者选择生命周期,而是说“我正在传递带有一些生命周期 'a 的引用,并希望您以相同的生命周期返回未来”。我们用于实现“我正在给您一些生命周期,但让我选择”的效果称为HRTB(Higher-Kinded Trait Bounds)。
如果您只想返回特定类型而不是通用类型,则应如下所示:
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;
}

游乐场

实际上,您甚至可以摆脱显式的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;
}

游乐场

更多信息:


谢谢,你提到'static的那一刻我就知道我的推理出了问题,再一次忘记了for<'a>,但是你的答案非常详细和有帮助! - KillianDS
仅作为跟进:我尝试通过实现FnOnce(&'a mut Arg)来使特质适用于闭包,其中Arg'static,但我遇到了这个Rust问题。因此,目前似乎存在语言限制。 - KillianDS

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