перевод статьи Peter Makholm
Perl exception handling is hard Всем хорошо известно, что обработка исключений в Perl реализуется при помощи eval и die, вместо try и catch.
eval {
do_something()
or die "Err!";
};
if ($@) {
print "Catched exception: $@";
}
Так советует делать Damian Conway в 16 главе своей книги Perl Best Practices и perldoc -f die. К сожалению, такая схема работает не во всех случаях. Рассмотрим пример:
#!/usr/bin/perl
package Foo;
sub new {
my $class = shift;
return bless {}, $class;
}
sub DESTROY {
my $self = shift;
eval { 1; };
}
package main;
eval {
my $foo = Foo->new();
open my $fh, ">", "/"
or die "Could not open /: $!";
print "Doing some work\n";
};
if ($@) {
print STDERR "Something bad happened: $@\n";
} else {
print "Everything went well\n";
}
Ввиду невозможноси открыть "/" для записи, мы ожидаем получить сообщение об ошибке. Но в реальности скрипт завершается успешно, хотя при этом не выводит "Doing some work". Что-же происходит? При выходе из области видимости $foo вызывается деструктор, в котором вызывается eval и изменяет значение $@.
Что можно сделать для решения проблемы? Прежде всего можно избежать модификации @_ в деструкторе. Добиться этого можно при помощи локализации @_. Даже если вы не используете eval явно, он может вызыватся где-то в глубине. Поэтому всегда начинайте писать деструктор по следующему шаблону:
sub DESTROY {
my $self = shift;
local $@;
...;
}
Скорее всего вы также захотите локализовать $?, но это уже другая история.
Еще одним вариантом решения будет изменение схемы эмуляции try/catch. В случае аварийного завершения блока eval возвращает значение undef. Поэтому можно явно прописать в блоке возврат "истинного" значения и заменить проверку $@ на логическое условие or.
eval {
do_work();
1;
} or {
print "catch: $@";
}
Конечно, описание ошибки в $@ может быть некорректным, поэтому такой метод не так хорош, как вариант с деструктором. В случае использования модулей сторонних авторов вы можете реализовать многоуровневую схему используя оба приведенных решения. Я думаю, в Perl::Critic есть политика обрабатывающая вышеприведенную try/catch конструкцию.
Мне стало интересно, как модули обработки исключений с CPAN ведут себя с вариантом вызова деструктора. Я проверил два из них: Error.pm обрабатывает вызов дестуктора корректно, т.к. в нем исключение запоминается до вызова die. Более современный модуль TryCatch.pm, к сожалению, работает в таком случае некорректно (см. rt #46294), хотя он намного функциональнее.
Дополнение: Я планирую написать патч для TryCatch.pm, который реализует функцию throw, аналогично Error.pm.