0

I'm new to Rust, and I'm struggling with the following question for which I cannot find simple answer.

Defining a struct with a HashMap field, I can't modify this HashMap in a mutable method:

use std::collections::HashMap;

struct ASet {
    myset: HashMap<String,u8>,
}

impl ASet {
    
    fn new() -> ASet {
        ASet {
            myset: HashMap::new(),
        }
    }
    
    fn fill(&mut self){
        self.myset.insert("John".to_string(), 12);
    }
    
    
    fn modify_all(&mut self){
        for (name, age) in &self.myset{
            println!("Removing {} who is {}", name, age);
            self.myset.remove(name);
        }
    }
}

fn main() {
    let mut my_struct_set = ASet::new();
    my_struct_set.fill();
    my_struct_set.modify_all();
}

Here is the error:

  Compiling playground v0.0.1 (/playground)
error[E0502]: cannot borrow `self.myset` as mutable because it is also borrowed as immutable
  --> src/main.rs:23:13
   |
21 |         for (name, age) in &self.myset{
   |                            -----------
   |                            |
   |                            immutable borrow occurs here
   |                            immutable borrow later used here
22 |             println!("Removing {} who is {}", name, age);
23 |             self.myset.remove(name);
   |             ^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here

error: aborting due to previous error

For more information about this error, try `rustc --explain E0502`.
error: could not compile `playground`

(Can be tested here)

Edit: the answer is here: Why do immutable borrow and usage occur at the same place?

2 Answers2

4

Consider yourself lucky: Doing exactly what your example does in C++, would have sneakily been undefined behavior (see: Iterator invalidation).

The problem with this code is, that you first acquire a view into your container (the hashmap) with the immutable borrow (&self.myset) which gets converted into an iterator (because you are iterating over that reference with a for-loop). This iterator comes with some guarantess / semantics for what items will be produced. In this case, it will probably traverse a list or tree data structure; we don't really care how the hashmap is implemented internally. We do expect that we will visit every item of the hashmap exactly once though.

In order to accomplish this requirement, the iterator itself will have some internal state, lets say an index into the list of items of the hashmap that it will increment with every iteration. If you now start removing items from the map, you might change the internal order of items in the map, thus this information that the iterator kept is outdated. This is where the undefined behavior in C++ would start.

Lucky for us, Rust stops us from writing this code with its type system. As pointed out by other answers, you need a mutable reference to the hash map to remove an item, but you cannot get one if there is an immutable reference already. The iterator immutably borrows the entire map, so that its internal bookkeeping doesn't get outdated until iteration is finished (or aborted early).

There are helper methods on HashMap that may fit your use-case. Take a look at .retain, .drain or just plain .into_iter.

Jmb
  • 18,893
  • 2
  • 28
  • 55
Niklas Mohrin
  • 1,756
  • 7
  • 23
0

There are apis for many situations like this, in this case you could use drain:

fn modify_all(&mut self){
    for (name, age) in self.myset.drain() {
        println!("Removing {} who is {}", name, age);
    }
}

Playground

And, about the borrowing. It is known that rust enforces a single mutable reference, or any number of immutable ones. But you can (shouldn't) never have a mutable and immutable one.

mkrieger1
  • 19,194
  • 5
  • 54
  • 65
Netwave
  • 40,134
  • 6
  • 50
  • 93
  • I think the question is *why* there is a mutable and an immutable borrow at the same time. – mkrieger1 Aug 30 '21 at 09:08
  • 1
    Thanks @mkrieger1 and @Netwave for your helpful comments and advices. Indeed, even if the `drain` method works, I think my problem is indeed this question "why there is a mutable and an immutable borrow at the same time?". – Pierre Peterlongo Aug 30 '21 at 09:12
  • @PierrePeterlongo, yeah, sorry the missunderstanding. Check the answer from the comments, since it is a duplicated question. Thanks :) – Netwave Aug 30 '21 at 09:13