Что такое исключения в программировании
Рассказ об ошибках, которые можно предусмотреть заранее.
Большинство наших проектов устроены так: когда во время работы программы возникает какая-то ошибка, то программа аварийно завершается. Иногда при этом она выдаёт сообщение об ошибке. Кажется, что это нормальная ситуация, но на самом деле большинство ошибок можно предусмотреть и научить программу правильно с ними работать. Для этого нам нужны обработчики ошибок.
Что такое обработчик ошибок
Чтобы программа знала, что делать, если возникла какая-то ошибка, используют обработчики исключительных ситуаций, или, проще говоря, обработчики исключений. Смысл такой:
Такие обработчики есть не в каждом языке программирования, но большинство современных языков это умеют делать.
Пример программы без обработчика исключений
Допустим, у нас в программе на Python предусмотрено чтение данных из файла и есть такой код:
Но если на диске этого файла не будет, то компьютер, когда дойдёт до этой строчки, выдаст ошибку:
Давайте нарисуем это в виде простой схемы:
Получается, что наша задача — предусмотреть вариант, что на диске не будет нужного файла, и придумать поведение программы в этом случае. Используем для этого обработчик исключений.
Программа с обработчиком исключений
Если мы знаем, что в каком-то месте возможна ошибка, то можем тогда предусмотреть этот сценарий и подстраховаться. Для этого используют обработчик и делают так:
В этой ситуации программа не зависнет и не вывалится с ошибкой, а сама сможет её обработать и делать дальше то, что нужно:
Команда try — это начало нашего обработчика исключений. Она говорит компьютеру: «Попробуй выполнить вот эту команду, а мы посмотрим, что произойдёт».
Except — это какую ошибку мы ожидаем здесь увидеть. В нашем случае мы хотим предусмотреть случай, что такого файла нет, поэтому пишем стандартную ошибку для такой ситуации.
👉 Сравните текст этой ошибки с тем, что нам выдал компьютер в предыдущем разделе.
В других языках конструкция обработчика исключений может выглядеть по-другому, но смысл тот же: говорим компьютеру, какую команду нужно выполнить и что делать, если появилась конкретная ошибка.
Когда что-то не предусмотрено — будет ошибка
Если программе в этом блоке встретится другая ошибка, не та, которую мы предусмотрели, то программа остановится и всё перестанет работать. Например, вот какие ошибки могут возникнуть с файлом:
Во всех этих случаях программа сломается, потому что мы не предусмотрели эти ситуации:
Получается, всё нужно делать с обработкой исключений?
Конечно, есть места в коде, которые лучше делать с обработкой ошибок: работа с файлами, сетевые запросы или получение внешних данных. Но запихивать исключения на каждую команду в программе точно не стоит.
Что такое баги, ворнинги и исключения в программировании
Разбираемся, какие бывают типы ошибок в программировании и как с ними справляться.
Многим известно слово баг (англ. bug — жук), которым называют ошибки в программах. Однако баг — это не совсем ошибка, а скорее неожиданный результат работы. Также есть и другие термины: ворнинг, исключение, утечка.
В этой статье мы на примере C++ разберём, что же значат все эти слова и как эти проблемы влияют на эффективность программы.
Пишет о программировании, в свободное время создает игры. Мечтает открыть свою студию и выпускать ламповые RPG.
Ошибки в программировании
Словом «ошибка» (англ. error) можно описать любую проблему, но чаще всего под ним подразумевают синтаксическую ошибку — некорректно написанный код, который даже не скомпилируется:
Компилятор тут же скажет, что в коде ошибка и скорее всего не хватает запятой или точки с запятой.
Также существуют ворнинги (англ. warning — предупреждение). Они не являются ошибками, поэтому программа всё равно будет собрана. Вот пример:
Предупреждения не являются чем-то критичным, но могут иметь негативные последствия. Например, ваша программа будет использовать больше памяти, чем должна. Так как C++ нужен в том числе и для разработки высоконагруженных систем, этого допускать нельзя.
После восклицательного знака в треугольнике — количество предупреждений
Третий вид ошибок — ошибки сегментации (англ. segmentation fault, сокр. segfault, жарг. сегфолт). Они возникают, если программа пытается записать что-то в ячейку, недоступную для записи. Например:
Вот результат работы такого кода:
Баги в программах
Мы выяснили, что баг — это не совсем ошибка, а скорее неожиданное поведение программы или результат такого поведения. Баги могут быть чем-то забавным или неприятным. Например, как в играх:
Но они могут привести и к более серьёзным последствиям. Если неправильно спроектировать работу многопоточного приложения, то потоки будут постоянно опережать друг друга. Например, сообщение об ошибке из одного потока может опоздать на миллисекунду, из-за чего второй поток подумает, что никакой ошибки не было, и продолжит работу.
Если ваш код приводит в действие какое-нибудь потенциально опасное устройство, то ценой такой ошибки может быть чья-нибудь жизнь. Такое случилось с кодом для аппарата лучевой терапии Therac-25 — как минимум два человека умерло и ещё больше пострадали из-за превышения дозы радиации.
Исключения в программах
Также во время работы программы могут возникать ситуации, которые мешают корректной работе программы. Например, если вы просите пользователя ввести число, а он вводит строку.
Конвертировать введённое значение не всегда возможно, поэтому функция, которая занимается преобразованием, «выбрасывает» исключение (англ. exception). Это специальное сообщение говорит о том, что что-то идёт не так.
Если разработчик не описывает логику работы программы при вы выбрасывании исключения, то программа аварийно закрывается. Подробнее мы рассказали об этом в статье про ввод и конвертацию в C++.
Одно из самых известных исключений — переполнение стека (англ. stack overflow). В честь него даже назвали сайт, на котором программисты ищут помощь в решении своих проблем.
Компилятор C++ при этом может выдать ошибку сегментации, а не сообщение о переполнении стека:
Вот аналогичный код на языке C#:
Однако сообщение в этот раз более конкретное:
В обоих случаях программа завершается, потому что не может дальше корректно работать.
Похожая ситуация — переполнение буфера (англ. buffer overflow). Она происходит, когда записываемое значение больше выделенной области в памяти.
Обратите внимание, что мы получили предупреждение об арифметическом переполнении (англ. integer overflow):
Тем не менее программа скомпилировалась. Если же такая ситуация возникнет во время вычислений, то мы можем не получить предупреждения.
Арифметическое переполнение стало причиной одной из самых дорогих аварий, произошедших из-за ошибки в коде. В 1996 году ракета-носитель «Ариан-5» взорвалась на 40-й секунде полёта — потери оценивают в 360–500 миллионов долларов.
Как избежать всех этих ошибок
К сожалению, вручную всё это заметить и исправить не получится. Однако существуют различные инструменты и технологии, которые могут помочь.
Один из таких инструментов — отладчик. Он помогает контролировать ход работы программы, чтобы отслеживать разные показатели.
Второй, более эффективный метод — unit-тесты. Они представляют из себя набор описанных ситуаций для каждого компонента программы с указанием ожидаемого поведения.
Например, у вас есть функция sum (int a, int b), которая возвращает сумму двух чисел. Вы можете написать unit-тесты, чтобы проверять следующие ситуации:
Если какой-то из этих тестов не пройден, вы узнаете об этом и сможете всё исправить. Это намного быстрее, чем проверять всё вручную.
Заключение
Ошибок существует слишком много. При этом самые опасные тяжелее обнаружить, что только усугубляет ситуацию.
Если вы хотите научиться писать качественный код и находить в нём ошибки, вы можете записаться на наш курс по разработке на C++.
Многие программисты почему-то считают, что исключения и ошибки — это одно и то же. Кто-то постоянно кидает exception, кто-то через errorHandler превращает ошибки в исключения. Некоторые пытаются увеличить производительность, используя исключения. Но, на самом деле, exception и ошибки — это совершенно разные механизмы. Не надо одним механизмом заменять другой. Они созданы для разных целей.
Когда появился php5 с исключениями, а затем ZendFramework, который всегда кидает исключения — я не мог понять: чем же exception лучше моего любимого trigger_error()? Долго думал, обсуждал с коллегами и разобрался в этом вопросе. Теперь я чётко знаю, где использовать trigger_error(), а где throw new Exception().
В чём же принципиальная разница между ними?
Ошибки
Ошибки — это то, что нельзя исправить, об этом можно только сообщить: записать в лог, отправить email разработчику и извинится перед пользователем. Например, если мой движок не может подключиться к БД, то это ошибка. Всё. Точка. Без БД сайт не работает, и я не могу с этим ничего сделать. Поэтому я вызываю ales_kaput() и trigger_error(), а мой errorHandler отправит мне email и покажет посетителю сообщение «Извините, сайт не работает».
Exception
Исключения — это не ошибки, это всего лишь особые ситуации, которые нужно как-то обработать. Например, если в калькуляторе вы попробуете разделить на ноль, то калькулятор не зависнет, не будет отсылать сообщения разработчику и извинятся перед вами. Такие ситуации можно обрабатывать обычным if-ом. Строго говоря, исключения — это конструкция языка позволяющая управлять потоком выполнения. Это конструкция, стоящая в одном ряду с if, for и return. И всё. Этот механизм ничем более не является. Только управление потоком.
Их основное предназначение: пробрасывать по каскаду. Покажу это на примере: есть три функции, которые вызывают друг друга каскадом:
Эту задачу можно было бы решить без механизма exception. Например, можно заставить все функции возвращать специальный тип (если ты матёрый пэхапэшник, то должен вспомнить PEAR_Error). Для простоты я обойдусь null-ом:
Задача выполнена, но, обратите внимание, мне пришлось модифицировать промежуточную функцию b(), чтобы она пробрасывала результат работы нижестоящей функции выше по каскаду. А если у меня каскад из 5 или 10 функций? То мне пришлось бы модифицировать ВСЕ промежуточные функции. А если исключительная ситуация в конструкторе? То мне пришлось бы подставлять костыли.
А теперь решение с использованием Exception:
Таким образом, получается, что ошибки и исключения — это совершенно разные инструменты для решения совершенно разных задач:
ошибка — не поправимая ситуация;
исключение – позволяет прервать выполнение каскада функций и пробросить некоторую информацию. Что-то вроде глобального оператора return. Если у Вас нет каскада, то вам достаточно использовать if или return.
Ошибки не всегда являются ошибками
Некоторые могут мне возразить: «Посмотри в Zend Framework — там всегда кидают исключения. Это best practics, и надо делать также. Даже если не удалось подключиться к БД, надо кидать исключение».
В этой статье я как раз хочу развеять это заблуждение. Zend действительно является best practics, но программисты Зенда находятся на другой лодке и делают другие вещи. Принципиальная разница между ними и мной в том, что они пишут универсальную библиотеку, которая будет использоваться во многих проектах. И они со своей колокольни не могут сказать, что является критической ошибкой, а что является поправимой.
Например, в вашем проекте может быть несколько MySQL серверов и вы можете переключаться между ними при падении одного из них. По этому, Zend_Db, как универсальная библиотека, кидает исключение, а что с ним делать — решайте сами. Exception это гибко — вы сами решаете на каком уровне и какой тип ситуаций ловить. Вы можете вывести сообщение об ошибке или попытаться исправить возникшую ситуацию, если знаете как. При написании универсальных библиотек необходимо всегда кидать исключения. Это делает библиотеку более гибкой.
В php давно был разработан механизм обработки ошибок, и он отлично работает. Я им отлично пользуюсь там, где это надо.
Как правильно использовать исключения
Исключения вместо if-ов: почему нет?
В большинстве случаев, мы читаем код чаще, чем пишем. Большинство практик программирования нацелены на упрощение понимания кода: чем проще код, тем меньше багов он содержит и тем проще его поддержка.
Использование исключений для контроля хода выполнения программы маскирует намерения программиста, поэтому это считается плохой практикой.
Метод ProcessItem усложняет понимание кода, т.к. невозможно сказать какие возможны результаты его выполнения просто глядя на его сигнатуру. Подобный код нарушает принцип наимешего удивления, т.к. бросает исключение даже в случае положительного исхода.
В данном конкретном случае решение очевидно — нужно заменить выбрасывание исключения на возврат булевского значения. Давайте рассмотрим более сложные примеры.
Исключения для валидации входящих данных
Возможно, наиболее часто встречающаяся практика в контексте исключений — это использование их в случае получения неверных входящих данных.
Очевидно, подобный подход имеет некоторые плюсы: он позволяет нам быстро «вернуться» из любого метода прямо в catch блок метода CreateEmployee.
Теперь давайте посмотрим на следующий пример:
Что общего имеют эти два сэмпла? Оба они позволяют прервать текущий поток выполнения и быстро перейти к определенной точке в коде. Единственная проблема в таком коде — он существенно ухудшает читаемость. Оба подхода затрудняют понимание кода, именно поэтому использование исключений для контроля потока выполнения программы часто уравнивают с использованием goto.
При использовании исключений сложно понять где именно они ловятся. Вы можете обернуть код, выбрасывающий исключение, в try/catch блок в том же методе, а можете поместить try/catch блок на несколько уровней по стеку выше. Вы никогда не можете знать наверняка сделано ли это намерено или нет:
Единственный способ узнать это — проанализировать весь стек. Исключения, использующиеся для валидации, делают код намного менее читаемым, т.к. они недостаточно ясно показывают намерения разработчика. На такой код невозможно просто посмотреть и сказать что может пойти не так и как необходимо отреагировать на это.
Есть ли способ лучше? Конечно:
Указание всех проверок явным образом делает ваши намерения намного более понятными. Эта версия метода проста и очевидна.
Исключения для исключительных ситуаций
Так когда же использовать исключения? Главная цель исключений — сюрприз! — обозначить исключительную ситуацию в приложении. Исключительная ситуация — это ситуация, в которой вы не знаете что делать и наилучшим выходом для вас является прекращение выполнения текущией операции (возможно, с предварительным логированием деталей ошибки).
Примерами исключительных ситуаций могут являться проблемы с подключением к БД, отсутствие необходимых конфигурационных файлов и т.п. Ошибки валидации не являются исключительной ситуацией, т.к. метод, проверяющий входящие данные, по определению ожидает что они могут быть неверными.
Другим примером корректного использования исключений является валидация контрактов (code contract). Вы, как автор класса, ожидаете, что клиенты этого класса будут соблюдать его контракты. Ситуация, при которой контракт метода не соблюден, является исключительной и заслуживает выбрасывания исключения.
Как работать с исключениями, брошенными другими библиотеками?
Является ли ситуация исключительной, зависит от контекста. Разработчик сторонней библиотеки может не знать как иметь дело с проблемами подключения к БД, т.к. он не знает в каком контексте будет использоваться его библиотека.
В случае подобной проблемы, разработчик библиотеки не имеет возможности что-либо с ней сделать, поэтому бросание исключения будет подходящим решением. Вы можете взять Entity Framework или NHibernate в качестве примера: они ожидают, что БД всегда доступна и если это не так, бросают исключение.
С другой стороны, разработчик, использующий библиотеку, может ожидать, что база данных уходит в офлайн время от времени и разрабатывать свое приложение с учетом этого факта. В случае отказа БД, клиентское приложение может попытаться повторить ту же операцию или отобразить сообщение пользователю с предложением повторить операцию позже.
Таким образом, ситуация может быть исключительной с точки зрения нижележащего кода и ожидаемой с точки зрения клиентского кода. Как в этом случае работать с исключениями, брошенными такой библиотекой?
Подобные исключения должны ловиться как можно ближе к коду, их выбрасыющему. Если это не так, ваш код будет иметь те же недостатки, что и пример кода с goto: невозможно будет понять где это исключение обрабатывается без анализа всего стека вызова.
Как можно видеть на примере выше, метод SaveCustomer ожидает проблемы с базой данных и намеренно отлавливает все ошибки, связанные с этим. Он возвращает булевский флаг, который затем обрабатывается кодом выше по стеку.
Метод SaveCustomer имеет понятную сигнатуру, говорящую нам, что в процессе сохранения клиента могут быть проблемы, что эти проблемы ожидаемы и что вы должны проверить возвращаемое значение чтобы убедиться, что все в порядке.
Стоит отметить широко известную практику, применимую в данном случае: не нужно оборачивать подобный код в generic обработчик. Generic обработчик утверждает, что любые исключения являются ожидаемыми, что по сути неправда.
Если вы действительно ожидаете какие-либо исключения, вы делаете это для очень ограниченного их числа, для которого вы точно знаете, что вы можете их обработать. Помещение generic обработчика приводит к тому, что вы проглатываете не ожидаемые вами исключения, приводя приложение в неконсистентное состояние.
Единственная ситуация, в которой generic обработчики применимы — это размещение их на самом высоком уровне по стеку приложения для отлова всех исключений, не пойманных кодом ниже, для того, чтобы залогировать их. Подобные исключения не следует пытаться обработать, все что можно сделать — это закрыть приложение (в случае со stateful приложением) или прекратить текущую операцию (в случае со stateless приложением).
Исключения и fail-fast принцип
Как часто вы встречаете код подобный этому?
Это пример некорректного использования generic обработчика исключений. Код выше подразумевает, что все исключения, приходящие из тела метода, являются признаком ошибки в процессе создания кастомера. В чем проблема подобного кода?
Помимо схожести с «goto» семантикой, обсуждаемой выше, пробема состоит в том, что исключение, приходящее в catch блок может не быть известным нам исключением. Исключение может быть как ArgumentException, которое мы ожидаем, так и ContractViolationException. В последнем случае, мы прячем баг, притворяясь, что нам известно как обрабатывать подобное исключение.
Итогда подобный подход применяется разработчиками, которые хотят защитить свое приложение от неожиданных падений. На самом же деле подобный подход только маскирует проблемы, делая более сложным процесс их вылавливания.
Наилучший способ работы с неожидаемыми исключениями — прекратить текущую операцию полностью и предотвратить распространение неконсистентного состояния по приложению.
Исключение (программирование)
Обработка исключительных ситуаций (англ. exception handling ) — механизм языков программирования, предназначенный для описания реакции программы на ошибки времени выполнения и другие возможные проблемы (исключения), которые могут возникнуть при выполнении программы и приводят к невозможности (бессмысленности) дальнейшей отработки программой её базового алгоритма. В русском языке также применяется более короткая форма термина: «обработка исключений».
Содержание
Исключения
Общее понятие исключительной ситуации
Во время выполнения программы могут возникать ситуации, когда состояние данных, устройств ввода-вывода или компьютерной системы в целом делает дальнейшие вычисления в соответствии с базовым алгоритмом невозможным или бессмысленными. Классические примеры подобных ситуаций:
Виды исключительных ситуаций
Исключительные ситуации, возникающие при работе программы, можно разделить на два основных типа: синхронные и асинхронные, принципы реакции на которые существенно различаются.
Обработчики исключений
Общее описание
В отсутствие собственного механизма обработки исключений для прикладных программ наиболее общей реакцией на любую исключительную ситуацию является немедленное прекращение выполнения с выдачей пользователю сообщения о характере исключения. Можно сказать, что в подобных случаях единственным и универсальным обработчиком исключений становится операционная система. Возможно игнорирование исключительной ситуации и продолжение работы, но такая тактика опасна, так как приводит к ошибочным результатам работы программ или возникновению ошибок впоследствии. Например, проигнорировав ошибку чтения из файла блока данных, программа получит в своё распоряжение не те данные, которые она должна была считать, а какие-то другие. Результаты их использования предугадать невозможно.
Обработка исключительных ситуаций самой программой заключается в том, что при возникновении исключительной ситуации, управление передаётся некоторому заранее определённому обработчику — блоку кода, процедуре, функции, которые выполняют необходимые действия.
Существует два принципиально разных механизма функционирования обработчиков исключений:
Существует два варианта подключения обработчика исключительных ситуаций к программе: структурная и неструктурная обработка исключений.
Неструктурная обработка исключений
Неструктурная обработка исключений реализуются в виде механизма регистрации функций или команд-обработчиков для каждого возможного типа исключения. Язык или системные библиотеки предоставляют программисту как минимум две стандартные процедуры: регистрации обработчика и разрегистрации обработчика. Вызов первой из них «привязывает» обработчик к определённому исключению, вызов второй — отменяет эту «привязку». Если исключение происходит, выполнение основного кода программы немедленно прерывается и начинается выполнение обработчика. По завершении обработчика управление передаётся либо в некоторую наперёд заданную точку программы, либо обратно в точку возникновения исключения (в зависимости от заданного способа обработки — с возвратом или без). Независимо от того, какая часть программы в данный момент выполняется, на определённое исключение всегда реагирует последний зарегистрированный для него обработчик. В некоторых языках зарегистрированный обработчик сохраняет силу только в пределах текущего блока кода (процедуры, функции), тогда процедура разрегистрации не требуется. Ниже показан условный фрагмент кода программы с неструктурной обработкой исключений:
Неструктурная обработка — практически единственный вариант для обработки асинхронных исключений, но для синхронных исключений она неудобна: приходится часто вызывать команды установки/снятия обработчиков, всегда остаётся опасность нарушить логику работы программы, пропустив регистрацию или разрегистрацию обработчика.
Структурная обработка исключений
Структурная обработка исключений требует обязательной поддержки со стороны языка программирования — наличия специальных синтаксических конструкций. Такая конструкция содержит блок контролируемого кода и обработчик (обработчики) исключений. Наиболее общий вид такой конструкции (условный):
Здесь «НачалоБлока» и «КонецБлока» — ключевые слова, которые ограничивают блок контролируемого кода, а «Обработчик» — начало блока обработки соответствующего исключения. Если внутри блока, от начала до первого обработчика, произойдёт исключение, то произойдёт переход на обработчик, написанный для него, после чего весь блок завершится и исполнение будет продолжено со следующей за ним команды. «ОбработчикНеобработанных» — это обработчик исключений, которые не соответствуют ни одному из описанных выше в данном блоке. Обработчики исключений в реальности могут описываться по-разному (один обработчик на все исключения, по одному обработчику на одно исключение и так далее), но принципиально они работают одинаково: при возникновении исключения находится первый соответствующий ему обработчик в данном блоке, его код выполняется, после чего выполнение блока завершается. Исключения могут возникать как в результате программных ошибок, так и путём явной их генерации с помощью соответствующей команды (в примере — команда «СоздатьИсключение»). С точки зрения обработчиков такие искусственно созданные исключения ничем не отличаются от любых других.
Блоки обработки исключений могут многократно входить друг в друга, как явно (текстуально), так и неявно (например, в блоке вызывается процедура, которая сама имеет блок обработки исключений). Если ни один из обработчиков в текущем блоке не может обработать исключение, выполнение данного блока немедленно завершается, и управление передаётся на ближайший подходящий обработчик более высокого уровня иерархии. Это продолжается до тех пор, пока обработчик не найдётся и не обработает исключение.
Иногда бывает неудобно завершать обработку исключения в текущем блоке, то есть желательно, чтобы при возникновении исключения в текущем блоке обработчик выполнил какие-то действия, но исключение продолжило бы обрабатываться на более высоком уровне (обычно так бывает, когда обработчик данного блока не полностью обрабатывает исключение, а лишь частично). В таких случаях в обработчике исключений генерируется новое исключение или возобновляется с помощью специальной команды ранее появившееся. Код обработчиков не является защищённым в данном блоке, поэтому созданное в нём исключение будет обрабатываться в блоках более высокого уровня.
Блоки с гарантированным завершением
Помимо блоков контролируемого кода для обработки исключений, языки программирования могут поддерживать блоки с гарантированным завершением. Их использование оказывается удобным тогда, когда в некотором блоке кода, независимо от того, произошли ли какие-то ошибки, необходимо перед его завершением выполнить определённые действия. Простейший пример: если в процедуре динамически создаётся какой-то локальный объект в памяти, то перед выходом из этой процедуры объект должен быть уничтожен (чтобы избежать утечки памяти), независимо от того, произошли после его создания ошибки или нет. Такая возможность реализуется блоками кода вида:
Заключённые между ключевыми словами «НачалоБлока» и «Завершение» операторы (основной код) выполняются последовательно. Если при выполнении их не возникает исключений, то затем выполняются операторы между ключевыми словами «Завершение» и «КонецБлока» (код завершения). Если же при выполнении основного кода возникает исключение (любое), то сразу же выполняется код завершения, после чего весь блок завершается, а возникшее исключение продолжает существовать и распространяться до тех пор, пока его не перехватит какой-либо блок обработки исключений более высокого уровня.
Принципиальное отличие блока с гарантированным завершением — он не обрабатывает исключение, а лишь гарантирует выполнение определённого набора операций перед тем, как включится механизм обработки. Стоит заметить, что блок с гарантированным завершением легко реализуется с помощью команд «возбудить исключение» и «структурный обработчик исключения».
Поддержка в различных языках
За исключением незначительных различий в синтаксисе, существует лишь пара вариантов обработки исключений. В наиболее распространённом из них исключительная ситуация генерируется специальным оператором ( throw или raise ), а само исключение, с точки зрения программы, представляет собой некоторый объект данных. То есть генерация исключения состоит из двух этапов: создания объекта-исключения и возбуждения исключительной ситуации с этим объектом в качестве параметра. При этом конструирование такого объекта само по себе выброса исключения не вызывает. В одних языках объектом-исключением может быть объект любого типа данных (в том числе строкой, числом, указателем и так далее), в других — только предопределённого типа-исключения (чаще всего он имеет имя Exception) и, возможно, его производных типов (типов-потомков, если язык поддерживает объектные возможности).
В целом, обработка исключений может выглядеть следующим образом (в некотором абстрактном языке):
В некоторых языках может быть лишь один обработчик, который разбирается с различными типами исключений самостоятельно.
Достоинства и недостатки
Достоинства использования исключений особенно заметно проявляется при разработке библиотек процедур и программных компонентов, ориентированных на массовое использование. В таких случаях разработчик часто не знает, как именно должна обрабатываться исключительная ситуация (при написании универсальной процедуры чтения из файла невозможно заранее предусмотреть реакцию на ошибку, так как эта реакция зависит от использующей процедуру программы), но ему это и не нужно — достаточно сгенерировать исключение, обработчик которого предоставляется реализовать пользователю компонента или процедуры. Единственная альтернатива исключениям в таких случаях — возврат кодов ошибок, которые вынужденно передаются по цепочке между несколькими уровнями программы, пока не доберутся до места обработки, загромождая код и снижая его понятность.
Использование исключений в целях контроля ошибок повышает читаемость кода, так как позволяет отделить обработку ошибок от самого алгоритма, и облегчает программирование и использование компонентов других разработчиков.
Недостаток исключений — в том, что их поддержка снижает скорость работы программы. Поэтому в местах программы, критичных по скорости, не рекомендуют возбуждать и обрабатывать исключения, хотя следует отметить, что в реальности такие случаи крайне редки, во всяком случае, в прикладном программировании.
Проверяемые исключения
Некоторые проблемы простой обработки исключений
Изначально (например, в C++), не существовало никакой формальной дисциплины описания, генерации и обработки исключений: любое исключение может быть возбуждено в любом месте программы, и если для него не находится обработчика в стеке вызовов, то выполнение программы прерывается аварийно. Если функция (особенно библиотечная) генерирует исключения, то для устойчивой работы использующая её программа должна перехватывать их все. Когда по какой-либо причине одно из возможных исключений оказывается необработанным, будет происходить неожиданное аварийное завершение программы. С подобными эффектами можно бороться организационными мерами, описывая возможные исключения, возникающие в библиотечных модулях, в соответствующей документации. Но при этом всегда остаётся возможность пропустить необходимый обработчик из-за случайной ошибки или несоответствия документации коду (что вовсе не редкость). Чтобы полностью исключить потерю обработки исключений, в обработчики приходится специально добавлять ветвь обработки «всех остальных» исключений (которая гарантированно перехватит любые, даже заранее неизвестные исключения), но такой выход не всегда оптимален.
Механизм проверяемых исключений
Позже в ряде языков, например, в Java, появились проверяемые исключения. Сущность этого механизма состоит в добавлении в язык следующих правил и ограничений:
Преимущества и недостатки
Проверяемые исключения снижают количество ситуаций, когда исключение, которое могло быть обработано, вызвало критическую ошибку в программе, поскольку за наличием обработчиков следит компилятор. Это особенно полезно при изменениях кода, когда метод, который не мог ранее выбрасывать исключение типа X, начал это делать: компилятор автоматически отследит все случаи его использования и проверит наличие соответствующих обработчиков.
Другим полезным качеством проверяемых исключений является то, что они способствуют осмысленному написанию обработчиков: программист явно видит полный и правильный список исключений, которые могут возникнуть в данном месте программы, и может написать на каждое из них осмысленный обработчик вместо того, чтобы создавать «на всякий случай» общий обработчик всех исключений, одинаково реагирующий на все нештатные ситуации.
У проверяемых исключений есть и недостатки.
Из-за перечисленных недостатков при обязательности использования проверяемых исключений этот механизм часто обходят. Например, многие библиотеки объявляют все методы как выбрасывающие некоторый суперкласс исключений (например, Exception ), и только на этот тип исключения создаются обработчики. Однако результатом становится то, что компилятор заставляет писать обработчики исключений даже там, где они объективно не нужны. Более правильным подходом считается перехват внутри метода новых исключений, порождённых вызываемым кодом, а при необходимости передать исключение дальше — «заворачивание» его в исключение, уже возвращаемое методом. Например, если метод изменили так, что он начинает обращаться к базе данных вместо файловой системы, то он может сам ловить SQLException и выбрасывать вместо него вновь создаваемый IOException, указывая в качестве причины исходное исключение. Обычно рекомендуется изначально объявлять именно те исключения, которые придётся обрабатывать вызывающему коду. Скажем, если метод извлекает входные данные, то для него целесообразно объявить IOException, а если он оперирует SQL-запросами, то, вне зависимости от природы ошибки, он должен объявлять SQLException. В любом случае, набор выбрасываемых методом исключений должен тщательно продумываться. При необходимости имеет смысл создавать собственные классы исключений, наследуя их от Exception или других подходящих проверяемых исключений.














