Скалярный и списковый контекст

Разбираемся с главной особенностью Perl, которая ставит в тупик почти всех новичков: одно и то же выражение может означать разные вещи в зависимости от того, «сколько значений» от него ожидают.

Контекст — то, каким образом Perl интерпретирует результат выражения: как одно значение (скалярный контекст) или как список значений (списковый контекст). Одно и то же выражение в разных контекстах может дать совершенно разные результаты.

Почему это вообще нужно понимать

Мы уже несколько раз в этом разделе сталкивались с намёками на контекст, не называя его прямо. Когда мы писали scalar(@massiv), чтобы получить количество элементов — это был как раз пример скалярного контекста. Пришло время разобрать эту идею полностью, потому что без неё поведение Perl-кода иногда кажется случайным и непредсказуемым, а на деле подчиняется чёткому, просто непривычному правилу.

Идея в двух словах такая: почти любое выражение в Perl — переменная, вызов функции, срез — «знает», ожидает ли от него код одно значение или список значений, и подстраивает свой ответ под этот запрос. Это фундаментально отличается от Python или JavaScript, где массив всегда остаётся массивом независимо от того, как его использовать. В Perl массив ведёт себя по-разному — и это не баг, а осознанное проектное решение Ларри Уолла, чтобы код можно было писать компактнее.

Массив в скалярном контексте — это число

Мы уже видели этот пример в уроке про массивы, но теперь разберём его как частный случай общего правила.

my @frukty = ("Yabloko", "Grusha", "Banan");

my $kolichestvo = @frukty;
print "Kolichestvo: $kolichestvo\n";

Вывод:

Kolichestvo: 3

Строка my $kolichestvo = @frukty; — самое интересное место в этом примере. Слева стоит скалярная переменная $kolichestvo, а справа — массив @frukty. Знак равенства заставляет Perl «спросить» у массива: «а что ты из себя представляешь в скалярном контексте?» — и массив отвечает не первым элементом, не последним, а количеством своих элементов. Это то самое поведение, которое явно вызывает функция scalar() — на самом деле scalar(@frukty) и my $x = @frukty дают один и тот же результат, просто scalar() делает намерение явным и читаемым для человека.

Тот же массив в списковом контексте — это все элементы

my @frukty = ("Yabloko", "Grusha", "Banan");

my @kopiya = @frukty;
print "Kopiya: @kopiya\n";

my ($pervyy, $vtoroy, $tretiy) = @frukty;
print "Po otdelnosti: $pervyy, $vtoroy, $tretiy\n";

Вывод:

Kopiya: Yabloko Grusha Banan
Po otdelnosti: Yabloko, Grusha, Banan

Здесь та же самая переменная @frukty ведёт себя совершенно иначе. В строке my @kopiya = @frukty; слева стоит массив, значит, Perl понимает, что от @frukty ожидают список — и просто копирует все элементы. А в строке my ($pervyy, $vtoroy, $tretiy) = @frukty; происходит ещё более интересная вещь: круглые скобки слева означают «список из трёх скалярных переменных», и Perl раскладывает элементы массива по этим трём переменным по порядку — первый элемент в $pervyy, второй в $vtoroy, третий в $tretiy. Это называется списковое присваивание, и это очень частый и удобный приём в реальном Perl-коде.

Сравнение двух контекстов на одной переменной

Чтобы разница стала совсем наглядной, положим оба варианта рядом.

my @chisla = (10, 20, 30, 40);

my $v_skalyarnom = @chisla;
my @v_spiskovom = @chisla;

print "Skalyarnyy kontekst: $v_skalyarnom\n";
print "Spiskovyy kontekst: @v_spiskovom\n";

Вывод:

Skalyarnyy kontekst: 4
Spiskovyy kontekst: 10 20 30 40

Один и тот же массив @chisla дал два совершенно разных результата — число 4 (сколько элементов) и полный список чисел — исключительно потому, что sigil переменной слева от знака равенства ($ или @) задал разный контекст. Это ключевая идея всего урока: контекст задаётся не самим выражением, а тем, как и куда его «кладут».

Списки в булевых условиях — тоже скалярный контекст

Проверка условия в if — ещё одно место, где Perl всегда требует скалярный контекст, даже если внутри стоит массив.

my @spisok = ();

if (@spisok) {
    print "V spiske est elementy\n";
} else {
    print "Spisok pust\n";
}

push @spisok, "element";

if (@spisok) {
    print "Teper v spiske est elementy\n";
}

Вывод:

Spisok pust
Teper v spiske est elementy

Здесь @spisok внутри if (...) оказывается в скалярном контексте — Perl превращает массив в число (количество элементов), и дальше это число проверяется как истина или ложь по обычному правилу Perl: 0 — ложь, любое другое число — истина. Пустой массив даёт 0 элементов, то есть ложь, а после добавления элемента функцией push массив содержит 1 элемент — истина. Программисту не нужно писать громоздкое if (scalar(@spisok) > 0) — достаточно просто if (@spisok), потому что контекст сам обо всём позаботится.

Мини-пример: подсчёт и вывод одновременно

Соберём идею контекста в практическом примере — сообщении о количестве покупок и их перечислении.

my @pokupki = ("Hleb", "Moloko", "Yaytsa");

my $skolko = @pokupki;

if ($skolko > 2) {
    print "Bolshoy spisok iz $skolko pokupok: @pokupki\n";
} else {
    print "Malenkiy spisok\n";
}

Вывод:

Bolshoy spisok iz 3 pokupok: Hleb Moloko Yaytsa

Переменная $skolko получает массив @pokupki в скалярном контексте (число 3), а строка внутри print использует тот же массив @pokupki уже в контексте интерполяции строки, где он превращается в список элементов через пробел. Один и тот же массив, использованный дважды в двух разных ролях в пределах одного примера — и оба раза корректно.

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

Самая частая ошибка — присвоить массив скалярной переменной, ожидая получить первый или последний элемент, а не количество. Новичок пишет my $x = @arr;, надеясь получить, например, первый элемент массива, а получает число элементов — и не понимает, откуда взялось «странное» значение, пока не узнает про контекст.

Вторая ошибка — противоположная: пытаться получить количество элементов, но случайно создать список из одной переменной. Например, запись my ($x) = @arr; (с круглыми скобками вокруг $x) — это списковое присваивание, и $x получит первый элемент массива, а не его длину, хотя выглядит почти как my $x = @arr; без скобок. Лишняя пара круглых скобок здесь всё меняет.

Третья ошибка — не замечать, что функция тоже может вести себя по-разному в разных контекстах (это уже более продвинутая тема, но полезно знать заранее): многие встроенные функции Perl проверяют контекст вызова через специальную конструкцию wantarray и возвращают разные данные в зависимости от того, ждут от них список или одно значение.

Итоги

  • Контекст — это то, «сколько значений» ожидает код от выражения: скалярный контекст ждёт одно значение, списковый — список.
  • Массив в скалярном контексте превращается в число — количество своих элементов; в списковом контексте остаётся полным списком элементов.
  • Контекст определяется не самим выражением, а тем, куда его помещают: sigil переменной слева от =, наличие круглых скобок, проверка в if — всё это задаёт контекст.
  • Условие if (@arr) работает благодаря тому, что массив в скалярном (булевом) контексте превращается в число элементов, а 0 элементов означает ложь.
  • Эта особенность — не случайность и не усложнение ради усложнения, а фундаментальная черта Perl, позволяющая писать компактный код без лишних явных преобразований.
Проверьте себя
1. Что окажется в переменной $x после my $x = @massiv;, если @massiv содержит 7 элементов?
AПервый элемент массива
BПоследний элемент массива
CЧисло 7 — количество элементов массива
DОшибку компиляции, так нельзя писать
2. Почему условие if (@spisok) { ... } корректно проверяет, пуст ли массив @spisok?
AПотому что if всегда проверяет, объявлена ли переменная
BПотому что внутри if массив оказывается в скалярном контексте и превращается в число элементов, а 0 элементов трактуется как ложь
CЭто не работает, нужно обязательно писать if (scalar(@spisok) > 0)
DПотому что @spisok автоматически превращается в true, если массив создан через my