![]() |
![]() |
|
||
![]() |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [5 апреля 2002 г.] |
|
TIMTOWTDI: There Is More Than One Way To Do It (всегда есть несколько способов сделать это) Принцип самоубийцы |
В предыдущей набле я писал, как обычно создаются ссылки в программах на Perl. Конечно, от ссылок мало проку, если мы не умеем их
В настоящей набле будет довольно много примеров кода на Perl, чтобы вы дополнительно смогли уловить закономерности, которых придерживается интерпретатор. Возможно, вам придется приложить некоторые усилия, чтобы до конца понять работу ссылок в Perl (может быть, даже прочитать эту наблу несколько раз). Однако оно того стоит. Следует оговориться: в Perl действует важный принцип TIMTOWTDI, вынесенный в цитату этой наблы. Он особенно характерен для механизма разыменования, что стабильно обеспечивает головную боль людям, еще не успевшими освоиться со ссылками в Perl. Разрозненные сведения всегда поначалу кажутся весьма запутанными и сложными, однако, если их систематизировать, все оказывается не так уж и плохо.
|
Предположим, мы в программе имеем некоторую ссылку (я буду дальше обозначать ее вот так:
Вообще говоря, существует 2 типа разыменования ссылки на массив.
В первом случае применяется следующий синтаксис:
| Листинг 1 |
# получение <b>массива целиком</b> по ссылке
@{ССЫЛКА} |
К сожалению, отказаться от фигурных скобок в большинстве случаев
В конце наблы я расскажу о некоторых альтернативных приемах разыменования, которые иногда помогают сократить программу. Но не сейчас.
|
Более того: выражение
| Листинг 2 |
# цикл по всем элементам массива
foreach my $e (@{ССЫЛКА}) { print $_ }
# присваивание одного массива другому
@A = @{ССЫЛКА};
# присваиваем массиву по ссылке новое
# содержимое; при этом его адрес <i>не меняется</i>
@{ССЫЛКА} = @A;
# это <i>не то же самое</i>, что:
# ССЫЛКА = \@A;
# взятие ссылки от массива
$r = \ @{ССЫЛКА};
# что полностью эквивалентно следующему:
$r = ССЫЛКА; # зачем городить сложности? |
Для получения отдельного элемента массива этот синтаксис совершенно непригоден.
Непригоден концептуально. Практически же можно написать что-то вроде |
Для того, чтобы добраться до отдельного элемента массива, следует применить вот такой причудливый синтаксис:
| Листинг 3 |
# получение отдельного элемента массива (ССЫЛКА)->[номер]; |
В отличие от предыдущих рассуждений, здесь использовать круглые скобки не обязательно (если выражение
Итак, вот примеры кода:
| Листинг 4 |
# вывод второго элемента массива
print ССЫЛКА->[2];
# перебор массива (массив в скалярном
# контексте возвращает число элементов)
for(my $i=0; $i<@{ССЫЛКА}; $i++) {
print ССЫЛКА->[$i];
} |
В общем-то это все, что достаточно знать про разыменование массивов в Perl. Существуют и другие способы, более лаконичные, но более «капризные», об
| Листинг 5 |
# Создаем двумерный массив.
@A = (
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
);
# Обходим его и печатаем по строкам
for(my $y=0; $y<@A; $y++) {
# текущая строка - @{$A[$y]}
for(my $x=0; $x<@{$A[$y]}; $x++) {
print(($A[$y])->[$x]." ");
# можно было бы записать и так:
# print "$A[$y]->[$x] ";
}
print "\n";
}
# Другой способ
foreach (@A) {
foreach (@{$_}) {
print "$_ ";
}
print "\n";
}
# И еще один (короче не бывает!)
print join "\n", map { join " ", @$_ } @A;
# Обнуляем массив
for(my $y=0; $y<@A; $y++) {
for(my $x=0; $x<@{$A[$y]}; $x++) {
$A[$y]->[$x]=0;
}
}
# Или так (пользуемся, что foreach создает
# $_ как <i>синоним</i> для очередного элемента)
foreach (@A) {
foreach (@$_) {
$_=0;
}
}
# Или с помощью map
map { map { $_=0 } @$_ } @A;
# А если бы @A сама была ссылкой...
$r = \@A;
# ...то доступ к элементу (2,1):
$e = (($r)->[2])->[1];
# или чуть короче:
$e = $r->[2]->[1];
# или даже так (см. конец наблы):
$e = $r->[2][1]; |
Может показаться странным, но хэши разыменовываются точно так же, как и массивы, только вместо
| Листинг 6 |
# получения <b>хэша целиком</b>
%{ССЫЛКА};
# получение <b>отдельного элемента</b> хэша
# по его ключу
(ССЫЛКА)->{ключ}; |
Используя эти 2 типа разыменований, можно написать программу, работающую с хэшами, как повар с картошкой. Вот некоторые примеры:
| Листинг 7 |
# Создаем хэш ссылок на массивы
my $missingTeeth = {
me => [],
boxer => [4,5,8],
granny => [1..7,10..32],
comb => [10,11,12,13],
saw => [38],
};
# добавляем новый массив в хэш
($missingTeeth)->{mom}=[18];
# еще один элемент
$missingTeeth->{dad} =
[@{$missingTeeth->{mom}}, 26];
# здесь сначала @{...} разворачивается в список,
# к которому добаляется 26, а затем создается
# анонимный массив.
# Перебор и печать всего хэша
while(my ($k,$v)=each(%$missingTeeth)) {
print "$k: ";
foreach (@$v) { print "$_ " }
print "\n";
}
# доступ к отдельному элементу
print $missingTeeth->{dad}->[2]; |
В данном примере я намеренно не стал создавать обычную хэш-переменную, а сделал сразу ссылку. Теперь вы должны окончательно убедиться, что
Коль скоро
| Листинг 8 |
# получение значения скаляра по ссылке
${ССЫЛКА}; |
Опускать здесь фигурные скобки можно в тех же самых случаях, что и для массивов с хэшами. Вот несколько примеров:
| Листинг 9 |
# создаем ссылку на скаляр
$a = "test";
$r = $a;
# выводим значение скаляра
print ${$r};
# или так:
print $$r;
# а если ссылка на скаляр есть в массиве...
$A[10] = $a;
# ...то для ее разыменования:
print ${$A[10]}; |
Довольно интересно обстоят дела со ссылками на константы (в примере выше была создана ссылка на скалярную переменную):
| Листинг 10 |
# ссылка на константу $r = \ "test"; # мы можем ее вывести... print $$r; # но не способны изменить: $$r = "error"; # - генерируется ошибка! |
Таким способом можно создавать и переменные, доступные только для чтения:
| Листинг 11 |
# создаем readonly-переменную *a = \"test"; # теперь ее можно читать... print $a; # но нельзя - писать: $a = 10; # - ошибка! |
Однако это затрагивает технологию работы с таблицами символов Perl (тип-
Ссылка на анонимную
| Листинг 12 |
# создаем анонимную функцию
$f = sub { print "Hello\n" };
# обращаемся к ней и передаем в
# параметрах числа 10, 20
($f)->(10,20);
# или короче:
$f->(10,20);
# второй способ:
&{$f}(10,20);
# или короче:
&$f(10,20); |
Итак, если символика
| Листинг 13 |
# первый способ:
(ССЫЛКА)->(параметры);
# второй способ:
&{ССЫЛКА}(параметры); |
Расскажу один случай, который произошел со мной некоторое время назад. Возникла необходимость хранить в программе нечто вроде хэша, но только чтобы порядок элементов в нем был не произвольный, а заданный. Было принято решение использовать для этих целей обычный массив, но с условием, что четные элементы в нем обозначают ключи, а
| Листинг 14 |
# Этот массив создается точно так же,
# как и обычный хэш - никакой разницы:
@H = (
a => 10,
b => 20,
c => 30,
);
# При случае его легко можно преобразовать
# в хэш для работы с ключами:
%H = @H;
print $H{b}; # имеется в виду %H |
В общем, практически беспроигрышный вариант, не правда ли?.. Однако в последней строчке меня смущало использование временной переменной
| Листинг 15 |
# Внимание - неверный код!
# я пробовал так:
print %{@H}{b};
# и так:
print (\%{@H})->{b};
# и вот так:
print (\%@H)->{b};
# и даже вот так:
print (\%{\@H})->{b};
# а результат один... |
Разумеется, ни один из этих вариантов не хотел работать, и я уже начал думать, что решения не существует. В те времена я еще не подозревал о систематизации методов разыменования хэшей и массивов, которую я привел выше, а предпочитал делать все «методом тыка». Но, как только я задумался о том, почему же Perl так упорно не хочет принимать мои варианты, я вдруг понял: чудес не бывает, и преобразовать массив в хэш напрямую (без участия списков) невозможно! То есть, я не могу вот так взять и к той памяти, которая используется для массива, разом обратиться как к хэшу. В любом случае необходимо создание нового объекта, что, кстати, и происходило в примере с временной переменной.
Но как же создать новый объект в программе, не задействовав временные переменные? Такой способ только
| Листинг 16 |
# А вот так - работает!
$s = {@H}->{b};
# И даже вот так:
print scalar {@H}->{b}; |
Обратите внимание на то, что в последней строчке обязательно требуется оператор |
Конечно, если массив велик, то преобразование его в список, а затем
Прием с созданием «хэшемассива» оказался настолько удобным, что я до сих пор его использую. Для упрощения работы даже была написана специальная функция
| Листинг 17 |
# Пусть $rH - ссылка на хэш или на
# "хэшемассив", заранее неизвестно.
# Тогда следующий код перебирает
# пары ключ=>значение в хэше или
# "хэшемассиве", причем для последнего -
# в порядке их "настоящей" очередности
while(my ($k,$v)=eachEx($rH)) {
print "$k => $v\n";
} |
Получается, что мы используем дополнительные преимущества «хэшемассива», если работа ведется именно с ним, а для обычного хэша... Что же, хоть как-то его
| Листинг 18 |
# ($k,$v) eachEx(\@%Hash)
# Если этой функции передана ссылка на
# хэш, то ее вызов эквивалентен вызову each().
# Если же передана ссылка на массив, то
# осуществляется перебор всех пар в массиве.
sub eachEx($)
{ # Если ссылка на хэш, работаем максимально быстро
if(ref($_[0]) eq "HASH") { return each(%{$_[0]}) }
# Иначе анализируем ситуацию.
my ($r)=@_;
if(ref($r) eq "ARRAY" || index("$r","=ARRAY")>=0) {
my $i=$eachExBuffer{$r};
if(($$i||=0)>scalar(@$r)-1) {
$$i=0; return ()
}
return ($$r[$$i++],$$r[$$i++]);
}
# Может, это bless-хэш...
if(index("$r","=HASH")) { return each(%{$_[0]}) }
# Все совсем плохо.
require Carp;
Carp::croak("Argument must be an array or hash reference");
} |
Теперь, вооруженные систематизированным аппаратом разыменования, вы без труда сможете работать даже со ссылками на ссылки. Нужно заметить, что необходимость в двойных (тройных и т. д.) косвенных ссылках возникает крайне редко, обычно хватает и одного уровня вложенности. Однако, чтобы поставить точку в вопросе разыменования, приведу примеры кода.
Следует помнить, что |
| Листинг 19 |
# создаем ссылку на ссылку на число
$tS = \\10;
# выводим это число
print ${${$tS}}."\n";
# или короче:
print $$$tS."\n";
# ссылка на ссылку на ссылку на массив
$tA = \\[10,20];
# выводим первый элемент массива
print ${${$tA}}->[1]."\n";
# или короче:
print $$$tA->[1]."\n"; |
Но, в общем-то, это все экзотика.
До этого я описывал приемы, которые работают всегда и везде. Если вы сомневаетесь, как лучше написать, то пишите по
С самым простым из наипростейших упрощений мы уже
| Листинг 20 |
# этот код:
@a = @{$r}; %a = %{$r};
# работает точно так же, как:
@a = @$r; %a = %$r; |
Второе
| Листинг 21 |
# можно писать и так...
$a = $A->[10]->{abc}->[0]->{d};
# но гораздо короче - так:
$a = $A->[10]{abc}[0]{d}; |
Заметьте, что стрелочку в самом начале мы пропустить не
Третье упрощение позволяет избавиться от ведущей стрелочки за счет еще одного доллара:
| Листинг 22 |
# Билл Гейтс в детстве писал так:
$a = $A->[10]{abc}[0]{d};
# но Ларри Уолл - иногда и так:
$a = $$A[10]{abc}[0]{d};
# а если со скобками - то так:
$a = $${A[10]{abc}[0]{d}}; |
Последний способ, хотя и экономит один символ (стрелочка занимает 2 позиции, а |
Наконец, стоит сказать еще о возможности разыменования ссылки, которую вернула функция. Делается это обычным способом:
| Листинг 23 |
# функция возвращает ссылку на массив
sub func { return [1,$_[0],3] }
# обращаемся к нулевому элементу
# (вызываем без параметров)
$a = func->[1];
# обращаемся к первому элементу
$a = func(8)->[1]; |
В первом случае скобки у функции можно пропустить, во
До сих пор мы присваивали значения ссылкам явно, однако Perl настолько услужлив, что временами делает это самостоятельно. Давайте рассмотрим такой пример кода:
| Листинг 24 |
# вначале $r не определена print $r; # выводит, что $r не определена # теперь работаем, как будто в $r # содержится ссылка на массив @$r = (1,2,3); # выводим $r вновь print $r; # и получаем ARRAY(0x1abf008), то есть, # ссылка создалась сама собой! |
Когда ссылка преобразуется в строку, как раз и получается что-то типа |
Таким образом, попытка разыменовать скаляр с
Давайте теперь побезумствуем и напишем такой код:
| Листинг 25 |
# работаем, как будто в $r содержится # ссылка на ссылку на ссылку на массив @$$$r = (1,2,3); # выводим $r по всякому print $$$r; # получаем ARRAY(0x1abf0c8) print $$r; # SCALAR(0x1abf0bc) print $r; # SCALAR(0x1abf008) |
Мы убедились в одной интересной вещи: автосоздание ссылок работает на любом уровне вложенности, хоть даже пятидесятикратном (напишите 50 долларов в этом примере и проверьте сами). Представляете, как пришлось помучаться Ларри Уоллу, чтобы добиться такого эффекта?..
Хотя
| Листинг 26 |
# создаем хэш ссылок на хэши
%H = (
a => { x=>1 },
b => { x=>2 },
);
# всего-то - проверили существование...
print "ya-ya!" if exists $H{c}{x};
# а теперь вывели ключи хэша через запятую
print join ",", keys %H; |
Несмотря на то, что мы ничего в коде не присваивали, в хэше
| Листинг 27 |
# вот так правильно
print "ya-ya!"
if exists $H{c} && exists $H{c}{x}; |
Все это чревато лишь созданием лишних ключей в массиве, что само по себе не так страшно.
Какие же цели преследовал Ларри Уолл, когда разрабатывал механизм автосоздания ссылок?.. Нетрудно догадаться: он бежал от неудобного формализма языков со строгой типизацией (типа Java), чтобы можно было писать вот так:
| Листинг 28 |
# вначале $A не определена
# присваиваем, попутно создавая ссылки
$A[10]{a}[20]{b} = 100;
# а теперь выводим 100
print $A[10]{a}[20]{b}; |
Этот код выполняется без единого предупреждения и уж тем более без ошибок. В общем-то, в точности, как мы и предполагали: если мы присвоили чему-то 100, а затем сразу вывели это «что-то», то и получить должны 100. Теперь представьте, во что бы это все вылилось, если бы нас обязали создавать все промежуточные ссылки...
Итак, подводя черту, я замечу, что практически у каждого объекта в Perl существует пара способов разыменования, которые выглядят вот так:
| Листинг 29 |
# первый способ
ССЫЛКА->/параметры\;
# второй способ
!{ССЫЛКА}; |
Здесь метасимволы
Что будет, если сделать еретический шаг и вместо выражения
| Листинг 30 |
# что же выведется?..
print join ",", @{"INC"}; |
Оказывается, в этом случае происходит весьма любопытная вещь: Perl обращается к глобальной переменной, чье имя указано в строке. Это правило действует для скаляров, хэшей, массивов, функций и тип-
Пусть, например, нам необходимо обратиться к переменной, имя которой содержится в
| Листинг 31 |
# $v - содержит имя переменной
# $p - содержит имя пакета
# Выводим содержимое указанной
# переменной из указанного пакета.
print ${$p."::".$v};
# заметьте, что нельзя написать
# ${"$p::$v"}
# потому что символ :: управляющий.
# Зато можно его экранировать:
# ${"$p\::$v"} |
Конечно, можно было бы написать и
Напоследок и в качестве отступления: все глобальные переменные в действительности хранятся в хэше со странным именем |
![]() |
| ||||||||||||||||||||||||
| Дмитрий Котеров | 5 апреля 2002 г. ©1999-2012 | | Контакт | Вернуться к оглавлению |