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

时间:2018-04-12 21:45:55

标签: rust closures ownership borrowing

考虑以下代码:

# Map static resources from the /public folder to the /assets URL path
GET     /assets/*file                 controllers.Assets.versioned(path="/public", file: Asset)
GET     /assets/                      mypackage.AssetController.getData(name: Option[String])
GET     /assets/:id                   mypackage.AssetController.getDataId(id: Long)

我的期望:

  • 类型T的有效期为fn foo<'a, T: 'a>(t: T) -> Box<Fn() -> &'a T + 'a> { Box::new(move || &t) }
  • 'a的有效期为t
  • T移动到闭包,因此只要t
  • ,闭包就会生效
  • 闭包返回对t的引用,该引用已移至闭包。因此,只要闭包存在,引用就是有效的。
  • 没有生命周期问题,代码编译。

实际发生的事情:

  • 代码无法编译:
t

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

4 个答案:

答案 0 :(得分:14)

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

tl; dr move闭包不能返回对其环境的引用,因为它会引用self。这样的引用无法返回,因为Fn*特征不允许我们表达。这与streaming iterator problem基本相同,可以通过GAT修复(通用关联)类型)。

手动实施

正如您可能知道的,当您编写闭包时,编译器将为适当的impl特性生成struct和Fn块,因此闭包基本上是语法糖。让我们尝试避免所有糖并手动构建您的类型。

你想要的是拥有另一种类型的类型,并且可以返回对该拥有类型的引用。并且您希望拥有一个返回所述类型的盒装实例的函数。

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实例将移至Boxs现在由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的悬空引用1}}会超出Baz之前的范围。

我想用这个片段做点:我们不需要在类型baz上注释任何生命周期以使其安全; Rust自己想出来并强制s的生命不超过Fn。这在下面很重要。

为它写一个特征

到目前为止,我们只介绍了基础知识。让我们尝试写一个像trait MyFn { type Output; fn call(&self) -> Self::Output; } 这样的特质来更接近你原来的问题:

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

在我们的特质中,没有函数参数,但它与the real Fn trait完全相同。

让我们实现它!

???

现在我们遇到了一个问题:我们写的是什么而不是&T?天真地会写impl<T> Baz<T> { fn call(&self) -> &T { &self.0 } } ...但我们需要一个生命周期参数用于该参考。我们从哪里得到一个?返回值甚至有几年的寿命?

让我们检查之前实施的功能:

&T

所以我们在这里使用fn call(&self) -> &T没有生命周期参数。但这仅仅是因为终身省略。基本上,编译器填充空白,以便fn call<'s>(&'s self) -> &'s T 等同于:

self

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

(作为旁注:为什么返回的引用不依赖于T本身的生命周期?如果'static引用了非Baz<T>的内容,那么必须考虑到这一点,对吗?是的,但它已经被考虑了!请记住,T的任何实例都不能比self可能引用的东西长寿。所以T的生命周期已经比生命周期短self可能有。因此我们只需要专注于self生命周期)

但是我们如何在特质impl中表达呢?事实证明:我们不能(还)。在流迭代器的上下文中经常提到此问题 - 也就是说,返回一个生命周期为trait MyFn { type Output<'a>; // <-- we added <'a> to make it generic fn call(&self) -> Self::Output; } 生命周期的项的迭代器。在今天的Rust中,很难实现这一点;类型系统不够强大。

未来怎么样?

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

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

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

要退回装箱fn make_baz<T>(t: T) -> Box<for<'a> MyFn<Output<'a> = &'a T>> { Box::new(Baz(t)) } ,我们需要写一下(根据this section of the RFC

Fn

如果我们想使用真正的 Fn特征怎么办?据我所知,即使有GAT,我们也无法做到。我认为改变现有Fn特征以向后兼容的方式使用GAT是不可能的。因此,标准库可能会保持不那么强大的特性。 (旁注:如何以向后不兼容的方式发展标准库以使用新的语言功能已经有几次I wondered about了;到目前为止,我还没有听说过这方面的任何真实计划;我希望Rust团队想出了一些东西......)

摘要

你想要的不是技术上不可能或不安全的(我们把它实现为一个简单的结构,它可以工作)。但是,遗憾的是,现在无法以Rust的类型系统中的闭包/ {{1}}特征的形式表达您想要的内容。这是流迭代器正在处理的问题。

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

答案 1 :(得分:8)

  

我的期望:

     
      
  • 类型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特征。也就是说,可以使用by-value self参数调用 Rust中的每个函数对象。这意味着每个函数对象必须仍然有效,并使用by-value 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
    }
}

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

答案 2 :(得分: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)
}

答案 3 :(得分:1)

其他答案都是一流的,但我想提出原始代码无法工作的其他原因。一个大问题在于签名:

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

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