采用异步闭包的函数,该闭包采用引用并通过引用捕获

Ala*_*mes 11 rust rust-futures

我想做这样的事情:


// 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;
}
Run Code Online (Sandbox Code Playgroud)

函数foo采用“异步闭包”,为其提供引用,并且“异步闭包”也允许通过引用捕获事物。上面的编译状态t的生命周期需要是“静态的”,这对我来说很有意义。

但我不确定我是否理解为什么当我将通用生命周期放在传递给它编译的“异步闭包”的引用类型上时:

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;
}
Run Code Online (Sandbox Code Playgroud)

但是,如果我向 , 和 foo 添加额外的生命周期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;
}
Run Code Online (Sandbox Code Playgroud)

为什么向 A 添加额外的生命周期并将其指定为 'static 会导致tA 的生命周期需要更长(即 'static)?

Cha*_*man 9

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


在你问“为什么当我添加时它不起作用'static”之前,你需要问“为什么它在我没有时起作用'static(TL;DR - 隐含边界。如果你知道这意味着什么,你可以跳过本节)。

让我们从头开始。

如果我们有一个返回未来的闭包,并且一切都很好'static,当然一切都很好。

如果它返回的 future 需要依赖于它的参数,那也很好。由于我们提供参数,我们需要告诉编译器“对于我们将提供的任何参数生命周期,我们希望返回具有相同生命周期的未来”。你用 HRTB 做到了,正确:

type Fut<'a> = Pin<Box<dyn Future<Output = ()> + 'a>>;
async fn foo<C: for<'params> FnOnce(&'params Params) -> Fut<'params>>(c: C)
Run Code Online (Sandbox Code Playgroud)

现在想象一下,闭包不需要其返回的 future 依赖于其参数,但它确实需要它依赖于其捕获的环境。这也是可能的;由于我们不提供环境(及其生命周期),而是由闭包的创建者(我们的调用者)提供,因此我们需要调用者来选择生命周期。使用通用生命周期参数可以轻松实现这一点:

async fn foo<'env, C: FnOnce(&Params) -> Fut<'env>>(c: C) {
Run Code Online (Sandbox Code Playgroud)

但如果我们两者都需要怎么办?这是你的情况,而且问题很大。问题在于你需要的东西和语言让你表达的东西之间存在差距。

我们需要的(对于参数,让我们暂时忽略环境)是“无论我给予什么一生,我想要一个未来......”。

而 Rust 允许你用较高等级的特征边界来表达的实际上是“无论存在什么生命,我想要......”。

显然,问题在于我们不需要每一个存在的生命周期。例如,“无论存在什么生命周期”包括'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
}
Run Code Online (Sandbox Code Playgroud)

不幸的是,编译器不同意(是的,我知道这个可以编译,但这并不是因为编译器同意。它不同意,相信我)。它不能得出结论'env总是比 更长寿'params。这是对的:虽然事情确实如此,但我们从未保证这一点。因此,如果编译器接受我们基于此的代码,未来的更改可能会意外破坏客户的代码。我们违背了 Rust 的核心理念:每一个潜在的破坏都必须反映在函数签名中

如何在签名中体现“我们永远不会给你比你的环境更长的一生”的保证?啊,我有一个主意!

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

没有。那是行不通的。whereHRTB 不支持这些条款(目前;将来可能)。

或者他们是吗?

它们不受直接支持;但有一种方法可以欺骗编译器。存在隐含的生命周期界限

隐含边界的想法很简单。假设我们得到以下类型:

&'lifetime Type
Run Code Online (Sandbox Code Playgroud)

在这里,我们知道这一点Type: 'lifetime必须成立。也就是说,每个生命周期Type都必须长于或等于'lifetime(更准确地说,它们是 的子类型'lifetime,但这里我们忽略方差)。这是格式良好&'lifetime Type所必需的:简单地说,能够存在。如果包含的生命周期短于,并且我们有一个生命周期的引用,我们就可以使用整个生命周期- 即使内部较短的生命周期不再有效!这可能会导致释放后使用,因此我们无法构建比其引用对象的生命周期更长的引用(您可以尝试)。Type'lifetimeType'lifetimeType'lifetime

由于&'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,两者都是可满足的。为了理解这里发生了什么,我们必须研究借用检查器的内部结构。


当借用检查器看到此关闭时:

&'lifetime Type
Run Code Online (Sandbox Code Playgroud)

这与它无关。具体来说,它不知道所涉及的生命周期。它们都被事先删除了。借用检查器不会验证闭包的生命周期 - 相反,它会推断出它们的要求并将它们传播到包含函数,在那里它们将被验证(如果不能验证,则发出错误)。

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

  • 闭包的类型 - 类似main::{closure#0}.
  • 闭包的种类 - 在本例中,FnOnce.
  • 闭包的调用函数的签名。在本例中,它是(请注意 和'env'static被删除):
|a| {
    async {
        println!("{} {}", t, a.v);
    }
    .boxed_local()
}
Run Code Online (Sandbox Code Playgroud)
  • 闭包的捕获列表。在本例中,它是&'erased i32(表示为元组,但这并不重要)。这是对捕获的 的引用t

借用检查器为每个生命周期分配一个唯一的新生命周期'erased。为了简单起见,我们将它们命名为'env和,并将它们命名为捕获。'my_staticParams'env_borrowt

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

让我们关注'env: 'params(更准确地说'env_borrow: 'params。但我们可以在分析中忽略它)。我们自己无法证明这一点,因为它'params是本地生命周期。我们自己用 声明了它for<'params>,它不是来自我们的环境。如果我们温和地要求main()证明'env: 'params,它会这样回应:“ 'env……嗯,我知道'env,这是借用的生命周期t。什么??'params那是什么?我不知道!抱歉,我可以”不为你做那件事。” 不是很好。

所以我们希望提供main()它知道的一生。我们怎样做呢?好吧,我们需要找到长于最小寿命。这是因为,如果比 更大的生命周期更长,那么它肯定会比它自己更长。我们需要最短生命周期,因为否则即使可以证明,也可能无法证明。可能有几个这样的生命周期,我们想要证明它们全部[1]'params'env'params'params'env: 'some_longer_lifetime'env: 'params

在这种情况下,“更大”的生命周期是'env'my_static。这是因为我们每个都有界限,'env: 'params并且'my_static: 'params(隐含的界限)。因此我们知道它们更大(这不是唯一的限制,请参阅此处了解精确的定义)。

所以我们要求main()证明'env: 'env(更准确地说'env_borrow: 'env,但同样,这并不重要)和'env: 'my_static。但因为my_static'static,我们将无法证明这一点'env: 'static(再次,'env_borrow: 'static),因此我们失败了,说“t活得不够长”。


[1] 应该足以证明其中只有一个已经存在,但根据此评论

for<'params> extern "rust-call" fn((
    &'params Params<'erased, 'erased>,
)) -> Pin<Box<dyn Future<Output = ()> + 'params>>
Run Code Online (Sandbox Code Playgroud)

我不确定它所说的非决定论是什么。引入此评论的 PR 是#58347(具体是提交 79e8c311765),它说这是为了修复回归。但它甚至在这个 PR 之前就没有编译:即使在它之前,我们也只是根据我们知道的闭包内部的约束来判断,而我们当时并不知道'my_static == 'static。我们需要将 OR 绑定传播到包含函数,但据我所知,情况从未如此。