Обработка исключений и Perl

Ничто не должно отвлекать программиста от написания основной логики программы. Вся обработка ошибок должна происходить далеко (и в смысле исходного кода, и в смысле загруженной в память программы) от работы этой логики. Эти принципы лежат в основе современной концепции обработки исключений. Если программа работает с объектами классов, без такой обработки не обойтись. При разговоре о современной концепции обработки исключений прежде всего имеются в виду языки C++ и Java (ну и C#,если угодно). Строгая типизация Java требовала соответствующей модели обработки ошибок. Осмелюсь заявить, что отсутствие такой модели в Perl является единственным уязвимым его местом. Но тому есть свои причины.

Во-первых, классы Perl, являющиеся просто пакетами, писались "на коленке" — было, видимо, сделано все возможное, чтобы не переписывать созданный ранее код. Так, Ларри Уолл — создатель языка — воплотил в жизнь свой тезис о трех великих добродетелях программиста, одной из которых является лень (напомню, что две другие — высокомерие и нетерпимость). Во-вторых, существующая в Perl обработка ошибок все же во многих случаях соответствует стоящим перед создаваемыми программами задачам (многие до сих пор уверены, что у Perl хорошо получается только обработка текстов, и, к тому же, он нетороплив и жаден до памяти). Но пора уже наконец признать, что пресловутая конструкция "open or die"- прошлый век программирования! Причем где-то начало 70-х. Так что лучший способ управлять ошибками в Perl — последовать примеру Ларри Уолла и использовать имеющийся багаж языка для создания собственного обработчика исключений (тем более, что другого я не вижу). Этим и займемся.

Дадим определение, от которого будем отталкиваться в дальнейшей работе. В некоторых книгах по ООП можно встретить фразу наподобие следующей: когда мы возбуждаем исключение, мы говорим: "я исключаю такую возможность". Возможно, здесь вина переводчика, но понимания сути исключения это не дает. Ведь все обстоит как раз наоборот: наличие обработчика исключения в методе говорит компилятору, что существует отличная от нуля вероятность возникновения такой ситуации, при которой работа программы пойдет совсем не так, как мы ожидаем, т.е. мы, напротив, не исключаем нештатного развития событий. Говоря о Java, нужно вспомнить, что объекты там создаются в динамической памяти (куче), а простые типы — в стеке (если мы не выделяем для них память оператором new). Состоянием программы S в данный момент времени будем называть множество значений кучи и стека. Рассмотрим множество значений N, любой элемент которого, находясь в куче или стеке, вызовет невозможность дальнейшего продолжения или нормального завершения программы. Например, в базе данных, хранящей сведения о возрасте сотрудников, это будет множество всех отрицательных чисел. Тогда можно дать следующее определение:

Исключительной называется ситуация, в которой пересечение множеств S и N не пусто.

Гурманы могут исследовать данные множества на рефлективность и транзитивность. Сейчас речь не об этом. Далее: раз уж мы приняли рабочую гипотезу, что модель обработки исключений Java является на сегодняшний день самой эффективной (одной из самых, апологеты C# и Python!),то и возьмем ее за образец. А для этого рассмотрим подробнее, как она работает. Когда мы возбуждаем в методе исключение (ну хорошо: метод сам возбуждает исключение) оператором throw, происходит следующее:

1. Создается объект класса Exception,являющегося потомком класса Throwable, который, в свою очередь, реализует интерфейс Serializable. Ссылка на этот объект передается оператору throw.

2. Метод возвращает значение, тип которого соответствует типу полученной ссылки.

3. Выброшенная ссылка перехватывается где-то в другом месте кода и там обрабатывается.

Пуристы языка Java, прочитав п.2,вероятно, заскрежетали зубами от злости. Хорошо, скажем по-другому: эта ссылка выбрасывается из текущего метода, а сам метод завершает выполнение. Хотя суть от этого не меняется: метод возвращает ссылку, но не вызвавшему его методу, а куда-то в другое место. Когда в книгах по Java появляется фраза вида "метод разбрасывает исключения", имеется в виду именно это. Здесь нужно сделать небольшое отступление и сказать, что вообще существуют два способа обрабатывать исключения. Первый — обработка с возобновлением — наиболее оптимистичен. Он полагает, что, насколько бы ни была серьезна ситуация во время выполнения программы, обработчик исключения способен ее решить. Таким образом, в обработчике пишется некоторый код, который пытается исправить положение и возвращает управление обратно в возбудивший исключение метод. И все бы хорошо, только неясно, что делать, если проблема все-таки не будет решена, и метод опять выбросит исключение. Единственный выход — в цикле вызывать обработчик, но тогда можно и не дождаться завершения программы. Именно поэтому современные языки используют второй способ — обработку с прерыванием. Например, Java считает, что возникшую в программе ошибочную ситуацию исправить не удастся, и остается только передать управление обработчику ошибок, а что делать дальше, оставляется на усмотрение программиста. Таким образом, последний словно говорит пользователю: я сделал все, что мог, но если ты все-таки решил писать в файл /etc/passwd, то без меня.

Теперь мы можем приступить к созданию собственного обработчика исключений в Perl. Все описанные ниже классы на этапе тестирования разместим в одном файле, а затем можно будет разбросать их по модулям. Вначале создадим интерфейс с названием Object. Опять же, обращаюсь к пуристам Java: это не будет интерфейс в строгом понимании слова. Он будет содержать вполне реальные методы, но по своему значению будет являться именно интерфейсом (свободная манера излагать мысли, принятая в Perl, позволяет нам это сделать).

package Object;
my $tref=NULL;
sub throw
{
$type=shift;
bless $type,Exception;
$tref=$type;
print "Exception $tref has been thrown in Exception::throw\n";
return $type;
}

sub catch
{
print $tref;
}

Здесь определены наши методы throw() и catch(). Значение поля $tref тоже очевидно. Эта переменная получает ссылку на объект исключения. Создадим класс myclass, производный от Object.

package myclass;
use locale;
use Fcntl;
@ISA=Object;
sub try
{
$code=shift;
$p=eval $code;
if($p!=1)
{
print "In method try: ";
Object::throw new Exception;
return;
}
}

sub catch
{
print "Exception $tref has been caught in myclass::catch\n";
}

Этот класс содержит метод try(), определяющий охраняемый участок — фрагмент кода, в котором возможно возникновение исключительной ситуации. Как работает код, содержащий охраняемый участок, станет ясно позже. Класс Exception, описывающий собственно исключение, имеет следующий вид:

package Exception;
sub new
{
$name=shift;
$tr={};
bless $tr,$name;
return $tr;
}

Теперь попробуем создать исключительную ситуацию — например, открыть несуществующий файл.

package main;
sub fop
{
my $f=shift;
if (!open F,$f)
{
print "In method fop: ";
Object::throw new Exception;
return; }
else { print "Open succesfully!\n"};
}

fop "D:\\does_not_exist.txt";
myclass::catch();

Как видим, исключение успешно перехвачено. Теперь попробуем сделать то же самое, используя охраняемый участок кода (при помощи метода try() ):
myclass::try ('sysopen F,"D:/does_not_exist.txt",O_RDWR ');
myclass::catch();

Результат тот же. Откроем теперь существующий файл, используя охраняемый участок.

Исключение, перехваченное в методе catch(),представляет собой пустую ссылку, т.е. объект Exception не создан. Теперь подробнее об охраняемом участке кода. Что происходит в методе fop()? Исключение, выброшенное при попытке открыть несуществующий файл, "умрет" здесь же, в методе catch(). Если же поместить вызов fop() в метод try(), выброшенное исключение "пролетит" через стек методов и будет перехвачено на самой верхушке этого стека. Что делать с перехваченным исключением, зависит, конечно, только от фантазии программиста. Но, как и везде, главное правило — не "убивать" исключения в обработчике. При наличии многоступенчатого стека вызовов это может создать труднообнаруживаемые подводные камни.

Вадим Шандриков


Компьютерная газета. Статья была опубликована в номере 07 за 2007 год в рубрике программирование

©1997-2024 Компьютерная газета