接受一个异步闭包作为参数,该闭包接受一个引用并通过引用捕获。函数的名称是提问标题。

11

我想做类似这样的事情:


// NOTE: This doesn't compile

struct A { v: u32 }

async fn foo<
    C: for<'a> FnOnce(&'a A) -> Pin<Box<dyn Future<Output = ()> + 'a>>
>(c: C) {
    c(&A {
        v: 8,
    }).await
}

#[tokio::main]
async fn main() {
    let t = 9;
    foo(|a| async {
        println!("{} {}", t, a.v);
    }.boxed_local()).await;
}

当函数foo接受一个"异步闭包"并给它一个引用时,这个"异步闭包"也被允许通过引用捕获其他东西。在编译时,t的生命周期需要是'static,这对我来说很有意义。

但我不确定为什么当我给传递给"异步闭包"的引用类型加上通用生命周期时,它就可以进行编译

struct A<'a> { v: u32, _phantom: std::marker::PhantomData<&'a ()> }

async fn foo<
    'b,
    C: for<'a> FnOnce(&'a A<'b>) -> Pin<Box<dyn Future<Output = ()> + 'a>>
>(c: C) {
    c(&A {
        v: 8,
        _phantom: Default::default(),
    }).await
}

#[tokio::main]
async fn main() {
    let t = 9;
    foo(|a| async {
        println!("{} {}", t, a.v);
    }.boxed_local()).await;
}

但是,如果我给A添加一个额外的生命周期,并且foo将其指定为'static,它就无法进行编译

struct A<'a, 'b> { v: u32, _phantom: std::marker::PhantomData<(&'a (), &'b ())> }

async fn foo<
    'b,
    C: for<'a> FnOnce(&'a A<'b, 'static>) -> Pin<Box<dyn Future<Output = ()> + 'a>>
>(c: C) {
    c(&A {
        v: 8,
        _phantom: Default::default(),
    }).await
}

#[tokio::main]
async fn main() {
    let t = 9; // Compile again states t's lifetime needs to be 'static
    foo(|a| async {
        println!("{} {}", t, a.v);
    }.boxed_local()).await;
}

为什么将额外的生命周期添加到A,并将其指定为“静态”,会导致t的生命周期需要更长(即“静态”)?
1个回答

9

TL;DR:这是借用检查器的限制。


在你问“为什么我添加了‘static’后它不起作用”之前,你需要问“为什么在没有‘static’的情况下它能够正常工作”(简而言之-暗示边界。如果您知道这是什么意思,则可以跳过此部分)。
让我们从头开始。
如果我们有一个返回future的闭包,并且一切都是“静态”的,那当然没问题。
如果其返回的future需要依赖于其参数,那也没问题。由于我们提供了参数,我们需要告诉编译器“对于我们提供的任何参数生命周期,我们希望返回具有相同生命周期的future”。您使用HRTB正确地完成了这一点。
type Fut<'a> = Pin<Box<dyn Future<Output = ()> + 'a>>;
async fn foo<C: for<'params> FnOnce(&'params Params) -> Fut<'params>>(c: C)

现在想象一下,闭包不需要其返回的 future 取决于其参数,但却需要取决于其捕获的环境。这也是可能的;由于我们不提供环境(因此也不提供其生命周期),而是由闭包的创建者 - 我们的调用者提供,因此我们需要调用者选择生命周期。这可以通过泛型生命周期参数轻松实现:
async fn foo<'env, C: FnOnce(&Params) -> Fut<'env>>(c: C) {

但如果我们需要同时使用两者呢?这是您的情况,它相当棘手。问题在于您需要的东西和语言允许您表达的之间存在差距。
我们需要的是(对于参数来说,暂时忽略环境):“无论我给出多长时间,我都希望有一个未来……”。
而Rust允许您通过Higher-Ranked Trait Bounds表达的实际上是“无论存在多长时间,我都希望……”。
显然,问题在于我们不需要每个存在的生命周期。例如,“任何存在的生命周期”包括'static。因此,闭包需要准备好接收'static数据并返回'static的未来。但我们知道我们永远不会提供'static数据,然而编译器却要求我们处理这种不可能的情况。
然而,有一个潜在的解决方案。我们知道我们只会给闭包局部变量。局部变量的生命周期始终比环境的生命周期短。因此,理论上,我们应该能够做到:
async fn foo<'env, C: for<'params> FnOnce(&'params Params<'env>) -> Fut<'params>>(c: C) {
    c(&Params { v: 8, _marker: PhantomData }).await
}

很不幸,编译器并不认同(是的,我知道这个可以编译,但这不是因为编译器认可。相信我,它不认可)。它无法推断出'env总是比'params存在更长的时间。而且它是正确的:虽然确实如此,但我们从未保证过这一点。因此,如果编译器基于此接受我们的代码,未来的更改可能会意外地破坏客户端代码。我们违反了Rust的核心哲学:每一个潜在的破坏机会都必须反映在函数签名中

我们如何在签名中反映“我们永远不会给您比您的环境寿命更长的寿命”这一保证呢?啊,我有一个想法!

async fn foo<
    'env,
    C:
        for<'params where 'env: 'params>
        FnOnce(&'params Params<'env>) -> Fut<'params>
>(c: C)

不行。这样做行不通。where从句在HRTB中不被支持(目前;但未来可能会支持)。
或者说呢?
它们没有被直接支持;但有一种方法可以欺骗编译器。存在暗示的生命周期边界
暗示边界的想法很简单。假设我们有以下类型:
&'lifetime Type

在这里,我们知道必须保持“Type:'lifetime”的状态。也就是说,每个具有“Type”类型的生命周期都必须比“lifetime”更长或相等(更准确地说,它们是“'lifetime”子类型,但让我们在这里忽略差异)。这是为了使“&'lifetime Type”能够存在,即说,符合形式规范:简单来说,它可以存在。如果“Type”中包含比“lifetime”更短的生命周期,并且我们具有具有生命周期“lifetime”的“Type”引用,则我们可以在整个“lifetime”中使用“Type”,即使内部较短的生命周期不再有效!这可能会导致使用后释放,并因此我们不能建立一个寿命超过其参考物的参考物的寿命(您可以尝试)。

&'lifetime Type只有在Type: 'lifetime的情况下才能存在,为了避免重复,如果你的包裹(例如在参数列表中)中有&'lifetime Type,编译器会假设Type: 'lifetime成立。换句话说,拥有&'lifetime Type意味着Type: 'lifetime。关键的一点是,这些约束条件甚至在for子句中传播

如果我们遵循这样的思路,那么&'lifetime Type<'other_lifetime>意味着'other_lifetime: 'lifetime(再次忽略方差)。因此,&'params Params<'env>意味着'env: 'params。神奇!我们得到了隐含的约束条件,而不必将其明确写出!

所有这些都是必要的背景,但仍不能解释代码失败的原因。隐含的约束条件应该是'env: 'params'static: 'params,两者都是可以满足的。为了理解这里发生了什么,我们必须深入探究借用检查器的内部。


当借用检查器看到这个闭包时:
|a| {
    async {
        println!("{} {}", t, a.v);
    }
    .boxed_local()
}

这个并不关心它的具体内容,特别是,它不知道涉及到了哪些生命周期。这些都会在此之前被擦除。借用检查器不会验证闭包的生命周期——相反,它推断出它们的需求,并将其传播到包含的函数中,在那里进行验证(如果无法满足则会发出错误)。

借用检查器看到以下信息:

  • 闭包类型——类似于 main::{closure#0}
  • 闭包种类——在本例中,是 FnOnce
  • 闭包调用函数的签名。在此例中,它是(请注意,'env'static 被擦除):
for<'params> extern "rust-call" fn((
    &'params Params<'erased, 'erased>,
)) -> Pin<Box<dyn Future<Output = ()> + 'params>>
  • 闭包的捕获列表。在这种情况下,它是&'erased i32(表示为元组,但这不重要)。这是对捕获的t的引用。

借用检查器为每个'erased生命周期分配一个新的唯一生命周期。为简单起见,让我们将它们命名为'env'my_static,用于Params,以及'env_borrow用于t捕获。

现在我们计算暗示的边界。我们有两个相关的- 'env:'params'my_static:'params

让我们专注于'env: 'params(更确切地说是'env_borrow: 'params)。但是,对于我们的分析,我们可以忽略这一点。我们无法证明它,因为'params是本地生命周期。我们自己用for<'params>声明了它,它并不来自我们的环境。如果我们轻轻地询问main()证明'env: 'params,它会回答:" 'env...嗯,我知道'env,它是t的借用寿命。什么?'params?那是什么?我不知道!抱歉,我不能为你做到这一点。" 这不好。
所以我们想要为main()提供一个它知道的生命周期。我们该怎么做呢?嗯,我们需要找到比'params更长的最小生命周期。这是因为如果'env'params更长的某个生命周期存在,那么它肯定比'params本身更长寿。我们需要最小的生命周期,否则即使可以证明'env: 'params,也可能无法证明'env: 'some_longer_lifetime。可能有几个这样的生命周期,我们将想要证明它们全部[1]
在这种情况下,“更大”的生命周期是“env”和“my_static”。这是因为我们对每个都有边界,“env:'params”和“my_static:'params”(隐含的边界)。因此,我们知道它们更大(这不是唯一的约束,请参见here以获取精确定义)。
所以我们要求“main()”证明“'env:'env”(更精确地说是“'env_borrow:'env”,但再次强调,这并不重要)和“'env:'my_static”。但是因为“my_static”是“'static”,我们将无法证明“'env:'static”(同样,“'env_borrow:'static”),因此我们失败了,并说“'t' does not live long enough”。

[1] 只需要证明其中一个比另一个更长寿就足够了,但根据这条评论所述:

// This is slightly too conservative. To show T: '1, given `'2: '1`
// and `'3: '1` we only need to prove that T: '2 *or* T: '3, but to
// avoid potential non-determinism we approximate this by requiring
// T: '1 and T: '2.

我不确定它所谈论的非确定性是什么。引入此注释的PR是#58347(具体来说是提交79e8c311765),它说这是为了修复一个回归。但即使在此PR之前,它也无法编译:即使在此之前,我们只根据闭包内已知的约束条件进行判断,并且我们当时并不知道'my_static == 'static。我们需要将OR绑定传播到包含函数中,就我所知,这从未发生过。


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