如何将数据移动到多个Rust闭包中?

时间:2018-09-23 09:08:42

标签: rust gtk-rs

我在一个简单的GTK应用程序中有两个小部件:

extern crate gdk;
extern crate gtk;

use super::desktop_entry::DesktopEntry;

use gdk::enums::key;
use gtk::prelude::*;

pub fn launch_ui(_desktop_entries: Vec<DesktopEntry>) {
    gtk::init().unwrap();

    let builder = gtk::Builder::new_from_string(include_str!("interface.glade"));

    let window: gtk::Window = builder.get_object("main_window").unwrap();
    let search_entry: gtk::SearchEntry = builder.get_object("search_entry").unwrap();
    let list_box: gtk::ListBox = builder.get_object("list_box").unwrap();

    window.show_all();

    search_entry.connect_search_changed(move |_se| {
        let _a = list_box.get_selected_rows();
    });

    window.connect_key_press_event(move |_, key| {
        match key.get_keyval() {
            key::Down => {
                list_box.unselect_all();
            }
            _ => {}
        }
        gtk::Inhibit(false)
    });

    gtk::main();
}

我需要从两个事件中更改list_box。我有两个move闭包,但是由于出现错误,无法同时将list_box移动到两个闭包:

error[E0382]: capture of moved value: `list_box`

我该怎么办?

3 个答案:

答案 0 :(得分:6)

如Shepmaster的回答中所述,您只能将一个值移出变量一次,编译器将阻止您第二次执行该操作。我将尝试为此用例添加一些特定的上下文。大部分是由于我在C时代使用过GTK的记忆,而我只是在gtk-rs文档中进行了查询,所以我确定我得到了一些细节错误,但是我认为一般要点是正确的

首先让我们看看为什么首先需要将值移到闭包中。您在两个闭包中调用list_box的方法都引用了self,因此您实际上并没有使用闭包中的列表框。这意味着定义两个不带move的闭包是完全有效的–您只需要对list_box的只读引用,就可以一次拥有多个只读引用,并且list_box的生存时间至少与封包一样长。

但是,虽然允许您在不将list_box移入两个闭包的情况下定义,但是您不能将以这种方式定义的闭包传递给gtk-rs:所有连接事件的函数处理程序,只有“静态”函数除外,例如

fn connect_search_changed<F: Fn(&Self) + 'static>(
    &self, 
    f: F
) -> SignalHandlerId

处理程序的类型F具有特征绑定Fn(&Self) + 'static,这意味着闭包根本不能容纳任何引用,或者它持有的所有引用必须具有静态生存期。如果我们不将list_box移到闭包中,则闭包将保留对其的非静态引用。因此,在能够将该函数用作事件处理程序之前,我们需要先摆脱引用。

为什么gtk-rs施加此限制?原因是gtk-rs是一组C库的包装,并且指向回调的指针最终传递到基础glib库。由于C没有任何生存期的概念,因此安全地执行此操作的唯一方法是要求没有任何引用可能成为无效引用。

我们现在已经确定,我们的闭包不能包含任何引用。我们仍然需要从闭包中访问list_box,那么我们有什么选择?如果您只有一个闭合,则使用move可以解决问题–通过将list_box移到闭合中,闭合成为其所有者。但是,我们已经看到这对于多个闭包不起作用,因为我们只能将list_box移动一次。我们需要找到一种拥有多个多个所有者的方法,Rust标准库提供了这样一种方法:引用计数指针RcArc。前者用于只能从当前线程访问的值,而后者可以安全地移至其他线程。

如果我没记错的话,glib在主线程中执行所有事件处理程序,并且闭包的特征范围反映了这一点:闭包不需要是SendSync,所以我们应该可以解决Rc。 Morevoer,我们只需要对闭包中的list_box进行读取访问,因此在这种情况下,我们不需要RefCellMutex来实现内部可变性。总而言之,您可能只需要这样:

use std::rc::Rc;
let list_box: gtk::ListBox = builder.get_object("list_box").unwrap();
let list_box_1 = Rc::new(list_box);
let list_box_2 = list_box_1.clone();

现在您有两个指向同一列表框的“拥有”指针,这些指针可以移到两个闭包中。

免责声明:由于您的示例代码不是独立的,因此我无法真正测试其中的任何一个。

答案 1 :(得分:1)

您实际上无法做到这一点。我鼓励您返回并重新阅读The Rust Programming Language,以重新获得所有权。当非Copy类型移动了 时,它就消失了–这是Rust甚至存在的一个重要原因:要跟踪它,以便程序员不必这样做。

如果类型为Copy,则编译器将自动为您创建副本。如果类型为Clone,则必须显式调用克隆。

您将需要更改为shared ownership,并且很可能更改为interior mutability

共享所有权允许单个数据由多个值共同拥有,并通过克隆创建其他所有者。

内部可变性是必需的,因为Rust不允许同时对一个项目进行多个可变引用。

list_box包裹在Mutex中,然后包裹在ArcArc<Mutex<T>>)中。克隆每个处理程序的Arc并将其克隆到处理程序中。然后,您可以锁定list_box并进行所需的任何更改。

另请参阅:

答案 2 :(得分:1)

您可以在gtk-rs小部件上使用克隆。

在gtk-rs中,每个实现gtk::Widget的对象(因此基本上您可以在gtk::Window中使用的每个GTK对象)也必须实现Clone特性。调用clone()非常便宜,因为它只是一个指针副本和一个参考计数器更新。

知道以下内容是有效且便宜的:

let list_box_clone = list_box.clone();
search_entry.connect_search_changed(move |_se| {
    let _a = list_box.get_selected_rows();
});

但是由于此解决方案很冗长,而且如果要移动的对象不只一个,很快就会变得很丑陋,因此社区提出了以下宏:

macro_rules! clone {
    (@param _) => ( _ );
    (@param $x:ident) => ( $x );
    ($($n:ident),+ => move || $body:expr) => (
        {
            $( let $n = $n.clone(); )+
            move || $body
        }
    );
    ($($n:ident),+ => move |$($p:tt),+| $body:expr) => (
        {
            $( let $n = $n.clone(); )+
            move |$(clone!(@param $p),)+| $body
        }
    );
}

用法很简单:

search_entry.connect_search_changed(clone!(list_box => move |_se| {
    let _a = list_box.get_selected_rows();
}));

此宏能够克隆任意数量的移入闭包的对象。

有关更多说明和示例,请查看gtk-rs团队的本教程:Callbacks and closures