Reputation: 7401
I am trying to write some toy code that stores the number of times it sees a word in a HashMap
. If the key exists, it increments a counter by one, if the key doesn't exist, it adds it with the value 1
. I instinctively want to do this with a pattern match, but I hit a borrow mutable more than once error:
fn read_file(name: &str) -> io::Result<HashMap<String, i32>> {
let b = BufReader::new(File::open(name)?);
let mut c = HashMap::new();
for line in b.lines() {
let line = line?;
for word in line.split(" ") {
match c.get_mut(word) {
Some(i) => {
*i += 1;
},
None => {
c.insert(word.to_string(), 1);
}
}
}
}
Ok(c)
}
The error I get is:
error[E0499]: cannot borrow `c` as mutable more than once at a time
--> <anon>:21:21
|
16 | match c.get_mut(word) {
| - first mutable borrow occurs here
...
21 | c.insert(word.to_string(), 1);
| ^ second mutable borrow occurs here
22 | }
23 | }
| - first borrow ends here
I understand why the compiler is grumpy: I've told it I'm going to mutate the value keyed on word
, but then the insert isn't on that value. However, the insert is on a None
, so I would have thought the compiler might have realized there was no chance of mutating c[s]
now.
I feel like this method should work, but I am missing a trick. What am I doing wrong?
EDIT: I realize I can do this using
if c.contains_key(word) {
if let Some(i) = c.get_mut(s) {
*i += 1;
}
} else {
c.insert(word.to_string(), 1);
}
but this seems horribly ugly code vs the pattern match (particularly having to do the contains_key()
check as an if, and then essentially doing that check again using Some
.
Upvotes: 22
Views: 9050
Reputation: 88556
This is basically not an issue anymore. With non-lexical lifetimes (NLL), your code compiles without problems. Your example on the Playground.
NLL is a new way the compiler reasons about borrows. NLL has been enabled in Rust 2018 (≥ 1.31). It is planned to be enabled in Rust 2015 eventually as well. You can read more about NLL and editions in this official blog post.
In this particular case, I still think A.B.'s answer (entry(word).or_insert(0)
) is the best solution, simply because it is very concise.
Upvotes: 4
Reputation: 16630
HashMap::entry()
is the method to use here. In most cases you want to use with Entry::or_insert()
to insert a value:
for word in line.split(" ") {
*c.entry(word).or_insert(0) += 1;
}
In case the value to be inserted need to be expensively calculated, you can use Entry::or_insert_with()
to make sure the computation is only executed when it needs to. Both or_insert
methods will probably cover all of your needs. But if you, for whatever reason, want to do something else, you can still simply match
on the Entry
enum.
Upvotes: 16
Reputation: 4709
You have to use the Entry "pattern":
use std::collections::HashMap;
use std::collections::hash_map::Entry::{Occupied, Vacant};
fn main() {
let mut words = vec!["word1".to_string(), "word2".to_string(), "word1".to_string(), "word3".to_string()];
let mut wordCount = HashMap::<String, u32>::new();
for w in words {
let val = match wordCount.entry(w) {
Vacant(entry) => entry.insert(0),
Occupied(entry) => entry.into_mut(),
};
// do stuff with the value
*val += 1;
}
for k in wordCount.iter() {
println!("{:?}", k);
}
}
The Entry object allows you to insert a value if its missing, or to modify it if it already exists.
https://doc.rust-lang.org/stable/std/collections/hash_map/enum.Entry.html
Upvotes: 19