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

В данном разделе мы рассмотрим некоторые примеры, связанные с основным применением ссылок — конструированием структур данных,
В качестве первой структуры построим массив массивов или двумерный массив. Для примера рассмотрим массив @саlendar, содержащий календарь на 2000 год. Значением элемента $calendar[$i][$j] является название дня недели, приходящегося нa (j + 1)-й день (i + 1)-го месяца, i = (0..11), j = (0..30).
Для заполнения массива @calendar нам потребуется функция, которая по заданному году, месяцу и дню месяца вычисляет соответствующий день недели.
Вычисление дня недели основано на том, что:
Листинг 7.1. Программа-календарь
sub GetDay {
  my $year = shift;
  my @days = (0,31,59,90,120,151,181,212,243,273,304,334);
  my @week = ("Monday", "Tuesday", "Wednesday", "Thursday",
              "Friday", "Saturday", "Sunday"); 
  my $previous_years_days =
  ($year - 1)*365 + int(($year – 1)/4) –
  int($year - 1)/100) + int(($year – 1)/400);
  return sub { my ($month, $day) = @_; 
               my $n = $previous_years_days +
               $days[$month - 1] + $day – 1;
               $n++ if ($year % 4 == 0 and $year % 100 != 0 or
                        $year % 400 == 0 and $montn > 2); 
               return $week[$n % 7];
             }
};
Аргументами функции GetDay( ) являются номер года, номер месяца и номер дня месяца. Внутри тела функции им соответствуют переменные $year, $month и $day. Функция подсчитывает число дней $n, прошедших с 1 января 1 года. Остаток от деления этого числа на 7 ($n % 7) определяет день недели как элемент массива
$week[$n % 7]
ПРИМЕЧАНИЕ Для передачи параметров в подпрограмму используется предопределенный массив @_. Встроенная функция shift( ) без параметров, вызванная внутри подпрограммы, возвращает первый элемент массива @_ и осуществляет сдвиг всего массива влево, так, что первый элемент пропадает, второй становится первым и т. Д. Элемент массива $days[$i] равен суммарному числу дней в первых i месяцах невисокосного года, i = (0..11). В переменной $previous_years_days запоминается вычисленное значение общего количества дней, прошедших с 1 января 1 года до начала заданного года.
Обратите внимание на то, что значением функции GetDay( ) является не название дня недели, а ссылка на анонимную функцию, которая возвращает название дня недели. Объясним, зачем это сделано.
Если бы функция GetDay( ) возвращала день недели, то для заполнения календаря на 2000 год к ней необходимо было бы сделать 366 обращений, вычисляя каждый раз значение переменной $previous_years_days. Для каждого года это значение постоянно, поэтому его достаточно вычислить всего один, а не 366 раз.
В нашем примере используется свойство анонимных функций образовывать замыкание по отношению к лексическим переменным, объявленным при помощи функции my( ). Иными словами, если определить анонимную функцию в некоторый момент времени при некоторых значениях лексических переменных, то в дальнейшем при вызове этой функции ей будут доступны значения этих лексических переменных, существовавшие на момент ее определения.
Чтобы анонимной функцией можно было воспользоваться в дальнейшем, присвоим ссылку на нее скалярной переменной:
$f = GetDay(2000,1,1);
Во время обращения к GetDay( ) было сформировано вычислительное окружение анонимной функции, на которую сейчас указывает переменная $f. Вычислительное окружение включает, в том числе, и переменную $previous_years_days с ее значением. Обратите внимание, что внутри анонимной функции значение этой переменной не вычисляется. В дальнейшем для заполнения календаря мы будем вызывать анонимную функцию через ссылку $f.

Массив массивов

Сформируем массив @calendar, используя результаты предыдущего раздела.
Листинг 7.2. Формирование массива массивов
for $i (1,3..12) { 
  for $j (1..30) {
    $calendar[$i-1][$j-1] = &$f($i, $j);
  }
};
for $i (1,3,5,7,8,10,12) {
  $calendar[$i-1][30] = &$f($i, 31);
};
for $j (1..28) {
  $calendar[1][$j-1] = &$f(2, $j);
};
# Если год високосный, то добавляется еще один элемент
$calendar[1][28] = &$f(2,29);
Массив @calendar состоит из 12 элементов по числу месяцев в году. Каждый элемент массива является ссылкой на другой массив, имеющий столько элементов, сколько дней в соответствующем месяце. Значениями элементов вложенных массивов являются английские названия соответствующих дней недели: «Mondays, «Tuesday» и т.д.
Обращаем внимание на то, что при формировании массива @сalendar осуществляется неявное создание ссылок $calendar[$i] и применяется компактная запись $calendar[$i][$j] для обозначения индивидуального элемента двумерного массива, рассматривавшаяся в разделе «Операция разыменования».
Содержимое массива @саlendar можно вывести для просмотра при помощи следующих операторов:
for $i (0..11) {
  for $j (0..$#{$calendar[$i]}) {
    print $j+1,",",$i+1, " is $calendar[$i][$j]\n";
  }
};
Напомним, что запись $#array обозначает верхнее значение индекса массива @аrrау. В результате выполнения данного цикла будет выведена длинная последовательность строк вида
1.1 is Saturday 
2.1 is Sunday
. . . . . . . . 

Другие структуры данных

На основе массива @calendar, содержащего календарь на 2000 год, покажем, как можно строить более сложные структуры данных. Структура двумерного массива не очень удобна для представления содержащихся в ней данных в привычном виде настенного календаря. Перегруппируем данные, объединяя их в группы по дням недели, Для этого построим новую структуру, которую для краткости назовем «массивом хэш-массивов массивов», отдавая себе отчет в том, что такое словосочетание не только далеко не изящно, но и по существу неточно.
Новая структура представляет собой массив @months, состоящий из 12 элементов, по числу месяцев в году. Каждый элемент содержит ссылку на анонимный хэш-массив. Каждый вложенный хэш-массив содержит набор ключей, имеющих имена, совпадающие с английскими названиями дней недели: «Monday», «Tuesday» и т. д. Каждому ключу соответствует значение, являющееся, в свою очередь, ссылкой на анонимный массив, содержащий все числа данного месяца, приходящиеся на день недели, соответствующий ключу: все понедельники, все вторники и т. д.
Листинг 7.3. Формирование массива хэш-массивов массивов
for $i (0..11) {
  for $j (0..$#{$calendar[$i]}) { 
    push @{$months[$i]{$calendar[$i][$j]}}, $j+1;
  }
};
ПРИМЕЧАНИЕ Функция push @array, list помещает список list в конец массива @array.
Первым аргументом встроенной функции push является массив, в который попадают все дни (i + 1)-го месяца, приходящиеся па один и тот же лень недели: все понедельники, все вторники и т. д. На этот массив указывает ссылка $months[$i]{"key"}, где ключ "key" принимает значения "Monday", "Tuesday" и т. д. Для обращения к самому массиву ссылку следует разыменовать, заключив в фигурные скобки: @{$months[$i]{"key"}}. Если вместо ключа "key" подставить нужное значение из $calendar[$i][$j], то получим аргумент функции push.
Вновь сформированную структуру удобно использовать для вывода календаря в традиционном виде. Последовательность операторов
for $i (0..11) {
  print "month #", $i+1, "\n";
  for $DayName (keys %{$months[$i]}) { 
    print "${DayName}  @{$ months[$i]{$DayName}}\n";
  }
};
распечатает календарь в виде:
month #1
Monday      3 10 17 24 31
Thursday    6 13 20 27
Wednesday   5 12 19 26
Sunday      2 9 16 23 30
Saturday    1 8 15 22 29
Friday      7 14 21 28
Tuesday     4 11 18 25
. . . . . . . . . . . . .
Встроенная функция keys %hash возвращает список всех ключей ассоциативного массива %hash. Вывод ключей осуществляется функцией keys в случайном порядке, поэтому дни недели расположены не в естественной последовательности, а случайным образом.
Для вывода ключей в порядке следования дней недели воспользуемся встроенной функцией сортировки:
sort SUBNAME LIST
Функция sort( ) сортирует список LIST и возвращает отсортированный список значений. По умолчанию используется обычный лексикографический (словарный) порядок сортировки. Его можно изменить при помощи аргумента SUBNAME. Аргумент SUBNAME является скалярной переменной, содержащей имя функции или ссылку на функцию. Подпрограмма SUBNAME возвращает целое число, определяющее порядок следования элементов списка. Любая процедура сортировки состоит из последовательности сравнений двух величин. Для того чтобы правильно задать порядок сортировки, надо представить себе SUBNAME как функцию двух аргументов. В данном случае аргументы в подпрограмму SUBNAME передаются не общим для Perl способом — через массив @_, а через переменные $а и $b, обозначающие внутри подпрограммы соответственно первый и второй аргумент. Подпрограмму SUBNAME надо составить таким образом, чтобы она возвращала положительное целое, нуль, отрицательное целое, когда при сравнении аргумент $а назначается меньшим аргумента $b, равным аргументу $b, большим аргумента $b соответственно. Для этого внутри подпрограммы удобно использовать операции числового (<=>) и строкового (cmp) сравнения, возвращающие значения -1, 0, 1, если первый аргумент соответственно меньше второго, равен второму, больше второго. Вместо имени подпрограммы в качестве аргумента SUBNAME может использоваться блок, определяющий порядок сортировки.
Зададим функцию WeekOrder, определяющую порядок сортировки:
sub WeekOrder {
  my %week = ("Monday"    => 0,
              "Tuesday"   => l,
              "Wednesday" => 2, 
              "Thursday"  => 3,
              "Friday"    => 4,
              "Saturday"  => 5,
              "Sunday "   => 6);
  $week { $a } <=> $week { $b }
};
Используя функцию sort( ) с заданным порядком сортировки:
for $i (0..11) {
  print "month # ", $i+1, "\n";
  for $DayName (sort WeekOrder keys %{$months[$i]}) { 
    print "$DayName @{$months[$i]{$DayName}}\n";
  }
};
получим структурированный вывод календаря в виде, упорядоченном по месяцам и дням недели:
month #1
Monday      3 10 17 24 31
Tuesday     4 11 18 25
Wednesday   5 12 19 26
Thursday    6 13 20 27
Friday      7 14 21 28
Saturday    1 8 15 22 29
Sunday      2 9 16 23 30
. . . . . . . . . . . . .
В качестве следующего примера построим на основе массива @months новую структуру, которую можно было бы назвать «хэш-массивом хэш-массивов массивов», если бы такое название имело право на существование. В действительности все просто. Речь идет о том, чтобы заменить в массиве @months числовые индексы ключами, совпадающими с названиями месяцев, и таким образом получить ассоциативный массив %months со сложной внутренней структурой.
При построении хэш-массива %months воспользуемся вспомогательным хэш-массивом %OrderedMonths, который будем использовать для задания порядка сортировки.
Листинг 7.4. Формирование хэш-массива %months
# вспомогательный массив %OrderedMonths 
$OrderedMonths = ("January"   => 0,
                  "February"  => 1,
                  "March"     => 2,
                  "April"     => 3,
                  "May"       => 4,
                  "June"      => 5,
                  "July"      => 6,
                  "August"    => 7,
                  "September" => 8,
                  "October"   => 9,
                  "November"  => 10,
                  "December"  => 11 );
# (формирование структуры
for $month (sort {$OrderedMonths{$a} <=> $OrderedMonths{$b}}
            keys %OrderedMonths) {
  $i = $OrderedMonths{$month};
  $months{$month} = $months[$i];
};
# вывод элементов хэш-массива %months
for $month (sort {$OrderedMonths {$a} <=> $OrderedMonths{$b}}
            keys %OrderedMonths) {
  print "$month\n";
  $i = $OrderedMonths{$month};
  for $DayName (sort WeekOrder keys %{$months{$month}}) {
    print "$DayName  @{$months[$i]{$DayName}}\n";
  }
};
В результате выполнения примера из листинга 7.3 будет распечатан календарь на 2000 год в виде:
January
Monday		3 10 17 24 31
Tuesday		4 11 18 25
Wednesday	5 12 19 26
Thursday	6 13 20 27
Friday		7 14 21 28
Saturday	1 8 15 22 29
Sunday		2 9 16 23 30
. . . . . . . . . . . . . 
Рассмотренные примеры иллюстрируют подход, используемый в Perl для построения сложных структур данных. Любая сложная структура в Perl на «верхнем» уровне представляет собой массив или ассоциативный массив, в который вложены ссылки на массивы или хэш-массивы следующего уровня и т.д. В этой иерархии ссылки на массивы и хэш-массивы могут чередоваться в произвольном порядке. При помощи такого подхода средствами Perl можно представить любую структуру С или запись языка Pascal. Perl позволяет с легкостью создавать структуры, которые в других языках создать трудно или невозможно, например структуру, эквивалентную массиву, состоящему из элементов разных типов:
@аrrау = (1, 2, 3, {"оnе"=>1, "two"=>2}, \&func, 4, 5};
В заключение несколько слов о фрагментах массивов. Как известно, для доступа к элементам массива мы имеем специальную нотацию, состоящую из префикса $, имени массива и индекса элемента в квадратных скобках, например $array[7]. Если здесь вместо индекса поместить список индексов, а префикс $ заменить префиксом @, то такая запись будет обозначать фрагмент массива, состоящий из элементов с индексами из заданного списка. Подобную нотацию можно использовать в выражениях:
@subarrayl = @array[7..12];
@subarray2 = @аrrау[3,5,7];
Массив @subarray1 является фрагментом массива @аrrау, состоящим из элементов со значениями индекса от 7 до 12. Массив @subarray2 является фрагментом массива @аrrау, состоящим из элементов со значениями индекса 3,5 и 7. В первом случае список индексов задан при помощи операции «диапазон», во втором случае — перечислением.
Для многомерного массива понятие «фрагмент» обобщается и означает подмножество элементов, получающееся, если для некоторых индексов из диапазона их изменения выделить список допустимых значений. Для выделения одномерных фрагментов можно воспользоваться приведенной выше нотацией. Например, для выделения из массива @calendar фрагмента, содержащего календарь на первую неделю апреля, можно использовать запись:
@april_first_week = @{$calendar[3]}[0..6];
Если выделяемый фрагмент является многомерным, то для его обозначения специальной нотации не существует. В этом случае следует сформировать новый массив, являющийся фрагментом исходного массива. Например, для выделения из массива @calendar календаря на первый квартал можно воспользоваться циклом:
for $i (0..2) {
  for $j (0..#{$calendar[$i]}) { 
    $quarter1[$i][$j] = $calendar[$i][$j];
  }
};

Следующая страница Содержание главы


Реклама