如何在Rust中从Fn闭包内部更改变量?

17

我有以下代码(示例):

struct A {
    pub vec: Vec<u64>,
}

impl A {
    fn perform_for_all<F: Fn(&mut u64)>(&mut self, f: F) {
        for mut i in &mut self.vec {
            f(i);
        }
    }
}
fn main() {
    let mut a = A {
        vec: vec![1, 3, 44, 2, 4, 5, 6],
    };

    let mut done = false;

    a.perform_for_all(|v| {
        println!("value: {:?}", v);
        done = true;
    });

    if !done {
        a.perform_for_all(|v| {
            println!("value {:?}", v);
        });
    }
}

以下错误发生:
error[E0594]: cannot assign to `done`, as it is a captured variable in a `Fn` closure
  --> src/main.rs:21:9
   |
21 |         done = true;
   |         ^^^^^^^^^^^ cannot assign
   |
help: consider changing this to accept closures that implement `FnMut`
  --> src/main.rs:19:23
   |
19 |       a.perform_for_all(|v| {
   |  _______________________^
20 | |         println!("value: {:?}", v);
21 | |         done = true;
22 | |     });
   | |_____^

我有一个已加载对象列表和一个数据库中的对象列表。我需要一个函数,它接受一个闭包并在已加载的对象上执行它,如果我们没有这些对象,则在来自数据库的对象列表上执行它。
该函数如下所示:
pub fn perform_for_match_with_mark<F>(&mut self, mark: MatchMark, f: F)
where
    F: Fn(&mut GameMatch),
{
    self.perform_for_all_matches(
        |m| {
            // runtime list
            if let Game::Match(ref mut gm) = *m {
                if gm.match_stamp().mark == mark {
                    f(gm);
                }
            }
        },
        None,
    );
    // if we have called `f` above - don't execute lines below.
    let tx = self.match_tx.clone();
    GamesDatabase::perform_for_match_with_mark(mark, |ms| {
        // database
        self.perform_for_all_matches(
            |m| {
                if let Game::Match(ref gm) = *m {
                    if gm.match_stamp().id == ms.id {
                        f(&mut GameMatch::new_with_match_stamp(
                            tx.clone(),
                            ms.clone(),
                            gm.needs_server_set,
                            gm.server_id,
                        ))
                    }
                }
            },
            None,
        );
    });
}

我们只能在运行时列表中找不到对象时才需要操作数据库中的对象。因此,我决定制作一个变量,指示“我们已经在列表中找到这些对象,不要再操作数据库了”。


4
请注意,将闭包声明为 Fn 的整个意图是仅允许那些不修改所捕获状态的闭包。(这反过来使它们可在多线程和其他上下文中接受。)使用 Fn 是在 使用 闭包方面最自由的选择,但在 接受 何种类型的闭包方面最保守。 - user4815162342
2个回答

15

将您的perform_for_all函数更改为使用FnMut而不是Fn

fn perform_for_all<F>(&mut self, mut f: F)
where
    F: FnMut(&mut u64),
{
    for mut i in &mut self.vec {
        f(&mut i);
    }
}

正如Peter所说,编译器进行了一些神奇的操作。

Fn::call的签名为:

extern "rust-call" fn call(&self, args: Args) -> Self::Output
这需要一个不可变的对self的引用,这就是为什么你不能修改任何被捕获的变量。 FnMut::call_mut 的签名允许您改变变量,因为它采用了&mut self
extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output

通过将你的闭包从 Fn 改为 FnMut,你允许它修改其捕获变量,只要你传递给它的引用是可变的。


好的,我刚刚尝试使用Rc<RefCell<bool>>,也起作用了。您是否能够在答案中包含它,并解释一下为什么它可行? - Victor Polevoy
@Victor,你的Rc<RefCell<T>>是可行的,因为这样你可以获取到T的共享可变引用。尽管如此,这并不是最好的解决方案。我编辑了我的答案来展示为什么FnMut的方式可行。 - SplittyDev
@VictorPolevoy Rc<RefCell<bool>> 是一种hack,因为它会将可变性隐藏在Rust的静态分析器中,而且没有什么好的理由。例如,在使用它来允许Fn改变状态时,闭包可以从多个线程中调用,但如果两个线程尝试同时执行闭包,则会在运行时发生panic(终止线程)。为什么你需要将函数声明为Fn而不是FnMut - user4815162342

10

稍微扩展一下SplittyDev的答案。

当您使用闭包时,编译器会进行一些魔法以让闭包访问其环境中的变量。实际上,它将创建一个新的结构体,其成员是您尝试访问的变量。

这不完全准确(以下内容无法编译),但在概念上是一个合理的近似:

struct Closure_1 {
    done: bool
}

impl FnMut<&mut u64> for Closure_1 {
    fn call_mut(&mut self, v: &mut u64) {
        println!("value: {:?}", v);                                                                 
        self.done = true;         
    }
} 

当你调用它时,这些变量将被借用、复制(如果使用move关键字,则会移动)。

let mut c1 = Closure_1 { done : done };
a.perform_for_all(|v| c1.call(&v)); 
done = c1.done;

当闭包修改其环境时,它不能是一个 Fn,因为它必须还要改变自身的变量:

impl Fn<&mut u64> for Closure_1 {
    fn call(&self, v: &mut u64) {
        println!("value: {:?}", v);                                                                 
        self.done = true; // Can't do this because self is not a mutable ref
    }
}

请参阅Rust编程语言中关于闭包及其环境的部分内容以获取更多信息。


1
你能否在源代码中添加一个“magic”的链接或者进一步扩展一下它?我认为这对于培养直觉(并提高理解)非常关键。 - naktinis
1
添加了一个指向文档的链接。这个文档解释得很清楚,但并没有详细说明结构体实际上是什么样子的。 - Peter Hall
我更多地是指编译器代码所进行的所谓“魔法”。我很好奇在你的类比中,如何使done变成了self.done。我理解这只是为了阐明,并不是实际发生的事情,因此我想知道实际上会发生什么。 - naktinis
1
@naktinis,这个问题在编译器源代码的一些注释中有讨论:https://github.com/rust-lang/rust/blob/ff261d3a6b5964e1e3744d055238de624afc5d76/src/librustc/ty/sty.rs#L183,但可能不完全符合您的要求。 - Peter Hall

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接