Search code examples
rusthashmapborrow-checkerownership

Erronous mutable borrow (E0502) when trying to remove and insert into a HashMap


I am a beginner to Rust and tried using a HashMap<u64, u64>. I want to remove an element and insert it with a modified value:

let mut r = HashMap::new();
let mut i = 2;
...
if r.contains_key(&i) {
    let v = r.get(&i).unwrap();
    r.remove(&i);
    r.insert(i, v+1);
}

Now, the borrow checker complains that r is borrowed immutable, then mutable and then immutable again in the three lines of the if-block. I don't understand what's going on...I guess since the get, remove and insert methods have r as implicit argument, it is borrowed in the three calls. But why is it a problem that this borrow in the remove call is mutable?


Solution

  • But why is it a problem that this borrow in the remove call is mutable?

    The problem is the spanning: Rust allows either any number of immutable borrows or a single mutable borrow, they can not overlap.

    The issue here is that v is a reference to the map contents, meaning the existence of v requires borrowing the map until v stops being used. Which thus overlaps with both remove and insert calls, and forbids them.

    Now there are various ways to fix this. Since in this specific case you're using u64 which is Copy, you can just dereference and it'll copy the value you got from the map, removing the need for a borrow:

    if r.contains_key(&i) {
        let v = *r.get(&i).unwrap();
        r.remove(&i);
        r.insert(i, v+1);
    }
    

    this is limited in its flexibility though, as it only works for Copy types[0].

    In this specific case it probably doesn't matter that much, because Copy is cheap, but it would still make more sense to use the advanced APIs Rust provides, for safety, for clarity, and because you'll eventually need them for less trivial types.

    The simplest is to just use get_mut: where get returns an Option<&V>, get_mut returns an Option<&mut V>, meaning you can... update the value in-place, you don't need to get it out, and you don't need to insert it back in (nor do you need a separate lookup but you already didn't need that really):

    if let Some(v) = r.get_mut(&i) {
        *v += 1;
    }
    

    more than sufficient for your use case.

    The second option is the Entry API, and the thing which will ruin every other hashmap API for you forever. I'm not joking, every other language becomes ridiculously frustrating, you may want to avoid clicking on that link (though you will eventually need to learn about it anyway, as it solves real borrowing and efficiency issues).

    It doesn't really show its stuff here because your use case is simple and get_mut more than does the job, but anyway, you could write the increment as:

    r.entry(i).and_modify(|v| *v+=1);
    

    Incidentally in most languages (and certainly in Rust as well) when you insert an item in a hashmap, the old value gets evicted if there was one. So the remove call was already redundant and wholly unnecessary.

    And pattern-matching an Option (such as that returned by HashMap::get) is generally safer, cleaner, and faster than painstakenly and procedurally doing all the low-level bits.

    So even without using advanced APIs, the original code can be simplified to:

    if let Some(&v) = r.get(&i) {
        r.insert(i, v+1);
    }
    

    I'd still recommend the get_mut version over that as it is simpler, avoids the double lookup, and works on non-Copy types, but YMMV.

    Also unlike most languages Rust's HashMap::insert returns the old value (f any), not a concern here but can be useful in some cases.

    [0] as well as Clone ones, by explicitly calling .clone(), that may or may not translate to a significant performance impact depending on the type you're cloning.