Обработка исключительных ситуаций (exception handling) — механизм языков программирования, предназначенный для описания реакции программы на ошибки времени выполнения и другие возможные проблемы (исключения), которые могут возникнуть при выполнении программы и приводят к невозможности (бессмысленности) дальнейшей отработки программой её базового алгоритма. В русском языке также применяется более короткая форма термина: «обработка исключений».
Общее понятие исключительной ситуации
Во
время выполнения программы могут возникать ситуации, когда состояние
данных, устройств ввода-вывода или компьютерной системы в целом делает
дальнейшие вычисления в соответствии с базовым алгоритмом невозможным
или бессмысленными. Классические примеры подобных ситуаций приведены
ниже.
Нулевое значение знаменателя при выполнении операции
целочисленного деления. Результата у операции быть не может, поэтому ни
дальнейшие вычисления, ни попытка использования результата деления не
приведут к решению задачи.
Ошибка при попытке считать данные с
внешнего устройства. Если данные не удаётся ввести, любые дальнейшие
запланированные операции с ними бессмысленны.
Исчерпание доступной
памяти. Если в какой-то момент система оказывается не в состоянии
выделить достаточный для прикладной программы объём оперативной памяти,
программа не сможет работать нормально.
Появление сигнала аварийного
отключения электропитания системы. Прикладную задачу, по всей видимости,
решить не удастся, в лучшем случае (при наличии какого-то резерва
питания) прикладная программа может озаботиться сохранением данных.
Появление
на входе коммуникационного канала данных, требующих немедленного
считывания. Чем бы ни занималась в этот момент программа, она должна
перейти к чтению данных, чтобы не потерять поступившую информацию.
Виды исключительных ситуаций
Исключительные
ситуации, возникающие при работе программы, можно разделить на два
основных типа: синхронные и асинхронные, принципы реакции на которые
существенно различаются.
Синхронные исключения могут возникнуть
только в определённых, заранее известных точках программы. Так, ошибка
чтения файла или коммуникационного канала, нехватка памяти — типичные
синхронные исключения, так как возникают они только в операции чтения из
файла или из канала или в операции выделения памяти соответственно.
Асинхронные
исключения могут возникать в любой момент времени и не зависят от того,
какую конкретно инструкцию программы выполняет система. Типичные
примеры таких исключений: аварийный отказ питания или поступление новых
данных.
Некоторые типы ошибок могут быть отнесены как к
синхронным, так и к асинхронным. Например, инструкция деления на нуль на
многих программно-аппаратных платформах приводит к синхронному
исключению, но на некоторых платформах за счёт глубокой конвейеризации
исключение может оказаться асинхронным.
Обработчики исключений
В отсутствие собственного механизма
обработки исключений для прикладных программ наиболее общей реакцией на
любую исключительную ситуацию является немедленное прекращение
выполнения с выдачей пользователю сообщения о характере исключения.
Можно сказать, что в подобных случаях единственным и универсальным
обработчиком исключений становится операционная система. Например, в
операционную систему Windows встроена утилита Dr. Watson, которая
занимается сбором информации об необработанном исключении и ее отправкой
на специальный сервер компании Microsoft.
Возможно игнорирование
исключительной ситуации и продолжение работы, но такая тактика опасна,
так как приводит к ошибочным результатам работы программ или
возникновению ошибок впоследствии. Например, проигнорировав ошибку
чтения из файла блока данных, программа получит в своё распоряжение не
те данные, которые она должна была считать, а какие-то другие.
Результаты их использования предугадать невозможно.
Обработка
исключительных ситуаций самой программой заключается в том, что при
возникновении исключительной ситуации, управление передаётся некоторому
заранее определённому обработчику — блоку кода, процедуре, функции,
которые выполняют необходимые действия.
Существует два принципиально разных механизма функционирования обработчиков исключений.
Обработка с возвратом
подразумевает, что обработчик исключения ликвидирует возникшую проблему
и приводит программу в состояние, когда она может работать дальше по
основному алгоритму. В этом случае после того, как выполнится код
обработчика, управление передаётся обратно в ту точку программы, где
возникла исключительная ситуация (либо на команду, вызвавшую исключение,
либо на следующую за ней, как в некоторых старых диалектах языка BASIC)
и выполнение программы продолжается. Обработка с возвратом типична для
обработчиков асинхронных исключений (которые обычно возникают по
причинам, не связанным прямо с выполняемым кодом), для обработки
синхронных исключений она малопригодна.
Обработка без возврата заключается
в том, что после выполнения кода обработчика исключения управление
передаётся в некоторое, заранее заданное место программы, и с него
продолжается исполнение.
Существует два варианта подключения
обработчика исключительных ситуаций к программе: структурная и
неструктурная обработка исключений.
Неструктурная обработка исключений
Неструктурная
обработка исключений реализуется в виде механизма регистрации функций
или команд-обработчиков для каждого возможного типа исключения. Язык
программирования или его системные библиотеки предоставляют программисту
как минимум две стандартные процедуры: регистрации обработчика и
разрегистрации обработчика. Вызов первой из них «привязывает» обработчик
к определённому исключению, вызов второй — отменяет эту «привязку».
Если исключение происходит, выполнение основного кода программы
немедленно прерывается и начинается выполнение обработчика. По
завершении обработчика управление передаётся либо в некоторую наперёд
заданную точку программы, либо обратно в точку возникновения исключения
(в зависимости от заданного способа обработки — с возвратом или без).
Независимо от того, какая часть программы в данный момент выполняется,
на определённое исключение всегда реагирует последний зарегистрированный
для него обработчик. В некоторых языках зарегистрированный обработчик
сохраняет силу только в пределах текущего блока кода (процедуры,
функции), тогда процедура разрегистрации не требуется. Ниже показан
условный фрагмент кода программы с неструктурной обработкой исключений:
УстановитьОбработчик(ОшибкаБД, ПерейтиНа ОшБД)
// На исключение "ОшибкаБД" установлен обработчик - команда "ПерейтиНа ОшБД"
... // Здесь находятся операторы работы с БД
ПерейтиНа СнятьОшБД // Команда безусловного перехода - обход обработчика исключений
ОшБД: // метка - сюда произойдёт переход в случае ошибки БД по установленному обработчику
... // Обработчик исключения БД
СнятьОшБД:
// метка - сюда произойдёт переход, если контролируемый код выполнится без ошибки БД.
СнятьОбработчик(ОшибкаБД)
// Обработчик снят
Неструктурная обработка
— практически единственный вариант для обработки асинхронных
исключений, но для синхронных исключений она неудобна: приходится часто
вызывать команды установки/снятия обработчиков, всегда остаётся
опасность нарушить логику работы программы, пропустив регистрацию или
разрегистрацию обработчика.
Структурная обработка исключений
Структурная
обработка исключений требует обязательной поддержки со стороны языка
программирования — наличия специальных синтаксических конструкций. Такая
конструкция содержит блок контролируемого кода и обработчик
(обработчики) исключений. Наиболее общий вид такой конструкции
(условный):
НачалоБлока
... // Контролируемый код
...
если (условие) то СоздатьИсключение Исключение2
...
Обработчик Исключение1
... // Код обработчика для Исключения1
Обработчик Исключение2
... // Код обработчика для Исключения2
ОбработчикНеобработанных
... // Код обработки ранее не обработанных исключений
КонецБлока
Здесь
«НачалоБлока» и «КонецБлока» — ключевые слова, которые ограничивают
блок контролируемого кода, а «Обработчик» — начало блока обработки
соответствующего исключения. Если внутри блока, от начала до первого
обработчика, произойдёт исключение, то произойдёт переход на обработчик,
написанный для него, после чего весь блок завершится и исполнение будет
продолжено со следующей за ним команды. «ОбработчикНеобработанных» —
это обработчик исключений, которые не соответствуют ни одному из
описанных выше в данном блоке. Обработчики исключений в реальности могут
описываться по-разному (один обработчик на все исключения, по одному
обработчику на каждый тип исключение), но принципиально они работают
одинаково: при возникновении исключения находится первый соответствующий
ему обработчик в данном блоке, его код выполняется, после чего
выполнение блока завершается. Исключения могут возникать как в
результате программных ошибок, так и путём явной их генерации с помощью
соответствующей команды (в примере — команда «СоздатьИсключение»). С
точки зрения обработчиков такие искусственно созданные исключения ничем
не отличаются от любых других.
Блоки обработки исключений могут
многократно входить друг в друга, как явно (текстуально), так и неявно
(например, в блоке вызывается процедура, которая сама имеет блок
обработки исключений). Если ни один из обработчиков в текущем блоке не
может обработать исключение, то выполнение данного блока немедленно
завершается, и управление передаётся на ближайший подходящий обработчик
более высокого уровня иерархии. Это продолжается до тех пор, пока
обработчик не найдётся и не обработает исключение или пока не выйдет из
обработчиков заданных программистом и не будет переданно системному
обработчику, поумолчанию аварийно закроющий программу.
Иногда
бывает неудобно завершать обработку исключения в текущем блоке, то есть
желательно, чтобы при возникновении исключения в текущем блоке
обработчик выполнил какие-то действия, но исключение продолжило бы
обрабатываться на более высоком уровне (обычно так бывает, когда
обработчик данного блока не полностью обрабатывает исключение, а лишь
частично). В таких случаях в обработчике исключений генерируется новое
исключение или возобновляется с помощью специальной команды ранее
появившееся. Код обработчиков не является защищённым в данном блоке,
поэтому созданное в нём исключение будет обрабатываться в блоках более
высокого уровня.
Блоки с гарантированным завершением
Помимо
блоков контролируемого кода для обработки исключений, языки
программирования могут поддерживать блоки с гарантированным завершением.
Их использование оказывается удобным тогда, когда в некотором блоке
кода, независимо от того, произошли ли какие-то ошибки, необходимо перед
его завершением выполнить определённые действия. Простейший пример:
если в процедуре динамически создаётся какой-то локальный объект в
памяти, то перед выходом из этой процедуры объект должен быть уничтожен
(чтобы избежать утечки памяти), независимо от того, произошли после его
создания ошибки или нет. Такая возможность реализуется блоками кода
вида:
НачалоБлока
... // Основной код
Завершение
... // Код завершения
КонецБлока
Заключённые
между ключевыми словами «НачалоБлока» и «Завершение» операторы
(основной код) выполняются последовательно. Если при выполнении их не
возникает исключений, то затем выполняются операторы между ключевыми
словами «Завершение» и «КонецБлока» (код завершения). Если же при
выполнении основного кода возникает исключение (любое), то сразу же
выполняется код завершения, после чего весь блок завершается, а
возникшее исключение продолжает существовать и распространяться до тех
пор, пока его не перехватит какой-либо блок обработки исключений более
высокого уровня.
Принципиальное отличие блока с гарантированным
завершением от обработки — то, что он не обрабатывает исключение, а лишь
гарантирует выполнение определённого набора операций перед тем, как
включится механизм обработки. Стоит заметить, что блок с гарантированным
завершением легко реализуется с помощью команд «возбудить исключение» и
«структурный обработчик исключения».
Поддержка в различных языках
Большинство
современных языков программирования, такие как Ada, C++, D, Delphi,
Objective-C, Java, JavaScript, Eiffel, OCaml, Ruby, Python, Common Lisp,
SML, PHP, все языки платформы .NET и др. имеют встроенную поддержку
структурной обработки исключений. В этих языках при возникновении
исключения, поддерживаемого языком, происходит раскрутка стека вызовов
до первого обработчика исключений подходящего типа, и управление
передаётся обработчику.
За исключением незначительных различий в
синтаксисе, существует лишь пара вариантов обработки исключений. В
наиболее распространённом из них исключительная ситуация генерируется
специальным оператором (throw или raise), а само исключение, с точки
зрения программы, представляет собой некоторый объект данных. То есть,
генерация исключения состоит из двух этапов: создания объекта-исключения
и возбуждения исключительной ситуации с этим объектом в качестве
параметра. При этом конструирование такого объекта само по себе выброса
исключения не вызывает. В одних языках объектом-исключением может быть
объект любого типа данных (в том числе строкой, числом, указателем и так
далее), в других — только предопределённого типа-исключения (чаще всего
он имеет имя Exception) и, возможно, его производных типов
(типов-потомков, если язык поддерживает объектные возможности).
Область
действия обработчиков начинается специальным ключевым словом try или
просто языковым маркером начала блока (например, begin) и заканчивается
перед описанием обработчиков (catch, except, resque). Обработчиков может
быть несколько, один за одним, и каждый может указывать тип исключения,
который он обрабатывает. Если язык поддерживает наследование и
типы-исключения могут наследоваться друг от друга, то обработкой
исключения занимается первый обработчик, совместимый с исключением по
типу.
Некоторые языки также допускают специальный блок (else),
который выполняется, если ни одного исключения не было сгенерировано в
соответствующей области действия. Чаще встречается возможность
гарантированного завершения блока кода (finally, ensure). Заметным
исключением является Си++, где такой конструкции нет. Вместо неё
используется автоматический вызов деструкторов объектов. Вместе с тем
существуют нестандартные расширения Си++, поддерживающие и
функциональность finally (например в MFC).
В целом, обработка исключений может выглядеть следующим образом (в некотором абстрактном языке):
try {
line = console.readLine();
if (line.length() == 0)
throw new EmptyLineException("Строка, считанная с консоли, пустая!");
console.printLine("Привет, %s!" % line);
}
catch (EmptyLineException exception) {
console.printLine("Привет!");
}
catch (Exception exception) {
console.printLine("Ошибка: " + exception.message());
}
else {
console.printLine("Программа выполнилась без исключительных ситуаций");
}
finally {
console.printLine("Программа завершается");
}
В некоторых языках может быть лишь один обработчик, который разбирается с различными типами исключений самостоятельно.