Исключение — это ошибка, приводящая к невозможности выполнения алгоритма во время выполнения программы, не приводящее к завершению работы самой программы. Исключительные ситуации в работе алгоритма могут возникать по множеству причин, самые распространённые из которых деление на ноль, выход за границы массива, нехватка памяти, ошибки ввода-вывода. Механизм обработки исключений очень сильно облегчает жизнь разработчику, не заставляя задумываться над некоторыми деталями работы алгоритма.
Общий вид обработчика исключения таков:
try {
<тело алгоритма>
} catch (<тип ошибки>) {
<обработка ошибки данного типа>
} finally {
<код, выполняющийся в независимости от возникновения ошибки после завершения алгоритма, либо аварийного выхода из него>
}
Во многих языках данная структура не меняется, есть только небольшие косметические изменения. Если в алгоритме произойдёт исключительная ситуация, совпадающая с типом ошибки, которые указаны в блоке catch (а их может быть несколько, при этом чем выше обработчик, тем приоритетнее обработка), то алгоритм завершит работу и перейдёт к выполнению обработчика. После завершения алгоритма или обработчика, если произошло исключение, выполняется блок finally. Обычно он нужен для закрытия потоков, удаления мусора, а ещё его можно опускать.
Исключения бывают нескольких типов, различающиеся источником. Так, автором исключения может быть операционная система или сам программист. При этом обработка исключения не зависит от типа. Такой механизм позволяет создавать собственные исключения и использовать их, вызывая в нужном месте. Это очень упрощает создание различных библиотек, которые потом будут использоваться другими программистами, а уже те разработчики должны будут позаботиться об обработке этих исключений.
Как уже сказал, механизм обработки исключений облегчает жизнь разработчику, а значит тут таится проигрыш в чём-то другом. Собственно, ради этого и написана статья. Так и есть, исключения достаточно сильно замедляют работу программы. Если рассмотреть программу как набор инструкций, то, добавляя в неё обработку исключений, мы, по сути, добавляем проверку на исключение к каждой инструкции, ведь неясно, какая инструкция может вызвать исключение. Современные компиляторы, конечно же, оптимизируют механизм обработки исключений, и условия накладываются не на все инструкции, а только на те, которые могут привести к исключению. Кроме того, многие программисты, не думая о том, где может быть исключение, а только зная, что оно может быть, оборачивают весь алгоритм в try-catch. Да, в этом случае программа не «падает», но она и не работает, что равноценно «падению». Ещё механизм исключений очень трудно включить в уже нарисованную блок-схему программы, что явно не лучшим образом сказывается на наглядности (как отображения кода на схему).
Кто-то скажет, что механизм исключений — единственное возможное средство борьбы с исключительными ситуациями. К сожалению, в большинстве фреймворков это так, ведь они построены на механизме исключений, но есть и другой выход. И другим выходом являются собственные условия на проблемные участки. Ждали число при парсинге строки, а там что-то иное? Почему бы не проверить перед этим строку посимвольно или регулярным выражением? Пытаетесь получить -3 элемент массива? Почему бы не проверить индекс перед этим? И уже нет вороха обратных вызовов (в случае если функции вложены), т.к. нет исключения, также нет навешанных обработчиков там, где это не нужно. Но что же делать, если функция должна вернуть результат, а мы не знаем о том, что там что-то не так. И тут мы имеем очень серьёзную недоработку механизма функций: возвращение только одного результата. Это ограничение архитектурное, поэтому решить его можно только на более высоком уровне. Не будем говорить о написании собственного компилятора или использовать ссылки для возврата значений, воспользуемся средствами ООП. Будем возвращать результат не в виде прямого результата, а в виде некой структуры, содержащей результат, а также код состояния.
Пример:
class Result<T>{
T value;
int code;
}
В этом псевдокоде T соответствует подстановке шаблона, т.к. в этом случае удобно использовать шаблонный класс. Пусть теперь функция будет возвращать объект данного класса, а мы всегда знать, что произошло в функции. Если код, к примеру 0, то функция (говорим о методах, ведь это ООП) верно завершила работу, а если какой-то иной, то заглянем в справочник кодов, либо сравним с какой-то из заданных констант, либо просто испугаемся, что не ноль и ничего делать не будем, а потом сделаем обработчик для этого кода. Получилось очень похоже на механизм обработки исключений, но без многих навесок этого самого механизма. Добавив в класс Result строковое поле с сообщением, мы ещё и текстовое описание ошибки сможем отдавать и получать. Наш алгоритм ничем не перегружен, всё поддаётся простейшим условиям, всё понятно и очевидно; блок-схема отражает всё, что есть в коде, а код содержит только то, что есть в блок-схеме. Минусом является только не совсем прямое использование результатов функций, а также формирование этих результатов в самой функции. Что интересно, многие функции WinAPI придерживаются именно такого стандарта (созданного ещё до времён внедрения ООП), возвращая состояние, а не результат. Возврат состояния позволяет очень точно контролировать поведение программы, не надеясь на обработку исключений кем-то и где-то.