Перехват ошибок в блоках eval
Иногда даже самый обычный, повседневный код приводит к фатальным ошибкам в программе. Любая из следующих типичных команд может стать причиной аварийного завершения:
$barney = $fred / $dino; # Деление на нуль?
print "match\n" if /^($wilma)/; # Некорректное регулярное выражение?
open CAVEMAN, $fred # Ошибка пользователя приводит к сбою die?
or die "Can't open file '$fred' for input: $!";
Некоторые из этих ошибок можно выявить заранее, но обнаружить их все невозможно. (Как проверить строку $wilma из этого примера и убедиться в том, что она содержит корректное регулярное выражение?) К счастью, в Perl имеется простой способ перехвата фатальных ошибок – проверяемый код заключается в блок eval:
eval { $barney = $fred / $dino } ;
Даже если переменная $dino равна нулю, эта строка не приведет к сбою программы. В действительности eval является выражением (а не управляющей конструкцией, как while или foreach), поэтому точка с запятой в конце блока обязательна. Если во время выполнения блока eval происходит фатальная (в обычных условиях) ошибка, блок продолжает работать, но программа аварийно не завершается. Таким образом, сразу же после завершения eval желательно проверить, завершился ли блок нормально или произошла фатальная ошибка. Ответ содержится в специальной переменной $@. Если в блоке была перехвачена фатальная ошибка, $@ будет содержать «последние слова» программы – сообщение вида «Недопустимое деление на нуль в my_program строка 12». Если ошибки не было, переменная $@ пуста. Конечно, это означает, что $@ содержит удобный логический признак для проверки (true в случае ошибки), поэтому после блоков eval часто встречается команда следующего вида:
print "An error occurred: $@" if $@;
Блок eval является полноценным блоком, поэтому он определяет новую область видимости для лексических (my) переменных. В следующем фрагменте показан блок eval в действии:
foreach my $person (qw/ fred wilma betty barney dino pebbles /) {
eval {
open FILE, "<$person"
or die "Can't open file '$person': $!";
my($total, $count);
while (<FILE>) {
$total += $_;
$count++;
}
my $average = $total/$count;
print "Average for file $person was $average\n";
&do_something($person, $average);
};
if ($@) {
print "An error occurred ($@), continuing\n";
}
}
Сколько возможных фатальных ошибок перехватывает этот блок? Если произойдет ошибка при открытии файла, она будет перехвачена. Вычисление среднего арифметического может привести к делению на нуль, и эта ошибка тоже перехватывается. Даже вызов загадочной функции &do_something защищается от фатальных ошибок, потому что блок eval перехватывает все фатальные ошибки, происходящие во время его активности. (В частности, это будет удобно, если вы вызываете пользовательскую функцию, написанную другим программистом, но не знаете, насколько надежно она написана.) Если ошибка происходит в ходе обработки одного из файлов, мы получим сообщение об ошибке, но программа спокойно перейдет к следующему файлу. Блоки eval даже могут вкладываться в другие блоки eval. Внутренний блок перехватывает ошибки во время выполнения, не позволяя им добраться до внешних блоков. (Конечно, после завершения внутреннего блока eval можно «перезапустить» ошибку при помощи die, чтобы она была перехвачена внешним блоком.) Блок eval перехватывает любые ошибки, происходящие во время выполнения, в том числе и ошибки при вызове функций (как показано в предыдущем примере).
Ранее мы уже упоминали о том, что eval является выражением, и поэтому после завершающей фигурной скобки необходима точка с запятой. Но как и любое выражение, eval имеет некоторое возвращаемое значение. При отсутствии ошибок значение вычисляется по аналогии с функциями: как результат последнего вычисленного выражения или как значение, возвращаемое на более ранней стадии необязательным ключевым словом return. А вот другой способ выполнения вычислений, при котором вам не нужно беспокоиться о делении на нуль:
my $barney = eval { $fred / $dino };
Если eval перехватывает фатальную ошибку, возвращаемое значение представляет собой либо undef, либо пустой список (в зависимости от контекста). Таким образом, в предыдущем примере $barney либо содержит правильный результат деления, либо undef; проверять $@ необязательно (хотя, вероятно, перед дальнейшим использованием $barney стоит включить проверку defined($barney)). Существуют четыре вида проблем, которые eval перехватить не может. Первую группу составляют очень серьезные ошибки, нарушающие работу Perl, – нехватка памяти или получение необработанного сигнала. Поскольку Perl при этом перестает работать, перехватить эти ошибки он не сможет. Конечно, синтаксические ошибки в блоке eval перехватываются на стадии компиляции – они никогда не возвращаются в $@. Оператор exit завершает программу немедленно, даже если он вызывается в пользовательской функции в блоке eval. (Отсюда следует, что при написании пользовательской функции в случае возникновения проблем следует использовать die вместо exit.)
Четвертую и последнюю разновидность проблем, не перехватываемых блоком eval, составляют предупреждения – пользовательские (из warn) или внутренние (включаемые ключом командной строки –w или директивой use warnings). Для перехвата предупреждений существует специальный механизм, не связанный с eval; за информацией обращайтесь к описанию псевдосигнала __WARN__ в документации Perl. Стоит также упомянуть еще об одной форме eval, которая может быть опасна при неправильном использовании. Более того, некоторые люди считают, что eval вообще не следует использовать в программах по соображениям безопасности. Действительно, eval следует использовать с осторожностью, но здесь имеется в виду другая форма eval – «eval для строк». Если же за ключевым словом eval сразу же следует блок программного кода в фигурных скобках, беспокоиться не о чем – эта разновидность eval безопасна.