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

Vic*_*voy 9 closures mutability rust

我有以下代码(playground):

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);
        });
    }
}
Run Code Online (Sandbox Code Playgroud)

发生以下错误:

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 | |     });
   | |_____^
Run Code Online (Sandbox Code Playgroud)

我有一个加载对象列表和一个数据库中的对象列表.我需要一个函数,它接受一个闭包并在加载的对象上执行它,如果我们没有列表中的对象,则在数据库的对象列表上执行它.

该功能如下:

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,
        );
    });
}
Run Code Online (Sandbox Code Playgroud)

只有当我们无法在运行时列表中找到它们时,我们才必须对数据库中的对象进行操作.这就是为什么我决定制作一个变量,说"我们已经在列表中找到了这些对象,让数据库保持独立".

Spl*_*Dev 9

将您的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);
    }
}
Run Code Online (Sandbox Code Playgroud)

正如彼得所说,有一些编译魔术正在进行中.

签名Fn::call是:

extern "rust-call" fn call(&self, args: Args) -> Self::Output
Run Code Online (Sandbox Code Playgroud)

这需要一个不可变引用self,这就是为什么你不能修改任何捕获的变量.

签名FnMut::call_mut允许您变量变量,因为它需要&mut self:

extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output
Run Code Online (Sandbox Code Playgroud)

通过将闭包更改FnFnMut,允许它修改其捕获的变量,前提是您传递给它的引用是可变的.


Pet*_*all 6

只是为了扩大一点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;         
    }
} 
Run Code Online (Sandbox Code Playgroud)

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

let mut c1 = Closure_1 { done : done };
a.perform_for_all(|v| c1.call(&v)); 
done = c1.done;
Run Code Online (Sandbox Code Playgroud)

当闭包修改它的环境时,它不能是一个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
    }
}
Run Code Online (Sandbox Code Playgroud)

有关更多信息,请参阅关闭闭包及其环境的Rust Programming Language 部分.