无法为返回引用的闭包推断适当的生存期 [英] Cannot infer an appropriate lifetime for a closure that returns a reference

查看:96
本文介绍了无法为返回引用的闭包推断适当的生存期的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

考虑以下代码:

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 as T.
  • t moves to the closure, so the closure live as long as t
  • 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 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 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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆