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

xar*_*das 16 closures ownership rust borrowing

考虑以下代码:

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

我期待的是:

  • 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>
Run Code Online (Sandbox Code Playgroud)

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

Luk*_*odt 14

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

tl; dr:moveclosures不能返回对其环境的引用,因为它将引用self.不能返回这样的引用,因为这些Fn*特征不允许我们表达这一点.这基本上与流迭代器问题相同,可以通过GAT(通用关联类型)修复.


手动实施

你可能知道,当你编写一个闭包时,编译器会生成一个struct并impl阻塞相应的Fntraits,所以闭包基本上就是语法糖.让我们试着避免所有糖并手动构建你的类型.

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

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))
}
Run Code Online (Sandbox Code Playgroud)

这相当于你的盒装封口.我们试着用它:

let outside = {
    let s = "hi".to_string();
    let baz = make_baz(s);
    println!("{}", baz.call()); // works

    baz
};

println!("{}", outside.call()); // works too
Run Code Online (Sandbox Code Playgroud)

这很好用.将字符串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!
Run Code Online (Sandbox Code Playgroud)

现在我们不能使生命周期baz大于生命周期s,因为baz包含一个引用s将是一个悬挂引用s会超出范围baz.

我想用这个片段做出的观点:我们不需要在类型上注释任何生命周期Baz以使其安全; 鲁斯特自己解决了这个问题并强制执行了baz不超过的生命s.这在下面很重要.

为它写一个特征

到目前为止,我们只介绍了基础知识.让我们尝试编写一个类似于Fn更接近原始问题的特征:

trait MyFn {
    type Output;
    fn call(&self) -> Self::Output;
}
Run Code Online (Sandbox Code Playgroud)

在我们的特性中,没有函数参数,但是它与真实Fn特征完全相同.

我们来实现吧!

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

现在我们遇到了一个问题:我们写什么而不是????天真地会写&T...但我们需要一个生命周期参数用于该参考.我们从哪里得到一个?返回值甚至有几年的寿命?

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

impl<T> Baz<T> {
    fn call(&self) -> &T {
        &self.0
    }
}
Run Code Online (Sandbox Code Playgroud)

所以这里我们也&T没有使用life参数.但这仅仅是因为终身省略.基本上,编译器填充空白,这fn call(&self) -> &T相当于:

fn call<'s>(&'s self) -> &'s T
Run Code Online (Sandbox Code Playgroud)

啊哈,所以返回引用的self生命周期必然会终结!(更有经验的Rust用户可能已经有了这样的感觉......).

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

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

未来怎么样?

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

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

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

功能签名在代码中没有改变,但请注意终身省略!以上fn call(&self) -> Self::Output相当于:

fn call<'s>(&'s self) -> Self::Output<'s>
Run Code Online (Sandbox Code Playgroud)

因此,关联类型的生命周期与生命周期相关联self.就像我们想要的那样!该impl如下所示:

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

要返回一个盒装,MyFn我们需要写这个(根据RFC的这一部分:

fn make_baz<T>(t: T) -> Box<for<'a> MyFn<Output<'a> = &'a T>> {
    Box::new(Baz(t))
}
Run Code Online (Sandbox Code Playgroud)

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


摘要

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

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


Seb*_*edl 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特质.也就是说,Rust中的每个函数对象都可以使用by-value self参数调用.这意味着每个函数对象必须仍然有效,并使用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
    }
}
Run Code Online (Sandbox Code Playgroud)

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