|
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.
|
© copyright 2000 Soft group
|
|
|
|
|