“借用时临时价值下降”,捕获闭包

问题描述

请考虑以下示例 (playground):

struct Animal<'a> {
    format: &'a dyn Fn() -> (),}

impl <'a>Animal<'a> {
    pub fn set_formatter(&mut self,_fmt: &'a dyn Fn() -> ()) -> () {} // Getting rid of 'a here satisfies the compiler
    pub fn bark(&self) {}
}

fn main() {
    let mut dog: Animal = Animal { format: &|| {()} };
    let x = 0;
    dog.set_formatter(&|| {
        println!("{}",x); // Commenting this out gets rid of the error. Why?
    });
    dog.bark(); // Commenting this out gets rid of the error. Why?
}

这会导致以下编译错误

Compiling playground v0.0.1 (/playground)
error[E0716]: temporary value dropped while borrowed
  --> src/main.rs:13:24
   |
13 |       dog.set_formatter(&|| {
   |  ________________________^
14 | |         println!("{}",x); // Commenting this out gets rid of the error. Why?
15 | |     });
   | |     ^ - temporary value is freed at the end of this statement
   | |_____|
   |       creates a temporary which is freed while still in use
16 |       dog.bark(); // Commenting this out gets rid of the error. Why?
   |       --- borrow later used here
   |
   = note: consider using a `let` binding to create a longer lived value

error: aborting due to prevIoUs error

For more information about this error,try `rustc --explain E0716`.
error: Could not compile `playground`

To learn more,run the command again with --verbose.

这种情况是有道理的,因为我传递给 dog.set_formatter(...) 的闭包确实是一个临时的,(我猜)在执行进行到 dog.bark(); 时被释放。

我知道在 set_formatter 的实现中去掉显式的生命周期注解似乎让编译器满意(注意 'a 之前缺少的 dyn):

pub fn set_formatter(&mut self,_fmt: & dyn Fn() -> ()) -> () {}

但是,我不明白以下内容

  1. 为什么当我在闭包中注释掉 println!("{}",x); 时问题会消失?我仍在传递一个我希望编译器抱怨的临时文件,但它没有。
  2. 为什么当我在最后注释掉 dog.bark(); 时问题就消失了?同样,我仍在传递一个临时关闭,该关闭已被释放,但现在编译器很高兴。为什么?

解决方法

首先要了解的是 &|| () 的生命周期为 'static

fn main() {
    let closure: &'static dyn Fn() = &|| (); // compiles
}

另一件值得一提的事情是,闭包的生命周期不能超过它从环境中捕获的任何变量的生命周期,这就是为什么如果我们尝试将非静态变量传递给我们的静态闭包,它将无法编译:

fn main() {
    let x = 0; // non-static temporary variable
    let closure: &'static dyn Fn() = &|| {
        println!("{}",x); // x reference captured by closure
    }; // error: trying to capture non-static variable in static closure
}

我们会回到那个话题。无论如何,如果我有一个对引用通用的结构,并且我向它传递一个 'static 引用,我将有一个该结构的 'static 实例:

struct Dog<'a> {
    format: &'a dyn Fn(),}

fn main() {
    let dog: Dog<'static> = Dog { format: &|| () }; // compiles
}

要理解的第二件事是一旦你实例化了一个类型,你就不能改变它。这包括其任何通用参数,包括生命周期。一旦您拥有 Dog<'static>,它将永远成为 Dog<'static>,您无法将其转换为 Dog<'1>,因为某个匿名生命周期 '1 较短比'static

这有一些强烈的暗示,其中之一是您的 set_formatter 方法可能不像您认为的那样表现。拥有 Dog<'static> 后,您可以'static 格式化程序传递给 set_formatter。该方法如下所示:

impl<'a> Dog<'a> {
    fn set_formatter(&mut self,_fmt: &'a dyn Fn()) {}
}

但既然我们知道我们正在使用 Dog<'static>,我们可以用 'a 替换通用生命周期参数 'static 以查看我们真正使用的是什么:

// what the impl would be for Dog<'static>
impl Dog {
    fn set_formatter(&mut self,_fmt: &'static dyn Fn()) {}
}

既然我们已经了解了所有这些背景,让我们开始回答您的实际问题。

为什么当我在闭包中注释掉 println!("{}",x); 时问题会消失?我仍在传递一个我希望编译器抱怨的临时文件,但它没有。

为什么失败,有评论:

struct Dog<'a> {
    format: &'a dyn Fn(),}

impl<'a> Dog<'a> {
    fn set_formatter(&mut self,_fmt: &'a dyn Fn()) {}
}

fn main() {
    let mut dog: Dog<'static> = Dog { format: &|| () };
    
    // x is a temporary value on the stack with a non-'static lifetime
    let x = 0;
    
    // this closure captures x by reference which ALSO gives it a non-'static lifetime
    // and you CANNOT pass a non-'static closure to a Dog<'static>
    dog.set_formatter(&|| {
        println!("{}",x); 
    });
}

通过注释掉 println!("{}",x); 行来“修复”此错误的原因是它再次赋予闭包 'static 生命周期,因为它不再借用非 'static 变量 { {1}}。

为什么当我在最后注释掉 x 时问题就消失了?同样,我仍在传递一个临时关闭,该关闭已被释放,但现在编译器很高兴。为什么?

这种奇怪的边缘情况似乎只发生在我们没有用 dog.bark(); 显式地对 dog 变量进行类型注释时。当一个变量没有明确的类型注解时,编译器试图推断它的类型,但它是懒惰的,并试图尽可能灵活,给程序员带来怀疑的好处,以便使代码编译。即使没有 Dog<'static>真的应该抛出一个编译错误,但它不会出于任何神秘的原因。关键是不是 dog.bark() 行导致代码无法编译,无论如何它都应该无法在 dog.bark() 行编译,但是无论出于何种原因,编译器都懒得抛出错误,直到您在该违规行之后再次尝试使用 set_formatter。即使只是删除 dog 也会触发错误:

dog

既然我们已经走了这么远,让我们回答你的非官方第三个问题,由我解释:

为什么去掉 struct Dog<'a> { format: &'a dyn Fn(),_fmt: &'a dyn Fn()) {} } fn main() { let mut dog = Dog { format: &|| () }; let x = 0; dog.set_formatter(&|| { println!("{}",x); }); drop(dog); // triggers "temp freed" error above } 方法中的 'a 会让编译器满意?

因为它改变了 set_formatter 的实际意义:

Dog<'static>

进入这个:

// what the impl would be for Dog<'static>
impl Dog {
    fn set_formatter(&mut self,_fmt: &'static dyn Fn()) {}
}

所以现在您可以将非 // what the impl would now be for Dog<'static> impl Dog { fn set_formatter(&mut self,_fmt: &dyn Fn()) {} } 闭包传递给 'static 虽然它毫无意义,因为该方法实际上并没有做任何事情,并且编译器会在您实际尝试设置Dog<'static> 结构中的闭包。