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

时间:2018-04-07 04:55:08

标签: rust closures borrow-checker

我的目标是创建一个独立于底层数据结构的函数(特别是floodfill)。我尝试通过传递两个闭包来做到这一点:一个用于查询,一个是不可靠地借用一些数据,另一个用于变异,它可以相互借用相同的数据。

示例(在the 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);
}

错误:(调试,每晚)

error[E0502]: cannot borrow `data` as mutable because it is also borrowed as immutable
  --> src/main.rs:16:27
   |
15 |     let closure = |n| data == n;
   |                   --- ---- previous borrow occurs due to use of `data` in closure
   |                   |
   |                   immutable borrow occurs here
16 |     let mut mut_closure = |n| {
   |                           ^^^ mutable borrow occurs here
17 |         data += n;
   |         ---- borrow occurs due to use of `data` in closure
18 |     };
19 |     foo(0, &closure, &mut mut_closure);
   |            -------- borrow later used here

我确实提出了一个解决方案,但它非常难看。如果我将闭包合并为一个并使用参数指定我想要的行为,它就可以工作:

// #![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);
}

如果没有这种奇怪的组合闭包的方法,我有什么方法可以安抚借用检查器?

1 个答案:

答案 0 :(得分:4)

问题的根源在于知道编译器不知道的信息。

正如评论中所提到的,当存在对它的不可变引用时,你不能改变一个值 - 否则它不会是不可变的!碰巧你的函数需要一次然后可变地访问数据,但是编译器不知道函数的签名。所有它可以告诉的是,该函数可以以任何顺序和任意次数调用闭包,其中包括在变异后使用不可变数据。

我猜你的原始代码确实这样做了 - 它可能会在变异之后循环并访问“不可变”数据。

编译时借阅检查

一种解决方案是停止捕获闭包中的数据。相反,将数据“提升”为函数和闭包的参数:

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,
    );
}

但是,我认为两个相互关联的闭包会导致可维护性差。相反,给这个概念命名并制作一个特征:

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);
}

运行时借用检查

因为你的代码对可变性有暂时的需求(你只需要在给定时间它是可变的或不可变的),你也可以从编译时借用检查切换到运行时借用检查。如评论中所述,可以使用CellRefCellMutex等工具:

use std::cell::Cell;

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

    foo(
        0,
        |n| data.get() == n,
        |n| data.set(data.get() + n),
    );
}

另见:

不安全的程序员 - 大脑时间借阅检查

RefCellMutex具有(少量)运行时开销。如果您已分析并确定该瓶颈,则可以使用不安全的代码。通常不安全的警告适用:现在由您,易受攻击的程序员来确保您的代码不执行任何未定义的行为。这意味着你必须知道什么是未定义的行为

use std::cell::UnsafeCell;

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

    foo(
        0,
        |n| unsafe { *data.get() == n },
        |n| unsafe { *data.get() += n },
    );
}

我,另一个容易错误的程序员,相信这段代码是安全的,因为永远不会有data的时间可变别名。但是,这需要了解 foo做什么。如果它与另一个同时调用一个闭包,那很可能会成为未定义的行为。

其他评论

  1. 没有理由引用闭包的通用闭包类型

  2. 没有理由在闭包类型上使用-> (),你可以省略它。