Некоторые сведения о Perl 5/Функции и процедуры
← Ссылки | Глава | Пакеты, библиотеки и модули → |
Функции и процедуры | ||
Подпрограммы или процедуры играют ту же роль, что и в других языках программирования:
- они позволяют разбить одну большую программу на несколько небольших частей, чтобы сделать ее понятной;
- они объединяют операторы в единый фрагмент, который может повторно вызываться, возможно при разных входных параметрах.
В Perl нет разницы между процедурами и функциями, однако мы условимся называть функциями такие процедуры, которые возвращают некоторый результат той части программы, которая их вызывает.
Определение процедуры
[править]Процедура может быть объявлена в любом месте основной программы следующим образом:
sub <имя-процедуры> [(<прототип>)] [{<основной-блок>}]
где
<имя-процедуры>
— имя процедуры, по которому ее можно вызывать явно.(<прототип>)
— специальная строка, которая описывает какие парараметры разрешено передавать процедуре.{<основной-блок>}
— блок операторов, являющийся определением процедуры и выполняющийся при каждом ее вызове.
Подобно языку Си, процедуру можно объявить, но не определить (не предоставить основной блок), возможно потому, что ее определение находится в другом файле. В этом случае объявление должно выглядеть так
sub <имя-процедуры> [(<прототип>)];
# Это требуется, чтобы функция была зарегистрирована в таблице символов.
Тело процедуры может хранится в разных местах:
- оно может быть определено в том же исходном файле;
- оно может быть определено в отдельном файле и загружаться с помощью
do
,require
иuse
; - текст процедуры может быть передан функции
eval()
: в этом случае компиляция процедуры будет происходить каждый раз при вызове; - текст процедуры может быть определен анонимно и вызываться по ссылке.
Вызов процедуры
[править]Процедуру можно вызвать, указав ее имя с идентификатором типа &
(т.н. старый стиль):
&example <args>;
&example (<args>);
&example;
# args - список аргументов
Если процедура вызывается со скобками после имени, причем не важно есть ли аргументы у функции на самом деле или нет, идентификатор типа &
можно опускать:
example(<args>);
example();
Если перед вызовом процедура была определена или импортирована, то можно опустить и &
и скобки:
sub example { return 0 }
example <args>;
example;
Если процедура анонимная, то идентификатор типа &
в вызове обязателен:
$example = sub { return 0 };
&example <args>;
&example;
Процедура может использоваться в выражениях, если она что-то возвращает. По умолчанию процедура всегда возвращает последнее вычисленное значение в ее блоке. Можно указать возвращаемое значение с помощью функции return()
в любой точке процедуры. Возвращать можно как простые скалярные значения, так и массивы.
Если процедура является целым файлом, то ее можно вызвать как perl-подпрограмму с помощью конструкции do <имя-файла>;
. Если указываемый файл недоступен для чтения, то do
возвращает неопределенное значение, а встроенной переменной $!
будет присвоен код ошибки. Если файл может быть прочитан, но возникают ошибки при компиляции или во время исполнения, то do
возвращает неопределенное значение, а в специальной переменной $@
будет хранится сообщение об ошибке.
Операция eval
[править]Именованная унарная операция
eval <выражение>
eval { <выражение> }
рассматривает параметр <выражение>
как текст программы Perl, компилирует его и, если не обнаруживает ошибок, выполняет в текущем вычислительном окружении. Если выражение передано блоком, то оно анализируется и компилируется один раз. Это удобно, так как ошибки обнаруживаются раньше. Если аргумент передается не блоком, то его ошибки будут обнаружены только во время исполнения.
Если <выражение>
отсутствует, то по умолчанию используется переменная $_
. Если выражение завершается успешно, то она возвращает последнее вычисленное значение внутри <выражение>
.
Если <выражение>
содержит синтаксические ошибки или вызывается die()
, или при исполнении возникает ошибка, то eval()
возвращает неопределенное значение, а в специальную переменную $@
заносится сообщение об ошибке.
Основным применением eval{}
является перехватывание исключительных ситуаций (исключений), т.е. таких ошибок, которые принудительно прерывают исполнение всей программы. К исключению можно отнести, например, ситуацию деления на ноль. Это работает потому, что исключение прервет только исполнение eval{}
и передаст управление вызвавшей части программы.
Иногда по логике программы вам нужно генерировать исключение. Для этого вы должны использовать функцию die()
.
Ключевое слово do
[править]Ключевое слово do
может использоваться в двух значениях:
- как именованная унарная операция;
- как способ вызывать сценарий Perl из другого файла, путь до которого является вычисляемым через выражение.
Именованная унарная операция do
по своей природе является термом, к которому также можно прикреплять модификаторы. Данная операция возвращает последнее вычисленное в ней выражение.
Операция do
обычно используется, чтобы порождать компактные многоуровневые конструкции. Ниже приведено несколько примеров.
# do может использоваться для компактного программирования процедур во время присваивания.
# В этом примере программа запрашивает число, и если оно отрицательное, то присваивает
# 0 переменной, иначе присваивает введенное значение. Обратите внимание, что вся процедура
# компактно совмещена с присваиванием.
my $value = do {
print "Enter a number: ";
my $input = <>;
$input < 0 ? 0 : $input;
};
print "$value\n";
# Обычно do используется, когда некоторому блоку кода требуется модификатор.
# Следующий код работает как echo, а именно, он выводит все что вводит пользователь в бесконечном цикле.
# Цикл можно прервать, если ввести точку.
print "Enter '.' to quit\n";
do {
$_ = <>;
print "$_";
} until $_ eq ".\n";
Если после do
указан литерал, либо литерал вычисляется, то do
воспринимает его как путевое имя файла с кодом Perl. Если путь относительный, то файл будет искаться в директориях, указанных во встроенном массиве @INC
. Если файл не может быть прочитан, то do
возвращает undef
, а встроенная переменная $!
инициируется сообщением об ошибке. Если файл удается прочитать и скомпилировать, то do
возвращает последнее вычисленное в нем выражение.
Чаще всего этой возможностью пользуются, чтобы строить динамичные конфигурационные файлы для Perl, которые объявляют глобальные переменные. Преимущество здесь в том, что вы можете встраивать логику в конфигурационные файлы, используя синтаксис языка Perl.
for $file ("/share/prog/defaults.rc",
"$ENV{HOME}/.someprogrc")
{
unless ($return = do $file) {
warn "couldn't parse $file: $@" if $@;
warn "couldn't do $file: $!" unless defined $return;
warn "couldn't run $file" unless $return;
}
}
Область видимости процедуры
[править]Точкой определения переменной в Perl является то место, где она впервые встречается в программе. Область действия большинства переменных ограничена пакетом. Исключение составляют некоторые специальные предопределенные переменные интерпретатора perl. Пакет — это способ порождения пространства имен для части программы. Другими словами, каждый фрагмент кода относится к какому-то пакету.
Переменные, которые встречаются в процедурах, по умолчанию видны всему пакету (т.е. они обладают глобальной видимостью). Иногда это нежелательно: обычно мы хотим ограничить время жизни переменных процедур в рамках тела этих процедур, а также не хотим, чтобы процедуры влияли косвенно на уже существующие глобальные переменные.
Перменные, которые видны только конкретной процедуре, называются локальными (говорят что они лексические) и такие переменные обладают локальной видимостью (lexical scope). В Perl существует два способа порождения локальных переменных: при помощи функции my()
и local()
.
Функция my()
[править]Функция my()
используется для объявления одной или нескольких переменных локальными и ограничивает область их действия:
- процедурой/функцией;
- блоком;
- выражением, переданным
eval()
; - исходным файлом программы, в зависимости от того, в каком месте была вызвана
my()
.
my $var;
my ($var, @arr, %hash); # Если переменных несколько, то скобки обязательны
my $pi = 3.14159;
# Объявление и инициализацию можно совмещать
my ($pi, $exp) = (3.14159, 2.71828);
Функция local()
[править]Функция local()
вызывается аналогично my()
, но создает не совсем локальные переменные, а временно заменяет текущие значения глобальных переменных локальными значениями внутри:
- процедуры/функции;
- блока;
- выражения, переданного
eval()
; - исходного файла с программой, в зависимости от того, в каком месте вызвана
local()
.
local $var;
local ($var, @arr, %hash); # Если переменных несколько, то скобки обязательны
local $pi = 3.14159;
# Объявление и инициализацию можно совмещать
local ($pi, $exp) = (3.14159, 2.71828);
Если при вызове функции глобальная переменная существует, то ее предыдущее значение сохраняется в стеке и заменяется новым значением. После выхода переменной из области видимости процедуры/блока/функции eval()
или файла, ее предыдущее значение восстанавливается из стека. Такие переменные иногда называют динамическими, а их область видимости — динамической областью видимости.
Функция my()
появилась в Perl с пятой версии, позже local()
, однако для создания истинно локальных переменных рекомендуется использовать именно функцию my()
. Впрочем и у local()
есть причины для применения.
Рекомендации по использованию my() и local()
[править]Функция my()
должна использоваться всегда, кроме следующих случаев, когда нужно использовать local()
:
- Присваивание временного значения глобальной переменной, в первую очередь это относится к предопределенным глобальным переменным типа
$_
,$ARGV
и другие. - Создание локального дескриптора файла, каталога или локального псевдонима или функции.
- Временное изменение массива или хеш-массива. Например, так следует поступать, если нужно временно изменить переменные окружения в предопределенном хеш-массиве
%ENV
.
Функция our()
[править]Чтобы явно обозначить пакетную область видимости переменной, используется функция our()
. Данная функция только наделяет переменную видимостью пакета, создавая лексический псевдоним внутри него, поэтому она может применяться как к уже объявленным переменным, так и не объявленным (в этом случае побочным эффектом будет их объявление). Использование our()
во многом аналогично описанным выше my()
и local()
.
Использование этой функции в общем то не обязательно, так как любая переменная получает эту видимость по умолчанию. Данная функция используется в следующих ситуациях:
- Когда включена директива
use strict 'vars';
, которая требует явного указания области видимости переменной (либо черезour()
, либо через квалифицированное имя) в любом месте, где она действует. - Когда переменная используется в блоках, например
use strict 'vars'; $main::example = 10; # Переменная объявляется с квалификатором пакета { our $example; # Создаем псевдоним на $main::example в пределах блока print $example, "\n"; # 10 }
- В частности, когда в одном пакете есть две переменные с одним именем, но разной видимостью
use strict 'vars'; $main::example = 10; my $example = 13; { our $example; # Переменная из глобальной таблицы символов print $example, "\n"; # 10 } # Локальная переменная print $example, "\n"; # 13
Функция our()
наделяет переменную пакетной видимостью на протяжении всего лексического пространства. Сравните
package one;
our $example = 10; # До конца лексического пространства через все пакеты
print "$example\n"; # 10
package two;
print "$example\n"; # 10
и
package one;
our $example = 10; # До конца лексического пространства через все пакеты
print "$example\n"; # 10
package two;
our $example = 12; # От этой точки и до конца лексического пространства
print "$example\n"; # 12
print "$one::example\n"; # 10
Такое поведение отличает our()
от до этого использовавшейся директивы use vars
, которая позволяла использовать неквалифицированные имена только внутри текущего пакета. Помните, что с версии 5.6.0 использование use vars
считается устаревшим подходом. Используйте только our()
.
Передача аргументов в процедуру
[править]Данные в процедуру передаются через ее аргументы. Для передачи аргументов используется специальный массив @_
, в котором $_[0]
– первый параметр, $_[1]
– второй параметр и так далее. Такой механизм позволяет передать в процедуру произвольное число аргументов.
Массив @_
является локальным для процедуры, но его элементы — это псевдонимы действительно переданных параметров (не копии). Изменение параметров в @_
приводит к изменению действительных параметров. Таким образом, в Perl параметры фактически передаются всегда по ссылке.
Чтобы реализовать передачу по значению, вы должны создать внутри процедуры локальные переменные и скопировать в них значения из @_
. Обычно это делается так
sub example {
my ($p1, $p2, $p3) = @_; # Параметры будут скопированы в локальные переменные
...
}
example (1, "a", 3.1415);
или с помощью функции shift
, каждый вызов которой возвращает очередной элемент массива @_
sub testArgs_1 {
my $arg1 = shift;
my $arg2 = shift;
print "arg1='$arg1', arg2='$arg2'\n";
}
testArgs_1 'one';
testArgs_1 'one', 'two';
arg1='one', arg2=''
arg1='one', arg2='two'
Передача аргументов по ссылкам
[править]К сожалению массивы не могут быть просто так переданы в процедуру с сохранением их идентичности. Если параметр является массивом или хеш-массивом, все его элементы сохраняются в @_
. При передаче в подпрограмму нескольких массивов, все они будут перемешаны в одном @_
:
sub example {
my ($p1, $p2, $p3, $p4) = @_;
print "@_", "\n";
print $p1, $p2, $p3, $p4, "\n";
}
@a = (1, 2);
@b = (3, 4);
example(@a, @b);
# Вывод:
# 1 2 3 4
# 1234
Передавать массивы можно одним из двух способов.
Первый подход, более старый, заключается в использовании типа typeglob
. При передаче typeglob
в процедуру, интерпретатор преобразует его в скаляр, который внутри процедуры может быть уточнен соответствующим идентификатором. Следующий пример демонстрирует это.
sub example {
local (*array, *hash) = @_;
foreach $item (@array) {
print "$item", "\n";
}
foreach $key (keys %hash) {
print "$hash{$key}", "\n";
}
$hash{name} = "Garry"; # Так как это ссылка на массив, его можно изменить из процедуры
}
@list = (1, 2, 3);
%person = ("name" => "Larry", "surname" => "Wall");
example (*list, *person);
foreach $key (keys %person) {
print "$person{$key}", "\n";
}
# Вывод:
# Wall
# Garry
В этом примере в процедуру мы передаем не сами массивы, а переменные типа typeglob
, которые легко выделить из @_
, так как фактически они являются скалярами. Мы использовали здесь local()
вместо my()
потому, что typeglob
представляет запись в таблице символов и поэтому не может быть локальной. Запись local (*array, *hash) = @_;
создает синонимы (псевдонимы), т.е. *array
фактически создает псевдоним для *list
, а *hash
для *person
. Таким образом, любые изменения по псевдонимам будут приводить к изменениям в оригиналах.
Второй подход, более новый, связан с передачей ссылок на массивы. Ссылка является скаляром, поэтому ее легко выделить в @_
. Таким образом, внутри процедуры достаточно просто применять операции разыменования ссылок. Вышеприведенный пример может быть переписан:
sub example {
my ($array, $hash) = @_;
foreach $item (@$array) {
print "$item", "\n";
}
foreach $key (keys %$hash) {
print "$$hash{$key}", "\n";
}
$$hash{name} = "Garry";
}
@list = (1, 2, 3);
%person = ("name" => "Larry", "surname" => "Wall");
example (\@list, \%person);
foreach $key (keys %person) {
print "$person{$key}", "\n";
}
В данном случае мы скопировали ссылки в локальные переменные при помощи my()
. Изменение оригинальных массивов через ссылки должна быть понятна.
Прототипы
[править]Встроенные функции Perl всегда имеют строго определенный синтаксис, другими словами, синтаксический анализатор Perl проверяет как они вызываются. По умолчанию, как вызываются пользовательские процедуры никак не проверяется: например, вы можете вызвать функцию и передавать ей аргументы, даже когда она их не ожидает, либо вы можете случайно передать переменные не тех типов и никаких ошибочных действий интерпретатор Perl не выявит.
Чтобы контролировать вызов процедур на этапе компиляции, используются прототипы. Прототип процедуры/функции — это строка из списка символов, определяющая количество и типы передаваемых параметров. Например следуюшая функция
sub example ($$) {
...
}
имеет прототип, который говорит, что функция ожидает две скалярные переменные. Следующие символы можно использовать в прототипе, чтобы обозначить передаваемый тип:
$
(скаляр)@
(массив)%
(хеш-массив)&
(анонимная процедура)*
(typeglob)
Если поставить перед символом в прототипе обратный слеш, например sub example (\$) {...}
, то имя фактического параметра всегда должно начинаться с идентификатора этого типа. В этом случае внутри процедуры в массиве параметров @_
будет передаваться ссылка на фактический параметр, указанный при вызове. Это позволяет упростить передачу массивов по ссылкам, например, сравните
sub variant1 {
my ($arr, $hash) = @_;
print "|@$arr|$$hash{name} $$hash{surname}|\n";
}
sub variant2 (\@\%) {
my ($arr, $hash) = @_;
print "|@$arr|$$hash{name} $$hash{surname}|\n";
}
@list = (1, 2, 3);
%person = (name => "Larry", surname => "Wall");
variant1(\@list, \%person); # Ссылки передаем явно
variant2(@list, %person); # По прототипу параметры преобразуются в ссылки автоматически
Обязательные параметры отделяются от необязательных внутри прототипа символом ;
. Например
sub example ($$$;$) { ... }
имеет 4 параметра, первые 3 из которых обязательны.
Следует помнить, что прототип не проверяется, когда вызов процедуры начинается с амперсанда &
(&example
).
Применение прототипов
[править]Интерпретатор имеет довольно много запутанных правил, связанных с использованием прототипов, однако, вам достаточно помнить самые значимые из них.
Прежде всего прототипы полезны тогда, когда вы хотите обезопасить пользователя от неправильного использования функции, которые должны вызываться в новом стиле (т.е. не через разыменовывание ссылок на функции). Таким образом, не имеет смысла писать прототипы для функций, которые вызываются как методы класса (см. Объектно-ориентированное программирование в Perl).
Прототипы проверяются на этапе компиляции, поэтому функции с прототипами должны быть видимыми на этом этапе.
Ниже приведены базовые примеры с пояснениями.
sub example ($$)
|
example $var1, $var2
|
Ожидает два скаляра, либо два литерала, которые могут быть записаны в скаляры |
sub example ($$;$)
|
example &some_func, 25
|
Ожидает три аргумента (скаляра), первые два из которых обязательные, а последний опционален. Обратите внимание, что в примере мы получаем скаляр в первом аргументе через вызов функции some_func
|
sub example (@)
|
example $a, $b, $c
|
Ожидает массив скаляров. Обратите внимание, что фактически такое объявление позволяет объявлять функции с произвольным числом аргументов |
sub example ($@)
|
example "text", $a, $b, $c
|
Ожидает скаляр и массив скаляров |
sub example (\@)
|
example @arr
|
Ожидает массив, который будет передан по ссылке. |
sub example (\@$$@)
|
example @arr1, 1, 2, @arr2
|
Ожидает массив, который передается по ссылке, два скаляра и массив. Обратите внимание, что последний массив автоматически развернется в список из скаляров, которые он хранит, поэтому фактическое число параметров зависит от размера @arr2
|
sub example (\[%@])
|
example %hash
|
Разрешает передать по ссылке в первом аргументе разные типы данных. В данном примере по ссылке может быть передан массив или хеш. |
sub example (*;$)
|
example HANDLE, $name
|
Ожидает обязательную typeglob-ссылку и необязательный скаляр |
sub example (&@)
|
example { "hello" } $a, $b, $c
|
Этот частный случай будет рассмотрен ниже. В общем случае функция ожидает процедуру или анонимный код и массив |
sub example (;$)
|
example 101
|
Функция имеет один необязательный аргумент |
sub example ($;)
|
example $a > $b
|
Частный случай, используемый для объявления унарной процедуры. В данном случае мы ставим точку с запятой в конце для того, чтобы выражение, передаваемое без скобок в аргументе, было интерпретировано как терм. Т.е. вызов в примере аналогичен example ($a > $b)
|
sub example ()
|
example
|
Запрещает передачу каких-либо аргументов функции |
- Идентификатор типа в прототипе в точности ожидает этот тип на этой позиции в фактическом вызове. Если идентификатор в прототипе снабжен обратной чертой, то фактически передаваемый тип должен соответствовать типу из прототипа (опционально он может быть передан из функций
my
,our
илиlocal
), а в массиве параметров@_
на этот объект будет передана ссылка. - Разрешается на одной позиции строки прототипа передавать ссылки на разные типы, если перечислить их после обратной черты в квадратных скобках. Например, функцию
ref
можно было бы представить таким прототипом:sub ref (\[$@%&*])
- В строке прототипа может использоваться символ
+
, который несет смысл аналогичный записи\[@%]
. Он полезен для функций, которые ожидают ссылки на массив или хеш. - Случай для прототипа
&@
является особым и позволяет вам создавать собственные синтаксические конструкции. Данный формат ожидает анонимный код в первой позиции, после которой не обязательно ставить разделитель аргументов. Благодаря этому, вы можете передавать лексемы, через которые, например, можно передавать имя процедуры и аргументы для неё (что-то похожее мы делали здесь, когда эмулировали конструкциюtry..catch
):sub try (&@) { my($try,$catch) = @_; eval { &$try }; if ($@) { local $_ = $@; &$catch; } } sub catch (&) { $_[0] } try { die "throw"; } catch { print "$_\n"; };
- Если определение функции идет намного позже её объявления, прототип должен быть одинаковым как в объявлении, так и в определении, однако проверка прототипа будет происходить только после того, как компилятор найдет определение.
Контекст выполнения функции
[править]Каждая функция может узнать в каком контексте она выполняется и в зависимости от этого отдавать результат в нужном контексте. Для этого используется функция wantarray()
, которая возвращает ИСТИНУ, если функция или блок eval{}
был вызван в списковом контексте, иначе ЛОЖЬ. Функция возвращает undef
значение, если она была вызвана в void-контексте.
Следующий небольшой пример демонстрирует работу этой функции.
sub getFruits {
my @arr = ('apple', 'pear', 'grapes');
if (wantarray) {
return @arr;
} else {
return \@arr;
}
}
@result = getFruits(); # Списковый контекст
print STDOUT "@{&getFruits}", "\n"; # Тоже списковый контекст
$result = getFruits(); # В скалярном контексте возвращается ссылка на массив
print STDOUT ref($result), "\n";
apple pear grapes
ARRAY
Рекурсивные вызовы
[править]Язык Perl допускает, чтобы процедура/функция вызывала саму себя. Такие вызовы называются рекурсивными.
Программирование рекурсивных функций мало чем отличается от других языков программирования. Основной рекомендацией является обязательное объявление всех переменных как my()
и local()
, чтобы создавать новые копии переменных на каждом новом уровне вызова.
Применять рекурсивный подход следует осторожно, так как рекурсивное программирование всегда расточительно в плане ресурсов. Однако, существуют алгоритмы, в которых применение рекурсии оправдано и необходимо. Самой известной задачей, где без рекурсии не обойтись, это проход по дереву каталогов для получения перечня файлов.
sub walk {
local (*ROOT);
my ($root) = $_[0];
opendir ROOT, $root;
my (@files) = readdir ROOT; # Получаем список файлов в каталоге
closedir ROOT;
for $file (@files) {
if ($file ne "." and $file ne "..") { # Файлы . и .. игнорируем
$file = $root . "/" . $file;
print " $file\n" if (-f $file);
if (-d $file) { # Если файл каталог
print "$file:\n";
walk($file); # Вызываем рекурсивно для этого каталога
}
}
}
}
walk "/etc";
← Ссылки | Пакеты, библиотеки и модули → |