Подсчёт данных с помощью HashMap

RustBeginner
Практиковаться сейчас

Введение

Вектор организует значения по числовым позициям. HashMap<K, V>, напротив, связывает каждый уникальный ключ типа K с одним значением типа V. Поэтому он удобен для хранения счётчиков, настроек и других данных с ключами.

Вы создадите небольшую карту для подсчёта слов, получите и замените счётчик, воспользуетесь API entry для существующих и отсутствующих слов, а затем переберёте все значения, не полагаясь на неопределённый порядок ключей в карте.

Вставка и получение значений по ключу

На этом шаге вы добавите две пары «ключ–значение» и безопасно получите один счётчик.

Перейдите в каталог проекта и откройте исходный файл:

cd /home/labex/project/word-counts
nano src/main.rs

Подготовленный путь std::collections::HashMap указывает на HashMap внутри модуля коллекций стандартной библиотеки Rust. Строка use присваивает этому длинному пути короткое локальное имя HashMap. Пока воспринимайте это как заранее подготовленный импорт; пути и импорты подробно рассматриваются в следующих модулях Lab. HashMap<String, u32> хранит принадлежащие карте строковые ключи и счётчики беззнакового целого типа. Замените комментарий Step 1a следующим кодом:

    let _ = counts.insert(String::from("rust"), 1);
    let _ = counts.insert(String::from("safe"), 1);

Метод insert возвращает значение, которое ранее хранилось по этому ключу. Эти ключи новые, поэтому результатом будет None. Конструкция let _ = ...; выполняет операцию и намеренно отбрасывает её результат. Отдельный символ _ отличается от имени вроде _returned_id: шаблон _ не создаёт привязку, которую можно использовать позже.

Замените комментарий Step 1b следующим кодом:

    match counts.get("rust") {
        Some(count) => println!("Rust count: {count}"),
        None => println!("Rust count: missing"),
    }

Метод get принимает заимствованный текст "rust" и возвращает Option<&u32>, не перемещая ключ или счётчик из карты. Сохраните файл и выйдите из редактора, затем проверьте и запустите программу:

cargo check
cargo run --quiet
Unique words: 2
Rust count: 1

Первая строка подтверждает, что ключи уникальны. Вторая показывает, что поиск нашёл сохранённый счётчик.

Замена существующего счётчика

На этом шаге вы вызовете insert для уже существующего ключа и проверите значение, которое было заменено.

Откройте исходный файл:

nano src/main.rs

Замените комментарий Step 2 следующим кодом:

    let previous = counts.insert(String::from("rust"), 2);
    println!("Previous rust count: {previous:?}");

Карта хранит только одно значение для каждого ключа. Новое значение 2 заменяет 1, а возвращённое Some(1) позволяет увидеть, что именно было заменено. Сохраните файл и выйдите из редактора, затем запустите программу:

cargo run --quiet

Новая строка должна выглядеть так:

Previous rust count: Some(1)

Количество уникальных слов остаётся равным двум, поскольку замена не создаёт дополнительный ключ.

Обновление счётчиков с помощью API entry

На этом шаге вы увеличите счётчик одного существующего слова и одного отсутствующего слова с помощью одного и того же API.

entry(key).or_insert(default) возвращает изменяемую ссылку на существующее значение ключа либо вставляет значение по умолчанию и возвращает изменяемую ссылку на него. Откройте исходный файл:

nano src/main.rs

Замените комментарий Step 3 следующим кодом:

    *counts.entry(String::from("safe")).or_insert(0) += 1;
    *counts.entry(String::from("fast")).or_insert(0) += 1;
    println!("Safe count: {}", counts["safe"]);
    println!("Fast count: {}", counts["fast"]);

Разберите компактное обновление изнутри наружу. Сначала entry(key) выбирает место, связанное с ключом, независимо от того, занято оно или свободно. Затем or_insert(0) гарантирует наличие значения и возвращает изменяемую ссылку &mut u32 на него.

Начальный * обращается к значению u32, находящемуся за этой ссылкой. Наконец, += 1 добавляет единицу и сохраняет новый счётчик обратно в карту.

Поэтому значение safe становится равным 2, а отсутствующее слово fast сначала получает ноль, а затем увеличивается до 1. Квадратные скобки позволяют прочитать значение по ключу. Индексация карты вызвала бы панику, если бы ключ отсутствовал; здесь она безопасна только потому, что два предыдущих вызова entry гарантируют наличие обоих ключей. Если ключ может отсутствовать, продолжайте использовать get.

Сохраните файл и выйдите из редактора, затем запустите программу:

cargo run --quiet

Теперь последние строки должны выглядеть так:

Safe count: 2
Fast count: 1

Одна операция обработала оба случая: занятый и свободный ключ.

Перебор без зависимости от порядка ключей

На этом шаге вы переберёте все сохранённые счётчики и вычислите сумму, не зависящую от порядка элементов.

Хеш-таблицы не гарантируют стабильный порядок перебора ключей, поэтому при проверке для начинающих не следует ожидать, что ключи будут выведены в определённой последовательности. Метод values() посещает каждое сохранённое значение, чего достаточно для вычисления общей суммы.

Откройте исходный файл и замените комментарий Step 4:

nano src/main.rs
    let mut total = 0;
    for count in counts.values() {
        total += count;
    }
    println!("Total occurrences: {total}");

Здесь count — это общая ссылка, а += может добавить целое число, на которое она указывает. Сохраните файл и выйдите из редактора, затем проверьте и запустите программу:

cargo check
cargo run --quiet

Последняя строка должна выглядеть так:

Total occurrences: 5

Сумма объединяет значения rust = 2, safe = 2 и fast = 1, не завися от порядка их перебора.

Итоги

Вы добавляли, получали, заменяли и увеличивали счётчики по ключам, а затем перебрали все значения карты, не предполагая стабильного порядка ключей.