无法为返回引用的闭包推断适当的生存期 [英] Cannot infer an appropriate lifetime for a closure that returns a reference
问题描述
考虑以下代码:
fn foo<'a, T: 'a>(t: T) -> Box<Fn() -> &'a T + 'a> {
Box::new(move || &t)
}
我的期望:
- 类型T的生存期为
'a
。 - 值
t
的生存时间与T
一样。 -
t
移至闭包,因此闭包的有效期只要t
- 闭包返回对
t
的引用,该引用已移至闭包。因此,只要闭包存在,引用就有效。 - 代码没有生命周期问题,可以编译。
- The type T has lifetime
'a
. - The value
t
live as long asT
. t
moves to the closure, so the closure live as long ast
- The closure returns a reference to
t
which was moved to the closure. So the reference is valid as long as the closure exists. - There is no lifetime problem, the code compiles.
实际发生的情况:
- 代码无法编译:
error[E0495]: cannot infer an appropriate lifetime for borrow expression due to conflicting requirements
--> src/lib.rs:2:22
|
2 | Box::new(move || &t)
| ^^
|
note: first, the lifetime cannot outlive the lifetime as defined on the body at 2:14...
--> src/lib.rs:2:14
|
2 | Box::new(move || &t)
| ^^^^^^^^^^
note: ...so that closure can access `t`
--> src/lib.rs:2:22
|
2 | Box::new(move || &t)
| ^^
note: but, the lifetime must be valid for the lifetime 'a as defined on the function body at 1:8...
--> src/lib.rs:1:8
|
1 | fn foo<'a, T: 'a>(t: T) -> Box<Fn() -> &'a T + 'a> {
| ^^
= note: ...so that the expression is assignable:
expected std::boxed::Box<(dyn std::ops::Fn() -> &'a T + 'a)>
found std::boxed::Box<dyn std::ops::Fn() -> &T>
我不了解冲突。我该如何解决?
I do not understand the conflict. How can I fix it?
推荐答案
一个非常有趣的问题!我认为我了解这里存在的问题。让我尝试解释。
Very interesting question! I think I understood the problem(s) at play here. Let me try to explain.
tl; dr :闭包不能返回对通过移动捕获的值的引用,因为那将是引用自己
。不能返回这样的引用,因为 Fn *
特性不允许我们表达这一点。这与 流迭代器问题 ,并且可以
tl;dr: closures cannot return references to values captured by moving, because that would be a reference to self
. Such a reference cannot be returned because the Fn*
traits don't allow us to express that. This is basically the same as the streaming iterator problem and could be fixed via GATs (generic associated types).
您可能知道,在编写闭包时,编译器将为适当的 Fn $生成一个struct和
impl
块c $ c>特征,因此闭包基本上是语法糖。让我们尝试避免所有这些麻烦,并手动构建您的类型。
As you probably know, when you write a closure, the compiler will generate a struct and impl
blocks for the appropriate Fn
traits, so closures are basically syntax sugar. Let's try to avoid all that sugar and build your type manually.
您想要的是一个拥有且可以返回引用的类型拥有的类型。并且您想要一个函数来返回所述类型的装箱实例。
What you want is a type which owns another type and can return references to that owned type. And you want to have a function which returns a boxed instance of said type.
struct Baz<T>(T);
impl<T> Baz<T> {
fn call(&self) -> &T {
&self.0
}
}
fn make_baz<T>(t: T) -> Box<Baz<T>> {
Box::new(Baz(t))
}
此与盒装封盖相当。让我们尝试使用它:
This is pretty equivalent to your boxed closure. Let's try to use it:
let outside = {
let s = "hi".to_string();
let baz = make_baz(s);
println!("{}", baz.call()); // works
baz
};
println!("{}", outside.call()); // works too
这很好。字符串 s
被移到 Baz
类型,而 Baz
实例将移到 Box
中。 s
现在由 baz
拥有,然后由外部
拥有。
This works just fine. The string s
is moved into the Baz
type and that Baz
instance is moved into the Box
. s
is now owned by baz
and then by outside
.
添加单个字符会变得更加有趣:
It gets more interesting when we add a single character:
let outside = {
let s = "hi".to_string();
let baz = make_baz(&s); // <-- NOW BORROWED!
println!("{}", baz.call()); // works
baz
};
println!("{}", outside.call()); // doesn't work!
现在我们不能使 baz
的生命周期比 s
的生命周期长,因为 baz
包含对 s $ c的引用$ c>是
s
的悬挂引用,它的作用域早于 baz
。
Now we cannot make the lifetime of baz
bigger than the lifetime of s
, since baz
contains a reference to s
which would be an dangling reference of s
would go out of scope earlier than baz
.
我想用此代码段说明这一点:我们不需要对 Baz
确保安全; Rust自己弄清楚了这一点,并坚称 baz
的生存期不超过 s
。这在下面很重要。
The point I wanted to make with this snippet: we didn't need to annotate any lifetimes on the type Baz
to make this safe; Rust figured it out on its own and enforces that baz
lives no longer than s
. This will be important below.
到目前为止,我们仅介绍了基础知识。让我们尝试写一个像 Fn
这样的特征来更接近您的原始问题:
So far we only covered the basics. Let's try to write a trait like Fn
to get closer to your original problem:
trait MyFn {
type Output;
fn call(&self) -> Self::Output;
}
在我们的特征中,没有函数参数,但是与真正的 Fn
特质。
In our trait, there are no function parameters, but otherwise it's fairly identical to the real Fn
trait.
让我们实现它!
impl<T> MyFn for Baz<T> {
type Output = ???;
fn call(&self) -> Self::Output {
&self.0
}
}
现在我们有一个问题:我们写什么而不是 ???
?天真的会写& T
...,但是我们需要一个生命周期参数作为引用。我们在哪里得到一个?回报值甚至有什么寿命?
Now we have a problem: what do we write instead of ???
? Naively one would write &T
... but we need a lifetime parameter for that reference. Where do we get one? What lifetime does the return value even have?
让我们检查一下我们之前实现的功能:
Let's check the function we implemented before:
impl<T> Baz<T> {
fn call(&self) -> &T {
&self.0
}
}
所以在这里,我们也使用& T
而没有生命周期参数。但这仅是由于终身淘汰而起作用。基本上,编译器会填充空格,以便 fn call(& self)-> & T
等同于:
So here we use &T
without lifetime parameter too. But this only works because of lifetime elision. Basically, the compiler fills in the blanks so that fn call(&self) -> &T
is equivalent to:
fn call<'s>(&'s self) -> &'s T
Aha,所以返回引用的生存期绑定到自我
一生! (更多有经验的Rust用户可能已经感觉到这种情况了……)。
Aha, so the lifetime of the returned reference is bound to the self
lifetime! (more experienced Rust users might already have a feeling where this is going...).
(请注意:为什么返回的引用不依赖于 T
本身的生存期?如果 T
引用了非'静态
的东西,那么这必须要考虑,对吗?是的,但是已经考虑了!请记住, Baz< T>
的任何实例寿命都不能超过 T
可能引用的事物。因此, self
的生存期已经比 T
可能拥有的生存期短,因此我们只需要关注 self
生命周期)
(As a side note: why is the returned reference not dependent on the lifetime of T
itself? If T
references something non-'static
then this has to be accounted for, right? Yes, but it is already accounted for! Remember that no instance of Baz<T>
can ever live longer than the thing T
might reference. So the self
lifetime is already shorter than whatever lifetime T
might have. Thus we only need to concentrate on the self
lifetime)
但是我们如何在特征暗示中表达呢?结果:我们不能(尚未)。在流迭代器的上下文中经常提到此问题,也就是说,迭代器返回的项的生存期绑定到 self
生存期。在当今的Rust中,很难实现这一点;类型系统不够强大。
But how do we express that in the trait impl? Turns out: we can't (yet). This problem is regularly mentioned in the context of streaming iterators -- that is, iterators that return an item with a lifetime bound to the self
lifetime. In today's Rust, it is sadly impossible to implement this; the type system is not strong enough.
幸运的是,有一个 RFC通用关联类型 。该RFC扩展了Rust类型系统,以允许相关的特征类型是通用的(在其他类型和生命周期上)。
Luckily, there is an RFC "Generic Associated Types" which was merged some time ago. This RFC extends the Rust type system to allow associated types of traits to be generic (over other types and lifetimes).
让我们看看如何制作示例(有点)与GAT配合使用(根据RFC;该工具尚不适用☹)。首先我们必须更改特征定义:
Let's see how we can make your example (kinda) work with GATs (according to the RFC; this stuff doesn't work yet ☹). First we have to change the trait definition:
trait MyFn {
type Output<'a>; // <-- we added <'a> to make it generic
fn call(&self) -> Self::Output;
}
函数签名在代码中没有改变,但要注意生存期省略踢!上面的 fn调用(& self)-> Self :: Output
等效于:
The function signature hasn't changed in the code, but notice that lifetime elision kicks in! The above fn call(&self) -> Self::Output
is equivalent to:
fn call<'s>(&'s self) -> Self::Output<'s>
因此,关联类型的生存期绑定到自身
寿命。正如我们想要的! impl
看起来像这样:
So the lifetime of the associated type is bound to the self
lifetime. Just as we wanted! The impl
looks like this:
impl<T> MyFn for Baz<T> {
type Output<'a> = &'a T;
fn call(&self) -> Self::Output {
&self.0
}
}
要返回装箱的 MyFn
,我们需要编写此代码(根据 RFC的本部分:
To return a boxed MyFn
we would need to write this (according to this section of the RFC:
fn make_baz<T>(t: T) -> Box<for<'a> MyFn<Output<'a> = &'a T>> {
Box::new(Baz(t))
}
如果我们想使用 real Fn
特性怎么办?据我了解,即使使用GAT,我们也无法做到。我认为这是不可能的更改现有的 Fn
特征以向后兼容的方式使用GAT。因此,标准库很可能会保留功能较弱的特征。(注意:如何以向后不兼容的方式发展标准库以使用新的语言功能是一件难事f = https://internals.rust-lang.org/t/evolving-the-standard-library-through-what/5874 rel = nofollow noreferrer>我已经想过几次;到目前为止,我还没有听说过这方面的任何实际计划;我希望Rust团队提出一些建议...)
And what if we want to use the real Fn
trait? As far as I understand, we can't, even with GATs. I think it's impossible to change the existing Fn
trait to use GATs in a backwards compatible manner. So it's likely that the standard library will keep the less powerful trait as is. (side note: how to evolve the standard library in backwards incompatible ways to use new language features is something I wondered about a few times already; so far I haven't heard of any real plan in this regards; I hope the Rust team comes up with something...)
您想要的东西在技术上并不是不可能或不安全的(我们将其实现为简单的结构,并且可以正常工作)。但是,不幸的是,现在无法在Rust的类型系统中以闭包/ Fn
特征的形式来表达您想要的内容。这是流式迭代器正在处理的相同问题。
What you want is not technically impossible or unsafe (we implemented it as a simple struct and it works). However, unfortunately it is impossible to express what you want in the form of closures/Fn
traits in Rust's type system right now. This is the same problem streaming iterators are dealing with.
借助计划中的GAT功能,可以在类型系统中表达所有这些信息。但是,标准库需要以某种方式赶上来,使您的确切代码成为可能。
With the planned GAT feature, it is possible to express all of this in the type system. However, the standard library would need to catch up somehow to make your exact code possible.
这篇关于无法为返回引用的闭包推断适当的生存期的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!