NAME/НАИМЕНОВАНИЕ

perlreftut - Краткое руководство по ссылкам, которое написал Марк

ОПИСАНИЕ

Одним из наиболее важных новых функций в Perl 5 была возможность управления сложными структурами данных такими, как многомерные массивы и вложенные хэши. Чтобы сделать это в Perl 5 появилась возможность называемая 'ссылки' ('references') и использование ссылок является ключом к управлению сложными, структурированными данными в Perl. К сожалению есть много смешного синтаксиса для изучения, и главная страница маны (perlref) может быть сложна для изучения. Это Руководство - довольно полное, и иногда люди находят и здесь проблемы, потому что иногда бывает трудно сказать, что важно, а что нет.

К счастью вам только нужно знать только 10% того, что находится на главной странице (perlref), чтобы получить 90% выгоды. Эта страница покажет вам, что это за 10%.

Кому нужны сложные структуры данных?

Все время вставала проблема представления данных в виде хэша, значениями которого являются списки. В Perl есть хэши, конечно, но значениями хэша должны быть скаляры; они не могут быть списками.

Для чего может понадобиться хэш списков? Возьмем простой пример: у вас есть файл с названиями городов и стран:

Chicago, USA
Frankfurt, Germany
Berlin, Germany
Washington, USA
Helsinki, Finland
New York, USA

и вы хотите получить на выходе следующее: название страны, затем в алфавитном порядке список городов в этой стране:

Finland: Helsinki.
Germany: Berlin, Frankfurt.
USA:  Chicago, New York, Washington.

Естественный путь сделать это - создать хэш, ключи которого - названия стран. Значением каждого ключа будет список городов этой страны. При чтении каждой входящей строки надо разделить ее на страну и город, затем добавить город в список городов данной страны, если такого города в списке еще не существует. После завершения чтения пройтись по хэшу в цикле, сортируя каждый список городов перед печатью.

Если значениями хэша не могут быть списки, то вы проиграли. Возможно, вам придется объединить названия всех городов в одну строку, затем, перед печатью разбить строку в список, который нужно отсортировать и преобразовать снова в строку. Это добавляет беспорядка и ошибок. И это нас расстраивает, потому что в Perl'е уже есть превосходные списки, которые могли бы решить данную проблему, и, если вы можете, то используйте их.

Решение

Ко времени обкатки Perl 5 мы уже завязли в этой концепции: Значениями хэша могут быть только скаляры. Решение пришло в виде ссылок.

Ссылка — это скалярная величина, которая ссылается на весь массив или на весь хэш (или еще на что-нибудь). Имена — один из видов ссылок, которые вам уже известны. Подумайте о президенте США: грязный, неудобный мешок с кровью и костями. Но чтобы говорить о нем, или представить его в компьютерной программе, все что для этого нужно - удобная скалярная строка "Барак Обама".

Ссылки в Perl похожи на имена для списков или хэшей. Это частные, внутренние имена perl программы, поэтому можно быть уверенным, что они однозначны. В отличие от "Барака Обама", ссылка ссылается только на один объект, и вы всегда знаете, на какой. Если у вас есть ссылка в массив, то весь массив можно восстановить из нее. Если у вас есть ссылка на хэш, то вы можете восстановить весь хэш. Но ссылка по-прежнему является простым, компактным скалярным значением.

Не может быть хэша, значения которого - массивы; значениями хэша могут быть только скалярами. Мы опять застряли. Но одна ссылка может указывать на целый массив, и при этом ссылка остается скаляром, так что можно создать хэш ссылок на массивы, и это будет работать, как хэш массивов, и будет таким же полезным, как и хэш массивов.

Мы вернемся к проблеме городов и стран позже, после того, как рассмотрим синтаксис управления ссылками.

Синтаксис

Есть только два способа создать ссылку и только два способа использовать её.

Создание ссылок

Правило создания 1

Поместив перед переменной обратный слеш \, вы получите ссылку на эту переменную.

$aref = \@array;         # $aref теперь содержит ссылку на @array
$href = \%hash;          # $href теперь содержит ссылку на %hash
$sref = \$scalar;        # $sref теперь содержит ссылку на $scalar

После того, как ссылка сохранена в переменной, такой как $aref или $href, ее можно копировать или просто хранить, как любую другую скалярную величину:

$xy = $aref;             # $xy теперь содержит ссылку на to @array
$p[3] = $href;           # $p[3] теперь содержит ссылку на %hash
$z = $p[3];              # $z теперь содержит ссылку на %hash

Эти примеры показывают, как сделать ссылки на именованные переменные. Может быть, вы захотите создать массив или хэш, у которого нет имени, по аналогии с возможностью использовать строку "\n" или число 80, не сохраняя их в именованной переменной.

Правило создания 2

Конструкция [ элемент1, элемент2, ... ] создает новый анонимный массив и возвращает ссылку на этот массив. Конструкция { элемент1 => элемент2, ... } создает новый анонимный хэш и возвращает ссылку на этот хэш.

[ ЭЛЕМЕНТЫ ] создает новый анонимный массив и возвращает ссылку на этот массив.

{ ЭЛЕМЕНТЫ } создает новый анонимный хэш и возвращает ссылку на этот хэш.

$aref = [ 1, "foo", undef, 13 ];
# $aref теперь содержит ссылку на массив

$href = { APR => 4, AUG => 8 };
# $href теперь содержит ссылку на хэш

Ссылки, которые вы получаете по правилу 2, такие же, как и те, которые получаются по правилу 1:

# Это:
$aref = [ 1, 2, 3 ];

# Делает то же самое, что и это:
@array = (1, 2, 3);
$aref = \@array;

Первая строка — сокращение последующих двух строк, кроме того, что она не создает лишней переменной массива @array.

Если вы напишете просто [], вы получите новый пустой анонимный массив. Если вы напишите просто {}, вы получите новый пустой анонимный хэш.

Использование ссылок

Что можно сделать со ссылкой после её создания ? Мы уже видели, что её можно хранить как скалярную величину и получить её значение. Есть только два способа её использования:

Правило использования 1

Можно использовать ссылку на массив в фигурных скобках, вместо имени массива. Например, @{$aref} вместо @array.

Вот некоторые примеры этого:

Массивы:

@a		@{$aref}		Массив
reverse @a	reverse @{$aref}	Массив в обратном порядке
$a[3]		${$aref}[3]		Элемент массива
$a[3] = 17;	${$aref}[3] = 17	Присваивание значения элементу массива

В каждой строке два выражения, которые делают то же самое. В левой версии работают с массивом @a. В правой версии работают с массивом, на который ссылается $aref. После нахождения массива оба варианта выполняют одинаковые операции.

Использование ссылки на хэш точно такое же:

%h		%{$href}	      Хэш
keys %h		keys %{$href}	      Получение ключей хэша
$h{'red'}	${$href}{'red'}	      Элемент хэша
$h{'red'} = 17	${$href}{'red'} = 17  Присвоение значения элементу

Все что можно сделать со ссылкой, делается по Правилу использования 1. Вы просто пишите код на Perl, который делает то, что нужно с обычным массивом или хэшем, а затем заменяете имя массива или хэша {$ссылкой}. "Как перебрать элементы массива, если у меня есть ссылка ?" Чтобы перебрать элементы массива, нужно написать

for my $element (@array) {
   ...
}

затем замените имя массива @array ссылкой:

for my $element (@{$aref}) {
   ...
}

"Как распечатать содержимое хэша, если у меня есть только ссылка?" Сначала напишем код для распечатки хэша:

for my $key (keys %hash) {
  print "$key => $hash{$key}\n";
}

И затем заменяем имя хэша ссылкой:

for my $key (keys %{$href}) {
  print "$key => ${$href}{$key}\n";
}

Правило использования 2

Правило использования 1 — это все, что вам действительно нужно, потому что оно описывает абсолютно все действия со ссылкой. Но чаще всего приходится извлекать единственный элемент массива или хэша, и запись по Правилу использования 1 громоздка. Поэтому есть несколько сокращений.

${$aref}[3] слишком трудно читать, так что вы можете писать $aref->[3] вместо этого.

${$href}{red} слишком трудно читать, так что вы можете написать $href->{red} вместо этого.

Если $aref содержит ссылку на массив, тогда $aref->[3] — четвертый элемент массива. Не перепутайте его с $aref[3], что есть четвертый элемент совершенно другого массива, обманчиво названного @aref. $aref и @aref не связаны также, как и $item и @item .

Аналогичным образом $href->{'red'} — является частью хэша, на который указывает скалярная переменная $href, возможно даже безымянного. $href{'red'} часть обманчиво названного хэша %href. Легко забыть вставить -> и в этом случае вы получите странные результаты, когда программа будет извлекать элементы совершенно неожиданных массивов и хэшей, которые вы совсем не хотели использовать.

Пример

Небольшой пример того, как все это можно использовать.

Во-первых, следует помнить, что [1, 2, 3] создает анонимный массив, содержащий (1, 2, 3), и возвращает ссылку на этот массив.

Теперь подумайте о

	@a = ( [1, 2, 3],
               [4, 5, 6],
	       [7, 8, 9]
             );

@a представляет собой массив из трех элементов, каждый из которых является ссылкой на другой массив.

$a[1] одна из этих ссылок. Она ссылается на массив, содержащий (4, 5, 6), и, так как это ссылка на массив, то по Правилу использования 2 мы можем написать $a[1]->[2], чтобы получить третий элемент этого массива. $a[1]->[2] это 6. Аналогично, $a[0]->[1] это 2. То, что у нас есть похоже на двумерный массив, можно писать $a[СТРОКА]->[КОЛОНКА], чтобы получить или внести элемент в любую строку любого столбца массива.

Но эта нотация все еще громоздка, поэтому есть еще одно сокращение:

Правило стрелки

Между двумя индексами стрелка не обязательна.

Вместо $a[1]->[2], мы можем написать $a[1][2]; это значит тоже самое. Аналогично, вместо $a[0]->[1] = 23, мы можем написать $a[0][1] = 23.

Теперь это в самом деле выглядит, как двумерный массив!

Из этого видно, почему важны стрелки. Без них пришлось бы писать ${$a[1]}[2] вместо $a[1][2]. Для трехмерных массивов можно писать $x[2][3][5] вместо нечитаемого ${${$x[2]}[3]}[5].

Решение

Теперь — решение поставленнного в начале вопроса о переформатировании названий городов и стран.

 1   my %table;

 2   while (<>) {
 3    chomp;
 4     my ($city, $country) = split /, /;
 5     $table{$country} = [] unless exists $table{$country};
 6     push @{$table{$country}}, $city;
 7   }

 8   foreach $country (sort keys %table) {
 9     print "$country: ";
10     my @cities = @{$table{$country}};
11     print join ', ', sort @cities;
12     print ".\n";
13	}

В программе две части: Строки 2—7 считывают данные и создают структуры данных, а строки 8—13 анализируют данные и распечатывают отчет. Мы получаем хэш %table, ключи которого — названия стран, а значения — ссылки на массивы названий городов. Данная структура будет выглядеть следующим образом:

   %table
+-------+---+
|       |   |   +-----------+--------+
|Germany| *---->| Frankfurt | Berlin |
|       |   |   +-----------+--------+
+-------+---+
|       |   |   +----------+
|Finland| *---->| Helsinki |
|       |   |   +----------+
+-------+---+
|       |   |   +---------+------------+----------+
|  USA  | *---->| Chicago | Washington | New York |
|       |   |   +---------+------------+----------+
+-------+---+

Рассмотрим сначала вывод. Предположим, что у нас уже есть эта структура, как ее распечатать?

 8   foreach $country (sort keys %table) {
 9     print "$country: ";
10     my @cities = @{$table{$country}};
11     print join ', ', sort @cities;
12     print ".\n";
13	}

%table — обычный хэш, и мы получаем список его ключей, которые, как обычно, сортируем и перебираем. Единственное использование ссылки в строке 10. $table{$country} ищет в хэше ключ $country и получает значение — ссылку на массив городов этой страны. По Правилу использования 1 мы можем получить массив обратно используя конструкцию @{$table{$country}}. Строка 10 равносильна такой конструкции:

@cities = @array;

кроме того, что имя array заменено ссылкой {$table{$country}}. Знак @ указывает интерпретатору Perl получить весь массив. Получив список городов, как обычно, сортируем его соединяем и распечатываем.

Строки 2-7 отвечают в первую очередь за построение структуры. Приводим их еще раз:

2   while (<>) {
3    chomp;
4     my ($city, $country) = split /, /;
5     $table{$country} = [] unless exists $table{$country};
6     push @{$table{$country}}, $city;
7   }

В строках 2-4 мы получаем название города и страны. Строка 5 проверяет наличие такой страны в ключах хэша. Если ее нет, программа использует запись [] (Правило создания 2), чтобы получить новый пустой анонимный массив городов, и установить ссылку на него в качестве значения для соответствующего ключа хэша.

Строка 6 добавляет название города в соответствующий массив. $table{$country} теперь содержит ссылку на массив городов в этой стране, которые внесены до данного момента. Строка 6 подобна

push @array, $city;

кроме того, что имя array заменено ссылкой {$table{$country}}. push добавляет название города в конец указываемого массива.

Есть один интересный момент, который я упустил. Строка 5 не является необходимой, и мы можем избавиться от неё.

2   while (<>) {
3    chomp;
4     my ($city, $country) = split /, /;
5   ####  $table{$country} = [] unless exists $table{$country};
6     push @{$table{$country}}, $city;
7   }

Если в %table уже есть запись для текущей страны $country, то ничего не изменится. Строка 6 найдет значение $table{$country}, которое указывает на массив, и добавит город $city в массив. Но что произойдет, если $country содержит ключ, которого еще нет в %table, например, Greece?

Это Perl, поэтому он сделает верно. Он видит, что вы хотите положить Athens в массив, который не существует, поэтому он услужливо делает новый, пустой, анонимный массив для вас, устанавливает его в %table, а затем помещает Athens в него. Это называется 'autovivification' — приведение вещей к жизни автоматически. Perl видит, что ключ не был в хэше, поэтому он создал новую запись хэша автоматически. Perl увидел, что вы хотели использовать хэш-значение как массив, поэтому он создал новый пустой массив и установил на него ссылку в хэш автоматически. И как обычно, Perl сделал массив на один элемент больше, чтобы сохранить имя нового города.

Остальное

Я обещал дать 90% возможностей, используя 10% подробностей, и это значит, что 90% подробностей я упустил. Теперь, после обзора самого важного, вам будет легче читать perlref, в котором даны 100% информации.

Некоторые из самых значимых моментов perlref:

  • Можно создавать ссылки на все объекты, включая скаляры, функции, и другие ссылки.

  • По правилу использования 1 можно опустить фигурные скобки, когда в них атомарная скалярная переменная такая как $aref. Например, @$aref — то же самое, что и @{$aref}, а $$aref[1] — то же, что и ${$aref}[1]. Пока вы не освоитесь, вам лучше привыкнуть всегда писать фигурные скобки.

  • Так массив не скопировать:

    $aref2 = $aref1;

    Вы получаете две ссылки на один и тот же массив. Если вы измените $aref1->[23] и затем посмотрите на $aref2->[23], вы увидите, что они не равны.

    Чтобы скопировать массив, используйте

    $aref2 = [@{$aref1}];

    Здесь используется запись [...] для создания нового анонимного массива. Переменной $aref2 присваивается ссылка на новый массив. Новый массив инициализируется содержимым массива, на который указывает $aref1.

    Похожим способом можно скопировать анонимный хэш:

    $href2 = {%{$href1}};
  • Чтобы узнать, содержит ли переменная ссылку, используйте функцию ref. Она возвращает истину, если ее аргумент является ссылкой. На самом деле она делает даже лучше: возвращает HASH для ссылки на хэш и ARRAY для ссылки на массив.

  • Если попробуете использовать ссылку как строку, вы получите строки вроде таких

    ARRAY(0x80f5dec)   или    HASH(0x826afc0)

    Если вы увидите строку, которая выглядят подобным образом, знайте — вы по ошибке распечатали ссылку.

    Побочный эффект такого представления в том, что вы можете использовать eq для проверки того, на один ли и тот же объект указывают ссылки. (Но обычно лучше использовать ==, потому что так работает намного быстрее).

  • Вы можете использовать строку, как если бы она была ссылкой. Если вы используете строку "foo" как ссылку на массив, она считается как ссылка на массив @foo. Это так называемая мягкая ссылка или символическая ссылка. Объявление use strict 'refs' отключает эту возможность, которая может вызвать всевозможные неприятности, если вы используете её случайно.

Возможно, вместо perlref, вам стоит сначала прочесть perllol, в ней детально обсуждаются списки списков и многомерные массивы. После этого ознакомьтесь с perldsc — справочник структур данных, в котором даны рецепты по использованию и распечатки массива хэшей, хэшей массивов, и других типов данных.

Резюме

Всем нужны сложные структуры данных, и Perl позволяет создавать их с помощью ссылок. Есть четыре правила управления ссылками: два правила создания и два правила использования. Когда вы усвоите эти правила, вы сможете делать почти все важное, что можно сделать со ссылками.

Похвала

Автор: Mark Jason Dominus, Plover Systems (mjd-perl-ref+@plover.com)

Впервые статья появилась в The Perl Journal ([1] http://www.tpj.com/ ) том 3, #2. Перепечатывается с разрешения.

Первоначально статья называлась Пойми ссылки сегодня (анг. Understand References Today).

Условия распространения

Copyright 1998 The Perl Journal.

Этот документ распространяется свободно, и вы можете свободно распространять и/или изменять его на тех же условиях, как и сам Perl.

Независимо от распространения, все примеры кода в этом файле являются публичным достоянием. Разрешается и поощряется использование этого кода в ваших собственных программах для развлечения или для получения прибыли. Простой комментарий с указанием авторства приятен, но не обязателен.

ССЫЛКИ

[1] http://www.tpj.com/

[2] http://perl5doc.ru/perlreftut

ПЕРЕВОДЧИКИ

  • Николай Мишин <mi@ya.ru>

  • Динар Жамалиев <admin@perl5doc.ru> [2]

  • Иван Рак <rak@tut.by>