Программист прочитал в документации на функцию splice
,
что с помощью нее можно реализовать push
, pop
,
shift
, unshift
и присваивание элементу массива.
Программист решил попробовать и реализовать фунцию rpush
,
которая работала бы как push
, но в качестве первого аргумента
принимала бы ссылку на массив.
# работает как push, но первым параметром принимает не массив, а ссылку на массив
sub rpush
{
my $arr = shift;
splice(@$arr, @$arr, 0, @_);
return scalar @$arr;
}
После этого программист заменил в своем проекте
все push @$arrref, ...
на rpush $arrref, ...
.
И что же из этого получилось?
Можно скачать примеры использования и посмотреть, что получилось: rpush.pl
Здесь $var
не меняется:
my $var;
rpush $var, 42;
А здесь – меняется:
my $var;
push @$var, 42;
Разница в поведении rpush $arrref, ...
и
push @$arrref, ...
появляется тогда, когда
переменная $arrref
содержит значение undef
перед вызовом.
Итак, что же происходит в этом случае?
В случае push @$arrref, ...
происходит
самооживление ссылки
(autovivification, см. также
perldoc perlref).
Самооживление – очень удобное свойства Perl:
если переменная со значением undef
используется в качестве Л-значения так, будто
она является ссылкой, то она и становится ссылкой.
На массив, на хеш, на скаляр –
в зависимости от контекста присваивания.
В случае push @$arrref, ...
переменная
$arrref
становится ссылкой на пустой массив,
и в него добавляются значения, перечисленные в push
.
В случае функции rpush
при вызове
splice(@$arr, @$arr, 0, @_);
тоже происходит самооживление,
однако самооживляется переменная $arr
,
существующая только во время выполнения функции.
Переменная $arrref
, переданная функции снаружи,
никак не меняется.
Поэтому после rpush $arrref, ...
в $arrref
останется undef
, а это явно не то,
чего хотел программист.
Можно сделать так, чтобы rpush
все-таки
оживляла неинициализированные переменные:
sub rpush
{
splice(@{$_[0]}, @{$_[0]}, 0, @_[1 .. @_-1]);
return scalar @{$_[0]};
}
Здесь не происходит копирование переданного параметра
во временную переменную, и
splice
выполняется над $_[0]
– алиасом
для первого фактического аргумента.
Или же можно сначала оживить неинициализированную ссылку, а затем скопировать ее во временную переменную и работать как раньше:
sub rpush
{
$_[0] = [] unless defined $_[0];
my $arr = shift;
splice(@$arr, @$arr, 0, @_);
return scalar @$arr;
}
Этот вариант кажется нам более читаемым.
Кроме того, с версии perl 5.14 встроенный push тоже умеет принимать ссылки на массивы.
Однако во-первых, эта фича находится в статусе
экспериментальной по крайней мере до 5.18 включительно,
так что полагаться на это поведение push
следует с осторожностью.
А во-вторых, push
не оживляет неопределенные неразыменованные ссылки,
по крайней мере в версии 5.14:
> perl -le 'push @$arr, 42; print $arr->[0];'
42
> perl -le 'push $arr, 42; print $arr->[0];'
Not an ARRAY reference at -e line 1.
Так что если требуется просто добавить
несколько элементов в массив – стоит использовать
простой push @$arrref, ...
Если же требуется какая-то другая
логика обработки массивов,
и функция, ее реализующая, будет принимать
ссылки на массивы – стоит позаботиться
о том, чтобы неопределенные ссылки корректно оживлялись бы.
С печалью отмечаем, что никто из читателей не заметил
неправильного оживления неинициализированных ссылок
в rpush
:(
Зато Тигран заметил еще одну особенность:
если в скрипте сначала написать вызов rpush
,
и только затем определить саму функцию,
то вокруг аргументов надо поставить скобки:
rpush($arrref, ...)
,
иначе perl воспримет конструкцию как вызов метода объекта
и вылетит с ошибкой Can't call method "f" without a package or object reference
.
Если функция определена в начале скрипта, или импортируется из модуля – скобки необязательны.