Обработчики ошибок как альтернатива исключению
Обработчик ошибок представляет собой подпрограмму, которая вызывается при обнаружении ошибки. Вы можете зарегистрировать подпрограмму для обработки определенной категории ошибок. При возникновении одной из этих ошибок происходит вызов обработчика.
Возникают ситуации, когда вам захочется применить обработчики ошибок вместо исключений или же одновременно с ними. Понятно, что, если вы работаете с языком С, не поддерживающим исключения, это один из нескольких возможных вариантов (см. Вопрос для обсуждения ниже). Но иногда обработчики ошибки могут использоваться даже в языках (типа Java), обладающих хорошей встроенной схемой обработки исключений.
Рассмотрим реализацию приложения «клиент-сервер» с использованием средства RMI (удаленный вызов метода) в языке Java. Поскольку RMI реализован определенным способом, каждое обращение к удаленной подпрограмме должно быть подготовлено, с тем чтобы обработать ситуацию RemoteException. Добавление программы обработки этих исключений может представлять собой утомительную процедуру и означает сложность написания программы, которая могла бы работать как с локальными, так и с удаленными подпрограммами. Обойти эту трудность возможно путем инкапсулирования удаленных объектов в класс, не являющийся удаленным. Тогда этот класс сможет реализовать интерфейс обработчика ошибок, позволяя программе клиента регистрировать подпрограмму, обращение к. которой происходит при обнаружении удаленной исключительной ситуации.
Другие разделы, относящиеся к данной теме:
• Мертвые программы не лгут
Вопросы для обсуждения
• В языках программирования, не поддерживающих исключительные ситуации, часто используется иной (нелокальный) способ передачи механизма управления (например, в языке С существует средство longjmp/setjmp). Подумайте, как можно реализовать некий «суррогатный» механизм исключения, используя указанные средства. В чем состоят преимущества и опасности? Какие специальные меры необходимо предпринять для гарантии того, что эти ресурсы не «осиротеют»? Есть ли смысл использовать подобное решение всякий раз, когда вы пишете программу на языке С?
Упражнения
21. При проектировании нового класса контейнера имеются три возможных состояния ошибки:
1. Не хватает памяти для нового элемента в подпрограмме add.
2. В подпрограмме fetch не обнаружена запрашиваемая точка входа.
3. Указатель null передан к подпрограмме add.
Каким образом необходимо обрабатывать каждую из этих ошибок? Нужна ли генерация ошибки, возбуждение исключительной ситуации, или же это состояние должно игнорироваться? (Ответ см. в Приложении В).
Балансировка ресурсов
"Я привел тебя в этот мир", – сказал бы мой отец, – "я же могу и отправить тебя обратно. Мне это без разницы. Я сделаю еще одного такого, как ты".
Билл Косби, Отцовство
При написании программ всем нам приходится управлять ресурсами: памятью, транзакциями, потоками, файлами, таймерами – словом, всеми типами объектов, доступность которых ограничена. Большую часть времени использование ресурса следует предсказуемой схеме: ресурс назначается, используется, а затем освобождается.
Однако многие разработчики не имеют сколько-нибудь завершенного плана, касающегося распределения и освобождения ресурсов. Поэтому предлагается простая подсказка:
Подсказка 35: Доводите до конца то, что начинаете
В большинстве случаев эту подсказку применить несложно. Она всего лишь означает, что подпрограмма или объект, который назначает ресурс, должен нести ответственность за освобождение этого ресурса. Использование указанной подсказки можно рассмотреть на примере неудачной программы – приложения, открывающего файл, считывающего из него информацию о заказчике, обновляющего поле и записывающего результат обратно в файл. Чтобы сделать пример более наглядным, мы опустили процедуру обработки ошибок.
void readCustomer(const char *fName, Customer *cRec) {
cFiie = fopen(fName, "r+");
fread(cRec, sizeof(*cRec), 1, cFile);
}
void writeCustomer(Customer *cRec) {
rewind(cFile);
fwrite(cRec, sizeof(*cRec), 1; cFile);
fclose(cFile);
}
void updateCustomer(const char *fName, double newBalance) {
Customer cRec;
readCustomer(fName, &cRec);
cRec.balance = newBalance;
writeCustomer(&cRec);
}
На первый взгляд, подпрограмма updateCustomer выглядит довольно прилично. Похоже, что она реализует нужную нам логику – считывает запись, обновляет баланс и осуществляет запись обратно в файл. Однако, за внешним приличием и скрывается главная проблема. Подпрограммы readCustomer и writeCustomer тесно связаны между собой [27] – они совместно используют глобальную переменную cFile. Подпрограмма readCustomer открывает файл и сохраняет указатель файла в переменной cFile, а подпрограмма writeCustomer использует сохраненный указатель для закрытия файла по окончании работы. Эта глобальная переменная даже не появляется в подпрограмме updateCustomer.
Почему эту программу можно считать неудачной? Представим несчастную даму-программистку из службы сопровождения, которой сказали, что спецификация изменилась – баланс должен обновляться только в том случае, если его новое значение неотрицательное. Дама открывает файл с исходным текстом и изменяет подпрограмму updateCustomer:
void updateCustomer(const char *fName, double newBalance) {
Customer cRec;
readCustomer(fName, &cRec);
if (newBalance >= 0.0) {
cRec.balance = newBalance;
writeCustomer(&cRec);
}
}
Во время испытания все выглядит замечательно. Однако когда программа вводится в эксплуатацию, она «падает» через несколько часов, выдавая сообщение "Слишком много открытых файлов". Поскольку подпрограмма writeBalance в ряде случаев не вызывается, то и файл не закрывается.
У этой проблемы есть весьма неудачное решение – использовать специальный оператор выбора в подпрограмме updateCustomer:
void updateCustomer(const char *fName, double newBalance) {
Customer cRec;
readCustomer(fName, &cRec);
if (newBalance»= 0.0) {
cRec.balance = newBalance;
writeCustomer(&cRec);
}
else
fclose(cFile);
}
Это устраняет проблему – файл закроется независимо от нового значения баланса, но эта процедура означает, что теперь связанными оказываются три подпрограммы (через глобальную переменную cFile). Мы попадаем в ловушку, и если мы продолжаем действовать в том же духе, то все полетит под откос.
Подсказка "Доводите до конца то, что начинаете" говорит нам о том, что в идеале подпрограмма, которая назначает ресурс, обязана его и освобождать. Мы можем применить ее в данном случае, осуществляя небольшую реорганизацию программы:
void readCustomer(FILE *cFile, Customer *cRec) {
fread(cRec, sizeof(*cRec), 1, cFile);
}
void writeCustomer(FILE *cFile, Customer *cRec) {
rewind(cFile);
fwrite(cRec, sizeof(*cRec), 1, cFile);
}
void updateCustomer(const char *fName, double newBalance) {
FILE *cFile;
Customer cRec;
cFile = fopen(fName, "r+"); // ->>>
readCustomer(cFile, &cRec); //
if (newBalance >= 0.0) { //
cRec.balance = newBalance; //
writeCustomer(cFile, &cRec); //
} //
Fclose(cFile); // <<<-
}
Теперь всю ответственность за файл несет подпрограмма updateCustomer. Она открывает файл и (заканчивая то, что уже начала) закрывает его перед выходом. Подпрограмма осуществляет балансировку использования файла: открытие и закрытие происходят на одном и том же месте, и очевидно, что каждой процедуре открытия будет соответствовать процедура закрытия. Реорганизация также позволяет избавиться от уродливой глобальной переменной.