【问题标题】:"temporary value dropped while borrowed" with capturing closure“借入时的临时价值下降”与捕获关闭
【发布时间】:2021-05-05 04:18:41
【问题描述】:

请考虑以下示例 (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 的实现中去掉显式生命周期注释似乎可以满足编译器的要求(注意dyn 之前缺少的'a):

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

但是,我不明白以下内容:

  1. 为什么当我在闭包中注释掉println!("{}", x); 时问题消失了?我仍然传递了一个我希望编译器抱怨的临时变量,但事实并非如此。
  2. 为什么我在最后注释掉dog.bark(); 后问题就消失了?同样,我仍在传递一个已释放的临时闭包,但现在编译器很高兴。为什么?

【问题讨论】:

    标签: rust closures lifetime borrow-checker trait-objects


    【解决方案1】:

    首先要了解的是&amp;|| () 有一个'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&lt;'static&gt;,它总是是一个Dog&lt;'static&gt;,你不能将它转换成一个Dog&lt;'1&gt;,因为'1'static更短。

    这有一些重要的含义,其中之一是您的 set_formatter 方法的行为可能与您认为的不同。一旦你有了Dog&lt;'static&gt;,你可以'static 格式化程序传递给set_formatter。该方法如下所示:

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

    但既然我们知道我们正在使用Dog&lt;'static&gt;,我们可以用'static 替换通用生命周期参数'a,看看我们真正在使用什么:

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

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

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

    为什么会失败,使用 cmets:

    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 变量 x,因此它再次为闭包提供了 'static 生命周期。

    为什么我在最后注释掉dog.bark(); 后问题就消失了?同样,我仍在传递一个已释放的临时闭包,但现在编译器很高兴。为什么?

    这种奇怪的边缘情况似乎只发生在我们没有用Dog&lt;'static&gt; 显式类型注释dog 变量时。当变量没有显式类型注释时,编译器会尝试推断其类型,但它会很懒惰地这样做,并尝试尽可能灵活,从而使程序员受益于怀疑,以使代码编译。它真的应该抛出编译错误,即使没有dog.bark(),但它不会出于任何神秘的原因。关键是不是dog.bark() 行导致代码无法编译,它应该在set_formatter 行编译失败,但无论出于何种原因,编译器都不会费心抛出错误,直到你尝试在该违规行之后再次使用dog。即使只是删除dog 也会触发错误:

    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 { format: &|| () };
        let x = 0;
        
        dog.set_formatter(&|| {
            println!("{}", x); 
        });
        
        drop(dog); // triggers "temp freed" error above
    }
    

    既然我们已经走到了这一步,让我们回答你的非官方第三个问题,由我转述:

    为什么去掉set_formatter方法中的'a会让编译器满意?

    因为它改变了 Dog&lt;'static&gt; 的有效内容:

    // 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&lt;'static&gt;,尽管这毫无意义,因为该方法实际上并没有做任何事情,并且当您实际尝试在@ 中设置闭包时,编译器会再次抱怨987654364@结构体。

    【讨论】:

      猜你喜欢
      • 2021-03-05
      • 2023-03-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-05
      • 1970-01-01
      • 1970-01-01
      • 2021-03-18
      相关资源
      最近更新 更多