Все задачи

Улучшенный push

23 Oct 2013

Программист прочитал в документации на функцию 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

Подсказка-2

Показать

Здесь $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. Если функция определена в начале скрипта, или импортируется из модуля – скобки необязательны.