Лекция 11. Ссылки

В этой лекции будут изучены ссылки и ссылочные структуры данных, которые играют очень важную роль в Perl, так как позволяют создавать многомерные массивы, массивы записей и различные динамические структуры данных произвольной сложности: очереди, списки, деревья, графы. Кроме того, умение работать со ссылками необходимо для понимания объектно-ориентированного программирования в Perl.

Цель лекции: научиться обращаться со ссылками, объектами ссылок и структурами данных, основанными на ссылках, чтобы применять их при программировании задач со сложными структурами данных.

Ссылки явно или неявно применяются во всех языках программирования. Они позволяют гибко создавать динамические структуры данных неограниченной сложности. Ссылки являются одним из скалярных типов данных языка Perl, наряду с числами и строками. Ссылка (reference) - это информация о том, где при выполнении программы располагается в памяти объект определенного типа. Эту информацию можно использовать для доступа к объекту ссылки (referent). Ссылка - это возможность обратиться к какой-то информации не по имени, которое известно при компиляции, а по ее расположению в памяти при выполнении программы. В отличие от указателей в некоторых других языках, в Perl ссылки реализованы надежно и эффективно. Программист не должен заботиться о явном удалении из памяти объектов ссылок, поскольку занимаемая память автоматически освобождается встроенным сборщиком мусора, когда объекты ссылок перестают использоваться (то есть когда на них больше не указывает ни одна ссылка). Для создания ссылки на существующий программный объект предусмотрена операция взятия ссылки, обозначаемая обратной косой чертой (backslash), которая ставится перед объектом ссылки. В следующем примере показано, как можно получить ссылку на скалярную переменную и сохранить ее в другой переменной:

my $scalar = 'Скаляр'; # объект ссылки

my $ref2scalar = $scalar; # ссылка на скаляр

На рис. 11.1 показан результат сохранения ссылки на значение скалярной переменной в другой скалярной переменной.

Рис. 11.1.Ссылка на скалярное значение

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

my $ref2literal = 'Литерал'; # ссылка на литерал

my $ref2expression = ($n1*$n2); # ссылка на выражение

Анонимные данные - это значения, не связанные ни с одной переменной, доступ к которым происходит только по ссылке. Ссылка всегда указывает на значение конкретного типа: скаляр, массив, хэш, подпрограмму (или элемент таблицы символов, о которых речи пока не было). На какой именно тип объекта указывает ссылка, можно узнать с помощью функции ref(), которая возвращает строку, описывающую тип значения объекта ссылки. Например:

print ref($ref2scalar); # выведет: 'SCALAR'

А что получится, если вывести значение самой ссылки? Ее значение будет преобразовано в строку, содержащую тип объекта ссылки и его адрес в виде шестнадцатиричного числа, например:

print $ref2scalar; # выведет, например: 'SCALAR(0x335b04)'

Обратно преобразовать в ссылку это строковое представление адреса не удастся.

Чтобы получить доступ к значению, на которое указывает ссылка, нужно выполнить разыменование ссылки (dereference). Для этого переменная, содержащая ссылку, заключается в фигурные скобки и перед ней ставится нужный разыменовывающий префикс: $ для скаляра, @ для массива, % для хэша, & для подпрограммы. (Другими словами, после разыменовывающего префикса, определяющего тип хранящегося в переменной значения, вместо имени переменной записывается содержащая ссылку переменная или выражение, возвращающее ссылку.) Если не возникает неоднозначности в интерпретации выражения, то переменную, хранящую ссылку, в фигурные скобки можно не заключать. Вот примеры разыменования ссылок на скалярные значения:

print "${$ref2scalar} "; # или: $$ref2scalar

print "${$ref2literal} "; # или: $$ref2literal

print "${$ref2expression} "; # или: $$ref2expression

Значение скалярной переменной при доступе по ссылке, естественно, может изменяться, но попытка с помощью ссылки изменить литерал вызовет ошибку при выполнении программы ("Modification of a read-only value attempted"):

${$ref2scalar} = 'Новый скаляр'; # вполне законно

${$ref2literal} = 'Новый литерал'; # ОШИБКА!!!

Когда на какое-то значение ссылается несколько ссылочных переменных, то обращаться к этому значению можно с помощью любой из них. Значение доступно до тех пор, пока на него имеется хотя бы одна ссылка. Например:

my $ref2scalar = $scalar; # ссылка на скаляр

my $one_more_ref = $ref2scalar; # копия ссылки на скаляр

# будет выведено одно и то же значение $scalar:

print "${$ref2scalar} ${$one_more_ref}";

На рис. 11.2 показана ситуация, когда несколько ссылок указывают на одну скалярную переменную.

Рис. 11.2.Несколько ссылок на скаляр

Если переменная $ref2scalar перестанет ссылаться на $scalar (например, после undef $ref2scalar), то значение $scalar все еще будет доступно через переменную $one_more_ref.

Значением объекта ссылки также может быть ссылка (косвенная ссылка) на другой объект, возможно, тоже на ссылку. Ссылка даже может содержать ссылку на саму себя! Таким образом при необходимости можно построить цепочку ссылок любой длины. Например:

$value = 'Полезное значение';

$ref1 = $value; # ссылка на значение

$ref2 = $ref1; # ссылка на ссылку на значение

$ref3 = $ref2; # ссылка на ссылку на ссылку на значение

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

$ref_chain = $value; # цепочка из трех ссылок

Для доступа по такой цепочке ссылок к исходному значению правило разыменования применяется нужное число раз. Например, в цепочке из трех ссылок с помощью трех префиксов $ мы последовательно получаем доступ к ссылочным переменным, а еще один префикс нужен для доступа к полезному значению:

# выведем исходное значение через $ref3:

print ${${${$ref3}}}; # или короче: print $$$$ref3;

# или через $ref_chain:

print $$$$ref_chain;

Если применить функцию ref() к переменной, содержащей ссылку на другой объект, то она вернет строку 'REF'. Если преобразовать в строку значение ссылки на ссылку, то будут выведено обозначение ссылочного типа и адрес объекта ссылки, например:

print ref($ref_chain); # выведет: 'REF'

print $ref_chain; # выведет, например: 'REF(0x334e8c)'

Подобным же образом можно работать со ссылками на массивы. Ссылка на переменную типа "массив" также создается с помощью операции взятия ссылки:

my @array = ('Это', 'список', 'в', 'массиве');

my $ref2array = @array; # ссылка на массив

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

my $ref2anon = [ # ссылка на анонимный массив

'Это', 'анонимный', 'массив'

]; # конец присваивания ссылки

my $ref2empty = []; # ссылка на пустой анонимный массив

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

my $ref2copy = [@array]; # ссылка на копию массива

Ссылка на именованный массив и ссылка на анонимный массив изображены на рис. 11.3.

Рис. 11.3.Ссылки на обычный и анонимный массивы

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

# будет выведено одно и то же значение @array:

print "@{$ref2array} @$ref2array ";

Естественно, что, обращаясь к массиву по ссылке, можно выполнять с ним любые действия, как и с именованным массивом, например:

my @array_copy = @{$ref2array}; # копия массива

@{$ref2array}[0,1] = ('Новый', 'список'); # срез массива

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

print ${$ref2array}[0]; # или: $$ref2array[0]

Обращение по ссылке к элементу массива более наглядно записывается с помощью инфиксной операции ->, слева от которой записывается имя переменной, содержащей ссылку на массив, а справа - индекс элемента массива в квадратных скобках. Операция "стрелка" наглядно представляет ссылку, символы -> в ней записываются без пробела между ними. Вот пример:

# доступ по ссылке к значению элемента массива:

my $element_value = $ref2array->[0];

# изменение значения элемента массива:

$ref2array->[0] = $new_value;

Как к обычным скалярным значениям можно обращаться по ссылке к отдельным элементам массива, например:

$ref2element = $array[0]; # ссылка на элемент массива

${$ref2element} = $new_value; # изменение элемента массива

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

@{$ref2NxM->[$n]} # вложенный массив

$ref2NxM->[$n]->[$m] # скалярный элемент двумерного массива

$ref2NxMxP->[$n]->[$m]->[$p] # элемент 3-мерного массива

Для удобства чтения программы допускается не записывать операцию "стрелка" между парами индексов массива в квадратных скобках:

$ref2NxM->[$n][$m] # так гораздо симпатичнее!

$ref2NxMxP->[$n][$m][$p] # а тем более так...

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

my $ref2RxC = []; # ссылка на анонимный массив массивов

for (my $row = 0; $row < 3; $row++) { # цикл по строкам

$ref2RxC->[$row] = []; # строка: вложенный массив

for (my $col = 0; $col < 5; $col++) { # по колонкам

$ref2RxC->[$row]->[$col] = ($row+1).'.'.($col+1);

}

}

Небольшие многомерные массивы удобно создавать, используя вложенные анонимные массивы. Это присваивание создаст такой же массив, что и в предыдущем примере:

$ref2RxC = [ # ссылка на двумерный анонимный массив

[1.1, 1.2, 1.3, 1.4, 1.5], # 1-я "строка"

[2.1, 2.2, 2.3, 2.4, 2.5], # 2-я "строка"

[3.1, 3.2, 3.3, 3.4, 3.5] # 3-я "строка"

]; # конец присваивания ссылки

На рис. 11.4 изображен получившийся в результате "массив массивов" (Аrray of Аrrays, AoA), представляющий собой многомерный массив.

Рис. 11.4.Организация многомерного 'массива массивов'

Для вывода значений многомерного массива обычно используется нужное число вложенных циклов for или других циклических конструкций:

# цикл по строкам (элементам массива верхнего уровня)

for (my $row = 0; $row < @{$ref2RxC}; $row++) {

# цикл по столбцам (элементам вложенных массивов)

for (my $col = 0; $col < @{$ref2RxC->[$row]}; $col++) {

print "$ref2RxC->[$row][$col] ";

}

print " ";

}

В результате выполнения этой программы построчно будет выведено значение всех элементов из массива массивов:

1.1 1.2 1.3 1.4 1.5

2.1 2.2 2.3 2.4 2.5

3.1 3.2 3.3 3.4 3.5

В любой массив можно поместить список ссылок на другие программные объекты, например, таким образом:

@reference_list = ($scalar, @array, \%hash);

Можно записать то же самое более простым способом, поставив операцию взятия ссылки перед списком объектов в круглых скобках:

@reference_list = ($scalar, @array, %hash);

Списки ссылок на объекты могут, например, передаваться в подпрограмму для изменения перечисленных в списке объектов. Передача аргументов в подпрограммы по ссылке и по значению будет рассмотрена в следующей лекции.

Если попытаться разыменовать ссылку на несуществующий объект, то он автоматически будет создан. В этом случае работает удивительный механизм, называемый автосозданием объекта ссылки (буквально: "автооживление" - autovivification). Например, во время обращения по ссылке к элементу массива автоматически создается массив из пяти элементов, ссылка на него присваивается в переменную $array_ref, а пятый элемент получает начальное значение:

$array_ref->[4] = '5-й элемент'; # присваивание значения

print ref($array_ref); # вызывает к жизни массив

print scalar(@{$array_ref}); # из 5 элементов!

print $$array_ref[4]; # печатаем значение

Подобным образом применяя автосоздание объектов, можно создать цепочку ссылок, указывающих на некоторое значение:

$$$$ref = 25; # при попытке присвоить значение

# создаются 2 ссылочных переменных и 1 скалярная

print "$ref $$ref $$$ref $$$$ref ";

# выведет: REF(0x334dd8) REF(0x334e8c) SCALAR(0x334e98) 25

Все, что говорилось о ссылках на массивы, применимо к ссылкам на хэши. Ссылка на переменную типа "хэш" получается с помощью операции взятия ссылки:

my %hash = ('Хэш' => 'ассоциативный массив');

my $ref2hash = \%hash; # ссылка на весь хэш

print ref($ref2hash); # вернет: HASH

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

my $ref2anon = { # ссылка на анонимный хэш

'language' => 'Perl',

'author' => 'Larry Wall',

'version' => 5.8

}; # конец присваивания ссылки

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

my $ref2copy = {%hash}; # ссылка на копию хэша

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

# будет выведено одно и то же значение %hash:

print %{$ref2hash}, %$ref2hash;

При помощи ссылок с хэшами можно выполнять любые действия, обращая внимание на правильное разыменование ссылки:

%hash_copy = %{$ref2hash}; # копия хэша

@hash_slice= @{$ref2hash}{$key1, $key2}; # срез хэша (массив)

@hash_keys = keys %{$ref2hash}; # ключи хэша (массив)

Разыменование ссылки на элемент хэша также записывается уже знакомым нам способом, когда перед ссылочной переменной ставится префикс скаляра $, а после нее - ключ элемента хэша в фигурных скобках. Ссылочная переменная может заключаться в фигурные скобки:

${$ref2hash}{'ключ'} = 'значение'; # изменение значения

print $$ref2hash{'ключ'}; # доступ к значению элемента хэша

В другой форме разыменования ссылки к переменной, содержащей ссылку на хэш, применяется операция "стрелка", после которой в фигурных скобках указывается ключ элемента хэша. Вот так:

$ref2hash->{'термин'} = 'определение'; # добавим элемент

$value = $ref2hash->{'Хэш'}; # найдем значение по ключу

Если ссылка используется как ключ хэша, она, как и любой ключ хэша, автоматически преобразуется в строку. Такие строки невозможно применять для доступа к объектам ссылки, но они могут служить отличными уникальными ключами хэша, поскольку строковое значение ссылки содержит адрес объекта в памяти, например, 'SCALAR(0x335b04)' или 'ARRAY(0x334dd8)'. Если все-таки требуется использовать ссылки в качестве ключей хэша, то можно воспользоваться модулем Tie::RefHash.

Аналогично созданию "массива массивов" создаются и другие разновидности ссылочных структур данных: массивы хэшей, хэши массивов и хэши хэшей. Ссылочные структуры применяются для структурированного представления взаимосвязанных данных. Для хранения в каждом элементе массива нескольких значений применяется массив хэшей (Array of Hashes, AoH). Вот пример массива, содержащий ссылки на анонимные хэши, в каждом из которых хранятся сведения о каком-либо объекте:

my $AoH = [ # этапы "Формулы-1" '2006 года

{grandprix=>'Бахрейна', date=>'2006.03.12'},

{grandprix=>'Малайзии', date=>'2006.03.19'},

{grandprix=>'Австралии', date=>'2006.04.02'},

{grandprix=>'Сан-Марино', date=>'2006.04.23'},

# и так далее...

];

# напечатать хэш, на который ссылается 4-й элемент массива

print "Гран-при $AoH->[3]->{grandprix} $AoH->[3]->{date}";

# выведет: Гран-при Сан-Марино 2006.04.23

Для того чтобы ассоциировать с каждым ключом хэша список скалярных значений, применяется хэш массивов (Нash of Аrrays, HoA). Приведем пример хэша массивов, где в каждом элементе хэша хранится ссылка на анонимный список ассоциированных значений:

my $HoA = { # годы создания языков программирования

1964 => ['SIMULA', 'BASIC', 'PL/1'],

1970 => ['Forth', 'Pascal', 'Prolog'],

1979 => ['Ada', 'Modula-2'],

1987 => ['Perl', 'Haskell', 'Oberon'],

1991 => ['Python', 'Visual Basic']

};

# напечатать список, ассоциированный с 1987 годом

foreach my $language (sort @{$HoA->{1987}}) {

print "$language ";

} # выведет: Haskell Oberon Perl

Элементы хэша также могут хранить ссылки на другие хэши, образуя хэш хэшей (Нash of Нashes, HoH). Вот пример описания хэша хэшей, где с каждым поисковым ключом ассоциируется анонимный хэш с информацией об объекте:

my $HoH = { # авторы и годы создания языков программирования

'Pascal' => {author=>'Niklaus Wirth', year=>1970},

'Perl' => {year=>1987, author=>'Larry Wall'},

'C' => {author=>'Dennis Ritchie', year=>1972}

};

# в каком году был создан Pascal?

print $HoH->{'Pascal'}->{'year'}; # выведет: 1970

# кто создал язык Си?

print $HoH->{'C'}->{'author'}; # выведет: Dennis Ritchie

Имеющиеся в других языках программирования записи (record) или структуры (struct), в Perl чаще всего представляются в виде хэшей, в которых ключи используются в качестве имен полей и применяются для доступа к значениям полей записи. Для завершающего примера создадим набор записей с информацией о людях. Каждая запись будет анонимным хэшем, а ссылки на записи будут храниться в массиве. В каждой записи дату рождения представим в виде анонимного массива, содержащего год, месяц и день. Вот таким образом:

my $family = [ # массив записей о семье

{name => 'Михаил', birthday => [1958, 11, 12]},

{name => 'Ирина', birthday => [1955, 03, 23]},

{name => 'Маша', birthday => [1980, 07, 27]},

{name => 'Миша', birthday => [1981, 11, 28]},

{name => 'Лев', birthday => [1988, 06, 24]}

];

# напечатаем год рождения Маши:

print "$family->[2]->{birthday}->[0]"; # или проще:

print "$family->[2]{birthday}[0]"; # выведет: 1980

Подобные структуры легко динамически модифицировать при выполнении программы. Например, добавим в каждую запись новое поле - 'address', в котором сохраним ссылку на запись о месте проживания человека. Адрес оформим в виде анонимного хэша из нескольких полей:

# адрес в виде анонимного хэша, в $address - ссылка на него:

$address = {country => 'Россия', index => 641870}; # и т.д.

# добавить поле адреса и поместить туда $address:

foreach my $person (@{$family}) { # пропишем всех

$person->{address} = $address; # по одному адресу

}

# выведем почтовый индекс для Ирины

print "$family->[1]->{address}->{index} "; # 641870

На рис. 11.5 приведена ссылочная структура данных, которая получилась в результате выполнения программы. Для доступа по ссылкам ко всем элементам этой структуры используется единственная именованная переменная $family.

Рис. 11.5.Пример ссылочной структуры данных

С помощью ссылок создаются и другие динамические структуры данных: связные списки, деревья и графы. Подытоживая все сказанное о ссылках, в таблице 11.1 приводится сводная информация о синтаксических конструкциях при работе со ссылками.

Таблица 11.1. Синтаксические конструкции для работы со ссылками на данные

Скаляр Массив Хэш Взятие ссылки на объект $sref = $scalar; $aref = @array; $href = \%hash; Создание ссылки на анонимный объект $sref = 'Литерал'; $aref = [$a, $b]; $href = {$a => $b}; Доступ к значению объекта ссылки ${$sref} $$sref @{$aref} @$aref %{$href} %$href Доступ к значению элемента объекта ссылки $aref-> [$index] ${$aref}[$index] $href->{$key} ${$href}{$key} Доступ к срезу объекта ссылки @{$aref}[$i1, $i2] @{$href}{$k1, $k2} Значение функции ref($ref) для объекта ссылки SCALAR ARRAY HASH

Программирующие на Perl на каждом шагу пользуются интерполяцией в строках скалярных переменных и массивов. Иногда требуется включить в строку результат вычисления какого-либо выражения. С помощью ссылок можно интерполировать любое выражение, например, вызов функции. Чтобы включить в строку значение скалярного выражения, его надо заключить в круглые скобки и взять на него ссылку операцией , а затем разыменовать ссылочное выражение как скаляр с помощью префикса $. Вот таким образом:

$s = "localtime() ${($x=localtime)} в скалярном контексте";

# значение выражения, например: 'Sun Mar 26 20:17:36 2006'

Чтобы включить в строку значение выражения, возвращающего список, его надо заключить в квадратные скобки, организуя ссылку на анонимный массив, а потом разыменовать ссылочное выражение как массив с помощью префикса @. Вот так:

$a = "localtime() @{[localtime()]} в списочном контексте";

# значение выражения, например: '36 17 20 26 2 106 0 84 1'

Ссылки, о которых до этого шла речь, называются жесткими ссылками. Жесткая ссылка (hard reference) - это программный объект, хранящий в памяти адрес референта и тип его значения. В Perl имеется еще одна разновидность ссылок, называемых символическими ссылками. Символическая ссылка (symbolic reference) - это строковое значение, которое хранится в скалярной переменной и представляет из себя имя глобальной переменной:

$referent1 = 'Референт'; # объект ссылки

$symlink = 'referent' . 1; # символическая ссылка

# доступ по символической ссылке к значению объекта

print ${$symlink}; # будет выведено: 'Референт'

Символические ссылки используются значительно реже и бывают нужны, когда требуется во время выполнения программы программно создавать имена переменных в виде строк, чтобы затем получать доступ к их значениям. Использование символических ссылок может приводить к трудно обнаруживаемым ошибкам, поэтому лучше запрещать использование в программе символических ссылок с помощью прагмы use strict 'refs'.

В этой лекции изучены ссылки и средства построения с помощью ссылок динамических структур данных. Ссылки дают программисту мощные и гибкие средства доступа к программным объектам. Поэтому многие механизмы работы с данными в Perl организованы с использованием ссылок, и мы будем обращаться к ссылкам в следующих лекциях при изучении подпрограмм, библиотечных модулей и объектного программирования.