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

Posted

技术标签:

【中文标题】无法为返回引用的闭包推断适当的生命周期【英文标题】:Cannot infer an appropriate lifetime for a closure that returns a reference 【发布时间】:2018-09-23 04:26:43 【问题描述】:

考虑以下代码:

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

我的期望:

类型 T 的生命周期为 'a。 值tT 一样长。 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>

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

【问题讨论】:

【参考方案1】:

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

tl;dr:闭包不能返回对通过移动捕获的值的引用,因为那将是对self 的引用。无法返回此类引用,因为 Fn* 特征不允许我们表达。这与 streaming iterator problem 基本相同,可以通过 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

我想用这个 sn-p 说明一点:我们不需要在类型 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
    

现在我们有一个问题:我们写什么而不是????天真地写&amp;T...但我们需要一个生命周期参数来引用。我们在哪里得到一个?返回值的生命周期是多少?

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

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

所以这里我们使用&amp;T 也没有生命周期参数。但这仅适用于终身省略。基本上,编译器会填空,这样fn call(&amp;self) -&gt; &amp;T 就相当于:

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

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

(附带说明:为什么返回的引用不依赖于T 本身的生命周期?如果T 引用了非'static 的东西,那么这必须考虑,对吧?是的,但是它已经考虑到了!请记住,Baz&lt;T&gt; 的任何实例都不能比 T 可能引用的东西寿命更长。因此,self 的生命周期已经比 T 的任何生命周期短。因此我们只需要集中注意力在self 生命周期内)

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

未来呢?

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

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

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

代码中的函数签名没有改变,但请注意生命周期省略!上面的fn call(&amp;self) -&gt; 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,我们需要这样写(根据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 功能,可以在类型系统中表达所有这些。但是,标准库需要以某种方式迎头赶上才能使您的确切代码成为可能。

【讨论】:

【参考方案2】:

我的期望:

T 类型的生命周期为 'a。 值tT 一样长。

这毫无意义。一个值不能像一个类型“活得一样长”,因为一个类型是不活的。 “T 有生命周期'a”是一个很不精确的说法,很容易被误解。 T: 'a 的真正含义是“T 的实例必须保持有效,至少与生命周期 'a 一样长。例如,T 不能是生命周期短于 'a 的引用,或者包含此类参考。请注意,这与形成 T 的参考无关,即&amp;T

那么,t 的值只要它的词法范围(它是一个函数参数)所说的时间就存在,这与 'a 完全没有关系。

t 移动到闭包,因此闭包与t 一样长

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

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

所有这一切都意味着返回对其自身捕获状态的引用的闭包必须将该引用的生命周期绑定到它自己的引用。很遗憾,这是不可能的

原因如下:

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 参数调用"——非常好的观察!我完全错过了。【参考方案3】:

您希望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 的语义有很大的不同。【参考方案4】:

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

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

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

【讨论】:

以上是关于无法为返回引用的闭包推断适当的生命周期的主要内容,如果未能解决你的问题,请参考以下文章

我如何让impl Trait使用适当的生命周期来对其中有另一个生命周期的值进行可变引用?

Rust 中的闭包生命周期通过简单的模式

这个对象生命周期扩展闭包是 C# 编译器错误吗?

如何为闭包参数声明生命周期?

迭代器通过引用返回项目,生命周期问题

使用refs实现迭代器时的生命周期推断问题