Perl 5 не идеален. Некоторые возможности сложно использовать корректно. Другие никогда не работали хорошо. Некоторые — странные комбинации других возможностей со странными граничными случаями. Хотя лучше избегать этих возможностей, знание того, почему их нужно избегать, поможет вам найти более подходящие решения.
Perl — податливый язык. Вы можете писать программы в предпочитаемой вами наиболее творческой, поддерживаемой, запутанной или эксцентричной манере. Хорошие программисты заботятся о поддерживаемости, но Perl не пытается диктовать, что вы считаете поддерживаемым.
Парсер Perl понимает встроенные функции и операторы Perl. Он использует сигилы для идентификации переменных и другую пунктуацию для распознавания вызовов функций и методов. Однако, иногда парсеру приходится догадываться, что вы имеете ввиду, особенно когда вы используете голое слово — идентификатор без сигила или другой синтаксически значимой пунктуации.
Хотя прагма strict
(Прагмы) справедливо запрещает неоднозначные голые слова, некоторые голые слова приемлемы.
Ключи хеша в Perl 5 обычно не допускают неоднозначности, потому что парсер может идентифицировать их как строковые ключи; pinball
в $games{pinball}
— это очевидно строка.
Иногда эта интерпретация — не то, чего вы хотите, особенно если вы намеревались вычислить встроенную или пользовательскую функцию, чтобы сгенерировать ключ хеша. В этом случае, устраните неоднозначность, указав аргументы, используя круглые скобки для аргументов функции, или предварите унарным плюсом, чтобы форсировать вычисление встроенной функции:
# ключом будет литеральный «shift»
my $value = $items{shift};
# ключом будет значение, возвращённое «shift»
my $value = $items{shift @_}
# унарный плюс приводит к использованию встроенной функции «shift»
my $value = $items{+shift};
Имена пакетов в Perl 5 тоже являютя голыми словами. Если вы следуете соглашениям об именовании, согласно которым имена пакетов начинаются с заглавных букв, а функций — нет, маловероятно, что вы встретитесь с коллизиями имён, но парсер Perl 5 должен определить, как парсить Package->method()
. Значит ли это «вызвать функцию с именем Package()
и вызвать метод method()
на возвращаемом ей значении» или «вызвать метод с именем method()
в пространстве имён Package
»? Ответ различается в зависимости от того, какой код парсер уже встретил в текущем пространстве имён.
Принудите парсер воспринимать Package
как имя пакета, добавив разделитель пакетов (::
) (footnote: Даже среди тех, кто понимает, почему это работает, очень немногие это делают.):
# вероятно метод класса
Package->method();
# определённо метод класса
Package::->method();
Специальные именованные блоки кода AUTOLOAD
, BEGIN
, CHECK
, DESTROY
, END
, INIT
и UNITCHECK
— это голые слова, которые объявляют функции без использования встроенной директивы sub
. Вы уже видели это раньше (Кодогенерация):
package Monkey::Butler;
BEGIN { initialize_simians( __PACKAGE__ ) }
sub AUTOLOAD { ... }
Хотя вы можете опустить sub
из объявления AUTOLOAD()
, немногие так делают.
Константы, объявленные с помощью прагмы constant
, можно использовать как голые слова:
# не используйте это для реальной аутентификации
use constant NAME => 'Bucky';
use constant PASSWORD => '|38fish!head74|';
return unless $name eq NAME && $pass eq PASSWORD;
Обратите внимание, что эти константы не интерполируются в строках, заключённых в двойные кавычки.
Константы — специальный случай прототипированных функций (Прототипы). Когда вы предварительно объявляете функцию с прототипом, парсер знает, как воспринимать эту функцию, и будет предупреждать об ошибках неоднозначности парсинга. Все остальные недостатки прототипов всё ещё имеют место.
Независимо от того, насколько внимательно вы пишете код, голые слова всё равно приводят к неоднозначностям. Вы можете избежать большинства их использований, но вам встретится несколько типов голых слов в унаследованном коде.
Код, написанный без strict 'subs'
, может использовать голые имена функций. Добавление скобок заставляет код пройти эти ограничения. Используйте perl -MO=Deparse,-p
(см. perldoc B::Deparse
) чтобы понять, как Perl парсит их, затем расставляйте скобки соответствующим образом.
Некоторый старый код может не заботиться о том, чтобы заключить в кавычки значения хеш-пар:
# плохой стиль; не использовать
my %parents =
(
mother => Annette,
father => Floyd,
);
Если не существует ни функции Floyd()
, ни Annette()
, Perl будет интерпретировать эти голые слова как строки. strict 'subs'
выдаст ошибку в этой ситуации.
До появления лексических дескрипторов файлов (Ссылки на дескрипторы файлов), все дескрипторы файлов и директорий использовали голые слова. Вы почти всегда можете безопасно переписать этот код на использование лексических дескрипторов файлов; исключения — STDIN
, STDOUT
и STDERR
. К счастью, парсер Perl распознаёт их.
Наконец, встроенная фукнция sort
может принимать в качестве второго аргумента имя функции, которую нужно использовать для сортировки. Хотя это редко бывает неоднозначным для парсера, это может смутить читающих людей. Альтернатива в виде передачи ссылки на функцию в скаляре немного лучше:
# стиль с использованием голого слова
my @sorted = sort compare_lengths @unsorted;
# ссылка на фукнцию в скаляре
my $comparison = \&compare_lengths;
my @sorted = sort $comparison @unsorted;
Второй вариант избегает использования голого слова, но результат на одну строчку длиннее. К сожалению, парсер Perl 5 не понимает однострочную версию вследствие специального парсинга sort
; вы не можете использовать произвольное выражение (такое как взятие ссылки на именованную функцию) там, где может пройти блок или скаляр.
# не работает
my @sorted = sort \&compare_lengths @unsorted;
В обоих случаях то, как sort
вызывает функцию и передаёт аргументы, может быть запутывающим (см. perldoc -f sort
для подробностей). Где возможно, рассмотрите использование вместо этого блочной формы sort
. Если же вы должны использовать какую-либо из форм с функцией, рассмотрите добавление объясняющего комментария.
В Perl 5 нет оператора new
; конструктором в Perl 5 является всё, что возвращает объект. По соглашению, конструкторы — методы классов, называемые new()
, но вы можете выбрать всё, что захотите. Некоторые старые руководства по объектам Perl 5 поощряют использование вызовов конструкторов в стиле C++ и Java:
my $q = new CGI; # НЕ ИСПОЛЬЗОВАТЬ
…вместо очевидного вызова метода:
my $q = CGI->new();
Эти варианты синтаксиса дают одинаковое поведение, за исключением случаев, когда это не так.
В непрямой объектной форме (более точно, в дательном случае) первого примера глагол (метод) предшествует существительному, к которому относится (объект). Это нормально в разговорных языках, но в Perl 5 создаёт неоднозначности парсинга.
Так как имя метода — голое слово (Голые слова), парсер должен предсказать правильную интерпретацию кода с помощью использования нескольких эвристик. Хотя эти эвристики хорошо протестированы и почти всегда корректны, их режимы сбоя сбивают с толку. Хуже того, они зависят от порядка компиляции кода и модулей.
Трудность парсинга увеличивается, если конструктор принимает аргументы. Непрямой стиль может выглядеть так:
# НЕ ИСПОЛЬЗОВАТЬ
my $obj = new Class( arg => $value );
…таким образом заставляя имя класса выглядеть как вызов функции. Perl 5 может устранить неоднозначность во многих подобных случаях, но его эвристики зависят от того, какие имена пакетов видит парсер, какие голые слова он уже разрешил (и как он разрешил их) и от имён функций, уже объявленных в текущем пакете.
Представьте противоречие в случае прототипированной функции (Прототипы) с именем, которому случилось каким-то образом вступить в конфликт с именем класса или метода, вызываемого непрямо. Это случается редко, но так неприятно для отладки, что стоит того, чтобы избегать непрямых вызовов.
Другая опасность такого синтаксиса в том, что парсер ожидает объект как одно скалярное выражение. Вывод в дескриптор файла, сохранённый в агрегатной переменной, выглядит очевидным, но таковым не является:
# НЕ РАБОТАЕТ КАК НАПИСАНО
say $config->{output} 'Fun diagnostic message!';
Perl попытается вызвать say
на объекте $config
.
print
, close
и say
— все встроенные функции, оперирующие с дескрипторами файлов — оперируют в непрямой манере. Это было нормально, когда дескрипторы файлов были глобальными переменными пакета, но лексические дескрипторы файлов (Ссылки на дескрипторы файлов) делают очевидными проблемы непрямого объектного синтаксиса. Чтобы устранить это, избавьтесь от неоднозначности подвыражения, выдающего подразумеваемый инвокант:
say {$config->{output}} 'Fun diagnostic message!';
Нотация прямого вызова не страдает этой проблемой неоднозначности. Чтобы сконструировать объект, вызовите метод-конструктор напрямую на имени класса:
my $q = CGI->new();
my $obj = Class->new( arg => $value );
Этот синтаксис всё ещё имеет проблему голого слова, в том смысле, что если у вас есть функция с именем CGI
, Perl будет интерпретировать голое имя класса как вызов функции:
sub CGI;
# вы написали CGI->new(), но Perl увидел
my $q = CGI()->new();
Хотя это случается редко, вы можете устранить неоднозначность имён классов, добавив разделитель пакетов (::
) или явно пометив имена классов как строковые литералы:
# разделитель пакетов
my $q = CGI::->new();
# не имеющий неоднозначности строковый литерал
my $q = 'CGI'->new();
Однако почти никто этого не делает.
Для ограниченного случая операций с дескрипторами файлов дательное использование настолько общепринято, что вы можете использовать подход непрямого вызова, если окружите свой подразумеваемый инвокант фигурными скобками. Если вы используете Perl 5.14 (или если вы загрузили IO::File
или IO::Handle
), вы можете использовать методы на лексических дескрипторах файлов (footnote: Хотя почти никто не делает этого для print
и say
.).
CPAN-модуль Perl::Critic::Policy::Dynamic::NoIndirect
(плагин для Perl::Critic
) может обнаружить непрямые вызовы во время проверки кода. CPAN-модуль indirect
может обнаружить и запретить их использование в запущенных программах:
# выдать предупреждение при использовании непрямых вызовов
no indirect;
# выбросить исключение при их использовании
no indirect ':fatal';
Прототип — это набор необязательных метаданных, присоединённых к функции, которые влияют на то, как парсер понимает её аргументы. Хотя внешне они могут выглядеть как сигнатуры функций в других языках, на самом деле они очень отличаются.
Прототипы позволяют пользователям определять свои собственные функции, ведущие себя как встроенные. Рассмотрим встроенную фукнцию push
, которая принимает массив и список. Хотя обычно Perl 5 разгладил бы массив и список в единый список, переданный в push
, парсер знает, что не нужно разглаживать массив, чтобы push
мог модифицировать его на месте.
Прототипы функций являются частью объявления:
sub foo (&@);
sub bar ($$) { ... }
my $baz = sub (&&) { ... };
Любой прототип, привязанный к предварительному объявлению, должен совпадать с прототипом, привязанным к объявлению функции. Perl выдаст предупреждение, если это не так. Как ни странно, вы можете опустить прототип в предварительном объявлении и включить его в полное объявление — но делать так нет причин.
Встроенная функция prototype
принимает имя функции и возвращает строку, представляющую её прототип. Используйте форму CORE::
, чтобы увидеть прототип встроенной функции:
$ perl -E "say prototype 'CORE::push';"
\@@
$ perl -E "say prototype 'CORE::keys';"
\%
$ perl -E "say prototype 'CORE::open';"
*;$@
prototype
вернёт undef
для тех встроенных функций, которые вы не можете эмулировать:
say prototype 'CORE::system' // 'undef'
# undef; невозможно эмулировать встроенную функцию system
say prototype 'CORE::prototype' // 'undef'
# undef; встроенная функция prototype
не имеет прототипа
Помните push
?
$ perl -E "say prototype 'CORE::push';"
\@@
Символ @
представляет список. Обратный слеш принуждает к использованию ссылки для соответствующего аргумента. Этот прототип обозначает, что push
принимает ссылку на массив и список значений. Вы можете написать mypush
так:
sub mypush (\@@)
{
my ($array, @rest) = @_;
push @$array, @rest;
}
Другие символы прототипов включают $
для скалярного аргумента, %
для обозначения хеша (чаще всего используется как ссылка) и &
для указания на блок кода. См. perldoc perlsub
для полной документации.
Прототипы изменяют то, как Perl парсит ваш код, и могут вызывать приведение типов аргументов. Они не документируют количество или типы аргументов, которые функция ожидает, как и не устанавливают соответствие аргументов именованным параметрам.
Приведение типов при использовании прототипов работает неочевидным образом, как навязывание скалярного контекста входным аргументам:
sub numeric_equality($$)
{
my ($left, $right) = @_;
return $left == $right;
}
my @nums = 1 .. 10;
say 'They're equal, whatever that means!'
if numeric_equality @nums, 10;
…но работает только с простыми выражениями:
sub mypush(\@@);
# ошибка компиляции: несоответствие прототипов
# (ожидался массив, получено скалярное присваивание)
mypush( my $elems = [], 1 .. 20 );
Чтобы отладить это, пользователи mypush
должны знать как то, что прототип существует, так и ограничения прототипов массивов. Хуже того, это простые ошибки, которые могут вызывать прототипы.
Немногие использования прототипов достаточно интересны, чтобы перевесить их недостатки, но они существуют.
Во-первых, они могут позволить вам переопределить встроенные функции. Сначала проверьте, что вы можете переопределить встроенную функцию, проверив её прототип в маленькой тестовой программе. Затем используйте прагму subs
, чтобы сказать Perl, что вы собираетесь переопределить встроенную функцию, и, наконец, объявите ваше переопределение с корректным прототипом:
use subs 'push';
sub push (\@@) { ... }
Имейте ввиду, что прагма subs
действует для всей оставшейся части файла, независимо от лексической области видимости.
Вторая причина использования прототипов — это определение констант времени компиляции. Когда Perl встречает функцию, объявленную с пустым прототипом (в противоположность отсутствию прототипа), и эта функция возвращает единственное константное выражение, оптимизатор превратит все вызовы этой функции в константы вместо вызовов функции:
sub PI () { 4 * atan2(1, 1) }
Весь последующий код будет использовать вычисленное значение Пи на месте голого слова PI
или вызова PI()
, с учётом области видимости.
Базовая прагма constant
справляется с этими деталями за вас. Модуль Const::Fast
из CPAN создаёт константные скаляры, которые вы можете интерполировать в строки.
Разумное использование прототипов — это расширение синтаксиса Perl 5 для оперирования анонимными функциями как блоками. CPAN-модуль Test::Exception
использует это во благо для предоставления приятного API с отложенными вычислениями (footnote: См. также Test::Fatal
). Его функция throws_ok()
принимает три аргумента: блок кода для выполнения, регулярное выражение для сравнения со строкой исключения и необязательное описание теста:
use Test::More tests => 1;
use Test::Exception;
throws_ok
{ my $unobject; $unobject->yoink() }
qr/Can't call method "yoink" on an undefined/,
'Method on undefined invocant should fail';
Экспортируемая функция throws_ok()
имеет протоип &$;$
. Его первый аргумент — это блок, который становится анонимной функцией. Второй аргумент — скаляр. Третий аргумент не обязателен.
Внимательные читатели, возможно, заметили отсутствие запятой после блока. Это причуда парсера Perl 5, который ожидает после прототипированного блока пробел, а не оператор запятой. Это недостаток синтаксиса прототипов.
Вы можете использовать throws_ok()
, не пользуясь прототипами:
use Test::More tests => 1;
use Test::Exception;
throws_ok(
sub { my $unobject; $unobject->yoink() },
qr/Can't call method "yoink" on an undefined/,
'Method on undefined invocant should fail' );
Последнее хорошее применение прототипов — при определении произвольной именованной функции для использования с sort
(footnote: Бен Тилли (Ben Tilly) предложил этот пример.):
sub length_sort ($$)
{
my ($left, $right) = @_;
return length($left) <=> length($right);
}
my @sorted = sort length_sort @unsorted;
Прототип $$
заставляет Perl передавать сортируемые пары в @_
. Документация sort
предполагает, что это немного медленнее, чем использование глобальных переменных пакета $a
и $b
, но использование лексических переменных зачастую оправдывает любое снижение скорости.
Объектная система Perl 5 преднамеренно минималистична (Благословлённые ссылки). Так как класс является пакетом, Perl не делает различий между функцией и методом, хранящимися в пакете. Одна и та же встроенная директива, sub
, объявляет и одно, и другое. Документация может прояснить ваши намерения, но Perl охотно диспетчеризует функцию, вызванную как метод. Подобным же образом вы можете вызвать метод как если бы он был функцией — полностью определённой, экспортированной или ссылкой — если вручную передадите свой инвокант.
Вызов не того, что нужно, не так, как нужно, вызывает проблемы.
Рассмотрим класс с несколькими методами:
package Order;
use List::Util 'sum';
...
sub calculate_price
{
my $self = shift;
return sum( 0, $self->get_items() );
}
Если есть объект $o
класса Order
, следующие вызовы этого метода могут выглядеть эквивалентными:
my $price = $o->calculate_price();
# сломано; не использовать
my $price = Order::calculate_price( $o );
Хотя в этом простом случае они дадут один и тот же вывод, последнее нарушает инкапсуляцию объекта, избегая поиска метода.
Если же $o
был бы подклассом или алломорфом (Роли) класса Order
, который переопределял бы calculate_price()
, обход диспетчеризации метода привёл бы к вызову неверного метода. Любое изменение реализации calculate_price()
, такое как модификация наследования или делегирования через AUTOLOAD()
— могло бы сломать вызывающий код.
В Perl есть одно обстоятельство, где это поведение может выглядеть необходимым. Если вы форсируете разрешение метода без диспетчеризации, как вы вызовете результирующую ссылку на метод?
my $meth_ref = $o->can( 'apply_discount' );
Здесь есть две возможности. Первая — отбросить возвращаемое значение метода can()
:
$o->apply_discount() if $o->can( 'apply_discount' );
Вторая — использовать саму ссылку посредством синтаксиса вызова метода:
if (my $meth_ref = $o->can( 'apply_discount' ))
{
$o->$meth_ref();
}
Если $meth_ref
содержит ссылку на функцию, Perl вызовет эту ссылку с $o
в качестве инвоканта. Это работает даже со включенным strict
, как и при вызове метода с помощью скаляра, содержащего его имя:
my $name = 'apply_discount';
$o->$name();
Есть один небольшой недостаток в вызове метода по ссылке; если структура программы изменяется между сохранением ссылки и вызовом по ссылке, ссылка может уже не ссылаться на наиболее соответствующий метод. Если класс Order
был изменён так, что Order::apply_discount
уже не является правильным методом для вызова, ссылка в $meth_ref
не будет обновлена.
Когда вы используете эту форму вызова, ограничьте область видимости ссылки.
Так как Perl 5 не делает различий между функциями и методами в точке объявления, и так как возможно (хотя и не рекомендуется) вызвать заданную функцию как функцию или метод, возможно написать функцию, которую можно вызывать любым из этих способов. Базовый модуль CGI
— главный обвиняемый. Его функции применяют несколько эвристик для определения того, является ли их первый аргумент инвокантом.
Недостатков этого множество. Трудно точно предсказать, какой инвокант потенциально валиден для заданного метода, особенно если вам приходится иметь дело с подклассами. Также труднее создать API, который пользователи не смогут с лёгкостью неправильно использовать, как и бремя документации становится тяжелее. Что случится, если одна часть проекта использует процедурный интерфейс, а другая — объектный?
Если вам необходимо предоставить отдельный процедурный и ОО интерфейс к библиотеке, создайте два отдельных API.
Тогда как перегрузка (Перегрузка) позволяет вам настраивать поведение классов и объектов для конкретных случаев приведения типа, механизм, называемый связыванием, позволяет вам настраивать поведение простых переменных (скаляров, массивов, хешей и дескрипторов файлов). Любая операция, которую вы можете выполнить на связанной переменной, транслируется в определённый вызов метода.
Встроенная директива tie
изначально позволяла вам использовать место на диске как резервную память для хешей, так что Perl мог осуществлять доступ к файлам, которые больше, чем можно поместить в память. Базовый модуль Tie::File
предоставляет похожую систему и позволяет вам обращаться с файлами как если бы они были массивами.
Класс, с которым вы связываете переменную с помощью tie()
, должен соотвествовать определённому интерфейсу для конкретных типов данных. Смотрите perldoc perltie
для получения общего представления, затем обратитесь к базовым модулям Tie::StdScalar
, Tie::StdArray
и Tie::StdHash
за более конкретными деталями. Начните с наследования от одного из этих классов, затем переопределите любые конкретные методы, которые вам нужно модифицировать.
Как будто бы tie
уже не достаточно сбивающий с толку, Tie::Scalar
, Tie::Array
и Tie::Hash
определяют необходимые интерфейсы для связывания скаляров, массивов и хешей, но реализацию по умолчанию предоставляют Tie::StdScalar
, Tie::StdArray
и Tie::StdHash
.
Так можно связать переменную:
use Tie::File;
tie my @file, 'Tie::File', @args;
Первый аргумент — это переменная для связывания, второй — имя класса, с которым её нужно связать, а @args
— необязательный список аргументов, требуемый для связывающей функции. В случае Tie::File
это допустимое имя файла.
Связывающие функции напоминают конструкторы: TIESCALAR
, TIEARRAY()
, TIEHASH()
или TIEHANDLE()
для скаляров, массивов, хешей и дескрипторов файлов соответственно. Каждая функция возвращает новый объект, представляющий связанную переменную. Обе встроенные директивы, tie
и tied
, возвращают этот объект. Однако, большинство использует tied
в булевом контексте.
Чтобы реализовать класс связанных переменных, унаследуйте от встроенного модуля, такого как Tie::StdScalar
(footnote: У Tie::StdScalar
отсутствует собственный файл .pm, так что используйте Tie::Scalar
, чтобы сделать его доступным.), затем переопределите конкретные методы для операций, которые вы хотите изменить. В случае связанного скаляра это, скорее всего, будут FETCH
и STORE
, возможно, TIESCALAR()
, и, вероятно, не DESTROY()
.
Вы можете создать класс, который логирует все операции чтения и записи скаляра, с помощью очень небольшого количества кода:
package Tie::Scalar::Logged
{
use Modern::Perl;
use Tie::Scalar;
use parent -norequire => 'Tie::StdScalar';
sub STORE
{
my ($self, $value) = @_;
Logger->log("Storing <$value> (was [$$self])", 1);
$$self = $value;
}
sub FETCH
{
my $self = shift;
Logger->log("Retrieving <$$self>", 1);
return $$self;
}
}
1;
Допустим, метод log()
класса Logger
принимает строку и количество фреймов вверх по стеку вызовов, которые нужно вывести.
Внутри методов STORE()
и FETCH()
, $self
работает как благословлённый скаляр. Присваивание этой ссылке на скаляр изменяет значение скаляра, а чтение из неё возвращает его значение.
Аналогично, методы Tie::StdArray
и Tie::StdHash
воздействуют на благословлённые ссылки на массив и хеш сооветственно. Документация в perldoc perltie
объясняет обширное количество методов, которые они поддерживают, как например, вы можете читать или записывать множественные значения в них, помимо других операций.
Опция -norequire
предотвращает прагму parent
от попытки загрузить файл для Tie::StdScalar
, поскольку этот модуль является частью файла Tie/Scalar.pm.
Связанные переменные выглядять как весёлые возможности для проявления ума, но они могут приводить к сбивающим с толку интерфейсам. Если у вас нет очень хорошей причины для того, чтобы заставлять объект вести себя так, как если бы он был встроенным типом данных, избегайте создания собственных связываний. К тому же, tie
намного медленнее, чем использование встроенных типов данных, ввиду различных причин, кроющихся в реализации.
Хорошие причины включают облегчение отладки (используйте логгируемый скаляр, чтобы понять, где изменяется значение) и необходимость сделать некоторые невозможные операции возможными (доступ к большим файлам способом с эффективным использованием памяти). Связанные переменнные менее полезны как первичные интерфейсы к объектам; зачастую слишком трудно и ограничивающе пытаться вместить весь ваш интерфейс в тот, что поддерживается tie()
.
Последнее слово предупреждения одновременно печально и убедительно; слишком много кода сбивается со своего пути, чтобы предотвратить использование связанных переменных, зачастую по случайности. Это неудачно, но нарушение ожиданий библиотечного кода склонно к раскрытию багов, исправить которые зачастую не в ваших силах.