ЭЛЕКТРОННАЯ БИБЛИОТЕКА КОАПП
Сборники Художественной, Технической, Справочной, Английской, Нормативной, Исторической, и др. литературы.



 
Глава 10 Подпрограммы

10.11. Прототипы функций

Проблема

Вы хотите использовать,прототипы функций, чтобы компилятор мог проверить типы аргументов.

Решение

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

Комментарий

Фактическая проверка аргументов функции становится возможной лишь в6 время выполнения программы. Если объявить функцию до ее реализации, компилятор сможет использовать очень ограниченную форму прототипизации. Не путайте прототипы Perl с теми, что существуют в других языках. В Perl прототипы предназначены лишь для эмуляции поведения встроенных функций. , Прототип функции Perl представляет собой ноль и более пробелов, обратный косых черт или символов типа, заключенных в круглые скобки после определения или имени подпрограммы. Символ тира с префиксом \ означает, что арга мент в данной позиции передается по ссылке и должен начинаться с указанной символа типа. Прототип принудительно задает контекст аргументов, используемых при вызове данной функции. Это происходит во время компиляции программы и в большинстве случаев вовсе не означает, что Perl проверяет количество или тип аргументов функции. Если Perl встретит вызов func(3, 5) для функции с протoтипом sub func($), он завершит компиляцию с ошибкой. Но если для того же прототипа встретится вызов func(@array), компилятор всего лишь преобразует ©array в скалярный контекст; он не скажет: "Массив передавать нельзя - здесь должна быть скалярная величина". Это настолько важно, что я повторю снова: не пользуйтесь прототипами Perl, если вы надеетесь, что компилятор будет проверять тип и количество аргументов. Тогда зачем нужны прототипы? Существуют два основных применения, хотя во время экспериментов вы можете найти и другие. Во-первых, с помощью прототипа можно сообщить Perl количество аргументов вашей функции, чтобы опустить круглые скобки при ее вызове. Во-вторых, с помощью прототипов можно создавать подпрограммы с тем же синтаксисом вызова, что и у встроенных функций. Пропуск скобок Обычно функция получает список аргументов, и при вызове скобки ставить не обязательно:
results = myfunc 3,5;
Без прототипа такая запись эквивалентна следующей:
@results = myfunc(3 ,5);
При отсутствии скобок Perl преобразует правую часть вызова подпрограммы в списковый контекст. Прототип позволяет изменить такое поведение:
sub myfunc($);
@results = myfunc 3,5;
Теперь эта запись эквивалентна следующей:
@results = ( myfunc(3), 5 );
Кроме того, можно предоставить пустой прототип, показывающий, что функция вызывается без аргументов, как встроенная функция time. Именно так реализованы константы LOCK_SH, LOCK_EX и LOCK_UN в модуле Fcntl. Они Представляют собой экспортируемые функции, определенные с пустым прототипом:
sub LOCK.SH () { 1 }
sub LOCK.EX () { 2 }
sub LOCKJJN () { 4 }
Имитация встроенных функций Прототипы также часто применяются для имитации поведения таких встроенных функций, как push и shift, передающих аргументы без сглаживания. При вызове push(@array, 1, 2, 3) функция получает ссылку на @аrrау вместо самого массива. Для этого в прототипе перед символом @ ставится обратная косая черта:
sub mypush (\@@)
{ my $array_ref = shift;
my @remainder = @_;
@ в прототипе означает "потребовать, чтобы первый аргумент начинался с символа @, и передавать его по ссылке". Второй символ @ говорит о том, что остальные аргументы образуют список (возможно, пустой). Обратная косая черта, с которой начинается список аргументов, несколько ограничивает ваши возможности. Например, вам даже не удастся использовать условную конструкцию ?: для выбора передаваемого массива:
mypush( $х > 10 ? @а : @>b , 3, 5 ); # НЕВЕРНО Вместо этого приходится изощряться со ссылками:
mypush( @{ $х > 10 ? @а : @b }, 3, 5 ); # ВЕРНО Приведенная ниже функция hpush работает аналогично push, но для хэшей. Функция дописывает в существующий хэш список пар "ключ/значение", переопределяя прежнее содержимое этих ключей.
sub hpush(\%@) {
my $href = shift;
while ( my ($k, $v) = splice(@_, 0, 2) ) { $href->{$k} = $v;
}
}
hpush(%pieces, "queen" => 9, "rook" => 5);

> Смотри также --------------------------------
Описание функции prototype в perlfunc(1); perlsub(1); рецепт 10.5.

10.12. Обработка исключений

Проблема

Как организовать безопасный вызов функции, способной инициировать исключение? Как создать функцию, инициирующую исключение?

Решение

Иногда в программе возникает что-то настолько серьезное, что простого возвращения ошибки оказывается недостаточно, поскольку та может быть проигнорирована вызывающей стороной. Включите в функцию конструкцию die СТРОКА, чтобы инициировать исключение:
die "some message"; # Инициировать исключение
Чтобы перехватить исключение, вызывающая сторона вызывает функцию из eval, после чего узнает результат с помощью специальной переменной $@:
eval { func() };
if ($@) {
warn "func raised an exception: $@";
}

Комментарий

Инициирование исключения - крайняя мера, и относиться к ней следует серьезно. В большинстве функций следует возвращать признак ошибки с помощью простого оператора return. Перехватывать исключения при каждом вызове функции скучно и некрасиво, и это может отпугнуть от применения исключений, Но в некоторых ситуациях неудачный вызов функции должен приводить к аварийному завершению программы. Вместо невосстановимой функции exit следует вызвать die-по крайней мере, у программиста появится возможность вмешаться в происходящее. Если ни один обработчик исключения не был установлен с помощью eval, на этом месте программа аварийно завершается. Чтобы обнаружить подобные нарушения, можно поместить вызов функции в блок eval. Если произойдет исключение, оно будет присвоено переменной $@; в противном случае переменная равна false.
eval { $val = func() };
warn "func blew up: $@" if $@;
Блок eval перехватывает все исключения, а не только те, что интересуют вас. Непредусмотренные исключения обычно следует передать внешнему обработчику. Предположим, функция инициирует исключение, описываемое строкой "Full moon!". Можно спокойно перехватить это исключение и дать другим обработчикам просмотреть переменную $@. При вызове die без аргументов новая строка исключения конструируется на основании содержимого $@ и текущего контекста. v
eval { $val = func() };
if ($@ && $@ !` /Full moon!/) {
die; # Повторно инициировать неизвестные ошибки
}
Если функция является частью модуля, можно использовать модуль Carp и вызвать croak или confess вместо die. Единственное отличие die от croak заключается в том, что croak представляет ошибку с позиции вызывающей стороны, а не модуля. Функция confess по содержимому стека определяет, кто кого вызвал и с какими аргументами. Другая интересная возможность заключается в том, чтобы функция могла узнать о полном игнорировании возвращаемого ею значения (то есть о том, что она вызывается в неопределенном контексте). В этом случае возвращение кода ошибки бесполезно, поэтому вместо него следует инициировать исключение. Конечно, вызов функции в другом контексте еще не означает, что возвращаемое значение будет должным образом обработано. Но в неопределенном контексте оно заведомо не проверяется.
if (defined wantarray()) {
return;
} else {
die "pay attention to my error!";
}

> Смотри также -------------------------------
Описание переменной $@ в perlvar(i); описание функций die и eval в perlfunc(i); рецепты 10.15,12.2 и 16.21.

10.13. Сохранение глобальных значений

Проблема

Требуется временно сохранить значение глобальной переменной.

Решение

Воспользуйтесь оператором local, чтобы сохранить старое значение и автоматически восстановить его при выходе из текущего блока:
$аgе = 18; # Глобальная переменная
if (CONDITION) {
local $аgе = 23;
func(); # Видит временное значение 23
} # Восстановить старое значение при выходе из блока

Комментарий

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

1. Глобальной переменной (особенно $_) присваивается временное значение.

2. Создается локальный манипулятор файла или каталога или локальная функция.

3. Вы хотите временно изменить один элемент массива или хэша.

Применение 1оса1() для присваивания временных значений глобальным переменным Первая ситуация чаще встречается для стандартных, нежели пользовательских переменных. Нередко эти переменные используются Perl в высокоуровневых операциях. В частности, любая функция, явно или косвенно использующая $_, должна иметь локальную копню $_. Об этом часто забывают. Одно из возможных решений приведено в рецепте 13.15. В следующем примере используется несколько глобальных переменных. Переменная $/ косвенно влияет на поведение оператора чтения строк, используемого в операциях .
$раrа = get_paragraph(*FH); # Передать glob файлового манипулятора
$para = get_paragraph(\"FH); # Передать манипулятор по ссылке на
glob $раrа = get_paragraph(*IO{FH}); # Передать манипулятор по ссылке на 10
sub get_paragraph {
my $fh = shift;
local $/='';
my $paragraph = <$fh>;
chomp($paragraph);
return $paragraph; Применение local() для создания локальных манипуляторов Вторая ситуация возникает в случае, когда требуется локальный манипулятор файла или каталога, реже - локальная функция. Начиная с Perl версий 5.000, можно воспользоваться стандартными модулями Symbol, Filehandle или IO::Handle. но и привычная методика с тип-глобом по-прежнему работает. Например:
$contents = get_motd();
sub get_motd {
local *MOTD;
open(MOTD, "/etc/motd") or die "can't open mold: $!";
local $/ = undef; # Читать весь файл
local $_ = ;
close (MOTD);
return $_;
}
Открытый файловый манипулятор возвращается следующим образом:
return *MOTD;
Применение lосаl() в массивах и хэшах Третья ситуация на практике почти не встречается. Поскольку оператор local в действительности является оператором "сохранения значения", им можно воспользоваться для сохранения одного элемента массива или хэша, даже если сам массив или хэш является лексическим!
my @nums = (0 .. 5);
sub first {
local $nums[3] = 3,14159;
second();
} sub second {
print "@nums\n";
} second();
012345 first();
0 1 2 3.14159 4 5
Единственное стандартное применение - временные обработчики сигналов.
sub first {
local $SIG{INT} = 'IGNORE';
second(); }

Теперь во время работы second () сигналы прерывания будут игнорироваться. После выхода из first () автоматически восстанавливается предыдущее значение $SIG{INT}. Хотя local часто встречается в старом коде, от него следует держаться подальше, если это только возможно. Поскольку local манипулирует значениями глобальных, а не локальных переменных, директива use strict ни к чему хорошему не приведет. Оператор local создает динамическую область действия. Она отличается от другой области действия, поддерживаемой Perl и значительно более понятной на интуитивном уровне. Речь идет об области действия my - лексической области действия, иногда называемой "статической". В динамической области действия переменная доступна в том случае, если она находится в текущей области действия - или в области действия всех кадров (блоков) стека, определяемых во время выполнения. Все вызываемые функции обладают полным доступом к динамическим переменным, поскольку последние остаются глобальными, но получают временные значения. Лишь лексические переменные защищены от вмешательства извне. Если и это вас не убедит, возможно, вам будет интересно узнать, что лексические переменные примерно на 10 процентов быстрее динамических. Старый фрагмент вида:
sub func {
1оса1($х, $у) = @_;
#... .
}
почти всегда удается заменить без нежелательных последствий следующим фрагментом:
sub func {
my($x, $y) = @_:
#.... }
Единственный случай, когда подобная замена невозможна, - если работа программы основана на динамической области действия. Это происходит в ситуации, когда одна функция вызывает другую и работа второй зависит от доступа к временным версиям глобальных переменных $х и $у первой функции. Код, который работает с глобальными переменными и вместо нормальной передачи параметров издалека вытворяет нечто странное, в лучшем случае ненадежен. Хорошие программисты избегают подобных выкрутасов как чумы. Если вам встретится старый код вида:
&func(*Global_Array);
sub func {
local(*aliased_array) = shift;
for (@aliased_array) { .... } }
вероятно, его удастся преобразовать к следующей форме:
func(\@Global_Array);
sub func {
my $array_ref = shift;
for ((g>$array_ref) { .... }
}
До появления в Perl нормальной поддержки ссылок, использовалась старая стратегия передачи тип-глобов. Сейчас это уже дело прошлое.

> Смотри также -------------------------------
Описание функций local и ту в perlfunc(1); разделы "Private Variables via my()" и "Temporary Values via local()" perlsub(1); рецепты 10.2; 10.16.

10.14. Переопределение функции

Проблема

Требуется временно или постоянно переопределить функцию, однако функциям нельзя "присвоить" новый код.

Решение

Чтобы переопределить функцию, присвойте ссылку на новый код тип-глобу имени функции. Используйте local для временной замены.
undef &grow; # Заглушить жалобы -w на переопределение
*grow = \&expand;
grow(); # Вызвать expand()
}
local *grow - \&shrink, # Только в границах олока
grow(); # Вызывает shrink()
}

Комментарий

В отличие от переменных (но по аналогии с манипуляторами) функции нельзя напрямую присвоить нужное значение. Это всего лишь имя. Однако с ней можно выполнять многие операции, выполняемые с переменными, поскольку вы можете напрямую работать с таблицей символов с помощью тип-глобов вида *foo и добиваться многих интересных эффектов. Если присвоить тип-глобу ссылку, то при следующем обращении к символу данного типа будет использовано новое значение. Именно это делает модуль Exporter при импортировании функции или переменной из одного пакета в другой. Поскольку операции выполняются непосредственно с таблицей символов пакета, они работают только для пакетных (глобальных) переменных, но не для лексических.
*one::var = \%two::Table; # %one::var становится синонимом для %two::Table
*one::big = \&two::small; # &one::big становится синонимом для &two::small
С тип-глобом можно использовать local, но не ту. Из-за local синоним действу-oo т только в границах текущего блока.
local *fred = \&barney; # временно связать &rred с &barney Если значение, присваиваемое тип-глобу, представляет собой не ссылку, а другой тип-глоб, то замена распространяется на все типы с данным именем. Полное присваивание тип-глоба относится к скалярным величинам, массивам, хэшам, функциям, файловым манипуляторам, манипуляторам каталогов и форматам. Следовательно, присваивание *Тор = * Bottom сделает переменную $Тор текущего пакета синонимом для $Bottom, @Тор - для ©Bottom, %Тор - для %Bottom и &Тор -для &Bottom. Замена распространяется даже на соответствующие манипуляторы файлов и каталогов и форматы! Вероятно, это окажется лишним. Присваивание тип-глобов в сочетании с замыканиями позволяет легко и удобно дублировать функции. Представьте, что вам понадобилась функция для генерации HTML-кода, работающего с цветами. Например:
$string = red("careful here");
print $string;
careful here
Функция red выглядит так:
sub red { "@_" }
Если вам потребуются другие цвета, пишется нечто подобное:
sub color_font {
my $color = shift;
return "@)_";
}
sub red { color_font("red", @_)
} sub green { color_font("green", @_)
} sub blue { color_font("blue", @_) } sub purple { color_font("purple", @_) } # И т. д. Сходство функций наводит на мысль, что общую составляющую можно как-то выделить. Для этого следует воспользоваться косвенным присваиванием тпп-глобы. Если вы используете рекомендуемую директиву use strict, сначала отключите strict "refs" для этого блока.
@colors = qw(red blue green yellow orange purple violet);
for my $name (@colors) {
no strict ' refs';
*$name = sub { "@>_" };
}

Функции кажутся независимыми, однако фактически код был откомпилирован лишь один раз. Подобная методика экономит время компиляции и память. Для создания полноценного замыкания все переменные анонимной подпрограммы должны быть лексическими. Именно поэтому переменная цикла объявляется с ключевым словом ту. Перед вами одна из немногочисленных ситуаций, в которых создание прототипа для замыкания оправдано. Если вам захочется форсировать скалярный контекст для аргументов этих функций (вероятно, не лучшая идея), ее можно записать в следующем виде:
*$name = sub ($) { "$_[0]" }; Однако прототип проверяется во время компиляции, поэтому приведенное выше присваивание произойдет слишком поздно и никакой пользы не принесет. Следовательно, весь цикл с присваиваниями следует включить в BEGIN-блок, чтобы форсировать его выполнение при компиляции.

> Смотри также -------------------------------
Описание замыканий в perlref(l); раздел "Symbol tables" perlmod(i); ре-цепты 10.11; 11.4.

10.15. Перехват вызовов неопределенных функций с помощью AUTOLOAD

Проблема

Требуется перехватить вызовы неопределенных функций и достойно обрабо-| ать их.

Решение

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

Комментарий

В подобных ситуациях обычно применяются вспомогательные функции (proxy). При вызове неопределенной функции вместо автоматического инициирования исключения также можно перехватить вызов. Если пакет, к которому принадлежит вызываемая функция, содержит функцию с именем AUTOLOAD, то она будет нызвана вместо неопределенной функции, а специальной глобальной переменной пакета $AUTOLOAD будет присвоено полное имя функции. Затем функция AUTOLOAD (может делать все, что должна была делать исходная функция.
sub AUTOLOAD {
use vars qw($AUTOLOAD);
my $color = $AUTOLOAD;
$color ="o s/.*:://;
return "";
}
# Примечание: функция sub chartreuse не определена
print chartreuseC'stutf");
При вызове несуществующей функции main: : chartreuse вместо инициирования исключения будет вызвана функция main: : AUTOLOAD с аргументами, переданными chartreuse. Пакетная переменная $AUTOLOAD будет содержать строку
main::chartreuse.
Методика с присваиваниями тип-глобов из рецепта 10.14 быстрее и удобнее. Быстрее - поскольку вам не приходится запускать копию и заниматься подстановками. Удобнее - поскольку вы сможете делать следующее:
{
local *yellow = \&violet;
local (*red, *green) = (\&green, \&red);
print_stuff();
}
При работе print_stuff или любой вызванной ей функции все, что должно выводиться желтым цветом, выводится фиолетовым; красный цвет заменяется зеленым, и наоборот. Однако подстановка функций не позволяет обрабатывать вызовы неопределенных функций. AUTOLOAD справляется с этой проблемой.

[> Смотри также --------------------------------
Раздел "Autoloading" perlsub(1); документация по стандартным модулям Auto-Loader и AutoSplit; рецепты 10.12; 12.10; 13.11.

10.16. Вложенные подпрограммы

Проблема

Требуется реализовать вложение подпрограмм, чтобы одна подпрограмма была видна и могла вызываться только из другой. Если попытаться применить очевидный вариант sub FOO { sub BAR { } . . .}, Perl предупреждает о переменных, которые "не останутся общими".

Решение

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

Комментарий

Вероятно, в других языках программирования вам приходилось работать с вложенными функциями, обладающими собственными закрытыми переменными. В Perl для этого придется немного потрудиться. Интуитивная реализация приводит к выдаче предупреждения. Например, следующий фрагмент не работает:
sub outer {
my $x = $_[0] + 35;
sub inner { return $x * 19 } # НЕВЕРНО
return $x + inner();
Обходное решение выглядит так:
sub outer {
my $x = $_[0] + 35;
local *inner = sub { return $x * 19 };
return $x + inner();
} Теперь благодаря временно присвоенному замыканию inner() может вызываться только из outer(). При вызове inner() получает нормальный доступ к лексической переменной $х из области действия outer(). В сущности, мы создаем функцию, которая является локальной для другой функции - подобная возможность не поддерживается в Perl напрямую. Впрочем, ее реализация не всегда выглядит понятно.
> Смотри также -------------------------------

Описание замыканий в perlref(l); раздел "Symbol tables" perlmod(i); рецепты 10.13-10.14.

10.17. Сортировка почты

Программа из примера 10.1 сортирует почтовый ящик по темам. Для этого она читает сообщения по абзацам и ищет абзац, начинающийся с "From:". Когда такой абзац будет найден, программа ищет тему, удаляет из нее все пометки "Re:", преобразует в нижний регистр и сохраняет в массиве @sub. При этом сами сообщения сохраняются в массиве @msgs. Переменная $msgno следит за номером сообщения. Пример 10.1. bysubl
#!/usr/bin/perl
# bysubl - simple sort by subject
my(@msgs, @sub);
my $msgno = -1;
$/=''; # Чтение по абзацам
while (<>) {
if (/-From/m) {
/"Subject:\s*(?:Re:\s*)*(.*)/mi;
$sub[++$msgno] = lc($1) || '';
}
$msgs[$msgno] ,= $_;
} for my $i (sort { $sub[$a] cmp $sub[$b] || $a <=> $b } (0 .. $#msgs)) {
print $msgs[$i];
}
}

В этом варианте сортируются только индексы массивов. Если темы совпадают, cmp возвращает 0, поэтому используется вторая часть | |, в которой номера сообщений сравниваются в порядке их исходного следования. Если функции sort передается список (0,1,2,3), после сортировки будет получена некоторая перестановка - например, (2, 1,3,0). Мы перебираем элементы списка в цикле fo г и выводим каждое сообщение. В примере 10.2 показано, как бы написал эту программу программист с большим опытом работы на awk. Ключ -00 используется для чтения абзацев вместо строк. Пример 10.2. bysub2
#!/usr/bin/perl
# bysub2 - сортировка по темам в стиле awk
BEGIN { $msgno = -1 }
$sub[++$msgno] = (/"Subject:\s*(?:Re:\s*)*(.*)/mi)[0] if /"From/m:
$msg[$msgno] .= $_;
END { print @msg[ sort { $sub[$a]
cmp $sub[$b] || $a <=> $b } (0 .. $ftmsg) ] } Параллельные массивы широко использовались лишь на ранней стадии существования Perl. Более элегантное решение состоит в том, чтобы сохранять сообщения в хэше. Анонимный хэш (см. главу 11) сортируется по каждому полю. Программа из примера 10.3 построена на тех же принципах, что и примеры 10.1 и 10.2. Пример 10.3. bysub3
#!/usr/bin/perl -00
# bysub3 - sort by subject using hash records
use strict;
my @msgs = ();
while (<>) {
push @msgs, {
SUBJECT => /"Subject:\s.(?;Re:\s*)*(.*)/mi, NUMBER => scalar @msgs,
# Номер сообщения TEXT =>
} if /~From/m;
$msgs[-1]{TEXT} ,= $_;
}
for my $msg (sort {
$a->{SUBJECT} cmp $b->{SUBJECT}
$a->{NUMBER} <=> $b->{NUMBER} } @msgs
}
{
print $msg->{TEXT}, }
Работая с полноценными хэшами, нетрудно добавить дополнительные критерии сортировки. Почтовые ящики часто сортируются сначала по теме, а затем по дате сообщения. Основные трудности связаны с анализом и сравнением дат. Модуль Date::Manip помогает справиться с ними и возвращает строку, которую можно сравнивать с другими. Тем не менее программа datesort из примера 10.4, использующая Date::Manip, работает в 10 раз медленнее предыдущей. Анализ дат в непредсказуемых форматах занимает слишком много времени. Пример 10.4. datesort
#!/usr/bln/perl -00
# datesort - сортировка почтового ящика по теме и дате
use strict;
use Date::Manip;
my @msgs = ();
while (<>) {
next unless /"From/m;
my $date = ' ';
if (/"Oate:\s*(.*)/m) {
($date = $1) =~ s/\s+\(.*//;
$date = ParseDate($date);
} push Omsgs, {
SUBJECT => /"Subject:\s*(?:Re:\s*)*(.*)/mi,
DATE => $date, NUMBER => scalar $msgs, TEXT => '' };
} continue {
$msgs[-1]{TEXT} .= $_;
}
for my $msg (sort {
$a->{SUBJECT} cmp $b->{SUBJECT}
$a->{DATE} cmp $b->{DATE}
||
$a->{NUMBER} <=> $b->{NUMBER} }
@msgs ) {
print $msg->{TEXT};
Особого внимания в примере 10.4 заслуживает блок continue. При достижении конца цикла (нормальном выполнении или переходе по next) этот блок выполняется целиком. Он соответствует третьему компоненту цикла for, но не ограничивается одним выражением. Это полноценный блок, который может состоять из нескольких команд.

> Смотри также -------------------------------
Описание функции sort в perlfunc(1); описание переменной $/ в perlvar(1) и во введении главы 8 "Содержимое файлов"; рецепты 3.7, 4.15, 5.9 и 11.9. 


© copyright 2000 Soft group


?????? ???????????