Хэши (ассоциативные массивы)
Хэш — это коллекция пар «ключ-значение». Если массив отвечает на вопрос «что под номером N», то хэш — «что под именем K».
Суть: хэш (Hash) хранит данные как пары ключ-значение; чаще всего ключами служат символы; доступ к значению — по ключу за почти мгновенное время.
Хэши — рабочая лошадка Ruby. Конфигурации, записи о пользователях, ответы API, именованные аргументы методов — всё это хэши. Современный синтаксис с ключами-символами особенно компактен.
user = { name: "Аня", age: 30, role: :admin }
puts user[:name] # => Аня
puts user[:age] # => 30
user[:email] = "[email protected]" # добавить пару
user[:age] = 31 # изменить значение
puts user.keys.inspect # => [:name, :age, :role, :email]
puts user.values.inspect # => ["Аня", 31, :admin, "anya@..."]
Разбор: перебор и значения по умолчанию
Перебирать хэш удобно через each с двумя параметрами — ключом и значением. А чтобы обращение к отсутствующему ключу возвращало не nil, а заданное значение, хэшу можно задать дефолт.
scores = { matem: 5, fizika: 4 }
scores.each do |subject, grade|
puts "#{subject}: #{grade}"
end
# хэш с дефолтом 0 — удобно для подсчётов
counts = Hash.new(0)
"банан".each_char { |c| counts[c] += 1 }
puts counts.inspect # => {"б"=>1, "а"=>2, "н"=>2}
Как работает под капотом
Хэш называется так, потому что внутри использует хэш-функцию: ключ превращается в число, по которому Ruby почти мгновенно находит ячейку со значением. Поэтому поиск по ключу не зависит от размера хэша. Именно из-за этого ключи должны быть «хэшируемыми» и стабильными — символы и строки подходят идеально, а вот мутировать объект-ключ нельзя.
user[:name]
|
v
[ хэш-функция ] превращает :name в число (хэш-код)
|
v
адрес ячейки в таблице --> [ "Аня" ]
|
мгновенный доступ независимо от размера хэша
Та же идея «словарь ключ-значение» на Python — это dict:
# Та же логика на Python ▶
from collections import defaultdict
counts = defaultdict(int) # дефолт 0
for c in "банан":
counts[c] += 1
print(dict(counts)) # {'б': 1, 'а': 2, 'н': 2}
Частые ошибки
- Путать ключ-символ и ключ-строку.
{ name: "Аня" }["name"]вернётnil: ключ — символ:name, а не строка. - Ждать ошибку при отсутствующем ключе. По умолчанию это
nil. Если нужна ошибка — используйтеfetch(:key). - Полагаться на «случайный» порядок. Хэши в Ruby сохраняют порядок вставки — это гарантия, но не повод хранить упорядоченные данные в хэше вместо массива.
Best practices
- Используйте символы как ключи — они быстрее и легче строк.
- Применяйте
fetchс дефолтом или сообщением, когда отсутствие ключа — это ошибка:config.fetch(:port, 8080). - Для подсчётов и группировки заводите
Hash.new(0)— это избавит от ручных проверок «есть ли ключ».
Глубже: хэши повсюду
Стоит осознать, насколько глубоко хэши вплетены в сам язык, а не только в хранение данных. Когда вы вызываете метод с ключевыми аргументами — create_user(name: "Аня", role: :admin) — под капотом это во многом про хэш-подобную передачу именованных значений. Когда библиотека принимает «опции» — это почти всегда хэш настроек. Когда вы конфигурируете гем или описываете маршрут в Rails — снова хэши. Поэтому уверенное владение хэшами окупается далеко за пределами темы коллекций. Полезно знать и продвинутые методы: merge объединяет два хэша (удобно для настроек по умолчанию плюс переопределений), transform_values применяет преобразование ко всем значениям, each_with_object и group_by строят хэши из массивов, а dig безопасно достаёт значение из глубоко вложенной структуры без россыпи проверок на nil. Связка «массив на входе — хэш на выходе» через group_by, tally или each_with_object — один из самых частых паттернов обработки данных в реальном коде.
Итог. Хэш хранит пары ключ-значение с почти мгновенным доступом по ключу за счёт хэш-функции. Ключи-символы — стандарт, отсутствующий ключ даёт nil (или ошибку через fetch), а порядок вставки сохраняется.