Замыкания в Perl::Журнал СА 2.2006
www.samag.ru
     
Поиск   
              
 www.samag.ru    Web  0 товаров , сумма 0 руб.
E-mail
Пароль  
 Запомнить меня
Регистрация | Забыли пароль?
Журнал "Системный администратор"
Журнал «БИТ»
Подписка
Архив номеров
Где купить
Наука и технологии
Авторам
Рекламодателям
Контакты
   

  Опросы
  Статьи

Дата-центры  

Дата-центры: есть ли опасность утечки данных?

Российские компании уже несколько лет испытывают дефицит вычислительных мощностей. Рост числа проектов,

 Читать далее...

Книжная полка  

Защиты много не бывает

Среди книжных новинок издательства «БХВ» есть несколько изданий, посвященных методам социальной инженерии

 Читать далее...

Событие  

В банке рассола ждет сисадмина с полей фрактал-кукумбер

Читайте впечатления о слете ДСА 2024, рассказанные волонтером и участником слета

 Читать далее...

Организация бесперебойной работы  

Бесперебойная работа ИТ-инфраструктуры в режиме 24/7 Как обеспечить ее в нынешних условиях?

Год назад ИТ-компания «Крок» провела исследование «Ключевые тренды сервисного рынка 2023». Результаты

 Читать далее...

Книжная полка  

Читайте и познавайте мир технологий!

Издательство «БХВ» продолжает радовать выпуском интересных и полезных, к тому же прекрасно

 Читать далее...

СУБД PostgreSQL  

СУБД Postgres Pro

Сертификация по новым требованиям ФСТЭК и роль администратора без доступа к данным

 Читать далее...

Критическая инфраструктура  

КИИ для оператора связи. Готовы ли компании к повышению уровня кибербезопасности?

Похоже, что провайдеры и операторы связи начали забывать о требованиях законодательства

 Читать далее...

Архитектура ПО  

Архитектурные метрики. Качество архитектуры и способность системы к эволюционированию

Обычно соответствие программного продукта требованиям мы проверяем через скоуп вполне себе понятных

 Читать далее...

Как хорошо вы это знаете  

Что вам известно о разработках компании ARinteg?

Компания ARinteg (ООО «АРинтег») – системный интегратор на российском рынке ИБ –

 Читать далее...

Графические редакторы  

Рисование абстрактных гор в стиле Paper Cut

Векторный графический редактор Inkscape – яркий представитель той прослойки open source, с

 Читать далее...

День сисадмина  

Учите матчасть! Или как стать системным администратором

Лето – время не только отпусков, но и хорошая возможность определиться с профессией

 Читать далее...

День сисадмина  

Живой айтишник – это всегда движение. Остановка смерти подобна

Наши авторы рассказывают о своем опыте и дают советы начинающим системным администраторам.

 Читать далее...

Виртуализация  

Рынок решений для виртуализации

По данным «Обзора российского рынка инфраструктурного ПО и перспектив его развития», сделанного

 Читать далее...

Книжная полка  

Как стать креативным и востребованным

Издательский дом «Питер» предлагает новинки компьютерной литературы, а также книги по бизнесу

 Читать далее...

1001 и 1 книга  
19.03.2018г.
Просмотров: 6412
Комментарии: 0
Машинное обучение с использованием библиотеки Н2О

 Читать далее...

12.03.2018г.
Просмотров: 7117
Комментарии: 0
Особенности киберпреступлений в России: инструменты нападения и защита информации

 Читать далее...

12.03.2018г.
Просмотров: 4394
Комментарии: 0
Глубокое обучение с точки зрения практика

 Читать далее...

12.03.2018г.
Просмотров: 3085
Комментарии: 0
Изучаем pandas

 Читать далее...

12.03.2018г.
Просмотров: 3881
Комментарии: 0
Программирование на языке Rust (Цветное издание)

 Читать далее...

19.12.2017г.
Просмотров: 3897
Комментарии: 0
Глубокое обучение

 Читать далее...

19.12.2017г.
Просмотров: 6385
Комментарии: 0
Анализ социальных медиа на Python

 Читать далее...

19.12.2017г.
Просмотров: 3232
Комментарии: 0
Основы блокчейна

 Читать далее...

19.12.2017г.
Просмотров: 3529
Комментарии: 0
Java 9. Полный обзор нововведений

 Читать далее...

16.02.2017г.
Просмотров: 7363
Комментарии: 0
Опоздавших не бывает, или книга о стеке

 Читать далее...

17.05.2016г.
Просмотров: 10724
Комментарии: 0
Теория вычислений для программистов

 Читать далее...

30.03.2015г.
Просмотров: 12444
Комментарии: 0
От математики к обобщенному программированию

 Читать далее...

18.02.2014г.
Просмотров: 14095
Комментарии: 0
Рецензия на книгу «Читаем Тьюринга»

 Читать далее...

13.02.2014г.
Просмотров: 9192
Комментарии: 0
Читайте, размышляйте, действуйте

 Читать далее...

12.02.2014г.
Просмотров: 7141
Комментарии: 0
Рисуем наши мысли

 Читать далее...

10.02.2014г.
Просмотров: 5446
Комментарии: 3
Страна в цифрах

 Читать далее...

18.12.2013г.
Просмотров: 4683
Комментарии: 0
Большие данные меняют нашу жизнь

 Читать далее...

18.12.2013г.
Просмотров: 3495
Комментарии: 0
Компьютерные технологии – корень зла для точки роста

 Читать далее...

04.12.2013г.
Просмотров: 3212
Комментарии: 0
Паутина в облаках

 Читать далее...

03.12.2013г.
Просмотров: 3449
Комментарии: 0
Рецензия на книгу «MongoDB в действии»

 Читать далее...

02.12.2013г.
Просмотров: 3090
Комментарии: 0
Не думай о минутах свысока

 Читать далее...

Друзья сайта  

 Замыкания в Perl

Архив номеров / 2006 / Выпуск №2 (39) / Замыкания в Perl

Рубрика: Программирование /  Автоматизация

Алексей Мичурин

Замыкания в Perl

Замыкания являются одной из тех особенностей Perl, которые редко встретишь в других языках программирования. Да и программисты на Perl зачастую не вдаются в такие дебри. Напрасно. Вы можете не иметь прав и автомобиля, не желать стоять в пробках, но правила дорожного движения должен знать каждый. Точно так же, даже если вы не используете замыканий, знать о них весьма полезно.

Суть замыканий

Суть замыканий состоит в том, что память, занятая под временные переменные, не освобождается дольше, чем соответствующие переменные остаются доступны. Сразу скажу, что возникновение замыканий никак не связано с ошибками в интерпретаторе Perl. Больше того, мы скоро убедимся в том, что интерпретатор «знает» о возникшем замыкании и может уведомить вас, если вы того хотите.

Почему же память освобождается не сразу после исчезновения переменной? Чтобы ответить на этот вопрос, надо сказать пару слов о системе сборки мусора, которой оснащён Perl. Он автоматически освобождает фрагменты памяти, на которые нет ссылок. Переменная может исчезнуть, но ссылка может остаться, тогда Perl не освободит память до тех пор, пока не исчезнет последняя ссылка.

Эта система работы со ссылками противоположна Cподходу, где ссылка хранится независимо от соответствующего фрагмента памяти. Поэтому в небрежно написанных программах на C случается, что ссылка указывает на фрагмент памяти, который уже возвращён системе. Попытка обратиться к информации по этой ссылке всегда приводит к серьёзным сбоям в работе программы.

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

Давайте проиллюстрируем сказанное простым примером:

1: #!/usr/bin/perl

2: sub f {

3:   my $v=shift;

4:   print 'f: \$v='.\$v.'  $v='.$v."\n";

5:   return \$v;

6: }

7: my $a=f(1);

8: print '    $a='.$a.' $$a='.$$a."\n";

Результат работы этого кода таков:

f: $v=SCALAR(0x8057b2c)  $v=1

    $a=SCALAR(0x8057b2c) $$a=1

Выполнение программы начинается с вызова функции f в седьмой строке. В теле функции создаётся временная переменная $v, печатается её адрес и значение. Функция возвращает указатель на переменную $v и завершает работу. После этого переменная $v пропадает. Но корректней было бы сказать не «переменная пропадает», а «имя становится недоступно». Потому что значение переменной $v попрежнему хранится в памяти. Мы больше не можем обратиться к этому значению по имени $v, но указатель на эту область памяти присвоен переменной $a, он существует, а значит Perl не освободит память, занятую ранее переменной $v.

Чтобы окончательно убедиться в этом, напечатаем значения переменной $a и разименуем этот указатель в строке 8. Сравните первую и вторую строки вывода программы. Первая напечатана, когда переменная $v ещё существовала. Вторая – когда переменной $v уже не было. Но, как видите, память, выделенная под значение 1, осталась нетронутой, как и само значение.

Подобная ситуация возникает и при замыкании: временной переменной уже нет, но память не освобождена. Однако замыканиями принято называть более узкий круг «явлений». К ним нас приближает следующий пример:

 1: #!/usr/bin/perl

 2: {

 3:   my $t=10;

 4:   print "   $t=".$t." $t=".$t." ";

 5:   sub f {

 6:     $t=shift if (@_);

 7:     print "f: $t=".$t." $t=".$t." ";

 8:   }

 9: }

10: f();

11: f(20);

12: f();

Суть того, что мы сделали, состоит в следующем. Мы создали блок (строки 2-9) и в нём определили локальную переменную $t. В этом же блоке создана функция f, которая использует переменную $t. Функция может изменять значение переменной $t (если вызвать её с аргументом) и печатает информацию об этой переменной (адрес и значение). Далее мы вызываем функцию f в строках 10-12, то есть за пределами блока, в котором существовала переменная $t. Сможет ли функция f нормально работать, ведь, казалось бы, переменной $t уже нет?

Давайте посмотрим на результат работы этой программы:

   $t=10 $t=SCALAR(0x8057ae4)

f: $t=10 $t=SCALAR(0x8057ae4)

f: $t=20 $t=SCALAR(0x8057ae4)

f: $t=20 $t=SCALAR(0x8057ae4)

Как видите, несмотря на то, что за пределами блока (строки 10-12) переменная $t исчезла и явных ссылок на неё не осталось, память не освободилась, а значение сохранилось. Чтобы убедиться, что с этой памятью можно работать, мы поменяли хранящееся в ней значение с 10 на 20.

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

Если вы поняли, как и почему работает этот пример, то взгляните на образчик классического замыкания:

  1: #!/usr/bin/perl

 2: sub gen {

 3:   my ($name, $count)=@_;

 4:   return sub { print ("$name: ".$count++." ") }

 5: }

 6: my $a=gen("CntA", 10);

 7: my $b=gen("CntB", 20);

 8: &$a();

 9: &$a();

10: &$b();

11: &$b();

12: &$a();

По сути, этот пример очень похож на предыдущий. Блок операторов теперь стал телом функции gen. Вместо переменной $t в нём созданы две локальные переменные $name и $count. Внутри же создаётся не именованная функция, как это было в предыдущем примере (там мы создавали функцию f), а анонимная функция, указатель на которую мы возвращаем как результат работы функции gen. Эта «вложенная» функция, как и в предыдущем примере, использует локальные переменные, которые были объявлены вне её тела и которые будут не видны в основной программе – за пределами функции gen. Таким образом, суть выполняемых операций не изменилась. Единственное принципиальное отличие состоит в том, что раньше наш блок выполнялся только единожды, а теперь мы можем вызывать функцию gen многократно.

Фактически функция gen является генератором функций. Больше того, генерируемые функции используют переменные, которые локальны для gen, но не для самих этих функций. Поэтому переменные $name и $count не исчезают бесследно после окончания работы функции gen – они сохранятся интерпретатором Perl для дальнейшего использования. Как вы понимаете, эти переменные сохранят свои значения и между вызовами полученных анонимных функций.

Так что же делает этот код? В строке 6 мы вызываем генератор. Внутри функции gen инициализируются две локальные переменные $name (значением CntA) и $count (значением 10). Там же создаётся анонимная функция-счётчик, использующая эти переменные, указатель на неё возвращается и присваивается переменной $a. После окончания работы gen имена $name и $count исчезают.

В строке 7 мы вызываем генератор повторно. Внутри функции снова создаются две локальные переменные. Обратите внимание, они создаются заново и память для них отводится новая(!). Им присваиваются соответствующие значения ($name="CntB", $count=20), создаётся новая анонимная функция, которая будет использовать уже эти новые значения, указатель на функцию возвращается, а gen завершает работу, и имена $name и  $count снова уходят в небытие.

В результате вызовов полученных функций в строках 812 мы видим следующий вывод:

CntA: 10

CntA: 11

CntB: 20

CntB: 21

CntA: 12

Мы получили два независимых счётчика, инициализированных разными значениями.

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

Специфика анонимных и именованных функций

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

 1: #!/usr/bin/perl

 2: sub gen {

 3:   my $v=shift;

 4:   return sub {print "ok.\n"}

 5: }

 6: my $a=gen(1);

 7: my $b=gen(2);

 8: print ($a."\n");

 9: &$a();

10: print ($b."\n");

11: &$b();

Здесь вложенная функция не содержит ссылок на локальные переменные. Но мы вызываем функцию gen дважды, и дважды же создаём анонимную функцию. Будут ли созданы две абсолютно одинаковые анонимные функции?

Взгляните на вывод:

CODE(0x8057bbc)

ok.

CODE(0x8057bbc)

ok.

Perl не настолько «прост», он не стал создавать две одинаковые функции, он дважды вернул ссылки на один и тот же код.

Локальная переменная $v в этом примере создана только для того, чтобы лучше читалась преемственность этого и других примеров.

Теперь чуть изменим этот пример. Пусть анонимная функция будет использовать локальную переменную:

 1: #!/usr/bin/perl

 2: sub gen {

 3:   my $v=shift;

 4:   print "gen: $v=".$v." $v=".$v." ";

 5:   return sub {print "$v ok. "}

 6: }

 7: my $a=gen(1);

 8: my $b=gen(2);

 9: print ($a." ");

10: &$a();

11: print ($b." ");

12: &$b();

Мы получили классическое замыкание:

gen: $v=SCALAR(0x8057b2c) $v=1

gen: $v=SCALAR(0x8057d18) $v=2

CODE(0x805716c)

1 ok.

CODE(0x8057d0c)

2 ok.

При двух вызовах gen дважды была создана локальная переменная $v; каждый раз новая в новой области памяти, о чём свидетельствуют первые две строки вывода программы. И дважды родилась новая функция. Как видите, и поведение, и адреса двух полученных анонимных функций отличаются. Собственно, такой пример мы уже видели в предыдущем разделе, но здесь я хотел подчеркнуть, что мы получили именно две разные функции.

Давайте рассмотрим, что будет, если генератор будет создавать именованную функцию. Ещё чуть-чуть изменим пример:

 1: #!/usr/bin/perl

 2: sub gen {

 3:   my $v=shift;

 4:   print "gen: $v=".$v." $v=".$v." ";

 5:   sub t {print "$v ok. "}

 6:   return &t;

 7: }

 8: my $a=gen(1);

 9: my $b=gen(2);

10: print $a." ";

11: &$a();

12: print $b." ";

13: &$b();

Может показаться, что никаких принципиальных изменений мы не сделали. Просто раньше мы сразу возвращали указатель на функцию, а теперь мы сперва создаём функцию t (строка 5), а потом возвращаем указатель (строка 6). Но это «незначительное» изменение коренным образом изменит результат:

gen: $v=SCALAR(0x8057b2c) $v=1

gen: $v=SCALAR(0x805728c) $v=2

CODE(0x8057bc8)

1 ok.

CODE(0x8057bc8)

1 ok.

Как видите, функция t была создана только при первом вызове gen. При повторном вызове функция t не была ни клонирована, ни изменена.

Любопытное поведение? Мы ещё вернёмся к рассмотрению этого случая, а пока сделаем вывод, что замыкание сопровождается рождением новой функции только при создании анонимных функций – только во втором из рассмотренных примеров.

Обратите также внимание на разницу в «поведении» функций и переменных, вызванную тем, что функция всегда глобальна, а все наши переменные локальны. (Кстати, сейчас широко обсуждается возможность создания локальных функций; так что, возможно, в шестой версии Perl мы сможем написать что-то вроде my sub f {...}.)

Нежелательные последствия замыканий

Сперва скажу несколько слов о том, когда же замыкания могут возникнуть без вашего ведома, «сами».

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

Существует множество задач, требующих, чтобы Perl-код компилировался только один раз, но мог выполняться многократно. Эта возможность будоражит умы Perl-программистов, наверное, с первых дней появления Perl. Самая известная среда, в которой реализован такой подход – mod_perl. (В этой статье я буду говорить о mod_perl, имея в виду, в основном, только один его компонент – Apache::Registry, который широко применяется для переноса обычных CGI-скриптов в mod_perl-окружение.) Там скорость выполнения CGI-приложений увеличивается многократно, благодаря тому, что эти приложения компилируются только один раз. Подобные приёмы используются при «кэшировании» некоторых частей кода, содержащих, например, много регулярных выражений, заданных переменными. Известно, что компиляция регулярных выражений – достаточно ресурсоёмкая задача.

Как же реализуется подобное одноразовое компилирование и многократные запуски? В большинстве случаев – очень просто: тело программы включается внутрь некой функции, которая потом может вызываться многократно, естественно, без повторной компиляции, выполняя всё, что должна была делать программа.

Поясню на примере. Допустим, у вас есть программа:

1: #!/usr/bin/perl

2: print "ok. ";

«Кэшировать» её можно следующим образом:

1: #!/usr/bin/perl

2: sub run {

3:   #!/usr/bin/perl

4:   print "ok. ";

5: }

6: # далее, сколько угодно

7: # вызовов функции run

Подобный подход обладает массой известных ограничений. Например, в программах нельзя использовать конструкции __END__ и __DATA__, вызывать функции типа exit, осторожно надо обращаться с глобальными переменными $^T, %ENV, $/ и прочими, не злоупотреблять модификатором /o в регулярных выражениях, аккуратно работать с файлами... Многие «оплошности», вполне простительные в обычных условиях, способны вызвать странные ошибки при работе программы в mod_perl-образном контексте. Но больше всего недоразумений вызывают замыкания.

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

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

Взгляните на следующую программу:

1: #!/usr/bin/perl

2: my $c=1;

3: sub incr { print("$c=".$c++." ") }

4: incr();

5: incr();

6: incr();

Если мы запустим её дважды, то увидим вполне закономерный и предсказуемый результат:

$c=1

$c=2

$c=3

$c=1

$c=2

$c=3

По три строчки от каждого из двух вызовов.

Но давайте представим, что эта программа оказалась в контексте, аналогичном тому, который создаёт mod_perl:

 1: #!/usr/bin/perl

 2: sub run {

 3:  my $c=1;

 4:  sub incr { print("$c=".$c++." ") }

 5:  incr();

 6:  incr();

 7:  incr();

 8: }

 9: print "step 1: ";

10: run();

11: print "step 2: ";

12: run();

Справедливости ради следует отметить, что здесь я несколько упростил ситуацию. В реальной жизни приходится избегать конфликтов имён, поэтому вместо простой функции в mod_perl, точнее в модуле Apache::Registry, используются методы, разнесённые в разные пространства имён – как бы в разные модули.

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

step 1:

$c=1

$c=2

$c=3

step 2:

$c=4

$c=5

$c=6

Если вы тоже обескуражены, то вернитесь к концу предыдущего раздела и сравните этот код с примером, который мы обсуждали там. По сути мы имеем одно и то же. Здесь функция incr тоже будет создана только при первом запуске run. При повторном вызове run, incr никак не изменится, Perl, фактически, просто проигнорирует строку 4.

Это, наверное, самые неприятные сюрпризы, возникающие из-за замыканий. С точки зрения Perl, никаких ошибок и нет, поэтому Perl просто выполняет то, что написано. Ни опция -w, ни прагма strict не помогут вам обнаружить эту ошибку.

Однако Perl имеет средства, позволяющие это сделать. Их мы ещё рассмотрим.

А сейчас давайте вернёмся к утечкам памяти. Perl достаточно «умён», чтобы не допустить никаких утечек. Если ссылка на функцию пропадает, то Perl удалит из памяти не только код, но и все переменные, замкнутые на него.

В следующем примере никаких утечек не будет:

1: #!/usr/bin/perl

2:

3: sub a {

4:   my $v=0;

5:   sub b { $v=1 }

6: }

7:

8: while (1) {a}

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

Не будет утечки в следующем примере:

1: #!/usr/bin/perl

2:

3: sub a {

4:   my $v=0;

5:   return sub { $v=1 }

6: }

7:

8: while (1) { my $f=a() }

Здесь мы сохраняем указатель на анонимную функцию, но в локальной переменной. Как только переменная исчезает – уничтожается и функция со всеми её замыканиями.

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

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

1: #!/usr/bin/perl

2: sub a { my $v; $v = sub { $v = 1 } }

3: a() while (1);

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

Если вы запустите эту программу, то уже через несколько минут (или даже секунд) почувствуете острую нехватку памяти.

Только в последнем примере мы получили полноценную утечку памяти. Возможна ли такая патологическая ситуация в реальной жизни? В результате ошибки – конечно. При организации рекурсий – возможно. Но если рекурсия организована правильно и вовремя останавливается, то и подобная ситуация не возникнет.

Понятно, что такого рода утечки могут быть вызваны и более сложными связями. Например, в функции может использоваться хэш, в одном из элементов которого хранится ссылка на массив, в котором хранится ссылка на функцию. Могут возникать ситуации, когда набор функций A обеспечивает сохранность функций B, а те, в свою очередь, отвечают взаимностью. Фантазировать можно бесконечно, но суть везде остаётся одной и той же.

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

Диагностика замыканий

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

use diagnostics;

во все скрипты, которые могут запускаться через mod_perl или другой кэширующий механизм. Кстати, если вы компилируете mod_perl самостоятельно, очень рекомендую задать опцию «PERL_DEBUG=1», тогда вся диагностическая информация будет выдаваться и без прагмы diagnostics.

При включённой диагностике и при замыкании в именованной функции Perl выдаёт весьма развёрнутое объяснение ситуации (рис. 1).

Variable "$tmp" will not stay shared at ./example.pl line 7 (#1)

    (W closure) An inner (nested) named subroutine is referencing a

    lexical variable defined in an outer subroutine.

 

    When the inner subroutine is called, it will probably see the value of

    the outer subroutine’s variable as it was before and during the *first*

    call to the outer subroutine; in this case, after the first call to the

    outer subroutine is complete, the inner and outer subroutines will no

    longer share a common value for the variable.  In other words, the

    variable will no longer be shared.

 

    Furthermore, if the outer subroutine is anonymous and references a

    lexical variable outside itself, then the outer and inner subroutines

    will never share the given variable.

 

    This problem can usually be solved by making the inner subroutine

    anonymous, using the sub {} syntax.  When inner anonymous subs that

    reference variables in outer subroutines are called or referenced, they

    are automatically rebound to the current values of such variables.

Рисунок 1. Perl выдаёт весьма развёрнутое объяснение ситуации

Если вы возьмёте на себя труд перевести это послание, то обнаружите, что в нём просто кратко изложены основные мысли этой статьи.

Как избежать замыканий

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

Если вы хотите изменить в теле функции значение «внешней» переменной, то можете воспользоваться одним из множества средств, предоставляемых Perl. Можно передать ссылку на внешнюю переменную:

my $a=0;

sub incr {

  my $v=shift;

  $$v++;

}

incr \$a;

Этот пример можно сделать более изящным, если использовать прототипы функций:

my $a=0;

sub incr (\$) { # <- прототип

  my $v=shift;

  $$v++;

}

incr $a; # <- слэш больше не нужен

Можно непосредственно работать с элементами массива аргументов @_. Это тоже позволяет изменить «внешние» переменные:

my $a=0;

sub incr {

  $_[0]++;

}

incr $a;

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

Если же от работы с глобальными переменными отказаться нельзя (трудно? лениво?), то можно использовать не my-переменные, а our-переменные. Давайте чуть модифицируем наш пример, имитирующий работу в mod_perl-контексте:

 1: #!/usr/bin/perl

 2: sub run {

 3:  our $c=1; # <-- my заменено на our

 4:  sub incr { print('$c='.$c++."\n") }

 5:  incr();

 6:  incr();

 7:  incr();

 8: }

 9: print "step 1:\n";

10: run();

11: print "step 2:\n";

12: run();

Теперь он будет выдавать «правильный» результат:

step 1:

$c=1

$c=2

$c=3

step 2:

$c=1

$c=2

$c=3

Аналогичного эффекта можно достичь, если использовать простые глобальные переменные:

 3:  $c=1; # <-- это уже не my-переменная

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

Можно использовать глобальные переменные с указанием полных имён:

 3:  $main::c=1;

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

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

Надеюсь, что читатель уже не считает замыкания недоработкой Perl. Они так же логичны, как «самооживление» переменных (autovivification) и другие механизмы Perl. Но можно ли использовать замыкания для своей пользы? Конечно!

Вы уже видели, что замыкания позволяют генерировать функции, варьируя некоторые аспекты работы этих функций. Причём перекомпиляция кода при этом фактически не происходит; то есть новые функции создаются практически мгновенно. Поэтому замыкания с успехом применяются для создания множества обработчиков событий при программировании приложений, основанных на событиях. Например, вы можете разработать библиотеку для создания графического пользовательского интерфейса. Каждому новому элементу «флажок» будет отвечать собственная функция-обработчик. Эти функции должны быть схожи, но допускать некоторые отличия (цвет, форма, начальное положение и прочее). Здесь уместно применить замыкания.

Проиллюстрируем сказанное:

 1: #!/usr/bin/perl

 2: sub new {

 3:   my $step=shift;

 4:   my $c=0;

 5:   return sub {$c+=$step},

 6:          sub {print "count=$c (step=$step) "};

 7: }

 8: my ($incr1, $print1)=new(1);

 9: my ($incr2, $print2)=new(100);

10: &$print1;

11: &$print2;

12: &$incr1;

13: &$incr1;

14: &$incr2;

15: &$incr2;

16: &$print1;

17: &$print2;

Здесь функция new создаёт две анонимные функции: первая – увеличивает счётчик, вторая – печатает значение счётчика.

Каждая создаваемая пара функций отличается шагом счётчика ($step), который задаётся как аргумент «конструктора» new при создании нового счётчика. Все создаваемые счётчики, естественно, работают независимо друг от друга.

Мы создаём две пары функций (два счётчика; строки 8, 9). Печатаем значения счётчиков (строки 10, 11). По два раза для каждого счётчика делаем инкремент (строки 12-15). И снова получаем значения счётчиков.

Запустив программу, можно убедиться, что всё работает правильно:

count=0 (step=1)

count=0 (step=100)

count=2 (step=1)

count=200 (step=100)

Этому коду можно придать и более объектно-ориентированный вид:

 1: #!/usr/bin/perl

 2: sub new {

 3:   my $step=shift;

 4:   my $c=0;

 5:   my $incr=sub {$c+=$step};

 6:   my $print=sub {print "count=$c (step=$step) "};

 7:   return {"incr" => $incr, "print’"=> $print}

 8: }

 9: my $c1=new(1);

10: my $c2=new(100);

11: $c1->{print}->();

12: $c2->{print}->();

13: $c1->{incr}->();

14: $c2->{incr}->();

15: $c1->{incr}->();

16: $c2->{incr}->();

17: $c1->{print}->();

18: $c2->{print}->();

Здесь функция new возвращает не две функции, а хэш, состоящий из пары функций, которые нам уже хорошо знакомы. То есть мы создаём не набор разрозненных методов, а единый «объект».

Результат работы этого кода, естественно, будет точно таким же, как и  предыдущего.

Обратите внимание, строки 5 и 6 должны обязательно заканчиваться точкой с запятой (довольно редкая ситуация: после закрывающей фигурной скобки обязательно требуется точка с запятой).

Заметьте также, что создаваемые функции должны быть строго анонимны. Если вы внесёте в код следующие изменения, то он будет работать иначе и, скорее всего, не так, как вы ожидаете.

...

 5:   sub incr {$c+=$step};

 6:   sub prnt {print "count=$c (step=$step) "};

 7:   return {"incr" => &incr, "print" => &prnt}

...

Читатель, конечно, спросит: «А почему бы нам не использовать просто ОО подход вместо замыканий?» Пожалуйста! Но всем известно, что пока объекты в Perl достаточно неповоротливы. Мой опыт показывает, что в некоторых ситуациях замыкания позволяют иногда получить выигрыш в быстродействии в полтора-два раза. Этот выигрыш становится особенно заметен, если методы выполняют короткие и простые операции.

Тех, кто сейчас скажет: «Ну уж я никогда не буду использовать в своих программах таких изысков», я спешу заверить, что большинство программистов на Perl постоянно используют замыкания, так как многие модули используют это средство. Достаточно сказать, что замыкания есть в модуле DBI, который широко применяется при программировании интерфейсов к базам данных.

Кроме того, считаться с замыканиями вам придётся, если вы захотите создать собственный модуль. Чтобы обеспечить его универсальность, вы должны обязательно учесть, что он может быть использован и в mod_perl-контексте.

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


Комментарии отсутствуют

Добавить комментарий

Комментарии могут оставлять только зарегистрированные пользователи

               Copyright © Системный администратор

Яндекс.Метрика
Tel.: (499) 277-12-45
E-mail: sa@samag.ru