不能在同一范围内的两个不同的闭包中可变地借入

Til*_*ded 4 closures rust borrow-checker

我的目标是制作一个独立于基础数据结构的功能(特别是泛洪)。我试图通过传递两个闭包来做到这一点:一个用于查询,它不变地借用一些数据,另一个用于变异,它可变地借用相同的数据。

示例(在Rust Playground上测试):

#![feature(nll)]

fn foo<F, G>(n: i32, closure: &F, mut_closure: &mut G)
where
    F: Fn(i32) -> bool,
    G: FnMut(i32) -> (),
{
    if closure(n) {
        mut_closure(n);
    }
}

fn main() {
    let mut data = 0;
    let closure = |n| data == n;
    let mut mut_closure = |n| {
        data += n;
    };
    foo(0, &closure, &mut mut_closure);
}
Run Code Online (Sandbox Code Playgroud)

错误:(调试,每晚)

#![feature(nll)]

fn foo<F, G>(n: i32, closure: &F, mut_closure: &mut G)
where
    F: Fn(i32) -> bool,
    G: FnMut(i32) -> (),
{
    if closure(n) {
        mut_closure(n);
    }
}

fn main() {
    let mut data = 0;
    let closure = |n| data == n;
    let mut mut_closure = |n| {
        data += n;
    };
    foo(0, &closure, &mut mut_closure);
}
Run Code Online (Sandbox Code Playgroud)

我确实提出了一个解决方案,但这非常丑陋。如果我将闭包合并为一个闭包,并使用参数指定想要的行为,那么它将起作用:

// #![feature(nll)] not required for this solution

fn foo<F>(n: i32, closure: &mut F)
where
    F: FnMut(i32, bool) -> Option<bool>,
{
    if closure(n, false).unwrap() {
        closure(n, true);
    }
}

fn main() {
    let mut data = 0;
    let mut closure = |n, mutate| {
        if mutate {
            data += n;
            None
        } else {
            Some(data == n)
        }
    };
    foo(0, &mut closure);
}
Run Code Online (Sandbox Code Playgroud)

如果没有这种结合关闭的怪异方法,有什么办法可以安抚借阅检查器?

She*_*ter 5

The problem is rooted in the fact that there's information that you know that the compiler doesn't.

As mentioned in the comments, you cannot mutate a value while there is a immutable reference to it — otherwise it wouldn't be immutable! It happens that your function needs to access the data immutably once and then mutably, but the compiler doesn't know that from the signature of the function. All it can tell is that the function can call the closures in any order and any number of times, which would include using the immutable data after it's been mutated.

I'm guessing that your original code indeed does that — it probably loops and accesses the "immutable" data after mutating it.

Compile-time borrow checking

One solution is to stop capturing the data in the closure. Instead, "promote" the data to an argument of the function and the closures:

fn foo<T, F, G>(n: i32, data: &mut T, closure: F, mut mut_closure: G)
where
    F: Fn(&mut T, i32) -> bool,
    G: FnMut(&mut T, i32),
{
    if closure(data, n) {
        mut_closure(data, n);
    }
}

fn main() {
    let mut data = 0;

    foo(
        0,
        &mut data,
        |data, n| *data == n,
        |data, n| *data += n,
    );
}
Run Code Online (Sandbox Code Playgroud)

However, I believe that two inter-related closures like that will lead to poor maintainability. Instead, give a name to the concept and make a trait:

trait FillTarget {
    fn test(&self, _: i32) -> bool;
    fn do_it(&mut self, _: i32);
}

fn foo<F>(n: i32, mut target: F)
where
    F: FillTarget,
{
    if target.test(n) {
        target.do_it(n);
    }
}

struct Simple(i32);

impl FillTarget for Simple {
    fn test(&self, n: i32) -> bool {
        self.0 == n
    }

    fn do_it(&mut self, n: i32) {
        self.0 += n
    }
}

fn main() {
    let data = Simple(0);
    foo(0, data);
}
Run Code Online (Sandbox Code Playgroud)

Run-time borrow checking

Because your code has a temporal need for the mutability (you only need it either mutable or immutable at a give time), you could also switch from compile-time borrow checking to run-time borrow checking. As mentioned in the comments, tools like Cell, RefCell, and Mutex can be used for this:

use std::cell::Cell;

fn main() {
    let data = Cell::new(0);

    foo(
        0,
        |n| data.get() == n,
        |n| data.set(data.get() + n),
    );
}
Run Code Online (Sandbox Code Playgroud)

See also:

Unsafe programmer-brain-time borrow checking

RefCell and Mutex have a (small) amount of runtime overhead. If you've profiled and determined that to be a bottleneck, you can use unsafe code. The usual unsafe caveats apply: it's now up to you, the fallible programmer, to ensure your code doesn't perform any undefined behavior. This means you have to know what is and is not undefined behavior!

use std::cell::UnsafeCell;

fn main() {
    let data = UnsafeCell::new(0);

    foo(
        0,
        |n| unsafe { *data.get() == n },
        |n| unsafe { *data.get() += n },
    );
}
Run Code Online (Sandbox Code Playgroud)

I, another fallible programmer, believe this code to be safe because there will never be temporal mutable aliasing of data. However, that requires knowledge of what foo does. If it called one closure at the same time as the other, this would most likely become undefined behavior.

Additional comments

  1. There's no reason to take references to your generic closure types for the closures

  2. There's no reason to use -> () on the closure type, you can just omit it.