无法推断返回引用的闭包的适当生命周期

22

考虑以下代码:

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

我期望的结果:

  • 类型 T 的生命周期为 'a
  • t 的寿命与 T 相同。
  • t 移动到闭包中,因此闭包的生命周期与 t 相同。
  • 闭包返回对已移动到闭包中的 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>

我不理解这场冲突。我该怎么解决它?

4个回答

20

非常有趣的问题!我认为我理解了这里涉及到的问题。让我试着解释一下。

tl;dr: 闭包不能返回对通过移动捕获的值的引用,因为那将是对self的引用。这样的引用无法返回,因为Fn*特征不允许我们表达这种情况。 这基本上与流迭代器问题相同,可以通过GAT(通用关联类型)来修复。


手动实现

您可能已经知道,当您编写一个闭包时,编译器会生成一个结构体和实现适当的Fn特征的impl块,因此闭包基本上是语法糖。让我们尝试避免所有这些语法糖,并手动构建您的类型。

您想要拥有另一种类型并能够返回该拥有的类型的引用。而且,您希望有一个函数,该函数返回所述类型的装箱实例。

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的作用域先于baz结束,那么s就会成为一个悬垂引用。
我想通过这个片段表达的意思是:我们不需要在类型Baz上注明任何生命周期就可以使其安全;Rust自己找出来并强制执行baz的生命周期不会超过s的生命周期。这在下面将很重要。
编写一个trait
到目前为止,我们只涵盖了基础知识。让我们尝试编写一个类似于Fn的trait,以更接近您的原始问题:
trait MyFn {
    type Output;
    fn call(&self) -> Self::Output;
}

在我们的特征中,没有函数参数,但除此之外,它与真正的Fn特征基本相同。

让我们来实现它!

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

现在我们有一个问题:在???的位置上应该写什么?天真地,我们会写&T…但是我们需要一个生命周期参数来引用它。我们该在哪里得到一个?返回值甚至具有哪个生命周期?
让我们检查一下之前实现的函数:
impl<T> Baz<T> {
    fn call(&self) -> &T {
        &self.0
    }
}

因为有生命周期省略,所以我们在这里使用不带生命周期参数的 &T。基本上,编译器会填充空白,使得 fn call(&self) -> &T 等价于:

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

啊哈,所以返回的引用的生命周期与self生命周期绑定!(更有经验的Rust用户可能已经感觉到了...)。
(顺便说一句:为什么返回的引用不依赖于T本身的生命周期?如果T引用了某个非'static的东西,那么这必须加以考虑,对吧?是的,但它已经被考虑过了!请记住,任何Baz<T>的实例都不能比T可能引用的东西存活得更长。因此,self生命周期已经短于T可能具有的任何生命周期。因此,我们只需要集中精力处理self生命周期)
但是,我们如何在特质实现中表达这一点呢?事实证明:我们无法(至少目前还无法)。在流式迭代器的上下文中经常提到这个问题--也就是说,迭代器返回的项目的生命周期受限于self生命周期。在当今的Rust中,很遗憾,无法实现这一点;类型系统还不够强大。

未来会怎样呢?

幸运的是,有一个RFC“通用关联类型”已经在一段时间前合并了。这个RFC扩展了Rust类型系统,允许特征的相关类型是泛型的(超过其他类型和生命周期)。
让我们看看如何使用GATs使您的示例(有点)能够工作(根据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
    }
}

为了返回一个带盒子的MyFn,我们需要按照RFC的这个部分编写以下内容:
fn make_baz<T>(t: T) -> Box<for<'a> MyFn<Output<'a> = &'a T>> {
    Box::new(Baz(t))
}

如果我们想要使用真正的Fn特质呢?据我所知,即使使用GATs,我们也无法做到这一点。我认为不可能以向后兼容的方式更改现有的Fn特质以使用GATs。因此,标准库很可能会保留较弱的特质。 (副注:如何以向后不兼容的方式演变标准库以使用新语言特性是我已经多次思考的问题;到目前为止,我还没有听说过任何实际计划;我希望Rust团队能够想出一些办法...)


概述

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

有了计划中的GAT功能,就可以在类型系统中表达所有这些。但是,标准库需要做出某些改进才能使你的精确代码成为可能。


9
我期望的是:
- 类型 `T` 有生命周期 `'a'`。 - 值 `t` 的生命周期与 `T` 相同。
这没有意义。一个值不能“与类型一样长寿”,因为类型不会存活。"`T` 有生命周期 `'a'`" 是一个非常不精确的陈述,容易误解。真正意思是 "`T: 'a`",即 "类型 `T` 的实例必须至少保持与生命周期 `'a'` 一样长时间有效。例如,`T` 不得是具有比 `'a'` 生命周期更短的引用或包含这样的引用的结构体。请注意,这与形成对 `T` 的引用(即 `&T`)无关。
然后,值 `t` 的生命周期取决于其词法作用域(它是函数参数),这与 `'a'` 没有任何关系。
`t` 移动到闭包中,所以闭包的生命周期和 `t` 一样长
这也是不正确的。闭包在词法上存在多久,闭包就会存在多久。它是结果表达式中的临时对象,因此存在直到结果表达式结束。 `t` 的生命周期根本不涉及闭包,因为它在内部有自己的 `T` 变量,即 `t` 的捕获。由于捕获是 `t` 的复制/移动,因此不受 `t` 生命周期的任何影响。
然后,临时闭包被移动到盒子的存储中,但这是一个具有自己生命周期的新对象。该闭包的生命周期绑定到盒子的生命周期,即它是函数的返回值,稍后(如果您在函数外部存储盒子)则绑定到存储盒子的任何变量的生命周期。
所有这些意味着返回对其自身捕获状态的引用的闭包必须将该引用的生命周期绑定到其自身引用。不幸的是,这是不可能的。
原因如下:
- `Fn` 特性暗示了 `FnMut` 特性,后者又暗示了 `FnOnce` 特性。也就是说,Rust 中的每个函数对象都可以用按值传递的 `self` 参数调用。这意味着每个函数对象必须仍然有效,使用按值传递的 `self` 参数调用并返回与始终相同的内容。 - 换句话说,尝试编写返回对其自身捕获状态的引用的闭包大致扩展为以下代码:
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
    }
}

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


“每个 Rust 函数对象都可以使用按值传递的 self 参数进行调用”——非常好的观察!我完全没有注意到。 - Lukas Kalbertodt

1
你希望类型 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)
}

我认为 OP 想要闭包拥有 T。在你的代码中,闭包并没有拥有 T,而只是拥有对 T 的引用。因此,处理返回的 Box 的语义大不相同。 - Lukas Kalbertodt

1
其他答案都非常出色,但我想补充一个原因,说明为什么您的原始代码无法工作。其中一个最大的问题在于签名:
fn foo<'a, T: 'a>(t: T) -> Box<Fn() -> &'a T + 'a>

这句话的意思是,在调用foo时,调用者可以指定任何生命周期,并且代码将是有效和内存安全的。但是对于这段代码来说,这显然是不可能的。把'a设置为'static是没有意义的,但是这个签名并不能阻止这种情况的发生。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接