Обработка исключений | это... Что такое Обработка исключений? (original) (raw)

Обрабо́тка исключи́тельных ситуа́ций (англ. exception handling) — механизм языков программирования, предназначенный для описания реакции программы на ошибки времени выполнения и другие возможные проблемы (исключения), которые могут возникнуть при выполнении программы и приводят к невозможности (бессмысленности) дальнейшей отработки программой её базового алгоритма. В русском языке также применяется более короткая форма термина: «обработка исключений».

Содержание

Исключения

Общее понятие исключительной ситуации

Во время выполнения программы могут возникать ситуации, когда состояние данных, устройств ввода-вывода или компьютерной системы в целом делает дальнейшие вычисления в соответствии с базовым алгоритмом невозможными или бессмысленными. Классические примеры подобных ситуаций приведены ниже.

Виды исключительных ситуаций

Исключительные ситуации, возникающие при работе программы, можно разделить на два основных типа: синхронные и асинхронные, принципы реакции на которые существенно различаются.

Некоторые типы исключений могут быть отнесены как к синхронным, так и к асинхронным. Например, инструкция деления на нуль формально должна приводить к синхронному исключению, так как логически оно возникает именно при выполнении данной команды, но на некоторых платформах за счёт глубокой конвейеризации исключение может фактически оказаться асинхронным.

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

Общее описание

В отсутствие собственного механизма обработки исключений для прикладных программ наиболее общей реакцией на любую исключительную ситуацию является немедленное прекращение выполнения с выдачей пользователю сообщения о характере исключения. Можно сказать, что в подобных случаях единственным и универсальным обработчиком исключений становится операционная система. Например, в операционную систему Windows встроена утилита Dr. Watson, которая занимается сбором информации о необработанном исключении и её отправкой на специальный сервер компании Microsoft.

Возможно игнорирование исключительной ситуации и продолжение работы, но такая тактика опасна, так как приводит к ошибочным результатам работы программ или возникновению ошибок впоследствии. Например, проигнорировав ошибку чтения из файла блока данных, программа получит в своё распоряжение не те данные, которые она должна была считать, а какие-то другие. Результаты их использования предугадать невозможно.

Обработка исключительных ситуаций самой программой заключается в том, что при возникновении исключительной ситуации управление передаётся некоторому заранее определённому обработчику — блоку кода, процедуре, функции, которые выполняют необходимые действия.

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

Существует два варианта подключения обработчика исключительных ситуаций к программе: структурная и неструктурная обработка исключений.

Неструктурная обработка исключений

Неструктурная обработка исключений реализуется в виде механизма регистрации функций или команд-обработчиков для каждого возможного типа исключения. Язык программирования или его системные библиотеки предоставляют программисту как минимум две стандартные процедуры: регистрации обработчика и разрегистрации обработчика. Вызов первой из них «привязывает» обработчик к определённому исключению, вызов второй — отменяет эту «привязку». Если исключение происходит, выполнение основного кода программы немедленно прерывается и начинается выполнение обработчика. По завершении обработчика управление передаётся либо в некоторую наперёд заданную точку программы, либо обратно в точку возникновения исключения (в зависимости от заданного способа обработки — с возвратом или без). Независимо от того, какая часть программы в данный момент выполняется, на определённое исключение всегда реагирует последний зарегистрированный для него обработчик. В некоторых языках зарегистрированный обработчик сохраняет силу только в пределах текущего блока кода (процедуры, функции), тогда процедура разрегистрации не требуется. Ниже показан условный фрагмент кода программы с неструктурной обработкой исключений:

УстановитьОбработчик(ОшибкаБД, ПерейтиНа ОшБД) // На исключение "ОшибкаБД" установлен обработчик - команда "ПерейтиНа ОшБД" ... // Здесь находятся операторы работы с БД ПерейтиНа СнятьОшБД // Команда безусловного перехода - обход обработчика исключений ОшБД: // метка - сюда произойдёт переход в случае ошибки БД по установленному обработчику ... // Обработчик исключения БД СнятьОшБД: // метка - сюда произойдёт переход, если контролируемый код выполнится без ошибки БД. СнятьОбработчик(ОшибкаБД) // Обработчик снят

Неструктурная обработка — практически единственный вариант для обработки асинхронных исключений, но для синхронных исключений она неудобна: приходится часто вызывать команды установки/снятия обработчиков, всегда остаётся опасность нарушить логику работы программы, пропустив регистрацию или разрегистрацию обработчика.

Структурная обработка исключений

Структурная обработка исключений требует обязательной поддержки со стороны языка программирования — наличия специальных синтаксических конструкций. Такая конструкция содержит блок контролируемого кода и обработчик (обработчики) исключений. Наиболее общий вид такой конструкции (условный):

НачалоБлока ... // Контролируемый код ... если (условие) то СоздатьИсключение Исключение2 ... Обработчик Исключение1 ... // Код обработчика для Исключения1 Обработчик Исключение2 ... // Код обработчика для Исключения2 ОбработчикНеобработанных ... // Код обработки ранее не обработанных исключений КонецБлока

Здесь «НачалоБлока» и «КонецБлока» — ключевые слова, которые ограничивают блок контролируемого кода, а «Обработчик» — начало блока обработки соответствующего исключения. Если внутри блока, от начала до первого обработчика, произойдёт исключение, то произойдёт переход на обработчик, написанный для него, после чего весь блок завершится и исполнение будет продолжено со следующей за ним команды. В некоторых языках нет специальных ключевых слов для ограничения блока контролируемого кода, вместо этого обработчик (обработчики) исключений могут быть встроены в некоторые или все синтаксические конструкции, объединяющие несколько операторов. Так, например, в языке Ада любой составной оператор (begin — end) может содержать обработчик исключений.

«ОбработчикНеобработанных» — это обработчик исключений, которые не соответствуют ни одному из описанных выше в данном блоке. Обработчики исключений в реальности могут описываться по-разному (один обработчик на все исключения, по одному обработчику на каждый тип исключение), но принципиально они работают одинаково: при возникновении исключения находится первый соответствующий ему обработчик в данном блоке, его код выполняется, после чего выполнение блока завершается. Исключения могут возникать как в результате программных ошибок, так и путём явной их генерации с помощью соответствующей команды (в примере — команда «СоздатьИсключение»). С точки зрения обработчиков такие искусственно созданные исключения ничем не отличаются от любых других.

Блоки обработки исключений могут многократно входить друг в друга, как явно (текстуально), так и неявно (например, в блоке вызывается процедура, которая сама имеет блок обработки исключений). Если ни один из обработчиков в текущем блоке не может обработать исключение, то выполнение данного блока немедленно завершается, и управление передаётся на ближайший подходящий обработчик более высокого уровня иерархии. Это продолжается до тех пор, пока обработчик не найдётся и не обработает исключение или пока не выйдет из обработчиков заданных программистом и не будет передано системному обработчику по умолчанию, аварийно закроющему программу.

Иногда бывает неудобно завершать обработку исключения в текущем блоке, то есть желательно, чтобы при возникновении исключения в текущем блоке обработчик выполнил какие-то действия, но исключение продолжило бы обрабатываться на более высоком уровне (обычно так бывает, когда обработчик данного блока не полностью обрабатывает исключение, а лишь частично). В таких случаях в обработчике исключений генерируется новое исключение или возобновляется с помощью специальной команды ранее появившееся. Код обработчиков не является защищённым в данном блоке, поэтому созданное в нём исключение будет обрабатываться в блоках более высокого уровня.

Блоки с гарантированным завершением

Помимо блоков контролируемого кода для обработки исключений, языки программирования могут поддерживать блоки с гарантированным завершением. Их использование оказывается удобным тогда, когда в некотором блоке кода, независимо от того, произошли ли какие-то ошибки, необходимо перед его завершением выполнить определённые действия. Простейший пример: если в процедуре динамически создаётся какой-то локальный объект в памяти, то перед выходом из этой процедуры объект должен быть уничтожен (чтобы избежать утечки памяти), независимо от того, произошли после его создания ошибки или нет. Такая возможность реализуется блоками кода вида:

НачалоБлока ... // Основной код Завершение ... // Код завершения КонецБлока

Заключённые между ключевыми словами «НачалоБлока» и «Завершение» операторы (основной код) выполняются последовательно. Если при выполнении их не возникает исключений, то затем выполняются операторы между ключевыми словами «Завершение» и «КонецБлока» (код завершения). Если же при выполнении основного кода возникает исключение (любое), то сразу же выполняется код завершения, после чего весь блок завершается, а возникшее исключение продолжает существовать и распространяться до тех пор, пока его не перехватит какой-либо блок обработки исключений более высокого уровня.

Принципиальное отличие блока с гарантированным завершением от обработки — то, что он не обрабатывает исключение, а лишь гарантирует выполнение определённого набора операций перед тем, как включится механизм обработки. Стоит заметить, что блок с гарантированным завершением легко реализуется с помощью команд «возбудить исключение» и «структурный обработчик исключения».

Поддержка в различных языках

Большинство современных языков программирования, такие как 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("Программа завершается"); }

В некоторых языках может быть лишь один обработчик, который разбирается с различными типами исключений самостоятельно.

Достоинства и недостатки

Достоинства использования исключений особенно заметно проявляются при разработке библиотек процедур и программных компонентов, ориентированных на массовое использование. В таких случаях разработчик часто не знает, как именно должна обрабатываться исключительная ситуация (при написании универсальной процедуры чтения из файла невозможно заранее предусмотреть реакцию на ошибку, так как эта реакция зависит от использующей процедуру программы), но ему это и не нужно — достаточно сгенерировать исключение, обработчик которого предоставляется реализовать пользователю компонента или процедуры. Единственная альтернатива исключениям в таких случаях — возврат кодов ошибок, которые вынужденно передаются по цепочке между несколькими уровнями программы, пока не доберутся до места обработки, загромождая код и снижая его понятность. Использование исключений в целях контроля ошибок повышает читаемость кода, так как позволяет отделить обработку ошибок от самого алгоритма, и облегчает программирование и использование компонентов других разработчиков. А обработка ошибок может быть централизованна в аспектах.

К сожалению, реализация механизма обработки исключений существенно зависит от языка, и даже компиляторы одного и того же языка на одной и той же платформе могут иметь значимые различия. Это не позволяет прозрачно передавать исключения между частями программы, написанными на разных языках; например, поддерживающие исключения библиотеки обычно непригодны для использования в программах на языках, отличных от тех, для которых они разработаны, и, тем более, на языках, не поддерживающих механизм обработки исключений. Такое состояние существенно ограничивает возможности использования исключений, например, в ОС UNIX и её клонах и под Windows, так как большинство системного ПО и низкоуровневых библиотек этих систем пишется на языке Си, не поддерживающем исключений. Соответственно, для работы с API таких систем с применением исключений приходится писать библиотеки-врапперы, функции которых анализировали бы коды возврата функций API и в нужных случаях генерировали исключения.

Поддержка исключений усложняет язык и компилятор. Также она снижает скорость работы программы, так как стоимость обработки исключения, как правило, выше стоимости обработки кода ошибки. Поэтому, в местах программы, критичных по скорости, не рекомендуют возбуждать и обрабатывать исключения, хотя следует отметить, что в прикладном программировании случаи, когда разница в скорости обработки исключений и кодов возврата действительно существенна, очень редки.

Корректная реализация исключений может быть затруднительной в языках с автоматическим вызовом деструкторов. При возникновении исключения в блоке необходимо автоматически вызвать деструкторы объектов, созданных в данном блоке, но только тех, которые не были ещё удалены обычным порядком. Кроме того, требование прерывания текущей операции при возникновении исключения вступает в противоречие с требованием обязательного автоматического удаления в языках с автодеструкторами: если исключение возникнет в деструкторе, то либо компилятор будет вынужден удалить не полностью освобождённый объект, либо объект останется существующим, то есть возникнет утечка памяти. Вследствие этого генерация неперехватываемых исключений в деструкторах в ряде случаев просто запрещается.

Джоэл Спольски считает, что код, рассчитанный на работу с исключениями, теряет линейность и предсказуемость. Если в классическом коде выходы из блока, процедуры или функции находятся только там, где их явно указал программист, то в коде с исключениями исключение (потенциально) может произойти в любом операторе и анализом самого кода невозможно узнать, где именно исключения могут происходить. В коде же, рассчитанном на исключения, предсказать, в каком месте произойдёт выход из блока кода, невозможно, и любой оператор должен рассматриваться как потенциально последний в блоке, в результате сложность кода возрастает, а надёжность снижается.[1]

Также в сложных программах возникают большие «нагромождения» операторов try ... finally и try ... catch (try ... except), если не использовать аспекты.

Проверяемые исключения

Некоторые проблемы простой обработки исключений

Изначально (например, в C++), не существовало никакой формальной дисциплины описания, генерации и обработки исключений: любое исключение может быть возбуждено в любом месте программы, и, если для него не находится обработчика в стеке вызовов, выполнение программы прерывается аварийно. Если функция (особенно библиотечная) генерирует исключения, то для устойчивой работы использующая её программа должна перехватывать их все. Когда по какой-либо причине одно из возможных исключений оказывается необработанным, будет происходить неожиданное аварийное завершение программы.

С подобными эффектами можно бороться организационными мерами, описывая возможные исключения, возникающие в библиотечных модулях, в соответствующей документации. Но при этом всегда остаётся возможность пропустить необходимый обработчик из-за случайной ошибки или несоответствия документации коду (что вовсе не редкость). Чтобы полностью исключить потерю обработки исключений, в обработчики приходится специально добавлять ветвь обработки «всех остальных» исключений (которая гарантированно перехватит любые, даже заранее неизвестные исключения), но такой выход не всегда оптимален. Более того, сокрытие всех возможных исключений может привести к ситуации, когда будут скрыты серьёзные, и при этом трудно обнаруживаемые ошибки.

Механизм проверяемых исключений

Позже в ряде языков, например, в Java, появились проверяемые исключения. Сущность этого механизма состоит в добавлении в язык следующих правил и ограничений:

Внешне (в языке Java) реализация такого подхода выглядит следующим образом:

int getVarValue(String varName) throws SQLException { ... // код метода, возможно, содержащий вызовы, способные возбудить исключение SQLException }

// Ошибка при компиляции - исключение не объявлено и не перехвачено int eval1(String expression) { ... int a = prev + getVarValue("abc"); ... }

// Правильно — исключение объявлено и будет передаваться дальше int eval2(String expression) throws SQLException { ... int a = prev + getVarValue("abc"); ... }

// Правильно — исключение перехватывается внутри метода и наружу не выходит int eval3(String expression) { ... try { int a = prev + getVarValue("abc"); } catch (SQLException ex) { // Обработка исключения } ... }

Здесь метод getVarValue объявлен как генерирующий исключение SQLException. Следовательно, любой использующий его метод должен либо перехватить это исключение, либо объявить его как генерируемое. В данном примере метод eval1 приведёт к ошибке компиляции, поскольку вызывает метод getVarValue, но не перехватывает исключение и не объявляет его. Метод eval2 объявляет исключение, а метод eval3 перехватывает и обрабатывает его, оба этих метода корректны в отношении работы с исключением, вызываемым методом getVarValue.

Преимущества и недостатки

Проверяемые исключения снижают количество ситуаций, когда исключение, которое могло быть обработано, вызвало критическую ошибку в программе, поскольку за наличием обработчиков следит компилятор. Это особенно полезно при изменениях кода, когда метод, который не мог ранее выбрасывать исключение типа X, начинает это делать; компилятор автоматически отследит все случаи его использования и проверит наличие соответствующих обработчиков. Например, если при изменении формулы вычислений в методе появилась ранее отсутствовавшая в нём операция деления, будет автоматически проверено наличие обработчика исключения «деление на нуль».

Другим полезным качеством проверяемых исключений является то, что они способствуют осмысленному написанию обработчиков: программист явно видит полный и правильный список исключений, которые могут возникнуть в данном месте программы, и может написать на каждое из них осмысленный обработчик вместо того, чтобы создавать «на всякий случай» общий обработчик всех исключений, одинаково реагирующий на все нештатные ситуации.

У проверяемых исключений есть и недостатки.

Из-за перечисленных недостатков при обязательности использования проверяемых исключений этот механизм часто обходят. Например, многие библиотеки объявляют все методы как выбрасывающие некоторый суперкласс исключений (например, Exception), и только на этот тип исключения создаются обработчики. Результатом становится то, что компилятор заставляет писать обработчики исключений даже там, где они объективно не нужны. Более правильным подходом считается перехват внутри метода новых исключений, порождённых вызываемым кодом, а при необходимости передать исключение дальше — «заворачивание» его в исключение, уже возвращаемое методом. Например, если метод изменили так, что он начинает обращаться к базе данных вместо файловой системы, то он может сам ловить SQLException и выбрасывать вместо него вновь создаваемый IOException, указывая в качестве причины исходное исключение. Обычно рекомендуется изначально объявлять именно те исключения, которые придётся обрабатывать вызывающему коду. Скажем, если метод извлекает входные данные, то для него целесообразно объявить IOException, а если он оперирует SQL-запросами, то, вне зависимости от природы ошибки, он должен объявлять SQLException. В любом случае, набор выбрасываемых методом исключений должен тщательно продумываться. При необходимости имеет смысл создавать собственные классы исключений, наследуя их от Exception или других подходящих проверяемых исключений.

Исключения, не требующие проверки

Невозможно сделать проверяемыми вообще все исключения, так как некоторые исключительные ситуации по своей природе таковы, что их возникновение возможно в любом или почти любом месте программы, а обработать их программист не в состоянии. В основном это исключения, относящиеся к одному из двух видов:

Выносить подобные ошибки вообще за пределы системы обработки исключений нелогично и неудобно, хотя бы потому, что иногда они всё-таки перехватываются и обрабатываются. Поэтому в системах с проверяемыми исключениями часть типов исключений выводится из-под механизма проверки и работает традиционным образом. В Java это классы исключений, унаследованные от java.lang.Error — фатальные ошибки среды исполнения и java.lang.RuntimeException — ошибки времени выполнения, как правило, связанные с ошибками кодирования или недостаточностью проверок в коде программы (неверный аргумент, обращение по пустой ссылке, выход за границы массива, неверное состояние монитора и т. п.).

См. также

Примечания

  1. 13 — Joel on Software

Ссылки

Просмотр этого шаблона Типы данных
Неинтерпретируемые БитНибблБайтТритТрайтСлово
Числовые ЦелыйС фиксированной запятойС плавающей запятой • Рациональный • КомплексныйДлинныйИнтервальный
Текстовые СимвольныйСтроковый
Указатель Адрес • Ссылка
Композитные Алгебраический тип данных (обобщённый) • МассивАссоциативный массивКлассСписокКортежОбъект • Option type • Product • СтруктураМножествоОбъединение (tagged)
Другие Логический • Низший тип • КоллекцияПеречисляемый типИсключение • First-class function • Opaque data type • Recursive data type • СемафорПотокВысший тип • Type class • Unit type • Void
Связанные темы Абстрактный тип данныхСтруктура данныхИнтерфейс • Kind (type theory) • Примитивный тип • Subtyping • Шаблоны C++ • Конструктор типа • Parametric polymorphism