Книга: Основы объектно-ориентированного программирования
Обработка исключений
Теперь у нас есть определение того, что может случиться, - исключения - и того, с чем мы бы не хотели столкнуться в результате появления исключения, - отказа. Давайте разыскивать способы справляться с исключениями так, чтобы не возникли отказы. Что может сделать программа, когда ее выполнение прервано из-за нежелательного поведения?
Помощь в нахождении разумного ответа могут дать примеры того, как не следует поступать в подобных ситуациях. Ими мы обязаны механизму сигналов языка C, пришедшему из Unix, и одному учебнику по языку Ada.
Как не следует делать это - C-Unix пример
Первым контрпримером механизма (наиболее полно представленным в Unix, но доступным и на других платформах, реализующих C) является процедура signal, вызываемая в следующей форме:
signal (signal_code, your_routine)
с эффектом вызова обработчика исключения - программы your_routine, когда выполнение текущей программы прерывается, выдавая соответствующий код сигнала (signal_code). Код сигнала - целочисленная константа, например, SIGILL (неверная инструкция - illegal instruction) или SIGFPE (переполнение с плавающей точкой - floating-point exception). В программу можно включить сколь угодно много вызовов процедуры signal, что позволяет обрабатывать различные, возможные ошибки.
Теперь предположим, что при выполнении некоторой инструкции произошло прерывание и выработан соответствующий код сигнала. Будет или нет вызвана процедура signal, но выполнение программы завершается в не нормальном состоянии. Предположим, что вызывается обработчик события - your_routine, пытающийся исправить ситуацию. Беда в том, что, завершив работу, он возвращает управление непосредственно в точку, где произошло прерывание (в не нормальное состояние). Это опасно, вероятнее всего, из этой точки невозможно нормально продолжить работу.
Что необходимо в большинстве подобных случаев - исправить ситуацию и продолжить выполнение, начиная с некоторой особой точки, но не точки прерывания. Мы увидим, что есть простой механизм, реализующий эту схему. Заметьте, он может быть реализован и на C, на большинстве платформ. Достаточно комбинировать процедуру signal с двумя другими библиотечными процедурами: setjmp, вставляющую маркер в точку, допускающую продолжение вычислений, и longjmp для возврата к маркеру. С механизмом setjmp-longjmp следует обращаться весьма аккуратно. Поэтому он не ориентирован на обычных программистов, но может использоваться разработчиками компиляторов для реализации высокоуровневого механизма ОО-исключений, который будет описан в этой лекции.
Как не следует делать это - Ada пример
Приведу пример программы, взятый из одного учебника12.1) по языку Ada.
sqrt (x: REAL) return REAL is
begin
if x < 0.0 then
raise Negative
else
normal_square_root_computation
end
exception
when Negative =>
put ("Negative argument")
return
when others => ...
end -- sqrt
Этот пример, вероятно, предназначался для синтаксической иллюстрации механизма Ada, и был написан быстро (он, например, отказывается возвращать значение в случае возникновения исключения). Поэтому было бы непорядочно критиковать его, как если бы это был настоящий пример хорошего программирования. Вместе с тем, он ясно показывает нежелательный способ обработки исключений. Поскольку Ada ориентирована на военные и космические приложения, то остается надеяться, что ни одна из реальных программ не следует буквально этой модели.
Целью программы является получение вещественного квадратного корня из вещественного числа. Но что если число отрицательно? В языке Ada нет утверждений, так что в программе проводится проверка, возбуждающая исключение для отрицательных чисел.
Инструкция raise Exc прерывает выполнение текущей программы и включает исключение с кодом Exc. Это исключение может быть захвачено и обработано при наличии предложений exception, имеющих вид:
exception
when code_a1, code_a2, ...=> Instructions_a;
when code_b1, ... => Instructions_b;
...
Если код исключения совпадает с одним из кодов, указанных в части when, то выполняются соответствующие инструкции. Если, как в примере, есть предложение when others, то его инструкции выполняются, когда код исключения не совпадает ни с одним из кодов предыдущих частей when. Если нет универсального обработчика when others, и код исключения не совпадает ни с одним кодом, то поиск обработчика будет вестись у вызывающей программы, если вызывающей программы нет, то достигнута программа main и программа завершается отказом.
В примере нет необходимости переходить к вызывающей программе, поскольку выброшенное исключение с кодом Negative захватывается обработчиком с таким же кодом.
Но что делают соответствующие инструкции? Посмотрите еще раз:
put ("Negative argument")
return
Напечатается сообщение - довольно глубокомысленное, а затем управление перейдет к вызывающей программе, которая, не будучи уведомлена о событии, продолжит свое выполнение, как если бы ничего не случилось. Вспоминая снова о типичных приложениях Ada, можно лишь надеяться, что этой схеме не следует артиллерийское приложение, в результате которой снаряды могут упасть на головы совсем не тех солдат, для которых вряд ли может служить утешением посланное сообщение об ошибке.
Эта техника, вероятно, хуже, чем C-Unix сигнальный механизм, позволяющий, по крайней мере, возобновить вычисление в точке, где оно остановилось. Обработчик исключения when, заканчивающийся инструкцией return, даже не продолжает текущую программу; он возвращает управление вызывающей программе, будто бы все прекрасно, в то время как все далеко не прекрасно.
Этот контрпример дает хороший урок Ada-программистам: почти ни при каких обстоятельствах обработчик when не должен заканчиваться return. Слово "почти" употреблено для полноты картины, поскольку есть особый допустимый случай ложной тревоги (false alarm), достаточно редкий, который мы обсудим чуть позже. Опасно и неприемлемо не уведомлять вызывающую программу о возникшей ошибке. Если невозможно исправить ситуацию и выполнить контракт, то программа должна выработать отказ. Язык Ada позволяет сделать это: предложение exception может заканчиваться инструкцией raise без параметров, повторно выбрасывая исходное исключение, передавая его вызывающей программе. Это и есть подходящий способ завершения выполнения, когда невозможно выполнить свой контракт.
Правило исключений языка Ada
Выполнение любого обработчика исключений должно заканчиваться либо выполнением инструкции raise, либо повторением объемлющего программного блока.
Принципы обработки исключений
Контрпримеры помогли указать дорогу к дисциплинированному использованию исключений. Следующие принципы послужат основой обсуждения.
Принципы дисциплинированной обработки исключений
Есть только два легитимных отклика на исключение, возникшее при выполнении программы:
1 Повторение (Retrying) - попытка изменить условия, приведшие к исключению, и выполнить программу повторно, начиная все сначала.
2 Отказ (Failure) - известный также как "организованная паника" (organized panic): чистка стека и других ресурсов, завершение вызова и отчет об отказе перед вызывающей программой.
В дополнение, некоторые сигналы операционной системы (случай (3) в классификации исключений) в редких случаях являются откликом на "ложную тревогу". Определив, что исключение безвредно, можно возобновить выполнение в точке прерывания.
Давайте начнем рассмотрение с третьего случая - ложной тревоги, обработка которого соответствует основному механизму C-Unix. Вот пример. Некоторые оконные системы будут вызывать исключения, если пользователь перестраивает размеры окна во время выполнения процесса в этом окне. Предположим, что процесс не выполняет никакого вывода в это окно, тогда исключение будет безвредным, и можно возобновить выполнение процесса в прерванной точке. Но даже в этом случае есть лучшие пути, такие как полная блокировка сигналов на время выполнения процесса, чтобы исключение вообще не встретилось. Именно так мы будем поступать с ложными тревогами в механизме, рассматриваемом в следующем разделе.
Ложные тревоги возможны лишь для одного вида сигналов операционной системы - благоприятных сигналов, но нельзя игнорировать арифметическое переполнение или невозможность выделения запрашиваемой памяти. Исключения всех других категорий также указывают на трудности, не допускающие игнорирования. Было бы абсурдно, например, запускать программу при ложном предусловии.
Повторение - более обнадеживающая стратегия: мы потерпели поражение в битве, но не проиграли войну. Хотя наш первоначальный план выполнения контракта потерпел неудачу, мы можем постараться удовлетворить клиента, применив другую тактику. Если она будет успешной, то исключение не оказывает никакого влияния на клиента. После одной или нескольких попыток, приведших к неудаче, в очередной попытке нам, возможно, удастся полностью выполнить контракт ("Миссия завершена, сэр. Обычные, небольшие проблемы, сэр. Теперь все хорошо, сэр").
Что значит "другая тактика", испытываемая при следующей попытке? Это может быть другой алгоритм; или тот же алгоритм, выполняемый после некоторых произведенных изменений в начальном состоянии (атрибуты, локальные переменные). В некоторых случаях это может быть просто повторный запуск той же программы в надежде, что изменились внешние условия - освободились временно занятые устройства, линии связи и так далее.
При отказе приходится признавать не только поражение в битве, но и невозможность выиграть войну. Мы сдаемся, но прежде следует выполнить два условия, объясняющие использование термина "организованная паника", как более точного синонима понятия "отказ":
[x]. Обеспечить появление исключения у вызывающей программы. В этом и состоит аспект "паники" - программа отказывается жить в соответствии с ее контрактом.
[x]. Восстановить согласованное состояние выполнения - "организованный" аспект.
Что является согласованным состоянием? Корректность класса позволяет дать ответ: состояние, удовлетворяющее инварианту. Мы уже говорили, что программа во время ее выполнения может нарушать инвариант, восстанавливая его в конце работы. Если возникло исключение, то инвариант может быть нарушен. Программа должна восстановить его до возвращения управления вызывающей программе.
Цепочка вызовов
Обсуждая механизм обработки исключений, полезно иметь ясную картину последовательности вызовов, приведших в итоге к исключению. Это понятие уже появлялось при рассмотрении механизма языка Ada.
Рис. 12.1. Цепочка вызовов
Пусть r0 будет корневой процедурой некоторой системы (в Ada это программа main). В каждый момент выполнения есть текущая программа, вызванная последней и ставшая причиной исключения. Пройдем по цепочке в обратном порядке, начиная с текущей программы, от вызываемой к вызывающей программе. Реверсная цепочка (r0, последняя вызванная r0 программа r1, последняя вызванная r1 программа r2 и так далее до текущей программы) называется цепочкой вызовов.
Если возникает исключение, то для его обработки, возможно, придется подняться по цепочке, пока не будет достигнута программа, способная справиться с исправлением ситуации. Этот процесс заканчивается, когда достигнута программа r0 и не найден нужный обработчик исключения.
- Генерирование исключений
- ГЛАВА 4 Обработка исключений
- ГЛАВА 6. Структурированная обработка исключений
- Исключения и обработчики исключений
- 26.7. Обработка дополнительных аргументов
- 19.7.9. Обработка сигналов и протоколирование
- Векторная обработка исключений
- Последний глобальный шанс для обработки исключений
- Пример: обработка ошибок как исключений
- SEH и обработка исключений в C++
- 9. ОБРАБОТКА ИСКЛЮЧЕНИЙ
- Лекция 12. Когда контракт нарушается: обработка исключений