Avatar ·

Как обновить значение в изменяемом HashMap в Rust?

Привет! Я только начал изучать Rust и столкнулся с проблемой при работе с `HashMap`. У меня есть изменяемая хеш-таблица, в которой хранятся пары "имя пользователя" -> "количество очков". Я хочу обновить значение для существующего ключа, например, увеличить счёт на 10. Вот мой код: ```rust use std::collections::HashMap; fn main() { let mut scores = HashMap::new(); scores.insert("Alice".to_string(), 50); scores.insert("Bob".to_string(), 30); // Пытаюсь обновить счёт Alice let key = "Alice".to_string(); scores[key] += 10; // Ошибка! println!("{:?}", scores); } ``` Компилятор выдаёт ошибку: `cannot index into a value of type HashMap`. Я понимаю, что `HashMap` не реализует трейт `IndexMut` для изменяемого доступа, но как тогда правильно обновить значение? Я пробовал использовать `get_mut()`, но всё равно путаюсь с заимствованием. Подскажите, пожалуйста, как в Rust правильно модифицировать значение в `HashMap`? Желательно с примерами кода, чтобы я разобрался в разных подходах. Спасибо! <<>>

Основной способ: метод get_mut()

В Rust для обновления значения по ключу в HashMap нужно использовать метод get_mut(), который возвращает Option<&mut V>. Это даёт изменяемую ссылку на значение, если ключ существует.

Вот как исправить ваш пример:

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert("Alice".to_string(), 50);
    scores.insert("Bob".to_string(), 30);

    let key = "Alice".to_string();
    if let Some(value) = scores.get_mut(&key) {
        *value += 10;
    }

    println!("{:?}", scores); // {"Alice": 60, "Bob": 30}
}

Обратите внимание на разыменование *value — это необходимо, чтобы получить доступ к самому значению через изменяемую ссылку.

Почему нельзя использовать scores[key]?

В Rust HashMap не реализует трейт IndexMut, потому что это может привести к панике при отсутствии ключа. Вместо этого Rust предлагает явную обработку случая, когда ключа нет — через get_mut() или entry().

Более идиоматичный способ: метод entry()

Если вы хотите обновить значение или вставить новое, если ключа нет, используйте entry():

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert("Alice".to_string(), 50);
    scores.insert("Bob".to_string(), 30);

    let key = "Alice".to_string();
    // Если ключ существует — обновляем, если нет — вставляем 0 и добавляем 10
    *scores.entry(key.clone()).or_insert(0) += 10;

    // Для нового ключа
    *scores.entry("Charlie".to_string()).or_insert(0) += 10;

    println!("{:?}", scores); // {"Alice": 60, "Bob": 30, "Charlie": 10}
}

Метод entry() возвращает перечисление Entry, которое позволяет выбрать действие:

  • or_insert(value) — вставляет значение, если ключа нет, и возвращает изменяемую ссылку
  • and_modify() — вызывает замыкание для изменения существующего значения
  • or_default() — вставляет значение по умолчанию (если тип реализует Default)

Пример с and_modify()

Если нужно только обновить существующее значение, не вставляя новое:

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert("Alice".to_string(), 50);

    scores.entry("Alice".to_string())
        .and_modify(|v| *v += 10)
        .or_insert(0); // или просто .or_insert(0) если нужно

    println!("{:?}", scores); // {"Alice": 60}
}

Метод and_modify() принимает замыкание, которое получает изменяемую ссылку на значение. Это более декларативный способ, чем ручная проверка get_mut().

Важные моменты

  • Заимствование: get_mut() заимствует HashMap изменяемо, поэтому пока вы используете полученную ссылку, вы не можете использовать другие ссылки на ту же хеш-таблицу.
  • Производительность: entry() выполняет поиск ключа только один раз, в отличие от комбинации contains_key() + insert().
  • Клонирование ключа: В примере с entry() я использовал key.clone(), потому что entry() потребляет ключ. Если ключ вам больше не нужен, можно передать его напрямую.
<<>>

Альтернативный подход: метод insert() с проверкой

Если вы не хотите использовать entry() или get_mut(), можно сначала проверить наличие ключа через contains_key(), а затем обновить через insert():

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert("Alice".to_string(), 50);

    let key = "Alice".to_string();
    if scores.contains_key(&key) {
        // Получаем текущее значение и вставляем новое
        if let Some(¤t) = scores.get(&key) {
            scores.insert(key, current + 10);
        }
    }

    println!("{:?}", scores); // {"Alice": 60}
}

Недостатки этого подхода:

  • Поиск ключа выполняется дважды: один раз в contains_key() и один раз в get(), что менее эффективно.
  • Код получается более многословным и менее идиоматичным.
  • В некоторых случаях может возникнуть конфликт заимствования, если вы попытаетесь использовать get() и insert() одновременно.

Этот метод стоит использовать только если вам нужно выполнить сложную логику перед обновлением, которая не помещается в замыкание and_modify(). В остальных случаях предпочтительнее entry() или get_mut().