19

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.

4

3 回答 3

15

您必须使用条目“模式”:

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

Entry 对象允许您在缺少值时插入一个值,或者如果它已经存在则对其进行修改。

https://doc.rust-lang.org/stable/std/collections/hash_map/enum.Entry.html

于 2015-06-15T18:06:18.820 回答
13

HashMap::entry()是这里使用的方法。在大多数情况下,您希望使用 withEntry::or_insert()来插入一个值:

for word in line.split(" ") {
    *c.entry(word).or_insert(0) += 1;
}

如果要插入的值需要进行昂贵的计算,您可以使用Entry::or_insert_with()它来确保仅在需要时执行计算。这两种or_insert方法都可能满足您的所有需求。但是,如果您出于某种原因想做其他事情,您仍然可以简单地match使用Entry枚举。

于 2015-06-15T18:12:26.247 回答
3

这基本上不再是问题了。使用非词法生命周期(NLL),您的代码可以毫无问题地编译。 你在操场上的例子

NLL 是编译器解释借用的一种新方式。NLL 已在 Rust 2018 (≥ 1.31) 中启用。计划最终也将在 Rust 2015 中启用。您可以在此官方博客文章中阅读有关 NLL 和版本的更多信息。

在这种特殊情况下,我仍然认为AB 的答案( entry(word).or_insert(0)) 是最好的解决方案,仅仅是因为它非常简洁。

于 2019-01-29T22:19:31.693 回答