Хотите добавить к HashMap, используя совпадение с шаблоном, получить заимствование с возможностью изменения более одного раза за раз

Я пытаюсь написать код игрушки, в котором хранится количество раз, когда оно видит слово в HashMap. Если ключ существует, он увеличивает счетчик на единицу, если ключ не существует, он добавляет его со значением 1. Я инстинктивно хочу сделать это с совпадением шаблонов, но я попал в заимствование с изменением более чем один раз:

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

Ошибка, которую я получаю:

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

Я понимаю, почему компилятор сварливый: я сказал, что собираюсь изменить значение, введенное в значение word, но тогда вставка не находится в этом значении. Однако вставка находится на None, поэтому я подумал бы, что компилятор мог убедиться, что теперь нет возможности мутировать c[s].

Я чувствую, что этот метод должен работать, но мне не хватает трюка. Что я делаю неправильно?

EDIT: Я понимаю, что могу сделать это, используя

        if c.contains_key(word) {
            if let Some(i) = c.get_mut(s) {
                *i += 1;
            }
        } else {
            c.insert(word.to_string(), 1);
        }

но это кажется ужасно уродливым кодом по сравнению с совпадением шаблонов (в частности, нужно выполнить проверку contains_key() как if, а затем, по существу, выполнить эту проверку снова, используя Some.

Ответ 1

Вы должны использовать шаблон "Enter":

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

Ответ 2

HashMap::entry() - это метод для использования здесь. В большинстве случаев вы хотите использовать с Entry::or_insert() для вставки значения:

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

В случае, если значение для вставки требует дорогостоящих вычислений, вы можете использовать Entry::or_insert_with() чтобы убедиться, что вычисление выполняется только тогда, когда это необходимо. Оба метода or_insert, вероятно, покроют все ваши потребности. Но если вы по какой-либо причине хотите заняться чем-то другим, вы все равно можете просто match перечисление Entry.

Ответ 3

Это больше не проблема. С нелексическими временами жизни (NLL) ваш код компилируется без проблем. Ваш пример на детской площадке.

NLL - это новый способ компиляции рассуждать о заимствованиях. NLL был включен в Rust 2018 (≥ 1,31). Его планируется включить в Rust 2015 в конце концов. Вы можете прочитать больше о NLL и изданиях в этом официальном сообщении в блоге.

В этом конкретном случае я все еще думаю, что ответ AB (entry(word).or_insert(0)) является лучшим решением, просто потому, что он очень лаконичен.