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

Материал из Deeptown Manual
Перейти к: навигация, поиск

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

Содержание

Идеология исключений

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

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

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

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

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

В некоторых местах, где заранее предусматривается возможность проявления ошибки (например при открытии файла), вставляется специальная конструкция исключения. Она состоит из основного блока и одного или нескольких блоков — перехватчиков исключений. Выглядит эта конструкция так: <source lang="kpp"> try {

   /* try-блок (основное тело) */

} catch (/* переменная для объекта исключения 1 */) {

   /* код перехватчика 1 */

} catch (/* переменная для объекта исключения 2 */) {

   /* код перехватчика 2 */

} </source>

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

Объект исключения

С точки зрения языка К++, объект исключения — это такой же объект (или класс объектов), как и все остальные, только он несет строго определенный смысл, привязанный к концепции исключений. Иначе говоря, объект исключения — это специальная сущность, которая содержит в себе некоторую известную информацию об ошибке. Например, в случае ошибки чтения файла, в объекте может содержаться путь к файлу. В случае ошибки формата исходных данных, он может содержать информацию о том, какие были входные данные и какие ожидались на самом деле. Словом, объект исключения содержит информацию, которая отражает возникшую ошибку. Как правило, все объекты исключения имеют строковое поле, в которое записывается текстовая информация, описывающая проблему. Например, оно может содержать строки вида: "не могу открыть файл", "отказано в доступе" или "так сложились звезды". Впоследствии, эти строки могут быть записаны в лог файл, либо показаны пользователю в виде сообщения.

Генерация исключения

Код, который первым обнаруживает исключительную ситуацию (то есть, стоит у ее истоков) и желающий сообщить о ней "наверх", должен создать объект исключения и "выбросить" его. Создается объект так же, как и любой другой объект в языке К++: либо с помощью оператора new, либо с помощью конструктора (если он предусмотрен). "Выбрасывание" объекта осуществляется с помощью специального оператора throw, который принимает объект в качестве параметра. Обычно, эти операции совмещают в одном действии.

Приведем пример некоторой функции, которая проверяет правильность передаваемых ей данных и выбрасывает исключение в случае несоответствия: <source lang="kpp"> function Process(const int idx) {

   unless (idx in 5 .. 10)
       throw "индекс должен быть в диапазоне от 5 до 10";
   /* нормальный код обработки */

} </source>

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

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

Приведем некоторые из наиболее распространенных классов исключений и поясним их назначение:

EInvalidCall неверный вызов (входные данные неверны)
EFailed операция не удалась (и все тут)
EAgain по тем или иным причинам требуется повтор операции
EDenied действие запрещено!
ECancelled операция была отменена (например пользователем)
EAlready операция уже была выполнена
ERangeError ошибка диапазона (выход значения за допустимые границы)
EDivisionByZero арифметическая ошибка деления на ноль
ETimeout истекло время ожидания чего-либо
EStreamError ошибка потока
ERegexpError ошибка при разборе регулярного выражения
EAbstractError попытка вызова абстрактного метода

Здесь приведены только наиболее общие описания классов. Более конкретная информация содержится в самом объекте исключения. Классы исключений необходимы для того, чтобы сортировать возникающие ошибки по типам, и на основе этой информации обрабатывать их различным образом.

Например, в описанном выше случае, наиболее подходящим классом ошибки будет ERangeError. Соответственно, код генерации ошибки можно написать так: <source lang="kpp"> throw ERangeError.create("индекс должен быть в диапазоне от 5 до 10"); </source>

Перехват исключений

Для обработки возникающих исключений используется конструкция перехвата. Она начинается с ключевого слова catch, следом за которым, в круглых скобках, идет описание переменной исключения, а затем, собственно, блок обработчика. Например, так мог бы выглядеть код вызова вышеописанной функции Process() и обработки возникающего исключения, если бы оно имело место: <source lang=kpp> try {

   Process(/*выражение задающее индекс*/);
   /* другой код */

} catch (e) {

   print("Ошибка обработки индекса. #{e.name}: #{e.description}\n");
   /* код обработки исключения */

} </source>

Для того чтобы сослаться на объект исключения используется переменная e. Все системные классы исключений имеют свойства name и description. Первое содержит имя исключения, а второе — описание ошибки.

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

<source lang=kpp> try {

   /* защищаемый код */

} catch (/*класс 1*/ /*имя объекта 1*/) {

   /* код обработки исключения 1 */

} catch (/*класс 2*/ /*имя объекта 2*/) {

   /* код обработки исключения 2 */

... } </source>

Таким образом, при объявлении переменной исключения, мы можем указывать ее тип и потом ссылаться на специфические поля объектов, соответствущих этому типу: <source lang=kpp> try {

   /* защищаемый код */

} catch (EAgain e) {

   /* запрос у пользователя на повторение операции */

} catch (ERangeError e) {

   /* отображение ошибки */

} catch (e) {

   /* запись в лог файл */

} </source>

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

Примечание 1: следует помнить, что правильность разбора исключений по классам, зависит от очередности расположения блоков перехватчиков в конструкции. При перехвате исключения выбор обработчика происходит по принципу первого совпадения, при котором проверяется, имеет ли объект исключения в своей иерархии класс, указанный в перехватчике. Если скажем, перехватчик catch (e) будет указан первым, то он и будет собирать все исключения, поскольку он сработает на любой класс объекта исключения. Вообще, нужно стараться располагать перехватчики по убыванию степени абстракции — от пользовательских классов до системных и далее, до общего перехватчика.

Ловить или не ловить?

Блоки перехвата исключений должны вставляться только в тех местах, где вы точно знаете, как надо поступить при возникновении исключения. Если такой уверенности нет — смело пропускайте исключение "мимо ушей". Рано или поздно оно будет словлено там, где программа располагает бо́льшими сведениями относительно дальнейшей модели поведения.

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

Исключения и работа с ресурсами

Основная проблема при работе со внешними ресурсами заключается в том, что их надо освобождать. Конечно, в нашем случае существует сборщик мусора, который рано или поздно обнаружит "ничейный" объект и инициирует процедуру его удаления. При этом, грамотно написанный unmanaged объект должен произвести все необходимые операции по освобождению ассоциированного с ним ресурса (файла, сетевого соединения, графической сущности и т. д.).

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

Разумеется, грамотно написанная программа должна освобождать ресурсы как можно быстрее (не путать с обычными объектами). Обычно это осуществляется вызовом специального unmanaged метода, который и производит все необходимые манипуляции на низком уровне. Тогда код работы с ресурсом может выглядеть как-то так: <source lang="kpp"> var resource = MyResource.open("some initial data"); // создаем ресурс resource.bla(bla); // работаем с ресурсом foo(resource); //... resource.release(); // освобождаем ресурс </source>

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

В принципе, можно попробовать схитрить и обернуть данный код в try блок, а в блоке перехвата исключений проводить принудительную финализацию ресурса. Тогда код будет выглядеть как-то так: <source lang="kpp"> var resource = MyResource.open("some initial data"); // создаем ресурс try {

 resource.bla(bla);                                   // работаем с ресурсом
 foo(resource);                                       
 //...
 resource.release();                                  // освобождаем ресурс

} catch (Exception e) {

 resource.release();                                  // освобождаем ресурс
 throw e;                                             // вторично выбрасываем исключение

} </source>

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

Для того чтобы сказать компилятору, что некоторый код необходимо выполнить в любом случае, независимо от того что произошло выше, необходимо использовать ключевое слово ensure. Тогда вышеприведенный код примет следующий вид: <source lang="kpp"> var resource = MyResource.open("some initial data"); // создаем ресурс try {

 resource.bla(bla);                                   // работаем с ресурсом
 foo(resource);                 
 // ...

} ensure {

 resource.release();                                  // освобождаем ресурс

} </source>

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

Конструкция работает следующим образом: сначала вызывается инициализатор ресурса, то есть первая строка. Затем управление передается в защищаемый блок. Если все прошло гладко и исключений выброшено не было, то после защищаемого блока вызывается ensure блок а затем управление передается дальше по коду.

Если же где то в защищаемом коде было выброшено исключение, управление сперва передается в ensure блок, а потом выброшенное исключение передается "наверх"; то есть управление будет передано ближайшему catch блоку, способному обработать данный класс исключений. Самое главное заключается в том, что освобождение ресурса гарантируется в любом случае.

Персональные инструменты
Пространства имён

Варианты
Действия
Навигация
информация
документация
Инструменты