Приглашаем посетить
Татищев (tatischev.lit-info.ru)

4.4. Выполнение операции с каждым элементом списка

Назад
Глава 4 Массивы
Вперед

4.4. Выполнение операции с каждым элементом списка

Проблема

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

Решение

Воспользуйтесь циклом to reach:

foreach $item (LIST) { # Выполнить некоторые действия с
$item }

Комментарий

Предположим, в массиве @bad_users собран синеок пользователей, превысивших свои дисковые квоты. В следующем фрагменте для каждого нарушителя вызывается процедура complain():

foreach $user (@bad_users) { cornplain($user);
}
Столь тривиальные случаи встречаются редко. Как правило, для генерации списка часто используются функции

foreach $var (sort keys %ENV) { print "$var=$ENV{$var}\n";
}
Функции sort и keys строят отсортированный список имей переменных окружения. Конечно, многократно используемые списки следует сохранять в массивах. Но для одноразовых задач удобнее работать со списком напрямую. Возможности этой конструкции расширяются не только за счет построения списка в foreach, по и за счет дополнительных операций в блоке кода. Одно из распространенных применений foreach - сбор информации о каждом элементе списка и принятие некоторого решения на основании полученных данных. Вернемся к примеру с квотами:

foreach $user (@all_users) {
$disk_space = get_usage($user); # Определить объем используемого # дискового пространства
if ($disk_space > $MAX_QUOTA) { # Если он больше допустимого,.,
complain($user); # ... предупредить о нарушении.
}
}
Возможны и более сложные варианты. Команда last прерывает цикл, next переходит к следующему элементу, a redo возвращается к первой команде внутри блока. Фактически вы говорите: "Нет смысла продолжать, это не то, что мне нужно" (next), "Я нашел то, что искал, и проверять остальные элементы незачем" (last) или "Я тут кое-что изменил, так что проверки н вычисления лучше выполнить заново" (redo). Переменная, которой последовательно присваиваются все элементы списка, называется переменной цикла или итератором. Если итератор не указан, используется глобальная неременная $_. Она используется по умолчанию во многих строковых, списковых и файловых функциях Perl. В коротких программных блоках пропуск $_ упрощает чтение программы (хотя в длинных блоках излишек неявных допущений делает программу менее понятной). Например:
foreach ('who') { if (/tchrist/) { print:
}
}
Или в сочетании с циклом while:

while () { # Присвоить $_ очередную прочитанную строку chomp; # Удалить из $_ конечный символ \n,
# если он присутствует foreach (split) { # Разделить $_ по пропускам и получить @_
# Последовательно присвоить $_
# каждый из полученных фрагментов
$_ = reverse;
# Переставить символы $_
# в противоположном порядке print:
# Вывести значение $_
}
}
Многочисленные применения $_ заставляют понервничать. Особенно беспокоит то, что значение $_ изменяется как в foreach, так и в while. Возникает вопрос - не будет ли полная строка, прочитанная в $_ через , навсегда потеряна после выполнения foreach? К счастью, эти опасения необоснованны - по крайней мере, в данном случае. Perl не уничтожает старое значение $_, поскольку переменная-итератор ($_) существует в течение всего выполнения цикла. При входе во внутренний цикл старое значение автоматически сохраняется, а при выходе - восстанавливается. Однако причины для беспокойства все же есть. Если цикл while будет внутренним, a foreach - внешним, ваши страхи в полной мере оправдаются. В отличие от foreach конструкция while разрушает глобальное значение $_ без предварительного сохранения! Следовательно, в начале любой процедуры (или блока), где $_ используется в подобной конструкции, всегда должно присутствовать объявление local $ . Если в области действия (scope) присутствует лексическая переменная (объявленная с ту), то временная переменная будет иметь лексическую область действия, ограниченную данным циклом. В противном случае она будет считаться глобальной переменной с динамической областью действия. Во избежание странных побочных эффектов версия 5.004 допускает более наглядную и понятную запись:

foreach my $item (Oarray) { print "i = $item\n";
}
Цикл foreach обладает еще одним свойством: в цикле иеременная-итератор является не копией, а скорее синонимом (alias) текущего элемента. Иными словами, изменение итератора приводит к изменению каждого элемента списка.

@аrrау = (1,2,3);
foreach $item ( array) { $item--;
}
print "@array";
0 1 2
# Умножить каждый элемент @а и @Ь на семь @а = (.5, З): @Ь = (0, 1);
foreach $item (@a, @b) <
$item .= 7;
print "$item ";
} 3.5 21 0 7
Модификация списков в цикле foreach оказывается более понятной и быстрой, чем в эквивалентном коде с циклом for и указанием конкретных индексов. Это не ошибка; такая возможность была намеренно предусмотрена разработчиками языка. Не зная о ней, можно случайно изменить содержимое списка. Теперь вы знаете. Например, применение s/// к элементам списка, возвращаемого функцией values, приведет к модификации только копий, но не самого хэша. Однако срез X3Uia@hash{keys %hash} (см. главу 5 "Хунт") дает нам нечто, что все же можно изменить с пользой для дела: # Убрать пропуски из скалярной величины, массива и всех элементов хэша
foreach ($scalar, @array, @hash{keys %hash}) {
s/-\s+//;
s/\s+$//;
}
По причинам, связанным с эквивалентными конструкциями командного интерпретатора Борна для UNIX, ключевые слова for и foreach взаимозаменяемы:

for $item (@array) { # То же, что и foreach $item (@array) # Сделать что-то
}
for (@аrrау) { # To же, что и foreach $_ (@array)
}
Подобный стиль часто показывает, что автор занимается написанием или сопровождением сценариев интерпретатора и связан с системным администрированием UNIX. Жизнь таких люден и без того сложна, поэтому не стоит судить их слишком строго.

Смотри также: Разделы "For Loops", "Foreach Loops" н "Loop Control" perlsyn(1) раздел "Temporary Values via localQ" per!sub(l). Оператор local() рассматривается в рецепте 10.13, a my() - в рецепте 10.2.

4.5. Перебор массива по ссылке

Проблема

Имеется ссылка ма массив. Вы хотите использовать f о reach для обращения к каждому элементу массива.

Решение

Для перебора разыменованного (dereferenced) массива используется цикл to reach или for:
# Перебор элементов массива
$ARRAYREF foreach $item(@'$ARRAYREF) {# Сделать что-то с $item
}
for ($i = 0; $l <= $#$ARRAYREF; $i++) { # Сделать что-то с
$ARARAYREF->[$i]
}

Комментарий

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

@fruits = ( "apple", "blackberry" );
$fruit_ref = \@fruits;
foreach $fruit (@$fruit_ref) {
print "$fruit tastes good in a pie.\n";
}

Apple tastes good in a pie,
Blackberry tastes good in a pie. Цикл foreach можно переписать в цикле for следующего вида:
for ($i=0; $i <= $#$fruit_ref; $i++) {
print "$fruit_ref->[$i] tastes good in a pie.\n";
}
Однако ссылка на массив нередко является результатом более сложного выражения. Для превращения такого результата в массив применяется конструкция @{ EXPR }:
$namelist{felines} = \@rogue_cats;
foreach $cat ( @>{ $namelist{felines} } ) {
print "Scat purrs hypnotically..\n";
}
print "--More--\nYou are controlled.\n";

Как и прежде, цикл foreach можно заменить эквивалентным циклом for:
for ($i=0; $i <= $#{ $namelist{felines} }; $i++) {
print "$namellst{felines}[$i] purrs hypnotically.\n";
}


Смотри также: perlref(l) и perllol{\y, рецепты 4.4; 11.1.

4.6. Выборка уникальных элементов из списка

Проблема

Требуется удалить из списка повторяющиеся элементы - например, при построении списка из файла или на базе выходных данных некоей команды. Рецепт в равной мере относится как к удалению дубликатов при вводе, так и в уже заполненных массивах.

Решение

Хэш используется для сохранения встречавшихся ранее элементов, а функция keys - для их извлечения. Принятая в Perl концепция истинности позволит уменьшить объем программы и ускорить ее работу. Прямолинейно
%seen = ();
@uniq =();
foreach $item (@list) { unless ($seen{$ltem})
# Если мы попали сюда, значит, элемент не встречался ранее
$seen{$ltem} = 1;
push(@uniq, $item);
}
}

Быстро
%seen = ();
foreach $item (Olist) {
push(@uniq, $item) unless $seen{$item}++;
} Аналогично, но с пользовательской функцией

%seen = ();
foreach $item (@list) {
some_func($item) unless $seen{$item}++;
} Быстро, но по-другому

%seen =();
foreach $iteni (@list) { $seen{$item}++;
} @unlq = keys %seen; Быстро и совсем по-другому
%seen =();
@unique = grер { ! $seen{$_} ++ } @list:

Комментарий

Суть сводится к простому вопросу - встречался ли данный элемент раньше? Хэши идеально подходят для подобного поиска. В нервом варианте ("Прямолинейно") массив уникальных значении строится но мере обработки исходного списка, а для регистрации встречавшихся значении используется хэш. Второй вариант ("Быстро") представляет собой самый естественный способ решения подобных задач в Perl. Каждый раз, когда встречается новое значение, в хэш с помощью оператора ++ добавляется новый элемент. Побочный эффект состоит в том, что в хэш попадают все повторяющиеся экземпляры. В данном случае хэш работает как множество. Третий вариант ("Аналогично, но с пользовательской функцией") похож на второй, однако вместо сохранения значения мы вызываем некоторую пользовательскую функцию и передаем ей это значение в качестве аргумента. Если ничего больше не требуется, хранить отдельный массив уникальных значений будет излишне. В следующем варианте ("Быстро, но по-другому") уникальные ключи извлекаются из хэша %seen лишь после того, как он будет полностью построен. Иногда это удобно, но исходный порядок элементов утрачивается. В последнем варианте ("Быстро и совсем по-другому") построение хэша %seen объединяется с извлечением уникальных элементов. При этом сохраняется исходный порядок элементов. Использование хэша для записи значений имеет два побочных эффекта: при обработке длинных списков расходуется много памяти, а список, возвращаемый keys, не отсортирован в алфавитном или числовом порядке и не сохраняет порядок вставки. Ниже показано, как обрабатывать данные по мере ввода. Мы используем 'who' для получения сведений о текущем списке пользователей, а перед обновлением хэша извлекаем из каждой строки имя пользователя: # Построить список зарегистрированных пользователей с удалением дубликатов
%ucnt =();
for ('who') {
s/\s.*\n//; # Стереть от первого пробела до конца строки
# остается имя пользователя
$ucnt{$_}++; # Зафиксировать присутствие данного пользователя }
# Извлечь и вывести уникальные ключи
@users = sort keys %ucnt;
print "users logged in: @users\n";


Смотри также: Раздел "Foreach Loops" perlsyn(1); описание функции keys в perlfunc(1). Аналогичное применение хэтей продемонстрировано в рецептах 4.7 и 4.8.

4.7. Поиск элементов одного массива, отсутствующих в другом массиве

Проблема

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

Решение

Мы ищем элементы @А, которых нет в @В. Постройте хэш из ключей @В - он будет использоваться в качестве таблицы просмотра. Затем проверьте каждый элемент @А и посмотрите, присутствует ли он в @В. Простейшая реализация # Предполагается, что @А и @В уже загружены
%seen =(); # Хэш для проверки принадлежности элемента В
@aonlу =(); # Ответ
# Построить таблицу просмотра
foreach $item (@B) { $seen{$item} = 1 }
# Найти элементы @А, отсутствующие в @В
foreach $item (@A) { unless $item (@A) {
# Отсутствует в %seen, поэтому добавить в @aоnlу
push(@aonly, $item):
}
}
1my %seen; # Таблица просмотра
my @aonly;
# Ответ
# Построить таблицу просмотра
@seen{@B} =();
foreach $item (@A) {
push(@aonly, $item.) unless exists $seen{$item};
}

Комментарий

Практически любая проблема, при которой требуется определить принадлежность скалярной величины к списку или массиву, решается в Perl с помощью хэ-uieii. Сначала мы обрабатываем @В и регнстрлрусм в хэше %seen все элементы @В, присваивая соответствующему элементу хэша значение 1. Затем мы последовательно перебираем все элементы @А и проверяем, присутствует ли данный элемент в хэше %seen (то есть в @В). В приведенном фрагменте ответ будет содержать дубликаты из массива @А. (Ситуацию нетрудно исправить, для этого достаточно включать элементы @А в %seen но мере обработки:
foreach $item (@А) {
push (@aonly, $item) unless $seen{$item};
$ seen{$item} =1; # Пометить как уже встречавшийся
}
Эти решения в основном отличаются по способу построения хэша. В первом варианте перебирается содержимое @В. Во втором для инициализации хэша используется срез. Следующий пример наглядно демонстрирует срезы хэша. Фрагмент:

$hash;"key1"} = 1;
$hash{"key2"} = 2;
# эквивалентен следующему:
@hash{"key1", "key2"} = (1,2);

Список в фигурных скобках содержит ключи, а список справа - значения. В нервом решении %seen инициализируется перебором всех элементов @В и присваиванием соответствующим элементам %seen значения 1. Во втором мы просто говорим:
@seen{@B} = ():
В этом случае элементы @В используются и качестве ключей для %seen, а с ними ассоциируется undef, поскольку количество значении в правой части меньше количества позиции для их размещения. Показанный вариант работает, поскольку мы проверяем только факт существования ключа, а не его логическую истинность или определенность. Но даже если с элементами @В потребуется ассоциировать истинные значения, срез все равно позволит сократить объем кода:
@seen{@B} = (1) х @В;


Смотри также: Описание срезов хэшей в perldata(1). Аналогичное применение хэшей продемонстрировано в рецептах 4.7 и 4.8.


Назад
Вперед