Rust borrow of a HashMap lasts beyond the scope it

2019-01-01 08:06发布

问题:

I\'ve got a persistent compile error where Rust complains that I have an immutable borrow while I\'m trying to mutably borrow, but the immutable borrow is from another scope, and I\'m not bringing anything across from it.

I have some code that checks for a value in a map, and if it\'s present, returns it, otherwise it needs to mutate the map in various ways. The problem is that I can\'t seem to find a way to get Rust let me do both, even though the two operations are totally separate.

Here\'s some nonsensical code that follows the same structure as my code and exhibits the problem:

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

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

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

This errors out with:

error[E0502]: cannot borrow `*map` as mutable because it is also borrowed as immutable
  --> src/main.rs:17:5
   |
7  |         match map.get(&key) { // borrow immutably
   |               --- immutable borrow occurs here
...
17 |     map.insert(0, 0); // borrow mutably, which errors
   |     ^^^ mutable borrow occurs here
18 |     None
19 | }
   | - immutable borrow ends here

This doesn\'t make any sense to me. How does the immutable borrow outlive that scope?! One branch of that match exits the function via return, and the other does nothing and leaves the scope.

I\'ve seen this happen before where I was mistakenly smuggling the borrow out of the scope in some other variable, but that\'s not the case here!

True, the borrow is escaping the scope via the return statement, but it\'s ridiculous that that blocks borrowing farther down in the function -- the program cannot possibly return AND keep going! If I return something else there, the error goes away, so I think this is what the borrow checker is getting hung up on. This feels like a bug to me.

Unfortunately, I\'ve been unable to find any way to rewrite this without hitting the same error, so it\'s a particularly nasty bug if that\'s the case.

回答1:

This is a known issue that is very likely to be solved by non-lexical scopes, which is itself predicated on MIR. If it so happens that you are inserting to the same key that you are looking up, I\'d encourage you to use the entry API.

You can add a smidgen of inefficiency to work around this for now. The general idea is to add a boolean that tells you if a value was present or not. This boolean does not hang on to a reference, so there is no borrow:

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

Similar cases with a vector instead of a HashMap can be solved by using the index of the element instead of the reference. Like the case above, this can introduce a bit of inefficiency due to the need to check the slice bounds again.

Instead of

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

You can write:

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]
}

Note that non-lexical lifetimes are being worked on. In nightly Rust, both original code examples compile as-is when NLL is enabled:

#![feature(nll)]

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
}
#![feature(nll)]

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