我想做一些像this:


// 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的生存期需要是"静态的",这对我来说很有意义.

但我不知道我为什么要对传递给"异步闭包"it compiles的引用类型设置泛型生存期:

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,那么它不会为compile:

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;
}

为什么将额外的生存期添加到,并将其指定为"static"会导致t的生存期需要更长(即"static"?

推荐答案

TL;DR:这是borrow 判断器的限制.


在你问"为什么我加'static时它不起作用"之前,你需要问"为什么did它起作用而我没有'static"(TL;DR-隐含界限.如果你知道这意味着什么,你可以跳过这一节).

让我们从头开始.

如果我们有一个返回future 的结束,而一切都是'static,那么一切当然都很好.

如果它返回的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 依赖于它的参数,但它确实需要它依赖于它的captured environment.这也是可能的;由于我们不提供环境(因此也不提供其生存期),而是由闭包的创建者(我们的调用者)提供,因此我们需要调用者 Select 生存期.使用通用生命周期 参数可以轻松实现这一点:

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

但如果我们需要both个呢?这就是你的情况,这是很有问题的.问题是,你需要的东西和语言让你表达的东西之间存在差距.

我们所需要的(对于参数,让我们暂时忽略环境)是"对于任何生命I will give,我想要一个future ……".

而Rust让你用更高等级的trait 界限来表达的实际上是"无论一生有exists个,我都想要……".

显然,问题是我们不需要存在的每一个生命.例如,"存在的生命周期"包括'static.因此,关闭需要准备'static个数据,并返回'static个future .但我们知道我们永远不会给出'static个数据,然而编译器正迫使我们处理这种不可能的情况.

然而,有一个潜在的解决方案.我们知道我们只会给出局部变量的闭包.局部变量的生存期比环境的生存期长will always be shorter.因此,理论上,我们应该能够做到:

async fn foo<'env, C: for<'params> FnOnce(&'params Params<'env>) -> Fut<'params>>(c: C) {
    c(&Params { v: 8, _marker: PhantomData }).await
}

不幸的是,编译器不同意(是的,我知道这可以编译,但这不是因为编译器同意.它不同意,相信我).不能断定'env永远比'params长寿.这是对的:虽然it happened to be so岁,但我们从来没有guaranteed岁.因此,如果编译器基于此接受我们的代码,future 的更改可能会意外地 destruct 客户的代码.我们反对铁 rust 的核心哲学:every potential for breakage must be reflected in the function signature.

我们如何在签名中体现"我们永远不会给您比您的环境更长的生命周期 "的保证?啊,我有个主意!

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

不.这行不通.HRTB中不支持where个子句(当前;将来可能会支持).

Or are they?

不支持directly;但有一种方法可以欺骗编译器.有100个.

隐含边界的概念很简单.假设我们得到以下类型:

&'lifetime Type

在这里,我们知道Type: 'lifetime人必须坚持.也就是说,每个生命周期Type保持的时间必须大于或等于'lifetime(更准确地说,它们是'lifetime的子类型,但我们在这里忽略方差).&'lifetime Type必须是Well-Formed:简单地说,能够存在.如果Type包含的生命周期短于'lifetime,并且我们有一个生命周期为'lifetimeType的参考,那么我们可以将Type用于整个'lifetime,即使其中的较短生命周期不再有效!这可能导致在释放后使用,因此我们无法构建一个引用的生存期比其引用的生存期更长(您可以try ).

由于&'lifetime Type只能在Type: 'lifetime时存在,为了防止重复性,如果包中有&'lifetime Type(例如,在参数列表中),编译器assumes Type: 'lifetime将保持不变.换句话说,有&'lifetime Type implies Type: 'lifetime.一个关键的部分是these bounds propagate even across 106 clauses.

如果我们遵循这条思路,那么&'lifetime Type<'other_lifetime>意味着'other_lifetime: 'lifetime(同样,忽略方差).因此,&'params Params<'env>意味着'env: 'params.魔术我们没有明确地写下我们的界限!

所有这些都是必要的背景知识,但它仍然无法解释代码失败的原因.隐含边界应为'env: 'params'static: 'params,两者都是可满足的.要了解这里发生了什么,我们必须查看借阅判断器的内部.


当借阅判断器看到此关闭时:

|a| {
    async {
        println!("{} {}", t, a.v);
    }
    .boxed_local()
}

这与它无关.具体来说,it does not know the lifetimes involved.它们都是事先擦除的.borrow 判断器不会验证闭包的生命周期,相反,它会推断闭包的需求并将其传播到包含函数,在那里它们将被验证(如果不能验证,则会发出错误).

borrow 判断器会看到以下信息:

  • 闭包类型-大约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的引用.

borrow 判断器为每'erased个生存期指定一个唯一的新生存期.为了简单起见,让我们将Paramst分别命名为'env'my_static,将t分别命名为'env_borrow.

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

让我们把重点放在'env: 'params(更准确地说是'env_borrow: 'params.但我们可以在分析时忽略这一点).我们自己无法证明,因为'params是一个局部生命.我们自己用for<'params>表示,它不是来self 们的环境.如果我们温和地要求main()证明'env: 'params,它会这样回答:"'env……嗯,我知道'env,这是borrow t的一生.什么?'params?那是什么?我不知道!对不起,我不能为你这么做.".这不好.

因此,我们希望为main()人提供其所知的一生.我们该怎么做?好吧,我们需要找到the minimal的生命周期 ,即longer'params.这是因为如果'env'params生命周期 长,那么它肯定比'params生命周期 长.我们需要最短的生命周期 ,因为否则'env: 'some_longer_lifetime可能无法证明,即使'env: 'params可以证明.可能有好几个这样的生命周期,我们想证明它们都是如此.

在这种情况下,"更大"的生命周期 是'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活得不够长".


[1] 这应该足以证明其中只有一个生命周期 更长,但每this comment人:

// 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.

我不确定它所说的非决定论是什么.引入这条 comments 的PR是#58347(特别是commit 79e8c311765),它说这是为了修复a regression.但它甚至在这个PR之前都没有编译:甚至在它之前,我们只根据闭包中已知的约束进行判断,而我们当时不知道'my_static == 'static.我们需要将OR绑定到包含函数,据我所知,情况从来都不是这样.

Rust相关问答推荐

抽象RUST中的可变/不可变引用

在Rust中有没有办法在没有UB的情况下在指针和U64之间进行转换?

如何编写一个以一个闭包为参数的函数,该函数以另一个闭包为参数?

如何为rust trait边界指定多种可能性

替换可变引用中的字符串会泄漏内存吗?

如何计算迭代器适配器链中过滤的元素的数量

在0..1之间将U64转换为F64

装箱特性如何影响传递给它的参数的生命周期 ?(举一个非常具体的例子)

如何将带有嵌套borrow /NLL 的 Rust 代码提取到函数中

如何迭代存储在 struct 中的字符串向量而不移动它们?

需要哪些编译器优化来优化此递归调用?

当没有实际结果时,如何在 Rust 中强制执行错误处理?

Rust 中指向自身的引用如何工作?

Rust: 目标成员属于哪个"目标家族"的列表是否存在?

我如何取消转义,在 Rust 中多次转义的字符串?

实现泛型的 Trait 方法中的文字

从 HashMap>, _> 中删除的生命周期问题

不能将 `*self` borrow 为不可变的,因为它也被borrow 为可变的 - 编译器真的需要如此严格吗?

使用 traits 时,borrow 的值不会存在足够长的时间

为什么我返回的 impl Trait 的生命周期限制在其输入的生命周期内?