Обработка исключений
Как уже было отмечено во введении, исключения — это мощный механизм, позволяющий в разы сократить время, требуемое на написание программ и значительно повысить их качество. В основном, это достигается за счет того то, программист пишет более простой код, не отвлекаясь от основной задачи на разного рода рутинные операции, вроде обработки ошибок, а значит, его внимание сконцентрировано на самой проблеме, что в итоге приводит к написанию более качественного кода. В этой главе будет рассмотрена сущность механизма исключений, а так же способы их применения на практике. Будут даны типовые схемы разработки программ и даны соответствующие пояснения.
Содержание |
Идеология исключений
Исключением, называется некоторое событие произошедшее при работе программы и несущее негативный характер. Имеется в виду, что это событие не должно происходить при нормальных условиях. Примером таких событий могут послужить следующие ситуации:
- Обрыв линии связи и, как следствие, прекращение передачи
- Внезапный конец файла конфигурации
- Ввод данных, не соответствующий требуемому формату (например строки, когда ожидалось число)
- Ошибка выделения памяти
- Падение метеорита, война, нашествие иноплянетян и т.д.
Все вышеперечисленные события так или иначе могут произойти в ходе работы программы, однако предсказать их практически невозможно, можно только предвидеть. Хорошая программа так и делает: в тех местах, где могут потенциально произойти исключительные ситуации вставляется код, проверяющий состояние и производящий некоторые действия по "ликвидации последствий". Например, в случае ошибки связи, программа может попытаться установить соединение заново, в то время как при ошибке внезапного конца файла остается только уведомить об этом пользователя, показав сообщение об ошибке.
Проблема заключается в том, что не всегда можно угадать место, где может произойти ошибка. Проверять же состояние ошибки, буквально в каждой операции, утомительно и практически невозможно. Это засоряет код алгоритма всевозможными вспомогательными конструкциями (условиями проверки на ошибку), делает его менее читаемым и усложняют и без того нелегкую задачу отладки.
При использовании концепции исключений, все выглядит совсем иначе. Большую часть кода, программист пишет из соображения, что "все в порядке". При этом, код получается кратким и содержит только те действия, ради которых он создавался.
В некоторых местах, где заранее предусматривается возможность проявления ошибки (например при открытии файла) вставляется специальная конструкция исключения. Она состоит из основного блока и одного или нескольких блоков — перехватчиков исключений. Выглядит эта конструкция так: <source lang="kpp"> try {
/* try-блок (основное тело) */
} catch (/* переменная для объекта исключения 1 */) {
/* код перехватчика 1 */
} catch (/* переменная для объекта исключения 2 */) {
/* код перехватчика 2 */
} </source>
Ключевое слово try объявляет начало защищенного блока. Затем идет основное тело или try-блок, в котором содержатся конструкции, соответствующие нормальной работе. В примере с файлом, здесь будет находиться код чтения содержимого файла и, возможно, его обработки. Если в ходе выполнения этого блока возникнет исключительная ситуация, то управление будет передано одному из блоков обработки, соответствующему типу возникшего исключения (об этом чуть позже). В круглых скобках указывается имя переменной которой будет назначен объект исключения — объект некоторого класса, который был "выброшен" из кода в качестве исключения.
Объект исключения
Объект исключения, это специальная сущность, которая содержит в себе известную информацию об ошибке. Например, в случае ошибки чтения файла, в объекте может содержаться путь к файлу. В случае ошибки формата исходных данных, он может содержать информацию о том какие были входные данные и какие ожидались на самом деле. Словом, объект исключения содержит информацию, которая так или иначе отражает возникшую ошибку. Как правило, все объекты исключения имеют строковое поле, в которое записывается текстовая информация, описывающая проблему. Например "не могу открыть файл", "отказано в доступе" или "так сложились звезды". Впоследствии, эти строки могут быть записаны в лог файл, либо показаны пользователю в виде сообщения.
Генерация исключения
Код, который первым обнаруживает исключительную ситуацию (то есть, стоит у ее истоков) и желающий сообщить о ней "наверх" должен создать объект исключения и "выбросить" его. Создается объект так же, как и любой другой объект в языке К++: либо с помощью оператора new, либо с помощью конструктора. "Выбрасывание" объекта осуществляется с помощью специального оператора throw, который принимает объект в качестве параметра. Обычно эти операции совмещают в одном действии.
Приведем пример некоторой функции, которая проверяет правильность передаваемых ей данных и выбрасывает исключение в случае несоответствия: <source lang="kpp"> function Process(const int idx) {
if (! idx in 5 .. 10) throw "индекс должен быть в диапазоне от 5 до 10"; /* нормальный код обработки */
} </source>
В первой строке тела функции проверяется условие, необходимое функции для работы. Если условие не выполняется, то производится генерация исключения, причем в качестве объекта исключения выступает объект строки.
На практике, применяются специальные классы исключений, которые объявлены еще в стандартной библиотеке. Таким образом, все языки использующие стандартную библиотеку и умеющие работать с исключениями, смогут успешно взаимодействовать с помощью этого механизма.
Приведем некоторые из наиболее распространенных классов исключений и поясним их назначение:
e_invalid_call неверный вызов (входные данные неверны) e_failed операция не удалась e_again требуется повтор операции e_denied действие запрещено e_cancelled операция была отменена e_already операция уже была выполнена e_range_error ошибка диапазона (выход за границы) e_division_by_zero ошибка деления на ноль e_timeout истекло время ожидания e_stream_error ошибка потока e_regexp_error ошибка при разборе регулярного выражения e_abstract_error попытка вызова абстрактного метода
Здесь приведены только наиболее общие описания классов. Более конкретная информация содержится в самом объекте исключения. Классы исключений необходимы для того чтобы сортировать возникающие ошибки по типам, и на основе этой информации обрабатывать их различным образом.
Например, в описанном выше случае, наиболее подходящим классом ошибки будет e_range_error. Соответственно, код генерации ошибки можно написать так: <source lang="kpp"> throw e_range_error.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 (e_again e) {
/* запрос у пользователя на повторение операции */
} catch (e_range_error e) {
/* отображение ошибки */
} catch (e) {
/* запись в лог файл */
} </source>
В вышеприведенном коде используются три обработчика исключений: первые два перехватывают исключения определенного типа, а третий — все оставшиеся. Структура обработчиков исключений чем то напоминает конструкцию switch.