本文介绍了不能在同一范围内的两个不同闭包中可变地借用的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我的目标是创建一个独立于底层数据结构工作的函数(特别是洪水填充).我试图通过传入两个闭包来做到这一点:一个用于查询,它不可变地借用一些数据,另一个用于变异,它可变地借用相同的数据.

My goal is to make a function (specifically, floodfill) that works independent of the underlying data structure. I tried to do this by passing in two closures: one for querying, that borrows some data immutably, and another for mutating, that borrows the same data mutably.

示例(在 Rust Playground 上测试)::>

Example (tested on 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

我确实想出了一个解决方案,但它非常难看.如果我将闭包合二为一并使用参数指定我想要的行为,它会起作用:

I did come up with a solution, but it is very ugly. It works if I combine the closures into one and specify which behavior I want with a parameter:

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

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

Is there any way I can appease the borrow checker without this weird way of combining closures?

推荐答案

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

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.

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

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

但是,我认为这样两个相互关联的闭包会导致可维护性差.取而代之的是,给这个概念取个名字并赋予其特征:

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

运行时借用检查

因为您的代码对可变性有时间需求(您只需要它在给定时间可变或不可变),您还可以从编译时借用检查切换到运行时借用检查.正如评论中提到的,像 CellRefCellMutex 之类的工具可以用于此:

Run-time borrow checking

Because your code has a temporal need for the mutability (you only need it either mutable or immutable at a given 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),
    );
}

另见:

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

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

我,另一个容易犯错的程序员,相信这段代码是安全的,因为永远不会有 data 的临时可变别名.然而,这需要foo 做什么的知识.如果它同时调用一个闭包和另一个闭包,这很可能成为未定义的行为.

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.

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

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

没有理由使用->() 关于闭包类型,可以省略.

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

这篇关于不能在同一范围内的两个不同闭包中可变地借用的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

05-28 10:57
查看更多