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