为什么在闭包内部克隆数据不能防止错误“闭包可能会超出当前函数的使用寿命”?

时间:2019-07-03 09:03:53

标签: rust gtk lifetime borrowing gtk-rs

我用gtk-rs构建了一个GTK应用程序。在构建主窗口时,我想使用一些动态参数,例如窗口高度。我创建了一个包含所有此类设置的结构,并希望将其用作构建UI的函数的输入参数:

fn main() {
    let application =
        gtk::Application::new(Some("id"), Default::default())
            .expect("Initialization failed...");

    let config = Config {width: 100., height: 100.};
    application.connect_activate(|app| {
        build_ui(app, config.clone());
    });

    // Use config further

    application.run(&args().collect::<Vec<_>>());
}

#[derive(Debug, Clone)]
pub struct Config {
    pub width: f64,
    pub height: f64,
}

fn build_ui(application: &gtk::Application, config: Config) {
    ...
}

调用config时不能使用对build_ui的引用,因为可以在主函数完成后调用此函数,因此config结构不再存在。

我的想法是创建一个config结构的副本(只有几个原始变量),该副本与原始结构分开存在,因此不会遇到生命周期或所有权问题。

这是正确的方法吗?我究竟做错了什么?我从借用config结构时得到了相同的错误:

error[E0373]: closure may outlive the current function, but it borrows `config`, which is owned by the current function
  --> src/main.rs:36:34
   |
36 |     application.connect_activate(|app| {
   |                                  ^^^^^ may outlive borrowed value `config`
37 |         build_ui(app, config.clone());
   |                       ------ `config` is borrowed here

2 个答案:

答案 0 :(得分:3)

一般说明

最小复制类似问题:

fn move_and_print(s: String) {
    println!("{}", s);
}

fn main() {
    let s = String::from("Hello");

    let print_cloned_s = || println!("{}", s.clone());

    move_and_print(s);
    print_cloned_s();
}

编译器抱怨:

error[E0505]: cannot move out of `s` because it is borrowed

我想克隆s以避免借入,因此以后可以使用它。那么,编译器如何说s是借来的?

以前的推理是完全正确的,但是有一个微妙之处:Clone::clone的签名是clone(&self) -> Self。因此,在调用clone时,数据是克隆函数借来的

解决方案是创建克隆之前先克隆数据,然后将其移动到闭合中:

fn move_and_print(s: String) {
    println!("{}", s);
}

fn main() {
    let s = String::from("Hello");

    // I clone `s` BEFORE creating the closure:
    let cloned_s = s.clone();

    // Then I move the cloned data into the closure:
    let print_cloned_s = move || println!("{}", cloned_s);

    move_and_print(s);
    print_cloned_s();
}

解决您的实际错误

正如我所说,您必须克隆配置并将此克隆移到闭包内:

let cloned_config = config.clone();

application.connect_activate(move |app| {
    build_ui(app, cloned_config.clone());
});

您还必须添加第二个克隆调用,以使闭包成为Fn而不是FnOnce。确实,如果您在build_ui内移动配置,则该函数不能使用两次。有关更多信息,请参见this question


如果我很了解您的需求,config旨在成为必须共享的只读配置。在这种情况下,我根本不会移动它,例如,将build_ui的签名更改为:

fn build_ui(application: &gtk::Application, config: &Config)

答案 1 :(得分:0)

讨厌说认可的答案不是很准确。正确,但与OP代码有细微差别。实际上,如果仔细阅读原始代码,就没有理由相信rustc不能得出局部变量config不能胜过connect_activate函数调用的结论。它出于其他原因拒绝了它。

一个更准确的最小可重现示例是:

fn reference_and_print(s: &str) {
    println!("{}", s);
}

fn closure_and_print<F: Fn()>(f: F) {
    f();
}

fn main() {
    let s = "Hello";

    reference_and_print(s);
    closure_and_print(|| {
        println!("{}", s);
    });
    reference_and_print(s);
}

编译。但是,如果只更改一行:

fn closure_and_print<F: Fn() + 'static>(f: F) {
    f();
}

这将导致may outlive borrowed value错误。太令人惊讶了。

实际上,通过检查gtk-rs代码,我注意到绑定了'static的闭包无处不在。除非您拥有'static,否则没有其他东西可以生存。这就是为什么在使用move时必须使用gtk-rs闭包来拥有捕获的变量的原因:

let cloned = config.clone();
application.connect_activate(move |app| {
    build_ui(app, cloned);
});