Хэши (ассоциативные массивы)

Хэш — это коллекция пар «ключ-значение». Если массив отвечает на вопрос «что под номером 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), а порядок вставки сохраняется.

Проверьте себя
1. Почему { name: «Аня» }[«name»] вернёт nil?
AСинтаксическая ошибка в хэше
BКлюч — это символ :name, а ищем по строке «name» — это разные ключи
CХэши не поддерживают строковые ключи
Dname — зарезервированное слово
2. Чем fetch(:key) отличается от обращения hash[:key]?
AНичем, это синонимы
Bfetch бросает ошибку при отсутствии ключа (или возвращает заданный дефолт), а [] возвращает nil
Cfetch работает только с символами
Dfetch изменяет хэш