Исключения
Исключения в программировании (exceptions) — это механизм, который позволяет программе обрабатывать нетипичную ситуацию и при этом не прекращать работу. Благодаря этому механизму разработчик может описать в коде реакцию программы на такие ситуации.
Освойте профессию «Java-разработчик»
Простой пример: в программе-калькуляторе исключением может стать ситуация, когда пользователь решит поделить на ноль. Это не должно стать ошибкой, из-за которой рушится вся программа, но чтобы ситуация не застопорила исполнение остального кода, нужно ее правильно обработать. Для этого необходимы обработчики исключений. Они позволяют «сказать» программе, что ей делать, если такое случится.
Механизм обработки исключений существует в большинстве языков программирования. Он может быть реализован немного по-разному, но общая суть схожа: это всегда какие-то особые случаи, которые надо обработать отдельно. Мы при описании будем отталкиваться от особенностей исключений в Java, но встретить их можно и в других языках: JavaScript, PHP, Python, C++ и так далее.
Профессия / 14 месяцев
Java-разработчик
Освойте востребованный язык
Зачем нужны исключения
Механизм обработки исключений может понадобиться любому разработчику. Если не отслеживать исключительные ситуации, может возникнуть незаметная ошибка, которая нарушит работу всего кода, или программа может «зависнуть» либо «упасть» — потому что сложный момент не был обработан как надо.
Исключения нужны, чтобы программа продолжала относительно корректно работать, даже если что-то пошло не так.
Какими бывают исключения
Исключения делятся на две большие группы, которые пересекаются друг с другом: синхронные и асинхронные. Синхронные могут возникнуть только в конкретном месте программы или при выполнении определенной операции: открытие файла, деление и так далее. Асинхронные могут возникнуть когда и где угодно. Их «ловят» по-разному, чтобы успешно отслеживать и те, и другие.
Мы сказали, что эти группы пересекаются друг с другом, хотя по логике они противоположны. Пересечение происходит потому, что при выполнении операций асинхронным может стать даже формально синхронное исключение, и наоборот.
Станьте Java-разработчиком
и создавайте сложные сервисы
на востребованном языке
Как происходит работа с исключениями
- Разработчик пишет код и понимает, что в какой-то момент в том или ином месте может возникнуть нештатная ситуация. Бывает, что исключения добавляют в уже написанный код — например, нештатную ситуацию обнаружили при тестировании.
- В этом месте пишется особый блок кода — обработчик. Он говорит программе: здесь может возникнуть особая ситуация, если она случится, выполни вот это.
- Внутри обработчика — функция, которая выполнится, если программа столкнется с описанной ситуацией. Она или исправит ситуацию, или скорректирует дальнейшее выполнение программы.
Бывают исключения, которые нельзя предусмотреть. Разработчики обрабатывают не все возможные нештатные ситуации, а только самые очевидные, чтобы не перегружать код. Это справедливо для большинства сфер разработки, кроме тех, где слишком высока цена ошибки.
Как устроена обработка исключений
Существуют разные виды обработки: структурная и неструктурная, с возвратом и без возврата. Они различаются механизмом действия, но общая суть одна: это функция, которая запускается, если в коде случилась та или иная исключительная ситуация. Тут можно использовать условный оператор if или специальные синтаксические конструкции.
В примере с делением на ноль обработчик может отменить попытку деления и сказать пользователю, что на ноль делить нельзя, — но это самый простой пример. В реальности все сложнее.
Обработка с возвратом и без возврата. Эти виды обработки различаются реакцией на случившееся исключение. Версия с возвратом предполагает, что обработчик попытается разрешить проблему, а когда ему это удастся, вернет программу к исходному поведению. В итоге она будет работать так, как если бы исключения не возникало.
Вот пример: не запустился скрипт, необходимый для работы следующего скрипта. Следующий скрипт заметил это, зафиксировал исключение и обратился к обработчику, который запустил нужный скрипт «вручную». После этого все может работать, как и было задумано.
Обработка без возврата — вид обработки, когда проблема не ликвидируется, а участок кода, который не получается выполнить, пропускается. В примере со скриптами обработка «переключила» бы выполнение кода на момент, где уже не понадобится незаработавший скрипт.
Структурная и неструктурная обработка. Это два способа подключить обработчики. В первом случае они встраиваются в код, а когда генерируется исключение, для него выбирается тот или иной обработчик в зависимости от ситуации. Во втором случае обработчики существуют отдельно и «подключаются» к конкретным видам исключений с помощью специальных команд. Способ выбирается в зависимости от вида исключения, особенностей кода и языка.
Обычно асинхронные исключения обрабатывают неструктурно, а синхронные — структурно.
Гарантированное завершение. Так называется отдельный вид функции, которая обычно пишется после обработчика. Она описывает действия, которые должны произойти в этой части кода вне зависимости от того, произошло исключение или нет.
Исключения и ошибки: разница
Кроме исключений, в языках программирования существует механизм обработки ошибок. Их часто путают, особенно новички. И то, и другое подразумевает нетипичную ситуацию, в которой работу программы нельзя продолжить корректно. Но есть и различия:
- ошибка означает, что программа «упала», что ее работу нельзя продолжить и она должна быть завершена. Ошибку невозможно исправить — только сообщить о ней пользователю, записать в лог и прекратить исполнение кода;
- исключение — это нештатная ситуация, которую тем не менее можно попробовать починить «на ходу», не закрывая программу. В этом есть смысл, в отличие от ситуации с ошибкой.
Это действительно похожие понятия. В Java, например, сущности исключений и ошибок наследуются от общего предка — интерфейса Throwable. Но ошибка — это явление, когда что-то сделать принципиально не получается. А исключение — ситуация, когда программа просто не знает, что делать, если не указать на это дополнительно.
Можно провести аналогию. Мама послала дочь в магазин за покупками и сказала ей купить батон хлеба. Если хлеба в магазине не оказалось, девочка не сможет его купить. Это ошибка. А если в магазине есть три вида батонов, или все батоны вчерашние, а девочка не знает, нужен ли маме только свежий хлеб, или батон есть, но только из ржаной муки, — это исключения.
В первом случае дочь просто вернется домой и ничего не купит. Из-за ошибки программа не выполняется. Во втором случае девочка позвонит маме и спросит, что ей делать. Программа передаст управление обработчику, чтобы тот разрешил сложную ситуацию.
Когда пользоваться исключениями, а когда — ошибками
В некоторых случаях разработчики описывают все нештатные ситуации как исключения. Например, при создании новых библиотек, которые должны быть очень гибкими и подразумевать многие ситуации — то, что критично для одной задачи, окажется поправимым в другой. Но это редкие случаи, и чаще приходится выбирать между обработкой ошибки и исключения.
Обработчики ошибок советуют использовать тогда, когда проблема не решаема изнутри программы. Например, у приложения нет связи с сервером — оно не может продолжать работу без этого. Или какие-то критичные файлы оказались повреждены, и из-за этого код просто нельзя исполнить. Или в системе закончилась свободная память. Это никак не поправить программными способами.
Исключениями стоит пользоваться, если возникла нештатная, неправильная ситуация, которую не подразумевает логика работы программы. Но программу при этом не нужно выключать и завершать — надо исправить или «перескочить» проблемный момент и сохранить все остальное.
Как начать пользоваться исключениями
В большинстве языков механизм обработки исключений есть по умолчанию — это популярная функция. Но приступать к работе с ними рекомендуют после изучения базовых возможностей языка. Мы советуем идти от простого к сложному: начать с основ и затем переходить к комплексным темам. Конкретно обработка исключений обычно изучается перед тем, как человек переходит к практическим проектам, потому что любая более-менее сложная программа может столкнуться с исключениями в ходе работы.
Java-разработчик
Java уже 20 лет в мировом топе языков программирования. На нем создают сложные финансовые сервисы, стриминги и маркетплейсы. Освойте технологии, которые нужны для backend-разработки, за 14 месяцев.
Исключение != ошибка
Многие программисты почему-то считают, что исключения и ошибки — это одно и то же. Кто-то постоянно кидает 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. И всё. Этот механизм ничем более не является. Только управление потоком.
Их основное предназначение: пробрасывать по каскаду. Покажу это на примере: есть три функции, которые вызывают друг друга каскадом:
function b() < c(99); >function c($x) < if ($x === 0) < // Некоторая особенная ситуация, // которая должна остановить выполнение функций c() и b(), // а функция a() должна узнать об этом >return 100 / $x; >
Эту задачу можно было бы решить без механизма exception. Например, можно заставить все функции возвращать специальный тип (если ты матёрый пэхапэшник, то должен вспомнить PEAR_Error). Для простоты я обойдусь null-ом:
echo 'a-stop'; > function b() < echo 'b-begin'; $result = c(0); if ($result === null) < return null; >echo 'b-stop'; return true; > function c($x) < echo 'c-begin'; if ($x === 0) < return null; >echo 'c-stop'; return 100 / $x; >
a-begin
b-begin
c-begin
Делить на ноль нехорошо
Задача выполнена, но, обратите внимание, мне пришлось модифицировать промежуточную функцию b(), чтобы она пробрасывала результат работы нижестоящей функции выше по каскаду. А если у меня каскад из 5 или 10 функций? То мне пришлось бы модифицировать ВСЕ промежуточные функции. А если исключительная ситуация в конструкторе? То мне пришлось бы подставлять костыли.
А теперь решение с использованием Exception:
a(); function a() < echo 'a-begin'; try < b(); echo 'a-stop'; >catch (Exception $e) < echo $e->getMessage(); > > function b() < echo 'b-begin'; c(0); echo 'b-stop'; >function c($x) < echo 'c-begin'; if ($x === 0) < throw new Exception('Делить на ноль нехорошо'); >echo 'c-stop'; return 100 / $x; >
Результат выполнения будет идентичен. Функция b() осталась в первоначальном виде, не тронутая. Это особенно актуально, если у вас длинные каскады. И ещё объект $e может содержать дополнительную информацию о произошедшей ситуации.
Таким образом, получается, что ошибки и исключения — это совершенно разные инструменты для решения совершенно разных задач:
ошибка — не поправимая ситуация;
исключение – позволяет прервать выполнение каскада функций и пробросить некоторую информацию. Что-то вроде глобального оператора return. Если у Вас нет каскада, то вам достаточно использовать if или return.
Ошибки не всегда являются ошибками
Некоторые могут мне возразить: «Посмотри в Zend Framework — там всегда кидают исключения. Это best practics, и надо делать также. Даже если не удалось подключиться к БД, надо кидать исключение».
В этой статье я как раз хочу развеять это заблуждение. Zend действительно является best practics, но программисты Зенда находятся на другой лодке и делают другие вещи. Принципиальная разница между ними и мной в том, что они пишут универсальную библиотеку, которая будет использоваться во многих проектах. И они со своей колокольни не могут сказать, что является критической ошибкой, а что является поправимой.
Например, в вашем проекте может быть несколько MySQL серверов и вы можете переключаться между ними при падении одного из них. По этому, Zend_Db, как универсальная библиотека, кидает исключение, а что с ним делать — решайте сами. Exception это гибко — вы сами решаете на каком уровне и какой тип ситуаций ловить. Вы можете вывести сообщение об ошибке или попытаться исправить возникшую ситуацию, если знаете как. При написании универсальных библиотек необходимо всегда кидать исключения. Это делает библиотеку более гибкой.
В итоге, могу сказать, что у обоих механизмов есть свои особенности и, самое главное, что у них есть своё предназначение и эти предназначения нисколько не пересекаются. . Не надо использовать исключения для улучшения быстродействия или сообщения об ошибках. Не надо в классе My_Custom_Exception реализовывать какую-либо логику исправления ситуации. Этот класс должен быть пустым, он создаётся только что бы определить тип ситуации и поймать только то, что надо. Название класса ‘My_Custom_Exception’ это такой древовидный аналог линейному списку констант E_*** (E_NOTICE, E_WARNING, . ).
В php давно был разработан механизм обработки ошибок, и он отлично работает. Я им отлично пользуюсь там, где это надо.