从HashMap或Vec返回引用会导致借用超出其所在的范围?

37

我一直遇到一个编译错误,Rust抱怨我在尝试可变借用时有一个不可变借用,但是不可变借用来自另一个范围,并且我没有从中携带任何内容。

我有一些代码,在映射中检查值,如果存在,则返回它,否则需要以各种方式改变映射。问题在于,即使这两个操作完全分开,我似乎也找不到让Rust让我这样做的方法。

这里有一些无意义的代码,它遵循与我的代码相同的结构并展示了问题:

use std::collections::BTreeMap;

fn do_stuff(map: &mut BTreeMap<i32, i32>, key: i32) -> Option<&i32> {
    // extra scope in vain attempt to contain the borrow
    {
        // borrow immutably
        if let Some(key) = map.get(&key) {
            return Some(key);
        }
    }

    // now I'm DONE with the immutable borrow, but rustc still thinks it's borrowed

    map.insert(0, 0); // borrow mutably, which errors
    None
}

这会出现错误:

error[E0502]: cannot borrow `*map` as mutable because it is also borrowed as immutable
  --> src/lib.rs:14:5
   |
3  | fn do_stuff(map: &mut BTreeMap<i32, i32>, key: i32) -> Option<&i32> {
   |                  - let's call the lifetime of this reference `'1`
...
7  |         if let Some(key) = map.get(&key) {
   |                            --- immutable borrow occurs here
8  |             return Some(key);
   |                    --------- returning this value requires that `*map` is borrowed for `'1`
...
14 |     map.insert(0, 0); // borrow mutably, which errors
   |     ^^^^^^^^^^^^^^^^ mutable borrow occurs here

这对我来说毫无意义。不可变的借用如何超出了那个作用域?!那个match语句的一个分支通过return退出函数,另一个分支什么也不做并离开了该作用域。

我曾经看到过类似情况,当时我错误地使用了另一个变量将借用移出了作用域,但这里并非如此!

的确,借用通过return语句逃逸了作用域,但是荒谬的是它阻止了函数下面更远处的借用——程序不可能返回并继续运行!如果我在那里返回其他内容,错误就消失了,因此我认为这是借用检查器卡住的原因。这感觉像是一个bug。

不幸的是,我一直无法找到任何重写方法而不会遇到相同错误,所以如果是这种情况,这就是一个特别棘手的bug。


很遗憾,.entry()对于这个函数需要做的事情来说不是正确的选择。我知道非词法作用域的问题,通常我可以解决它,但在这种情况下,我一直没有想到一个不会做大量重复工作的方法,无论解决方法多么丑陋...通常添加作用域可以解决该问题,但在这里并不适用;即使将一个borrow移动到另一个函数中也没有帮助。 - Bill Fraser
1个回答

34
这是一个已知问题,将在未来的非词汇生命周期迭代中得到解决,但目前在Rust 1.57中尚未处理

如果您正在向相同的键插入和查找值,则建议您改用entry API

您可以添加一些不太高效的方法来解决此问题。如果这种低效率无法接受,则有更深层次的解决方法

HashMap

一般思路是添加一个布尔值,告诉您值是否存在。这个布尔值不会挂起引用,因此没有借用:

use std::collections::BTreeMap;

fn do_stuff(map: &mut BTreeMap<i32, i32>, key: i32) -> Option<&i32> {
    if map.contains_key(&key) {
        return map.get(&key);
    }

    map.insert(0, 0);
    None
}

fn main() {
    let mut map = BTreeMap::new();
    do_stuff(&mut map, 42);
    println!("{:?}", map)
}

Vec

类似的情况可以通过使用元素的索引而不是引用来解决。与上面的情况类似,这可能会因为需要重新检查切片边界而引入一些效率问题。

不要使用:

fn find_or_create_five<'a>(container: &'a mut Vec<u8>) -> &'a mut u8 {
    match container.iter_mut().find(|e| **e == 5) {
        Some(element) => element,
        None => {
            container.push(5);
            container.last_mut().unwrap()
        }
    }
}

你可以写:
fn find_or_create_five<'a>(container: &'a mut Vec<u8>) -> &'a mut u8 {
    let idx = container.iter().position(|&e| e == 5).unwrap_or_else(|| {
        container.push(5);
        container.len() - 1    
    });
    &mut container[idx]
}

非词法生命周期

这些类型的示例是NLL RFC中的主要情况之一:问题案例#3:跨函数的条件控制流

不幸的是,截至Rust 1.57,这个特定情况还没有准备好。如果您在夜间版本中选择实验性的-Zpolonius功能(RUSTFLAGS="-Z polonius" cargo +nightly check),则每个原始示例将按原样编译:

use std::collections::BTreeMap;

fn do_stuff(map: &mut BTreeMap<i32, i32>, key: i32) -> Option<&i32> {
    if let Some(key) = map.get(&key) {
        return Some(key);
    }

    map.insert(0, 0);
    None
}

fn find_or_create_five(container: &mut Vec<u8>) -> &mut u8 {
    match container.iter_mut().find(|e| **e == 5) {
        Some(element) => element,
        None => {
            container.push(5);
            container.last_mut().unwrap()
        }
    }
}

另请参见:

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