无法为 returns 引用的闭包推断合适的生命周期

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

我的期望:

实际发生了什么:

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>

我不明白冲突。我该如何解决?

您希望类型 T 具有生命周期 'a,但 t 不是对类型 T 的值的引用。该函数通过参数传递获得变量 t 的所有权:

// t is moved here, t lifetime is the scope of the function
fn foo<'a, T: 'a>(t: T)

你应该这样做:

fn foo<'a, T: 'a>(t: &'a T) -> Box<Fn() -> &'a T + 'a> {
    Box::new(move || t)
}

What I expect:

  • The type T has lifetime 'a.
  • The value t live as long as T.

这毫无意义。值不能 "live as long" 作为类型,因为类型不存在。 “T has lifetime 'a”是一个非常不精确的说法,容易被误解。 T: 'a 的真正意思是“T 的实例必须至少在生命周期 'a 内保持有效。例如,T 不能是生命周期短于 'a 的引用],或包含此类引用的结构。请注意,这与形成引用无关 to T,即 &T.

然后,值 t 只要它的词法范围(它是一个函数参数)说它存在,它就存在,这与 'a 完全无关。

  • t moves to the closure, so the closure live as long as t

这也是不正确的。只要闭包在词法上有效,闭包就会存在。它在结果表达式中是临时的,因此一直存在到结果表达式的末尾。 t 的生命周期与闭包无关,因为它内部有自己的 T 变量,即 t 的捕获。由于捕获是 t 的 copy/move,因此它不受 t 生命周期的任何影响。

然后将临时闭包移入盒子的存储空间,但这是一个有自己生命周期的新对象。 that 闭包的生命周期绑定到盒子的生命周期,即它是函数的 return 值,之后(如果你将盒子存储在函数之外)你存储盒子的任何变量的生命周期。

所有这些意味着 return 引用其自身捕获状态的闭包必须将该引用的生命周期绑定到其自身的引用。不幸的是,这是不可能的

原因如下:

Fn 特征暗示了 FnMut 特征,后者又暗示了 FnOnce 特征。也就是说,Rust 中的每个 函数对象都可以使用按值 self 参数调用。这意味着每个函数对象在使用按值 self 参数调用时必须仍然有效,并且 return 与往常一样。

换句话说,尝试编写一个 return 引用其自身捕获的闭包大致扩展为以下代码:

struct Closure<T> {
    captured: T,
}
impl<T> FnOnce<()> for Closure<T> {
    type Output = &'??? T; // what do I put as lifetime here?
    fn call_once(self, _: ()) -> Self::Output {
        &self.captured // returning reference to local variable
                       // no matter what, the reference would be invalid once we return
    }
}

这就是为什么你试图做的事情根本不可能。退后一步,想一想您实际上想通过这个闭包完成什么,并找到其他方法来完成它。

非常有趣的问题!我 认为 我理解这里的问题所在。让我试着解释一下。

tl;dr:闭包不能 return 引用移动捕获的值,因为那将是对 self 的引用.这样的引用不能被 return 编辑,因为 Fn* 特性不允许我们表达它。 这与 基本相同,可以通过GAT(通用关联类型)。


手动实施

您可能知道,当您编写闭包时,编译器会为适当的 Fn 特征生成结构和 impl 块,因此闭包基本上是语法糖。让我们尽量避免所有糖分并手动构建您的类型。

您想要的是一种类型,它 拥有 另一种类型并且可以 return 引用该拥有的类型。并且您想要一个 return 是所述类型的盒装实例的函数。

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

这与您的盒装封口相当。让我们尝试使用它:

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 所有,然后由 outside 所有。

当我们添加单个字符时会变得更有趣:

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 的悬空引用s 会早于 baz 超出范围。

我想用这个片段说明的一点是:我们不需要在类型 Baz 上注释任何生命周期来确保安全; Rust 自己解决了这个问题并强制 baz 的寿命不超过 s。这在下面很重要。

为它写一个特征

到目前为止,我们只介绍了基础知识。让我们试着写一个像 Fn 这样的特征来更接近你原来的问题:

trait MyFn {
    type Output;
    fn call(&self) -> Self::Output;
}

在我们的 trait 中,没有函数参数,但除此之外它与 the real Fn trait.

完全相同

让我们来实施吧!

impl<T> MyFn for Baz<T> {
    type Output = ???;
    fn call(&self) -> Self::Output {
        &self.0
    }
}

现在我们有一个问题:我们写什么来代替 ????天真的人会写 &T... 但我们需要一个生命周期参数作为该引用。我们从哪里得到一个? return 值有多少生命周期?

让我们检查一下我们之前实现的功能:

impl<T> Baz<T> {
    fn call(&self) -> &T {
        &self.0
    }
}

所以这里我们也使用没有生命周期参数的&T。但这仅适用于生命周期省略。基本上,编译器会填充空白,使 fn call(&self) -> &T 等同于:

fn call<'s>(&'s self) -> &'s T

啊哈,所以 returned 引用的生命周期绑定到 self 生命周期! (更有经验的 Rust 用户可能已经感觉到这是怎么回事……)。

(附带说明:为什么 returned 引用不依赖于 T 本身的生命周期?如果 T 引用非 'static 的内容,那么这必须考虑在内,对吧?是的,但它已经考虑在内了!请记住,Baz<T> 的任何实例都不会比 T 可能引用的东西活得更久。所以 self lifetime 已经比 T 可能拥有的任何 lifetime 都短。因此我们只需要专注于 self lifetime)

但是我们如何在 trait impl 中表达它呢?事实证明:我们不能(还)。在 流式迭代器 的上下文中经常提到这个问题——也就是说,return 一个生命周期绑定到 self 生命周期的项目的迭代器。在今天的 Rust 中,可悲的是不可能实现这一点;类型系统不够强大。

未来呢?

幸运的是,有一个 RFC "Generic Associated Types" 前段时间被合并了。此 RFC 扩展了 Rust 类型系统,以允许关联类型的特征是通用的(超过其他类型和生命周期)。

让我们看看如何让您的示例(有点)与 GAT 一起工作(根据 RFC;这东西还不能工作 ☹)。首先我们必须改变特征定义:

trait MyFn {
    type Output<'a>;   // <-- we added <'a> to make it generic
    fn call(&self) -> Self::Output;
}

代码中的函数签名没有改变,但请注意生命周期省略开始了!上面的fn call(&self) -> Self::Output相当于:

fn call<'s>(&'s self) -> Self::Output<'s>

因此关联类型的生命周期绑定到 self 生命周期。正如我们所愿! impl 看起来像这样:

impl<T> MyFn for Baz<T> {
    type Output<'a> = &'a T;
    fn call(&self) -> Self::Output {
        &self.0
    }
}

为了 return 盒装 MyFn 我们需要这样写(根据 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。因此,标准库很可能会保留不那么强大的特性。 (旁注:如何以向后不兼容的方式发展标准库以使用新的语言特性已经I wondered about几次了;到目前为止,我还没有听说过这方面的任何实际计划;我希望 Rust团队想出了一些东西...)


总结

您想要的在技术上并非不可能或不安全(我们将其实现为一个简单的结构并且可以正常工作)。然而,不幸的是,现在无法在 Rust 的类型系统中以闭包/Fn 特征的形式表达你想要的东西。这与 流式迭代器 正在处理的问题相同。

有了计划中的 GAT 功能,就可以在类型系统中表达所有这些。但是,标准库需要以某种方式赶上来使您的确切代码成为可能。

其他答案是一流的,但我想补充一下您的原始代码无法运行的另一个原因。一个大问题出在签名上:

fn foo<'a, T: 'a>(t: T) -> Box<Fn() -> &'a T + 'a>

这表示 caller 可以在调用 foo 时指定 any 生命周期并且代码将是有效的并且内存-安全的。对于这段代码,这不可能是真的。将 'a 设置为 'static 来调用它是没有意义的,但是这个签名不会阻止它。