Функция (или подпрограмма) в Perl — это обособленная, инкапсулированная единица поведения. Программа — это набор маленьких чёрных ящиков, в котором взаимодействие функций руководит потоком управления. Функция может иметь имя. Она может потреблять входную информацию. Она может генерировать выходную информацию.
Функции — первичный механизм абстракции, инкапсуляции и повторного использования в Perl 5.
Используйте встроенную директиву sub
для объявления функции:
sub greet_me { ... }
Теперь функция greet_me()
доступна для вызова в любом месте программы.
Вы не обязаны определять функцию в той точке, в которой вы её объявляете. Предварительное объявление указывает Perl запомнить имя функции, несмотря на то, что вы объявите её позже:
sub greet_sun;
Используйте постфиксные (Фиксность) круглые скобки и имя функции для вызова этой функции и передачи необязательного списка аргументов:
greet_me( 'Jack', 'Brad' );
greet_me( 'Snowy' );
greet_me();
Хотя эти скобки не являются строго обязательными для этих примеров — даже с включенной прагмой strict
— они обеспечивают ясность для читателей и парсера Perl. Если есть сомнения, используйте их.
Аргументы функций могут быть произвольными выражениями, включая простые переменные:
greet_me( $name );
greet_me( @authors );
greet_me( %editors );
…хотя стандартная обработка параметров в Perl 5 иногда удивляет новичков.
Функция получает свои параметры в едином массиве, @_
(Подразумеваемая переменная-массив). Perl разглаживает все входные параметры в единый список. Функция должна либо распаковать все параметры в переменные, или работать с @_
напрямую:
sub greet_one
{
my ($name) = @_;
say "Hello, $name!";
}
sub greet_all
{
say "Hello, $_!" for @_;
}
Большая часть кода использует shift
или распаковку списка, однако @_
ведёт себя как обычный массив, так что вы можете ссылаться на отдельные элементы по индексу:
sub greet_one_shift
{
my $name = shift;
say "Hello, $name!";
}
sub greet_two_no_shift
{
my ($hero, $sidekick) = @_;
say "Well if it isn't $hero and $sidekick. Welcome!";
}
sub greet_one_indexed
{
my $name = $_[0];
say "Hello, $name!";
# or, less clear
say "Hello, $_[0]!";
}
… или использовать shift
, unshift
, push
, pop
, splice
и slice
с @_.
Вспомните, что встроенные функции работы с массивами используют @_
как операнд по умолчанию внутри функций. Пользуйтесь этой идиомой.
Присваивание скалярного параметра из @_
требует использования shift
, доступа по индексу к @_
или скобок списочного контекста в левом значении. В противном случае, Perl 5 с удовольствием вычислит для вас @_
в скалярном контексте и присвоит количество переданных параметров:
sub bad_greet_one
{
my $name = @_; # бажно
say "Hello, $name; you look numeric today!"
}
Списочное присваивание нескольких параметров зачастую яснее, чем несколько строчек shift
. Сравните:
sub calculate_value
{
# несколько вызовов shift
my $left_value = shift;
my $operation = shift;
my $right_value = shift;
...
}
…и:
sub calculate_value
{
my ($left_value, $operation, $right_value) = @_;
...
}
Иногда требуется извлечь несколько параметров из @_
, а остальные передать в другую функцию:
sub delegated_method
{
my $self = shift;
say 'Calling delegated_method()'
$self->delegate->delegated_method( @_ );
}
Используйте shift
, если ваша функция требует только одного параметра. Используйте списочное присваивание для доступа к нескольким параметрам.
Несколько дистрибутивов из CPAN расширяют обработку параметров в Perl 5 дополнительным синтаксисом и опциями. signatures
и Method::Signatures
очень мощны. Method::Signatures::Simple
— прост, но удобен. MooseX::Method::Signatures
отлично работает с Moose (Moose).
Разглаживание параметров в @_
происходит на стороне вызывающего кода вызова функции. Передача хеша в качестве аргумента генерирует список пар ключ/значение:
my %pet_names_and_types = (
Lucky => 'dog',
Rodney => 'dog',
Tuxedo => 'cat',
Petunia => 'cat',
);
show_pets( %pet_names_and_types );
sub show_pets
{
my %pets = @_;
while (my ($name, $type) = each %pets)
{
say "$name is a $type";
}
}
Когда Perl разглаживает %pet_names_and_types
в список, порядок пар ключ/значение из хеша будет различаться, но список всегда будет включать значение, следующее сразу за своим ключом. Присваивание хеша внутри show_pets()
работает по существу также, как более явное присваивание %pet_names_and_types
.
Разглаживание часто полезно, но остерегайтесь смешивать в списке параметров скаляры с разглаживаемыми агрегатными переменными. При написании функции show_pets_of_type()
, в которой один параметр — это тип животного для вывода, передавайте этот тип как первый аргумент (или используйте pop
для удаления его из конца @_
):
sub show_pets_by_type
{
my ($type, %pets) = @_;
while (my ($name, $species) = each %pets)
{
next unless $species eq $type;
say "$name is a $species";
}
}
my %pet_names_and_types = (
Lucky => 'dog',
Rodney => 'dog',
Tuxedo => 'cat',
Petunia => 'cat',
);
show_pets_by_type( 'dog', %pet_names_and_types );
show_pets_by_type( 'cat', %pet_names_and_types );
show_pets_by_type( 'moose', %pet_names_and_types );
Списочное присваивание агрегатной переменной всегда проявляет жадность, поэтому присваивание %pets
проглатывает все оставшиеся значения из @_
. Если параметр $type
находится в конце @_
, Perl предупредит о присваивании нечётного числа элементов хешу. Вы можете обойти это:
sub show_pets_by_type
{
my $type = pop;
my %pets = @_;
...
}
…ценой ясности. Тот же самый принцип, конечно же, применим к присваиванию массиву как параметру. Используйте ссылки (Ссылки) чтобы избежать нежелательного разглаживания и проглатывания агрегатных значений.
@_
имеет тонкость; он создаёт псевдонимы для аргументов функции, так что вы можете модифицировать их напрямую. Например:
sub modify_name
{
$_[0] = reverse $_[0];
}
my $name = 'Orange';
modify_name( $name );
say $name;
# выводит egnarO
Модифицируйте элемент @_
напрямую, и вы модифицируете исходный параметр. Будьте осторожны, и тщательно распаковывайте @_
.
Каждая функция содержится в некотором пространстве имён (Пакеты). Функции в необъявленном пространстве имён — функции, объявление которых не расположено после явной директивы package
— находятся в пространстве имён main
. Кроме того, вы можете объявить функцию в другом пространстве имён, добавив префикс к её имени:
sub Extensions::Math::add {
...
}
Это объявит функцию и создаст необходимое пространство имён. Помните, что пакеты в Perl 5 открыты для изменений в любой точке. Вы можете объявить только одну функцию с определённым именем в одном пространстве имён. В противном случае Perl 5 выдаст предупреждение о переопределении функции. Отключите эти предупреждения с помощью no warnings 'redefine'
— если точно уверены, что это именно то, что вам нужно.
Вызов функций в других пространствах имён осуществляется с помощью их полностью определённых имён:
package main;
Extensions::Math::add( $scalar, $vector );
Функции в пространствах имён видимы снаружи этих пространств имён по их полностью определённым именам. Внутри пространства имён вы можете использовать короткие имена для доступа к любой функции, объявленной в этом пространстве имён. Также вы можете импортировать имена из других пространств имён.
При загрузке модуля с помощью встроенной директивы use
(Модули), Perl автоматически вызывает метод import()
этого модуля. Модули могут предоставлять свои собственные методы import()
, делающие некоторые или все определённые символы доступными в вызывающем пакете. Любые аргументы после имени модуля в директиве use
будут переданы в метод import()
модуля. Так:
use strict;
…загружает модуль strict.pm и вызывает strict->import()
без аргументов, тогда как:
use strict 'refs';
use strict qw( subs vars );
…загружает модуль strict.pm, вызывает strict->import( 'refs' )
, а затем вызывает strict->import( 'subs', vars' )
.
use
имеет специальное поведение по отношению к import()
, но вы можете вызвать import()
и напрямую. Пример использования use
эквивалентен следующему:
BEGIN
{
require strict;
strict->import( 'refs' );
strict->import( qw( subs vars ) );
}
Встроенная директива use
добавляет неявный блок BEGIN
вокруг этих выражений, так что вызов import()
происходит сразу после того, как парсер скомпилирует всю директиву use
. Это гарантирует, что любые импортируемые символы будут видимы при компиляции остальной части программы. В противном случае любые функции, импортированные из других модулей, но не объявленные в текущем файле, будут выглядеть как голые слова, тем самым нарушая режим strict
.
Внутри функции проверить контекст её вызова можно с помощью встроенной функции caller
. Если не передано никаких аргументов, она возвращает список из трёх элементов, содержащий имя вызывающего пакета, имя файла, содержащего вызов, и номер строки файла, на которой произошёл вызов:
package main;
main();
sub main
{
show_call_information();
}
sub show_call_information
{
my ($package, $file, $line) = caller();
say "Called from $package in $file:$line";
}
Для инспектирования доступна полная цепочка вызовов. Передайте единственный целочисленный аргумент n в caller()
чтобы просмотреть вызов вызова вызова n раз. Другими словами, если бы show_call_information()
использовала caller(0)
, то получила бы информацию о вызове из main()
. Если же она использовала бы caller(1)
, то получила бы информацию о вызове из начала программы.
Этот необязательный аргумент также требует от caller
предоставить дополнительные возвращаемые значения, включая имя функции и контекст вызова:
sub show_call_information
{
my ($package, $file, $line, $func) = caller(0);
say "Called $func from $package in $file:$line";
}
Стандартный модуль Carp
успешно использует эту технику для оповещения об ошибках и выбрасывания предупреждений в функциях. При использовании вместо die
в коде библиотеки, croak()
выбрасывает исключение с точки зрения своего вызывающего кода. carp()
сообщает о предупреждении из файла, сообщая номер строки своего вызывающего кода (Генерация предупреждений).
Это поведение наиболее полезно при валидации параметров и предварительных условий фукнций, для указания того, что вызывающий код в чём-то неверен.
Хотя Perl старается как может делать то, что имеет ввиду программист, он предоставляет немного нативных способов проверки валидности аргументов, передаваемых в функции. Для определения того, что количество параметров, переданных в функцию, корректно, вычислите @_
в скалярном контексте:
sub add_numbers
{
croak 'Expected two numbers, received: ' . @_
unless @_ == 2;
...
}
Проверка типа более трудна из-за свойственного Perl оператор-ориентированного преобразования типов (Контекст). Модуль Params::Validate
из CPAN предлагает больше точности.
Функции — фундамент многих продвинутых возможностей Perl.
Встроенные функции Perl 5 знают, вызвали ли вы их в пустом, скалярном или списочном контексте. Как могут и ваши функции. Неудачно названная (footnote: См. perldoc -f wantarray
для подтверждения.) директива wantarray
возвращает undef
для обозначения пустого контекста, ложное значение для обозначения скалярного контекста и истинное значение для обозначения списочного контекста.
sub context_sensitive
{
my $context = wantarray();
return qw( List context ) if $context;
say 'Void context' unless defined $context;
return 'Scalar context' unless $context;
}
context_sensitive();
say my $scalar = context_sensitive();
say context_sensitive();
Это может быть полезно для функций, которые могут производить дорогие возвращаемые значения, чтобы избежать этого в пустом контексте. Некоторые идиоматические функции возвращают список в списочном контексте и первый элемент списка или ссылку на массив в скалярном контексте. Но помните, что для использования wantarray
не существует единых наилучших рекомендаций. Иногда понятнее будет написать отдельные однозначные функции.
Дистрибутивы Want
Робина Хьюстона (Robin Houston) и Contextual::Return
Демиена Конвея (Damian Conway) из CPAN предлагают множество возможностей для написания мощных и практичных интерфейсов, учитывающих контекст.
Предположим, вы хотите найти элемент в отсортированном массиве. Вы можете в поисках цели обойти в цикле каждый элемент массива отдельно, но в среднем вам придётся проверить половину элементов в массиве. Другой подход — разделить массив пополам, выбрать элемент посередине, сравнить, затем повторить либо с нижней, либо с верхней половиной. Разделяй и властвуй. Когда закончатся элементы для проверки или будет найден искомый, остановитесь.
Автоматизированный тест для этой техники может быть таким:
use Test::More;
my @elements =
(
1, 5, 6, 19, 48, 77, 997, 1025, 7777, 8192, 9999
);
ok elem_exists( 1, @elements ),
'found first element in array';
ok elem_exists( 9999, @elements ),
'found last element in array';
ok ! elem_exists( 998, @elements ),
'did not find element not in array';
ok ! elem_exists( -1, @elements ),
'did not find element not in array';
ok ! elem_exists( 10000, @elements ),
'did not find element not in array';
ok elem_exists( 77, @elements ),
'found midpoint element';
ok elem_exists( 48, @elements ),
'found end of lower half element';
ok elem_exists( 997, @elements ),
'found start of upper half element';
done_testing();
Рекурсия — обманчиво простое понятие. Каждый вызов функции в Perl создаёт новый фрейм вызова, внутреннюю структуру данных, представляющую сам вызов, включая лексическое окружение текущего вызова функции. Это значит, что функция может вызвать сама себя, рекурсивно.
Чтобы добиться прохождения предыдущего теста, напишите функцию под названием elem_exists()
, которая знает, как вызывать себя, каждый раз деля список пополам:
sub elem_exists
{
my ($item, @array) = @_;
# прервать рекурсию, если не осталось элементов для поиска
return unless @array;
# смещаемся вниз при нечётном количестве элементов
my $midpoint = int( (@array / 2) - 0.5 );
my $miditem = $array[ $midpoint ];
# возвращаем истину если элемент найден
return 1 if $item == $miditem;
# возвращаем ложь, если остался только один элемент
return if @array == 1;
# разбиваем массив надвое и продолжаем рекурсивно
return elem_exists(
$item, @array[0 .. $midpoint]
) if $item < $miditem;
# разбиваем массив надвое и продолжаем рекурсивно
return elem_exists(
$item, @array[ $midpoint + 1 .. $#array ]
);
}
Хотя вы можете написать этот код в процедурном виде и самостоятельно управлять делением списка, этот рекурсивный подход позволяет Perl управлять процессом.
Каждый новый вызов функции создаёт собственный экземпляр лексической области видимости. Даже несмотря на то, что объявление elem_exists()
создаёт единственную область видимости для лексических переменных $item
, @array
, $midpoint
и $miditem
, каждый вызов elem_exists()
— даже рекурсивный — сохраняет значения этих лексических переменных отдельно.
elem_exists()
не только может вызывать саму себя, но и лексические переменные каждого вызова будут защищены и разделены:
use Carp 'cluck';
sub elem_exists
{
my ($item, @array) = @_;
cluck "[$item] (@array)";
# далее следует другой код
...
}
Один из недостатков рекурсии — то, что вы должны корректно указать условия возврата, иначе ваша функция вызовет себя саму бесконечное количество раз. По этой причине функция elem_exists()
имеет несколько директив return
.
Perl выводит полезное предупреждение Deep recursion on subroutine
, если подозревает неконтролируемую рекурсию. Ограничение в 100 рекурсивных вызовов произвольно, но зачастую удобно. Отключите это предупреждение, указав no warnings 'recursion'
в области видимости рекурсивного вызова.
Так как каждый вызов функции требует нового фрейма вызова и пространства для хранения лексических переменных, глубокорекурсивный код может использовать больше памяти, чем итеративный код. Устранение хвостовых вызовов может помочь.
Хвостовой вызов — это вызов функции, который напрямую возвращает результаты этой функции. Эти рекурсивные вызовы elem_exists()
:
# разбиваем массив надвое и продолжаем рекурсивно
return elem_exists(
$item, @array[0 .. $midpoint]
) if $item < $miditem;
# разбиваем массив надвое и продолжаем рекурсивно
return elem_exists(
$item, @array[ $midpoint + 1 .. $#array ]
);
…кандидаты для устранения хвостовых вызовов. Эта оптимизация позволит избежать возврата в текущий вызов и последующего возврата в родительский вызов. Вместо этого, она возвращается в родительский вызов напрямую.
К сожалению, Perl 5 не устраняет хвостовые вызовы автоматически. Это делается вручную с помощью специальной формы встроенной директивы goto
. В отличие от формы, зачастую производящей спагетти-код, функциональная форма goto
заменяет текущий вызов функции вызовом другой функции. Вы можете указывать функцию с помощью имени или ссылки. Для передачи аргументов присваивайте напрямую в @_
:
# разбиваем массив надвое и продолжаем рекурсивно
if ($item < $miditem)
{
@_ = ($item, @array[0 .. $midpoint]);
goto &elem_exists;
}
# разбиваем массив надвое и продолжаем рекурсивно
else
{
@_ = ($item, @array[$midpoint + 1 .. $#array] );
goto &elem_exists;
}
Иногда оптимизации довольно уродливы.
Perl 5 всё ещё поддерживает старомодный вызов функций, полученный в наследство от старших версий Perl. Тогда как сейчас вы можете вызывать Perl-функции по имени, предыдущие версии Perl требовали их вызова с использованием ведущего символа амперсанда (&
). Perl 1 требовал использования встроенной директивы do
:
# устаревший стиль; избегайте
my $result = &calculate_result( 52 );
# стиль Perl 1; избегайте
my $result = do calculate_result( 42 );
# сумасшедшая мешанина; в самом деле избегайте, правда
my $result = do &calculate_result( 42 );
Кроме того, что этот рудиментарный синтаксис — визуальный хаос, форма с ведущим амперсандом имеет несколько неожиданных поведений. Во-первых, она отключает любую проверку прототипа. Во-вторых, она неявно передаёт содержимое @_
немодифицированным, если вы сами не укажете аргументы. И то, и другое может приводить к неожиданному поведению.
Последняя ловушка проистекает из опускания скобок из вызова фукнции. Парсер Perl 5 использует несколько эвристических методов для разрешения неоднозначных голых слов и количества параметров, передаваемых в функцию. Эвристики могут ошибаться:
# внимание; содержит хитрый баг
ok elem_exists 1, @elements, 'found first element';
Вызов elem_exists()
поглотит описание теста, которое должно было быть вторым аргументом ok()
. Так как elem_exists()
использует проглатывающий второй параметр, это может пройти незамеченным до тех пор, пока Perl не выдаст предупреждение о сравнении нечислового значения (описание теста, которое он не может сконвертировать в число) с элементом в массиве.
Хотя лишние скобки можут препятствовать читаемости, обдуманное использование скобок может прояснить код и сделать коварные баги маловероятными.
Область видимости в Perl обозначает продолжительность жизни и видимость именованных сущностей. В Perl всё, что имеет имя (переменная, функция), имеет область видимости. Ограничение области видимости помогает усилить инкапсуляцию — сохранение связанных понятий вместе и предотвращение их вытекания наружу.
Лексическая область видимости — это область видимости, видимая по ходу чтения программы. Компилятор Perl определяет эту область видимости во время компиляции. Блок, ограниченный фигурными скобками, создаёт новую область видимости, будь это голый блок, блок конструкции цикла, блок объявления sub
, блок eval
или любой другой блок, не заключающий в кавычки.
Лексическая область видимости определяет видимость переменных, объявленных с помощью my
— лексических переменных. Лексическая переменная, объявленная в одной области видимости, видима в этой области видимости и любых областях видимости, вложенных в неё, но невидима в областях видимости, находящихся на том же уровне вложенности или выше:
# внешняя лексическая область видимости
{
package Robot::Butler
# внутренняя лексическая область видимости
my $battery_level;
sub tidy_room
{
# более глубокая вложенная область видимости
my $timer;
do {
# самая глубокая лексическая область видимости
my $dustpan;
...
} while (@_);
# лексическая область видимости, находящаяся на том же уровне
for (@_)
{
# отдельная самая глубокая лексическая область видимости
my $polish_cloth;
...
}
}
}
…переменная $battery_level
видима во всех четырёх областях видимости. $timer
видима в методе, блоке do
и цикле for
. $dustpan
видима только в блоке do
, а $polish_cloth
— только в цикле for
.
Объявление во внутренней области видимости лексической переменной с тем же именем, что и лексическая переменная во внешней области видимости, скрывает, или затеняет, внешнюю переменную во внутренней области видимости. Зачастую это именно то, что вам нужно:
my $name = 'Jacob';
{
my $name = 'Edward';
say $name;
}
say $name;
Лексическое затенение может происходить по случайности. Ограничивайте область видимости переменных и вложенность областей видимости для уменьшения риска.
Эта программа выводит Edward
и затем Jacob
(footnote: Члены семьи, а не вампиры.), несмотря на то, что переобъявление лексической переменной с тем же именем и типом в той же самой лексической области видимости приводит к предупреждающему сообщению. Затенение лексической переменной — особенность инкапсуляции.
Некоторые лексические объявления имеют тонкости, как, например, лексическая переменная, используемая как переменная-итератор в цикле for
. Её объявление находится снаружи блока, но её область видимости — внутри блока цикла:
my $cat = 'Brad';
for my $cat (qw( Jack Daisy Petunia Tuxedo Choco ))
{
say "Iterator cat is $cat";
}
say "Static cat is $cat";
Аналогично, given
(given/when) создаёт лексический топик (как my $_
) внутри своего блока:
$_ = 'outside';
given ('inner')
{
say;
$_ = 'this assignment does nothing useful';
}
say;
…так что выход из блока восстанавливает предыдущее значение $_
.
Функции — именованные и анонимные — создают лексическую область видимости для своих тел. Это помогает созданию замыканий (Замыкания).
Внутри заданной области видимости можно объявить псевдоним переменной пакета с помощью встроенной директивы our
. Как и my
, our
обеспечивает лексическую область видимости псевдонима. Полностью определённое имя доступно из любого места, но лексический псевдоним виден только внутри своей области видимости.
our
наиболее полезна в применении к глобальным переменным пакета, таким как $VERSION
и $AUTOLOAD
.
Динамическая область видимости похожа на лексическую область видимости по своим правилам видимости, но вместо поиска наружу в области видимости времени компиляции, поиск проходит назад через контекст вызова. В то время как глобальная переменная пакета может быть видима внутри всех областей видимости, её значение изменяется в зависимости от локализации (local
) и присваивания:
our $scope;
sub inner
{
say $scope;
}
sub main
{
say $scope;
local $scope = 'main() scope';
middle();
}
sub middle
{
say $scope;
inner();
}
$scope = 'outer scope';
main();
say $scope;
Программа начинается с объявления our
-переменной, $scope
, а так же трёх функций. Заканчивается она присваиванием $scope
и вызовом main()
.
Внутри main()
программа выводит текущее значение $scope
, outer scope
, затем локализует переменную с помощью local
. Это изменяет видимость символа внутри текущей лексической области видимости, так же как и в любых функциях, вызываемых из текущей лексической области видимости. Таким образом, $scope
содержит main() scope
внутри тела как middle()
, так и inner()
. После возврата из main()
, когда поток управления достигает конца её блока, Perl восстанавливает исходное значение локализованной $scope
. Последняя директива say
снова выводит outer scope
.
Переменные пакетов и лексические переменные в Perl имеют разные правила видимости и механизмы хранения. Каждая область видимости, содержащая лексические переменные, имеет специальную структуру данных, называемую лексическая записная книжка (lexical pad, или lexpad), которая может хранить значения содержащихся в ней лексических переменных. Каждый раз, когда поток управления входит в одну из этих областей видимости, Perl создаёт ещё одну лексическую записную книжку для значений этих лексических переменных для этого конкретного вызова. Это позволяет функции работать корректно, особенно в рекурсивных вызовах (Рекурсия).
Каждый пакет имеет единую символьную таблицу, содержащую переменные пакета, а также именованные функции. Импорт (Импорт) инспектирует и манипулирует этой символьной таблицей. Также делает и local
. Вы можете локализовать только глобальные переменные и глобальные переменные пакета — но не лексические переменные.
local
чаще всего используется с магическими переменными. Например, $/
, разделитель входных записей, определяет, сколько данных операция readline
прочитает из дескриптора файла. $!
, переменная системной ошибки, содержит номер ошибки последнего системного вызова. $@
, переменная ошибки eval
, содержит любую ошибку из последней операции eval
. $|
, переменная автосброса, определяет, будет ли Perl сбрасывать текущий выбранный (select
) дескриптор файла после каждой операции записи.
Локализация этих переменных в как можно более узкой области видимости ограничивает эффект ваших изменений. Это может предотвратить странное поведение в других частях вашего кода.
Perl 5.10 добавил новую область видимости для поддержки встроенной директивы state
. Область видимости state похожа на лексическую область видимости в отношении условий видимости, но добавляет однократную инициализацию, а также персистентность значения:
sub counter
{
state $count = 1;
return $count++;
}
say counter();
say counter();
say counter();
При первом вызове counter
Perl выполняет единственную инициализацию $count
. При последующих вызовах $count
сохраняет своё предыдущее значение. Эта программа выведет 1
, 2
и 3
. Замените state
на my
, и программа будет выводить 1
, 1
и 1
.
Вы можете использовать выражение для установки начального значения state
-переменной:
sub counter
{
state $count = shift;
return $count++;
}
say counter(2);
say counter(4);
say counter(6);
Несмотря на то, что простое прочтение кода может привести к предположению, что выведено будет 2
, 4
и 6
, на самом деле выведено будет 2
, 3
и 4
. Первый вызов процедуры counter
установит значение переменной $count
. Последующие вызовы не будут изменять её значение.
state
может быть полезна для установки значения по умолчанию или подготовки кеша, но если вы её используете, убедитесь, что понимаете её инициализационное поведение:
sub counter
{
state $count = shift;
say 'Second arg is: ', shift;
return $count++;
}
say counter(2, 'two');
say counter(4, 'four');
say counter(6, 'six');
Счётчик для этой программы выведет 2
, 3
и 4
, как и ожидалось, но значения подразумеваемого второго аргумента вызовов counter()
будут two
, 4
и 6
— потому что shift
для первого аргумента происходит только при первом вызове counter()
. Или измените API для предотвращения этой ошибки, или защититесь от неё:
sub counter
{
my ($initial_value, $text) = @_;
state $count = $initial_value;
say "Second arg is: $text";
return $count++;
}
say counter(2, 'two');
say counter(4, 'four');
say counter(6, 'six');
Анонимная функция — это функция без имени. В остальном она ведёт себя в точности так же, как именованная функция — вы можете вызывать её, передавать в неё аргументы, возвращать из неё значения и копировать ссылки на неё. Однако единственный способ работать с ней — по ссылке (Ссылки на функции).
Распространённая идиома Perl 5, известная как таблица диспетчеризации, использует хеши для сопоставления входных данных с соответствующим поведением:
my %dispatch =
(
plus => \&add_two_numbers,
minus => \&subtract_two_numbers,
times => \&multiply_two_numbers,
);
sub add_two_numbers { $_[0] + $_[1] }
sub subtract_two_numbers { $_[0] - $_[1] }
sub multiply_two_numbers { $_[0] * $_[1] }
sub dispatch
{
my ($left, $op, $right) = @_;
return unless exists $dispatch{ $op };
return $dispatch{ $op }->( $left, $right );
}
Функция dispatch()
принимает аргументы в форме (2, 'times', 2)
и возвращает результат вычисления операции.
Встроенная директива sub
, использованная без имени, создаёт и возвращает анонимную функцию. Используйте эту ссылку на функцию в любом месте, где вы могли бы использовать ссылку на именованную функцию, как, например, объявление функций таблицы диспетчеризации прямо на месте:
my %dispatch =
(
plus => sub { $_[0] + $_[1] },
minus => sub { $_[0] - $_[1] },
times => sub { $_[0] * $_[1] },
dividedby => sub { $_[0] / $_[1] },
raisedto => sub { $_[0] ** $_[1] },
);
Эта таблица диспетчеризации создаёт некоторую степень защиты; только функции, входящие в таблицу, доступны для вызова пользователями. Если ваша функция диспетчеризации слепо предполагает, что строка, заданная как имя оператора, напрямую соответствует имени вызываемой функции, злонамеренный пользователь потенциально сможет вызвать любую функцию в любом пространстве имён, передав 'Internal::Functions::malicious_function'
.
Вам также может встретиться передача анонимных функций как аргументов функции:
sub invoke_anon_function
{
my $func = shift;
return $func->( @_ );
}
sub named_func
{
say 'I am a named function!';
}
invoke_anon_function( \&named_func );
invoke_anon_function( sub { say 'Who am I?' } );
Имея ссылку на функцию, вы можете определить, именованная она или анонимная, с помощью интроспекции (footnote: …или с помощью sub_name
из CPAN-модуля Sub::Identitiy
.):
package ShowCaller;
sub show_caller
{
my ($package, $file, $line, $sub) = caller(1);
say "Called from $sub in $package:$file:$line";
}
sub main
{
my $anon_sub = sub { show_caller() };
show_caller();
$anon_sub->();
}
main();
Результат может быть неожиданным:
Called from ShowCaller::main
in ShowCaller:anoncaller.pl:20
Called from ShowCaller::__ANON__
in ShowCaller:anoncaller.pl:17
__ANON__
во второй строке вывода показывает, что анонимная функция не имеет имени, которое Perl мог бы распознать. Это может усложнить отладку. Функция subname()
из CPAN-модуля Sub::Name
позволяет вам закреплять имена за анонимными функциями:
use Sub::Name;
use Sub::Identify 'sub_name';
my $anon = sub {};
say sub_name( $anon );
my $named = subname( 'pseudo-anonymous', $anon );
say sub_name( $named );
say sub_name( $anon );
say sub_name( sub {} );
Эта программа выведет:
__ANON__
pseudo-anonymous
pseudo-anonymous
__ANON__
Имейте ввиду, что обе ссылки ссылаются на одну и ту же лежащую в основе функцию. Использование subname()
на одной ссылке на функцию будет изменять имя этой анонимной функции так, что все остальные ссылки на неё будут видеть это новое имя.
Perl 5 позволяет объявлять анонимные фукнции неявно, используя прототипы (Прототипы). Хотя официально эта возможность существует для того, чтобы дать программистам возможность писать свой собственный синтаксис, подобный map
и eval
, интересным примером является использование отложенных функций, которые не выглядят как функции.
Рассмотрите CPAN-модуль Test::Fatal
, который принимает анонимную функцию как первый аргумент своей функции exception()
:
use Test::More;
use Test::Fatal;
my $croaker = exception { die 'I croak!' };
my $liver = exception { 1 + 1 };
like( $croaker, qr/I croak/, 'die() should croak' );
is( $liver, undef, 'addition should live' );
done_testing();
Вы можете переписать это более многословно следующим образом:
my $croaker = exception( sub { die 'I croak!' } );
my $liver = exception( sub { 1 + 1 } );
… или передать именованную функцию по ссылке:
sub croaker { die 'I croak!' }
sub liver { 1 + 1 }
my $croaker = exception \&croaker;
my $liver = exception \&liver;
like( $croaker, qr/I croak/, 'die() should die' );
is( $liver, undef, 'addition should live' );
…но вы не можете передавать их как скалярные ссылки:
my $croak_ref = \&croaker;
my $live_ref = \&liver;
# ОШИБОЧНО: не работает
my $croaker = exception $croak_ref;
my $liver = exception $live_ref;
…потому что прототип изменяет то, как парсер Perl 5 интерпретирует этот код. Он не может со стопроцентной точностью определить, что будут содержать $croaker
и $liver
, и поэтому выбросит исключение.
Type of arg 1 to Test::Fatal::exception
must be block or sub {} (not private variable)
Кроме того, имейте ввиду, что функция, принимающая анонимную функцию как первый из нескольких аргументов, не может иметь замыкающую запятую после блока функции:
use Test::More;
use Test::Fatal 'dies_ok';
dies_ok { die 'This is my boomstick!' }
'No movie references here';
Это временами сбивающий с толку изъян в остальном полезного синтаксиса, учтивость в отношении причуд парсера Perl 5. Синтаксическая ясность, обеспечиваемая повышением голых блоков до анонимных функций, может быть полезной, но используйте её экономно и тщательно документируйте API.
Каждый раз, когда поток управления входит в функцию, эта функцию получает новое окружение, представляющее лексическую область видимости (Область видимости) этого вызова. Это в той же мере применимо и к анонимным функциям (Анонимные функции). Следствия этого очень значимы. Термин компьютерных наук функции высшего порядка ссылается на функции, манипулирующие другими функциями. Замыкания — пример этой значимости.
Замыкание — это функция, использующая лексические переменные из внешней области видимости. Вы, вероятно, уже создавали и использовали замыкания, не осознавая этого:
package Invisible::Closure;
my $filename = shift @ARGV;
sub get_filename { return $filename }
Если этот код выглядит для вас очевидным, отлично! Конечно функция get_filename()
может видеть лексическую переменную $filename
. Именно так работают области видимости!
Предположим, вы хотите пройти в цикле по списку элементов без необходимости самостоятельно управлять итератором. Вы можете создать функцию, которая возвращает функцию, которая, будучи вызванной, вернёт следующий элемент в итерации:
sub make_iterator
{
my @items = @_;
my $count = 0;
return sub
{
return if $count == @items;
return $items[ $count++ ];
}
}
my $cousins = make_iterator(
qw( Rick Alex Kaycee Eric Corey Mandy Christine )
);
say $cousins->() for 1 .. 5;
Хотя и произошёл возврат из make_iterator()
, анонимная функция, сохранённая в $cousins
, замкнула значения этих переменных как они существовали внутри вызова make_iterator()
. Их значения продолжают существовать (Счётчики ссылок).
Так как каждый вызов make_iterator()
создаёт отдельное лексическое окружение, анонимная функция, которую она создаёт и возвращает, замыкается на уникальном лексическом окружении:
my $aunts = make_iterator(
qw( Carole Phyllis Wendy Sylvia Monica Lupe )
);
say $cousins->();
say $aunts->();
Так как make_iterator()
не возвращает эти лексические переменные по значению или по ссылке, никакой другой Perl-код, кроме замыкания, не сможет получить к ним доступ. Они инкапсулированы так же эффективно, как любая другая лексическая инкапсуляция, хотя любой код, разделяющий лексическое окружение, может получить доступ к этим значениями. Эта идиома предоставляет лучшую инкапсуляцию того, что в ином случае было бы глобальной переменной файла или пакета:
{
my $private_variable;
sub set_private { $private_variable = shift }
sub get_private { $private_variable }
}
Имейте ввиду, что вы не можете вкладывать именованные функции. Именованные функции имеют глобальную область видимости пакета. Любые лексические переменные, разделяемые между вложенными функциями, перестанут быть разделяемыми, как только внешняя функция уничтожит своё первое лексическое окружение (footnote: Если это приводит вас в замешательство, представьте реализацию.).
CPAN-модуль PadWalker
позволяет вам нарушать лексическую инкапсуляцию, но любой, кто использовал его и сломал ваш код, заслуживает права исправить любые сопутствующие баги без вашей помощи.
Итерирование по списку постоянного размера с помощью замыкания — интересный пример, но замыкания могут делать гораздо больше, как, например, итерирование по списку, который слишком дорог для вычисления или слишком велик, чтобы держать его в памяти целиком. Рассмотрим функцию, создающую последовательность чисел Фибоначчи по мере того, как вам понадобятся её элементы. Вместо повторного вычисления последовательности рекурсивно, используйте кеш и ленивое создание требуемых элементов:
sub gen_fib
{
my @fibs = (0, 1);
return sub
{
my $item = shift;
if ($item >= @fibs)
{
for my $calc (@fibs .. $item)
{
$fibs[$calc] = $fibs[$calc - 2]
+ $fibs[$calc - 1];
}
}
return $fibs[$item];
}
}
Каждый вызов функции, возвращаемой get_fib()
, принимает один аргумент, n-ный элемент последовательности Фибоначчи. Функция генерирует все предшествующие значения в последовательности по мере необходимости, кеширует их и возвращает запрошенный элемент — и даже откладывает вычисление до абсолютной необходимости. Однако здесь шаблон, характерный для кеширования, переплетается с числовыми последовательностями. Что получится, если вы выделите связанный с кешированием код (инициализация кеша, выполнение нужного кода для заполнения элементов кеша и возврат вычисленного или полученного из кеша значения) в функцию generate_caching_closure()
?
sub gen_caching_closure
{
my ($calc_element, @cache) = @_;
return sub
{
my $item = shift;
$calc_element->($item, \@cache)
unless $item < @cache;
return $cache[$item];
};
}
Встроенные функции map
, grep
и sort
сами по себе — функции высшего порядка.
Теперь gen_fib()
превращается в:
sub gen_fib
{
my @fibs = (0, 1, 1);
return gen_caching_closure(
sub
{
my ($item, $fibs) = @_;
for my $calc ((@$fibs - 1) .. $item)
{
$fibs->[$calc] = $fibs->[$calc - 2]
+ $fibs->[$calc - 1];
}
},
@fibs
);
}
Программа ведёт себя так же, как и раньше, но использование ссылок на функции и замыканий отделяет инициализирующее кеш поведение от вычисления следующего числа последовательности Фибоначчи. Кастомизация поведения кода — в данном случае get_caching_closure()
— с помощью передачи функции обеспечивает потрясающую гибкость.
Также замыкания могут удалить нежелательную универсальность. Рассмотрим случай функции, принимающей несколько параметров:
sub make_sundae
{
my %args = @_;
my $ice_cream = get_ice_cream( $args{ice_cream} );
my $banana = get_banana( $args{banana} );
my $syrup = get_syrup( $args{syrup} );
...
}
Мириады возможностей кастомизации могут быть очень полезны в полноценном магазине мороженого, но в случае передвижного фургончика с мороженым, в котором подают только мороженое Французская ваниль на бананах Кавендиш, каждый вызов make_sundae()
требует передачи аргументов, которые никогда не изменяются.
Техника, называемая частичное применение, позволяет вам привязать некоторые из аргументов к функции, а остальные предоставлять позже. Оберните функцию, которую намереваетесь вызывать, в замыкание, и передайте связанные аргументы.
Рассмотрим фургончик с мороженым, продающий только мороженое Французская ваниль на бананах Кавендиш:
my $make_cart_sundae = sub
{
return make_sundae( @_,
ice_cream => 'French Vanilla',
banana => 'Cavendish',
);
};
Вместо вызова make_sundae()
напрямую, вызовите ссылку на функцию в $make_cart_sundae
и передайте только нужные аргументы, не опасаясь позабыть о неизменяющихся или передать их некорректно (footnote: Вы можете даже использовать Sub::Install
из CPAN для импорта этой функции напрямую в другое пространство имён.).
Это только начало того, что вы можете делать с функиями высшего порядка. Higher Order Perl Марка Джейсона Доминуса (Mark Jason Dominus) — канонический справочник по функциям первого класса и замыканиям в Perl. Вы можете прочитать его онлайн по адресу http://hop.perl.plover.com/.
Замыкания (Замыкания) пользуются возможностями лексической области видимости (Область видимости) для предоставления опосредованного доступа к полу-приватным переменным. Даже именованные функции могут возпользоваться лексическими привязками:
{
my $safety = 0;
sub enable_safety { $safety = 1 }
sub disable_safety { $safety = 0 }
sub do_something_awesome
{
return if $safety;
...
}
}
Инкапсуляция функций для обеспечения безопасности позволяет всем трём функциям разделять состояние, не открывая лексическую переменную прямому воздействию внешнего кода. Эта идиома хорошо работает для тех случаев, когда внешний код должен иметь возможность изменять внутреннее состояние, но слишком тяжеловесна, если только одной функции нужно управлять состоянием.
Предположим, что каждый сотый покупатель вашего магазина мороженого получает бесплатную посыпку:
my $cust_count = 0;
sub serve_customer
{
$cust_count++;
my $order = shift;
add_sprinkles($order) if $cust_count % 100 == 0;
...
}
Этот подход работает, но создание новой лексической области видимости для единственной функции создаёт больше сопутствующей сложности, чем нужно. Встроенная директива state
позволяет вам объявлять переменные лексической области видимости со значением, сохраняющимся между вызовами:
sub serve_customer
{
state $cust_count = 0;
$cust_count++;
my $order = shift;
add_sprinkles($order)
if ($cust_count % 100 == 0);
...
}
Вы должны явно подключить эту возможность, используя модуль Modern::Perl
, прагму feature
(Прагмы) или требуя конкретную версию Perl 5.10 или выше (например, с помощью use 5.010;
или use 5.012;
).
state
также работает внутри анонимных функций:
sub make_counter
{
return sub
{
state $count = 0;
return $count++;
}
}
…хотя у этого подхода немного очевидных преимуществ.
Perl 5.10 не рекомендует использование техники из предыдущих версий Perl, с помощью который вы могли в сущности эмулировать state
. Именованная функция может замкнуться на своей предыдущей лексической области видимости с помощью злоупотребления нюансом реализации. Использование постфиксного условия, возвращающего ложь, с объявлением my
позволяет избежать повторной инициализации лексической переменной значением undef
или её инициализирующим значением.
Любое использование постфиксного условного выражения, модифицирующего объявление лексической переменной, теперь вызывает предупреждение о нерекомендуемости. С использованием этой техники слишком легко написать непреднамеренно ошибочный код; вместо этого используйте state
там где доступно, или настоящие замыкания в ином случае. Перепишите эту идиому, если она вам попадётся:
sub inadvertent_state
{
# my $counter = 1 if 0; # НЕ РЕКОМЕНДУЕТСЯ; не используйте
state $counter = 1; # используйте вместо
...
}
Именованные сущности в Perl — переменные и функции — могут иметь дополнительные метаданные, присоединённые к ним в виде атрибутов. Атрибуты — это произвольные имена и значения, используемые в определённых видах метапрограммирования (Кодогенерация).
Синтаксис объявления атрибутов довольно неуклюж, а использование атрибутов, в сущности, скорее искусство, чем наука. Большинство программ никогда их не используют, но при правильном использовании они обеспечивают ясность и удобство поддержки.
Простой атрибут — это идентификатор с предшествующим ему двоеточием, привязанный к объявлению:
my $fortress :hidden;
sub erupt_volcano :ScienceProject { ... }
Эти объявления будут приводить к вызову обработчиков атрибутов, названных hidden
и ScienceProject
, если они существуют для соответствующего типа (скаляров и функций соответственно). Эти обработчики могут делать что угодно. Если соответствующего обработчика не существует, Perl выбросит исключение времени компиляции.
Атрибуты могут включать списки параметров. Perl обрабатывает эти параметры как списки строковых и только строковых констант. Модуль Test::Class
из CPAN с успехом использует такие параметрические аргументы:
sub setup_tests :Test(setup) { ... }
sub test_monkey_creation :Test(10) { ... }
sub shutdown_tests :Test(teardown) { ... }
Атрибут Test
указывает на методы, содержащие тестовые проверки, и опционально устанавливает число проверок, которые метод намерен запустить. Хотя интроспекция (Рефлексия) этих классов может обнаружить соответствующие тестовые методы, имея хорошо спроектированные надёжные эвристики, атрибут :Test
делает ваши намерения ясными.
Параметры setup
и teardown
позволяют тестовым классам определить свои собственные методы поддержки, не беспокоясь о конфликтах с другими подобными методами в других классах. Это отделяет проблему указания того, что этот класс должен делать, от проблемы того, как другие классы делают свою работу, и обеспечивает высокую гибкость.
Веб-фреймворк Catalyst также использует атрибуты для определения видимости и поведения методов в веб-приложениях.
У атрибутов есть свои недостатки. Каноническая прагма для работы с атрибутами (прагма attributes
) обозначала свой интерфейс как экспериментальный в течение многих лет. Базовый модуль Attribute::Handlers
Демьена Конвея (Damian Conway) упрощает их реализацию. Attribute::Lexical
Эндрю Мейна (Andrew Main) — более новый подход. По возможности предпочтите использование любого из них использованию attributes
.
Наихудшая особенность атрибутов — их склонность создавать странные синтаксические действия на расстоянии. Имея фрагмент кода с атрибутами, можете ли вы предугадать их действие? Хорошо написанная документация помогает, но если невинно выглядящее объявление лексической переменной где-то сохраняет ссылку на эту переменную, ваши ожидания относительно срока жизни этой переменной могут быть ошибочны. Подобным же образом обработчик может обернуть функцию в другую функцию и заменить её в символьной таблице, не осведомив вас об этом, — рассмотрите атрибут :memoize
, который автоматически вызывает базовый модуль Memoize
.
Атрибуты в вашем распоряжении, когда они нужны вам для решения сложных проблем. Они могут быть очень полезными при правильном использовании — но большинству программ они никогда не понадобятся.
Perl не требует от вас объявлять каждую функцию, прежде чем её вызывать. Perl охотно попытается вызвать функцию, даже если она не существует. Расмотрим программу:
use Modern::Perl;
bake_pie( filling => 'apple' );
Когда вы её запустите, Perl вызовет исключение по причине вызова неопределённой функции backe_pie()
.
Теперь добавьте функцию, называемую AUTOLOAD()
:
sub AUTOLOAD {}
Теперь, когда вы запустите программу, ничего очевидного не произойдёт. Perl вызовет функцию пакета с именем AUTOLOAD()
— если она существует — в случае, если обычная диспетчеризация не сработает. Измените AUTOLOAD()
для вывода сообщения:
sub AUTOLOAD { say 'In AUTOLOAD()!' }
…чтобы продемонстрировать, что она вызывается.
Функция AUTOLOAD()
получает аргументы, переданные в неопределённую функцию, напрямую в @_
, а имя неопределённой функции доступно в глобальной переменной пакета $AUTOLOAD
. Манипулируйте этими аргументами по своему усмотрению:
sub AUTOLOAD
{
our $AUTOLOAD;
# оформленный вывод аргументов
local $" = ', ';
say "In AUTOLOAD(@_) for $AUTOLOAD!"
}
Объявление our
(Область видимости our) ограничивает $AUTOLOAD
в пределах тела функции. Переменная содержит полностью определённое имя неопределённой функции (в данном случае main::bake_pie
). Удалить имя пакета можно с помощью регулярного выражения (Регулярные выражения и сопоставление):
sub AUTOLOAD
{
my ($name) = our $AUTOLOAD =~ /::(\w+)$/;
# оформленный вывод аргументов
local $" = ', ';
say "In AUTOLOAD(@_) for $name!"
}
Оригинальный вызов получит то, что вернёт AUTOLOAD()
:
say secret_tangent( -1 );
sub AUTOLOAD { return 'mu' }
До сих пор приведённые примеры всего лишь перехватывали вызовы к неопределённым функциям. Но есть и другие возможности.
Распространённый шаблон в ОО-программировании (Moose) — делегировать или проксировать определённые методы одного объекта другому, зачастую содержащемуся в первом или доступному из него каким-либо иным способом. Проксирование логирования может помочь с отладкой:
package Proxy::Log;
sub new
{
my ($class, $proxied) = @_;
bless \$proxied, $class;
}
sub AUTOLOAD
{
my ($name) = our $AUTOLOAD =~ /::(\w+)$/;
Log::method_call( $name, @_ );
my $self = shift;
return $$self->$name( @_ );
}
Эта функция AUTOLOAD()
логирует вызов метода. Затем она разыменовывает проксируемый объект из благословлённой скалярной ссылки, извлекает имя неопределённого метода, а затем вызывает этот метод на проксируемом объекте с переданными параметрами.
Эта двойная диспетчеризация легка для написания, но неэффективна. Каждый вызов метода на прокси должен провалить нормальную диспетчеризацию, чтобы попасть в AUTOLOAD()
. Вы можете заплатить эту цену лишь раз, устанавливая новые методы в прокси-класс по мере того, как они понадобятся программе:
sub AUTOLOAD
{
my ($name) = our $AUTOLOAD =~ /::(\w+)$/;
my $method = sub
{
Log::method_call( $name, @_ );
my $self = shift;
return $$self->$name( @_ );
}
no strict 'refs';
*{ $AUTOLOAD } = $method;
return $method->( @_ );
}
Тело предыдущей функции AUTOLOAD()
стало замыканием (Замыкания), привязанным к имени неопределённого метода. Установка этого замыкания в соответствующую таблицу символов позволяет всем последующим обращениям к этому методу найти созданное замыкание (и обойти AUTOLOAD()
). Наконец, этот код вызывает метод напрямую и возвращает результат.
Хотя этот подход чище и почти всегда прозрачнее, чем обработка поведения прямо в AUTOLOAD()
, код, вызванный AUTOLOAD()
, может определить, что обращение прошло через AUTOLOAD()
. Короче говоря, caller()
будет отображать двойную диспетчеризацию обеих приведённых техник. Хотя забота о том, что это происходит, может быть нарушением инкапсуляции, утечка деталей того, как объект предоставляет метод, тоже может быть нарушением инкапсуляции.
Некоторый код использует хвостовые вызовы (Хвостовые вызовы) для замены текущего вызова AUTOLOAD()
вызовом целевого метода:
sub AUTOLOAD
{
my ($name) = our $AUTOLOAD =~ /::(\w+)$/;
my $method = sub { ... }
no strict 'refs';
*{ $AUTOLOAD } = $method;
goto &$method;
}
Это приводит к тому же эффекту, что и вызов $method
напрямую, за исключением того, что AUTOLOAD()
не будет появляться в списке вызовов, доступном через caller()
, так что это будет выглядеть так, как будто сгенерированный метод был просто вызван напрямую.
AUTOLOAD()
может быть полезным инструментом, хотя его и непросто использовать правильно. Наивный подход к генерированию методов во время исполнения означает, что метод can()
не будет выдавать правильную информацию о возможностях объектов и классов. Самое простое решение — предварительно объявить все функции, которые вы планируете генерировать в AUTOLOAD()
, с помощью прагмы subs
:
use subs qw( red green blue ochre teal );
Предварительные объявления могут пригодиться только в двух редких случаях использования атрибутов и автозагрузки (AUTOLOAD).
Плюсом этой техники является документирование ваших намерений, но недостаток в том, что вам придётся поддерживать статический список функций или методов. Перегрузка can()
(Пакет UNIVERSAL) иногда работает лучше:
sub can
{
my ($self, $method) = @_;
# использовать результат родительского can()
my $meth_ref = $self->SUPER::can( $method );
return $meth_ref if $meth_ref;
# добавить фильтрацию
return unless $self->should_generate( $method );
$meth_ref = sub { ... };
no strict 'refs';
return *{ $method } = $meth_ref;
}
sub AUTOLOAD
{
my ($self) = @_;
my ($name) = our $AUTOLOAD =~ /::(\w+)$/;>
return unless my $meth_ref = $self->can( $name );
goto &$meth_ref;
}
AUTOLOAD()
— большой молоток; он может ловить функции и методы, которые вы не собирались автозагружать, такие как DESTROY()
, деструктор объектов. Если напишете метод DESTROY
без имплементации, Perl с удовольствием направится к нему вместо AUTOLOAD()
:
# пропустить AUTOLOAD()
sub DESTROY {}
Специальные методы import()
, unimport()
и VERSION()
никогда не проходят через AUTOLOAD()
.
Если вы смешаете функции и методы в едином пространстве имён, наследующем от другого пакета, имеющего собственную AUTOLOAD()
, вы можете увидеть странную ошибку:
Use of inherited AUTOLOAD for non-method
slam_door() is deprecated
Если с вами это произошло, упростите свой код; вы вызвали несуществующую функцию в пакете, наследующем от класса, содержащего свою собственную AUTOLOAD()
. Проблема состоит из нескольких частей: смешивание функций и методов в одном пространстве имён зачастую является недостатком дизайна, наследование и AUTOLOAD()
очень быстро становятся сложными, и делать выводы о коде, когда вы не знаете, какие методы предоставляет объект, трудно.
AUTOLOAD()
полезна для быстрого и грязного программирования, но надёжный код её избегает.