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

Материал из Seo Wiki - Поисковая Оптимизация и Программирование

Перейти к: навигация, поиск

Обработка исключительных ситуаций (англ. 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("Программа завершается");
}

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

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

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

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

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

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

<span id="checkedex" />

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

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

Изначально (например, в 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 или других подходящих проверяемых исключений.

См. также

Ссылки

en:Exception handling es:Manejo de excepciones fa:مدیریت استثنا fi:Poikkeus fr:Système de gestion d'exceptions he:טיפול בחריגות (תכנות) hu:Kivételkezelés is:Frábrigðissýslari it:Gestione delle eccezioni ja:例外処理 ko:예외 처리 nl:Exception handling pl:Wyjątek pt:Tratamento de exceção sr:Изузетак (програмирање) sv:Undantagshantering ta:விதிவிலக்கை கையாளுதல் uk:Обробка винятків

Личные инструменты

Served in 0.203 secs.