Хеши: пары ключ-значение

Разбираемся, как Perl хранит данные не по номеру, а по «названию» — через хеши, ближайший родственник словарей Python и объектов JavaScript.

Хеш — переменная Perl, которая хранит данные парами «ключ — значение», где к каждому значению обращаются не по числовому индексу, а по имени-ключу. Sigil хеша — знак %.

Когда номера элементов неудобны

Массивы из прошлого урока отлично подходят, когда порядок важен, а данные однородны — список имён учеников, список чисел. Но представьте другую задачу: нужно хранить возраст конкретного человека. Можно было бы завести массив и договориться, что элемент с индексом 0 — это возраст Ани, элемент 1 — возраст Бориса, но это неудобно и легко забыть, что где лежит. Куда естественнее сказать: «дай мне возраст по имени человека». Именно для этого в Perl есть хеш — структура данных, где к каждому значению можно обратиться по осмысленному ключу, а не по номеру по порядку. Если вы знакомы с Python, хеш Perl — это практически то же самое, что словарь (dict); в JavaScript ближайший аналог — обычный объект {}.

Объявление хеша

Sigil хеша — знак процента %. Пары ключ-значение перечисляются через запятую, чередуя ключ и значение.

my %vozrast = (
    "Anya"   => 16,
    "Boris"  => 17,
    "Vera"   => 15,
);

print "Vozrast Any: $vozrast{Anya}\n";

Вывод:

Vozrast Any: 16

Разбираем построчно

Строка my %vozrast = (...) объявляет хеш с sigil %, потому что мы описываем всю коллекцию пар целиком. Внутри скобок пары записаны через специальную «жирную стрелку» => — это просто более наглядная замена обычной запятой, которая визуально показывает, что слева ключ, а справа — его значение ("Anya" => 16 означает ровно то же самое, что и "Anya", 16, но читается понятнее).

А вот обращение к одному конкретному значению снова меняет sigil на $ — точно так же, как с элементом массива в прошлом уроке. Запись $vozrast{Anya} означает «дай мне из хеша vozrast значение по ключу Anya». Обратите внимание на фигурные скобки {} вместо квадратных [] — это единственное различие в синтаксисе между обращением к элементу массива и обращением к элементу хеша, и его легко перепутать по невнимательности.

Обход хеша циклом

Чтобы пройтись по всем парам хеша, обычно используют функцию keys, которая возвращает список всех ключей, и затем в цикле достают значение по каждому ключу.

my %ocenki = (
    "Matematika"    => 5,
    "Fizika"        => 4,
    "Informatika"   => 5,
);

foreach my $predmet (keys %ocenki) {
    print "$predmet: $ocenki{$predmet}\n";
}

Вывод:

Matematika: 5
Fizika: 4
Informatika: 5

Функция keys %ocenki возвращает массив-список всех ключей хеша ("Matematika", "Fizika", "Informatika"), и цикл foreach проходит по ним по очереди, точно как по обычному массиву из прошлого урока. Внутри цикла переменная $predmet на каждом шаге содержит один ключ (скаляр — поэтому $), а $ocenki{$predmet} достаёт значение по этому ключу — заметьте, что внутри фигурных скобок стоит уже переменная $predmet, а не текст в кавычках, потому что мы подставляем ключ, а не пишем его буквально.

Важная деталь: порядок, в котором keys возвращает ключи, не гарантированно совпадает с порядком, в котором вы их вписывали при создании хеша — в отличие от массива, хеш не хранит порядок элементов. Если порядок важен, дополнительно сортируют ключи функцией sort.

Проверка существования ключа

Иногда нужно узнать, есть ли вообще такой ключ в хеше, не пытаясь сразу получить значение — для этого есть функция exists.

my %telefony = (
    "Anya" => "111-22-33",
);

if (exists $telefony{Anya}) {
    print "Nomer Any est: $telefony{Anya}\n";
}

if (!exists $telefony{Boris}) {
    print "Nomera Borisa net v spravochnike\n";
}

Вывод:

Nomer Any est: 111-22-33
Nomera Borisa net v spravochnike

Функция exists возвращает истину или ложь, не пытаясь ничего доставать из хеша по несуществующему ключу. Это отличается от простой проверки самого значения (if ($telefony{Boris})), потому что значением по ключу теоретически может быть и 0, и пустая строка — тогда простая проверка ошибочно скажет «значения нет», хотя ключ на самом деле существует, просто хранит «пустое» значение. exists проверяет именно наличие самого ключа, а не то, что по нему лежит.

Мини-пример: словарь оценок класса

Соберём идеи урока в небольшой практической задаче — хеш с оценками и подсчёт, у кого из учеников оценка ниже четырёх.

my %klass = (
    "Anya"  => 5,
    "Boris" => 3,
    "Vera"  => 4,
);

foreach my $imya (keys %klass) {
    if ($klass{$imya} < 4) {
        print "$imya: nuzhna pomosch (otsenka $klass{$imya})\n";
    }
}

Вывод:

Boris: nuzhna pomosch (otsenka 3)

Здесь мы обходим хеш %klass, и для каждого ученика проверяем его оценку через $klass{$imya}, сравнивая с числом 4 обычным оператором <. Только у Бориса оценка меньше четырёх, поэтому в выводе — только одна строка.

Частые ошибки новичков

Первая ошибка — использовать квадратные скобки [] вместо фигурных {} при обращении к элементу хеша, по привычке от массивов. Запись $hash[key] вместо $hash{key} — рабочий Perl-синтаксис, но означает совсем другое (обращение к массиву с именем hash по числовому индексу), и приведёт к путанице, а не к ошибке компиляции, что особенно коварно.

Вторая ошибка — забыть sigil $ при обращении к одному значению хеша, написав %hash{key} вместо $hash{key} — как и с массивами, sigil при обращении к одному элементу всегда $, а не sigil самой коллекции.

Третья ошибка — рассчитывать на порядок ключей при выводе через keys, не задумываясь о том, что хеш этот порядок не гарантирует. Если нужен предсказуемый порядок (например, по алфавиту), ключи нужно явно сортировать.

Итоги

  • Хеш хранит пары «ключ — значение», sigil хеша — %, аналог словаря Python или объекта JavaScript.
  • Обращение к одному значению идёт через $hash{ключ} — фигурные скобки, а не квадратные, и sigil $, потому что это одно значение.
  • Функция keys %hash возвращает список всех ключей — по нему удобно проходить циклом foreach.
  • Функция exists $hash{ключ} проверяет, есть ли такой ключ вообще, независимо от того, что по нему хранится.
  • Хеш не хранит порядок элементов — если порядок важен, ключи нужно сортировать отдельно.
Проверьте себя
1. Как правильно обратиться к значению по ключу "Anya" в хеше %vozrast?
A@vozrast[Anya]
B$vozrast{Anya}
C%vozrast(Anya)
D$vozrast[Anya]
2. Чем exists $hash{key} отличается от простой проверки if ($hash{key})?
AНичем, это два одинаковых способа записи одной и той же проверки
Bexists проверяет само наличие ключа, а простая проверка значения ошибочно посчитает отсутствующим и существующий ключ, если его значение 0 или пустая строка
Cexists работает только с массивами, а не с хешами
Dexists удаляет ключ из хеша после проверки