【问题标题】:Can't borrow mutably within two different closures in the same scope不能在同一范围内的两个不同闭包内可变地借用
【发布时间】:2018-09-17 03:25:57
【问题描述】:

我的目标是制作一个独立于底层数据结构工作的函数(特别是 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);
}

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

【问题讨论】:

  • 我认为无论你得到什么答案都将是它自己的、特殊的、Rust 风格的怪异品牌。每个闭包都引用同一个对象(data);无法保证它们不会同时被调用,这可能会导致未定义且明显非 Rust 的行为。当然,不用像 Mutex 之类的特殊包装器。
  • 可能有重复的问题,所以只是评论。在有不可变引用的情况下改变一个值是未定义的行为。你可以use RefCell来避免它。

标签: rust closures borrow-checker


【解决方案1】:

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

正如 cmets 中提到的,当一个值有一个不可变的引用时,你不能改变它——否则它就不是不可变的!碰巧您的函数需要一次不可变地访问数据,然后再可变地访问数据,但编译器不知道从函数的 signature 中知道这一点。它所能说明的是,该函数可以以任意顺序和任意次数调用闭包,其中包括在变异后使用不可变数据。

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

编译时借用检查

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

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:

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

运行时借用检查

因为您的代码在时间上需要可变性(您只需要它在给定时间可变或不可变),您还可以从编译时借用检查切换到运行时借用检查。如 cmets 中所述,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 做了什么。如果它同时调用一个闭包和另一个闭包,这很可能会成为未定义的行为。

其他 cmets

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

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

【讨论】:

    猜你喜欢
    • 2021-06-23
    • 2016-07-22
    • 1970-01-01
    • 2016-05-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多