Хранение пар ключ-значение с помощью хеш-таблиц
Последней коллекцией, которую мы рассмотрим, будет хеш-таблица. Тип HashMap<K, V>
хранит ключи типа K
ко значениям типа V
, используя функцию хеширования. Во множестве языков программирования есть данная структура, но часто она по-разному называется: hash_, map, object, словарь или ассоциативный массив.
Хеш-таблицы полезны, когда нужно искать данные, используя не индекс (как в случае с векторами), а ключ, который может быть любого типа. Например, в игре вы можете сохранять счёт каждой команды в хеш-таблице, в которой каждый ключ — это название команды, а значение — её счёт. Имея имя команды, вы можете получить её счёт из хеш-таблицы.
В этом разделе мы рассмотрим лишь основной API хеш-таблиц, но стандартная библиотека содержит ещё очень много полезного для HashMap<K, V>
. Как и прежде, советуем обращаться к документации стандартной библиотеки для получения дополнительной информации.
Создание хеш-таблицы
One way to create an empty hash map is to use new
and to add elements with insert
. In Listing 8-20, we’re keeping track of the scores of two teams whose names are Blue and Yellow. The Blue team starts with 10 points, and the Yellow team starts with 50.
Обратите внимание, что сначала нужно подключить HashMap
из стандартной библиотеки с помощью use
. Из трёх коллекций данная является наименее используемой, поэтому она не входит в функционал, по умолчанию подключаемый к области видимости (коротко говоря, в prelude). Хеш-таблицы также слабее поддерживаются стандартной библиотекой; например, нет встроенного макроса для их конструирования.
Подобно векторам, хеш-таблицы хранят свои данные в куче. Здесь тип HashMap
имеет в качестве типа ключей String
, а в качестве типа значений — i32
. Как и векторы, HashMap
однородны: все ключи должны иметь одинаковый тип, и все значения тоже должны иметь одинаковый тип.
Получение данных из хеш-таблицы
Мы можем получить значение из хеш-таблицы, использовав метод get
и передав ему ключ, как показано в Листинге 8-21.
Здесь в score
будет количество очков синей команды — 10
. Метод get
возвращает Option<&V>
; если для какого-то ключа в хеш-таблице нет значения, get
вернёт None
. Из-за такого подхода программе следует обрабатывать Option
, вызывая copied
для получения Option<i32>
вместо Option<&i32>
, затем unwrap_or
для установки score
в ноль, если scores
не содержит данных по этому ключу.
Мы можем перебирать каждую пару ключ-значение в хеш-таблице таким же образом, как мы делали с векторами, воспользовавшись циклом for
:
Этот код будет выведет каждую пару в произвольном порядке:
Yellow: 50
Blue: 10
Хеш-таблицы и владение
Для типов, которые реализуют трейт Copy
(например, i32
), значения копируются в хеш-таблицу. Для владеемых значений, таких как String
, значения будут перемещены в хеш-таблицу и она станет владельцем этих значений, что показано в Листинге 8-22.
Мы не можем использовать переменные field_name
и field_value
после того, как их значения были перемещены в хеш-таблицу вызовом метода insert
.
Если мы вставим в хеш-таблицу ссылки на значения, то значения не будут перемещены в хеш-таблицу. Значения, на которые указывают ссылки, должны быть действительными как минимум пока действительна хеш-таблица. Мы поговорим подробнее об этих вопросах в разделе "Валидация ссылок по времени жизни" Главы 10.
Обновление данных в хеш-таблице
Хотя количество ключей и значений в хеш-таблице может быть изменено, каждый ключ в один момент может иметь только одно значение (обратное утверждение неверно: команды "Синяя" и "Жёлтая" могут хранить в хеш-таблице scores
одинаковое количество очков: например, 10
).
Если вы хотите изменить данные в хеш-таблице, необходимо решить, как обрабатывать случай, когда ключ уже имеет связанное значение. Можно заменить старое значение новым, полностью проигнорировав старое. Можно сохранить старое значение и проигнорировать новое, если только в хеш-таблице ещё не было этого ключа. Или можно было бы вычислить на основе старого и нового значений третье. Давайте посмотрим, как реализовать каждый из вариантов!
Перезапись значения
Если мы вставим ключ и значение в хеш-таблицу, а затем вставим такой же ключ с новым значением, то старое значение, связанное с этим ключом, будет заменено на новое. Даже несмотря на то, что код в Листинге 8-23 вызывает insert
дважды, хеш-таблица будет содержать только одну пару ключ-значение, потому что мы вставляем значения для одного и того же ключа — ключа синей команды.
Код напечатает {"Синяя": 25}
. Начальное значение 10
было перезаписано.
Вставка значения только в том случае, если ключ вводится впервые
Обычно проверяют, существует ли конкретный ключ в хеш-таблице, приписано ли ему какое-либо значение, а затем предпринимаются следующие действия: если ключ в хеш-таблице есть, то существующее значение должно оставаться таким, какое оно есть. Если же ключ отсутствует, то вставляют его и его значение.
Хеш-таблицы имеют для этого специальный метод entry
, который принимает ключ, наличие которого нужно проверить. Возвращаемое значение метода entry
— это перечисление Entry
, представляющего значение, которое может как присутствовать, так и отсутствовать. Допустим, мы хотим проверить, имеется ли ключ и связанное с ним значение для жёлтой команды. Если хеш-таблица не имеет значения для такого ключа, то мы хотим вставить значение 50
. То же самое мы хотим проделать и для синей команды. Использование entry
показано в коде Листинга 8-24.
Метод or_insert
определён в Entry
так, чтобы возвращать изменяемую ссылку на соответствующее значение ключа внутри варианта перечисления Entry
, когда этот ключ существует, а если его нет, то вставлять параметр в качестве нового значения этого ключа и возвращать изменяемую ссылку на новое значение. Эта техника намного чище, чем самостоятельное написание логики и, кроме того, она более безопасна и согласуется с правилами заимствования.
При выполнении кода Листинга 8-24 будет напечатано {"Жёлтая": 50, "Синяя": 10}
. Первый вызов метода entry
вставит ключ со значением 50
для жёлтой команды, потому что для жёлтой команды ещё не имеется значения в хеш-таблице. Второй вызов entry
не изменит хеш-таблицу, потому что для ключа синей команды уже имеется значение 10
.
Обновление значения на основе старого значения
Другим распространённым вариантом использования хеш-таблиц является поиск значения по ключу, а затем обновление этого значения на основе старого значения. Например, в Листинге 8-25 показан код, который подсчитывает, сколько раз определённое слово встречается в некотором тексте. Мы используем хеш-таблицу со словами в качестве ключей и увеличиваем соответствующий слову счётчик, чтобы отслеживать, сколько раз мы встретили это слово. Если мы впервые встретили слово, то сначала вставляем значение 0
.
Этот код напечатает {"world": 2, "hello": 1, "wonderful": 1}
. Если вы увидите, что пары ключ-значение печатаются в другом порядке, то вспомните, что в разделе "Получение данных из хеш-таблицы" мы писали, что итерация по хеш-таблице происходит в произвольном порядке.
Метод split_whitespace
, выполненный на text
, возвращает итератор по подсрезам строки, разделённым пробелами. Метод or_insert
возвращает изменяемую ссылку (&mut V
) на значение ключа. Мы сохраняем изменяемую ссылку в переменной count
, поэтому чтобы присвоить переменной значение, необходимо разыменовать count
с помощью звёздочки (*
). Изменяемая ссылка удаляется сразу же после выхода из области видимости цикла for
, поэтому все эти изменения безопасны и согласуются с правилами заимствования.
Функции хеширования
По умолчанию HashMap
использует функцию хеширования SipHash, которая может противостоять атакам класса Denial of Service (DoS) с использованием хеш-таблиц1. Это не самый быстрый из возможных алгоритмов хеширования, но в данном случае производительность идёт на компромисс с обеспечением лучшей безопасности. Если после профилирования вашего кода окажется, что хеш-функция, используемая по умолчанию, очень медленная, вы можете заменить её используя другой хешер. Хешер — это тип, реализующий трейт BuildHasher
. Подробнее о трейтах мы поговорим в Главе 10. Вам совсем не обязательно реализовывать свою собственную функцию хеширования; на crates.io есть достаточное количество библиотек, предоставляющих разные реализации хешеров с множеством общих алгоритмов хеширования.
Подведём итоги
Векторы, строки и хеш-таблицы предоставят большое количество функционала для программ, когда необходимо сохранять, извлекать и модифицировать данные. Теперь вы готовы решить следующие учебные задания:
- Дан список целых чисел. Использовав вектор, напишите функции получения медианы (значение элемента из середины списка после его сортировки) и моды (наиболее частое значение; подсказка: используйте хеш-таблицы) списка чисел.
- Переведите текст с английского на поросячью латынь. Первая согласная каждого слова перемещается в конец и к ней добавляется окончание ay (пример: first станет irst-fay). К слову, начинающемуся на гласную, в конец добавляется hay (пример: apple становится apple-hay). Помните о деталях работы с кодировкой UTF-8!
- Используя хеш-таблицу и векторы, создайте текстовый интерфейс, позволяющий пользователю приписывать сотрудников по их именам к отделам компании. Например,
Add Sally to Engineering
илиAdd Amir to Sales
. Затем позвольте пользователю получить список всех людей из отдела или всех людей в компании, отсортированный по отделам в алфавитном порядке.
Документация API стандартной библиотеки содержит описания методов векторов, строк и хеш-таблиц. Рекомендуем пользоваться ей при решении упражнений!
Потихоньку мы переходим к более сложным программам, в которых операции могут потерпеть неудачу. Наступил идеальный момент для обсуждения обработки ошибок.