Автор: Sergey Teplyakov

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

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

И именно о семантике исключений сегодня и пойдет речь.

Я обычно рассматриваю исключения со следующей колокольни: исключения бывают системные и пользовательские, а также, восстанавливаемые или нет.

Drawing2

Итак, наши исключения могут передавать разный смысл:

  • Критические системные ошибки
  • Некритические системные ошибки
  • Некритические проблемы в бизнес-логике пользователя
  • Баги в пользовательской логике

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

Критические системные ошибки

Проще всего дела обстоят с невосстанавливаемыми системными ошибками, такими как StackOverflowException, OutOfMemoryException (*), ThreadAbortExcecption и ExecutionEngineException.

(*) Подобные исключения еще называют асинхронными, поскольку они могут возникнуть практически в любой точке приложения. При этом OOE достаточно хитрое, поскольку оно может быть синхронным, например, в случае неудачи выделения большого блока памяти, и асинхронным, когда памяти в системе ёк, и оно возникает, например, при упаковке параметра во время вызова метода. В первом случае, есть шансы что-то сделать в пользовательском коде, а во-втором случае уже ничего не выйдет.

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

Некритические системные ошибки

К некритическим (или восстанавливаемым) системным ошибкам относятся исключения, типа SocketException, IOException, DbException и им подобные. По сути, это подвид пользовательских исключений с определенным смыслом что-то физически пошло не так.

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

Сложностей здесь может быть две:

  • Их нельзя покрыть юнит-тестами, а значит вы можете заранее не знать о том, кто и когда их может выбросить.
  • От них можно восстановиться только локально, и если вы проморгали эту возможность и такое исключение проходит границу модуля, то оно становится невосстанавливаемым.
NullReferenceException

Отдельную категорию между пользовательскими и системными ошибками, занимает NullReferenceException. Оно генерируется CLR и не является ни системной ошибкой, ни пользовательским исключением. Но чем бы формально не являлось это исключение, по факту это баг в коде, восстановиться програмно от которого невозможно.

Пользовательские ошибки

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

Существует несколько подвидов пользовательских ошибок:

  • Исключения о невалидных аргументах метода (ArgumentException и его наследники).
  • Исключения о нарушенной бизнес-логике (обычно, кастомные пользовательские исключения).
  • Исключения о нарушенных инвариантах реализации (InvalidOperationException, InvalidCastException и другие).

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

Нарушения бизнес-логики. Код приложения может генерировать UserAlreadyRegisteredException, EntryNotFoundException или InvalidConfigurationException, чтобы сообщить о специфических проблемах. Такие исключения являются восстанавливаемыми и вызывающий код может выдать специализированное сообщение пользователю, создать запись в базе данных или создать конфигурационный файл, после чего вызвать метод снова.

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

Исключения о нарушениях инвариантов реализации это довольно хитрый вид исключений. Вот, например, InvalidOperationException, которое бросает метод Enumerable.First. Мы можем сгруппировать несколько вызовов этого метода и обработать их одним блоком catch. Но если мы его не обработаем по месту, и оно покинет пределы этого метода, то оно уже станет невосстанавливаемым.

Тут мы переходим к простому правилу: даже обабатываемое исключение перестает быть таковым, если оно поднимается по стеку вызовов от места генерации. Это значит, что мы можем обработать InvalidOperationException прямо в месте вызова метода Enumerable.First, но если мы его проигнорируем, то вызывающий метод его обработать уже не сможет!

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

Исключения и контракты

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

Поэтому библиотека Code Contracts по умолчанию генерирует специальный класс ContractException, который является внутренним и недоступным коду приложения. И по этой же причине, совершенно не нужно использовать конструкции вида: Contract.Requires<,EntryNotFoundException>,(HasEntry(id)), поскольку EntryNotFoundException является восстанавливаемым, а нарушение контракта таковым быть не должно.

Помогла статья? Оцените её!
0 из 5. Общее количество голосов - 0
 

You have no rights to post comments

Дмитрий Крикунов

Публикую статьи, обучающие курсы и новости по программированию: алгоритмам, языкам (С++, Java), параллельному программированию, паттернам и библиотекам (Qt, boost).