在调用一个借用为不可变的闭包时,在循环中不能作为可变借用。

8
这是代码:

fn test(){
    let mut numbers = vec![2];
    let f = || {
        for _ in numbers.iter(){
        }
        false
    };

    while false {
        let res = f();
        if res {
            numbers.push(10);
        }
    }
}

错误信息为:
   |
15 |     let f = || {
   |             -- immutable borrow occurs here
16 |         for _ in numbers.iter(){
   |                  ------- first borrow occurs due to use of `numbers` in closure
...
22 |         let res = f();
   |                   - immutable borrow later used here
23 |         if res {
24 |             numbers.push(10);
   |             ^^^^^^^^^^^^^^^^ mutable borrow occurs here

但是如果我将while关键字更改为if,就可以编译。怎么解决这个问题?我想在循环中调用匿名函数。

2个回答

9
我们可以通过使用一个简单的不可变引用来替换闭包,从而进一步简化您的示例。
let mut numbers = vec![2];
let r = &numbers;

while false {
    println!("{:?}", r);
    numbers.push(10);
}

这里出现了一个错误:

error[E0502]: cannot borrow `numbers` as mutable because it is also borrowed as immutable
 --> src/lib.rs:7:5
  |
3 | let r = &numbers;
  |         -------- immutable borrow occurs here
...
6 |     println!("{:?}", r); // use reference
  |                      - immutable borrow later used here
7 |     numbers.push(10);
  |     ^^^^^^^^^^^^^^^^ mutable borrow occurs here

就像你的例子一样,用if替换while可以消除错误。为什么?

你可能知道 Rust 的一个重要规则: 别名和可变性。它表明,在任何给定的时间,一个值可以被不可变地借用任意多次,或者可变地借用一次。

语句numbers.push(10)暂时以可变方式借用了numbers(仅在该语句中)。但我们也有一个不可变的引用r。为了使numbers.push(10)起作用,编译器必须确保此时不存在其他借用。但这里存在着引用r!这个引用不能同时存在于numbers.push(10)存在的时候。

我们先看一下if情况:

let mut numbers = vec![2];
let r = &numbers;            // <------+      (creation of r)   
                             //        |
if false {                   //        |
    println!("{:?}", r);     // <------+      (last use of r)
    numbers.push(10);
}

尽管词法作用域意味着变量r仅在函数结尾处被丢弃,但由于非词法生命周期,编译器可以看到r的最后一次使用是在println行中。然后编译器可以在此行之后将r标记为“死亡”。这反过来意味着,在numbers.push(10)行中没有其他借用,一切都能很好地处理。

那么对于循环情况呢?让我们假设循环迭代了三次:

let mut numbers = vec![2];
let r = &numbers;            // <------+      (creation of r)   
                             //        |
// First iteration           //        |
println!("{:?}", r);         //        |
numbers.push(10);            //        |  <== oh oh!
                             //        |
// Second iteration          //        |
println!("{:?}", r);         //        |
numbers.push(10);            //        |
                             //        |
// Third iteration           //        |
println!("{:?}", r);         // <------+     (last use of r)
numbers.push(10);

如下所示,可以看到r处于活动状态的时间与numbers.push(10)重叠(除了最后一个)。由此导致编译器会产生错误,因为这段代码违反了中心 Rust 规则。
对于闭包情况,解释是相同的:闭包以不可变方式借用numbers,而f();使用该闭包。在循环情况下,编译器无法缩小闭包的“存活时间”,以确保它不会与对push的可变借用重叠。
如何修复?
好吧,你可以每次将numbers传递到闭包中:
let mut numbers = vec![2];
let f = |numbers: &[i32]| {
    for _ in numbers.iter(){
    }
    false
};

while false {
    let res = f(&numbers);
    if res {
        numbers.push(10);
    }
}

现在这段代码能够工作是因为,numbers现在被不可变地借用,仅仅是为了执行f(&numbers);语句。

你也可以使用RefCell,就像另一个答案建议的那样,但这应该是最后的手段。


为什么从外部捕获借用内容与传递参数不同? - Ömer Erden
@ÖmerErden 这是一个好问题。 闭包的正常实现是一个匿名结构体,其中包含所有捕获的变量(如果以引用方式捕获,则为引用)。 因此,我们基本上有一个包含对numbers的不可变引用的结构体。 我不确定环境是否可以在每次调用闭包时自动传递给它。 不确定在所有情况下都是正确的。 可能值得提出一个适当的SO问题。 - Lukas Kalbertodt
非常好的答案!我给你点赞了。 - ruohola
谢谢,所以原因是闭包在创建时捕获变量,而不是在调用时。 - HYRY

3

不确定你想要达到什么目的,但是一种解决方法,而不用太大幅度地更改您的代码,是使用std::cell :: RefCell (在 std 中描述本书中):

use std::cell::RefCell;

fn test(){
    let numbers = RefCell::new(vec![2]);
    let f = || {
        for _ in numbers.borrow().iter(){
        }
        false
    };

    while false {
        let res = f();
        if res {
            numbers.borrow_mut().push(10);
        }
    }
}

这是一个稍微改进过的演示,实际上做了一些事情:

use std::cell::RefCell;

fn main() {
    test();
}

fn test() {
    let numbers = RefCell::new(vec![0]);
    let f = || {
        for n in numbers.borrow().iter() {
            println!("In closure: {}", n);
        }
        println!();
        true
    };

    let mut i = 1;
    while i <= 3 {
        let res = f();
        if res {
            numbers.borrow_mut().push(i);
        }
        i += 1;
    }
    println!("End of test(): {:?}", numbers.borrow());
}

Output:

In closure: 0

In closure: 0
In closure: 1

In closure: 0
In closure: 1
In closure: 2

End of test(): [0, 1, 2, 3]

Rust Playground演示


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接