Хотите добавить к 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)
) является лучшим решением, просто потому, что он очень лаконичен.