В c# базовый класс для Exception'ов - Exception. В C# нельзя выбросить исключение другого типа. При возникновении исключения CLR начинает подниматься по стеку вверх и искать подходящий обработчик(блок catch), при этом вызывая все блоки finally, использует pattern maching для определения первого подходящео блока catch и вызывает его записывая трассировку стека от места возникновения исключения до места обработки, далее вам нужно выполнить код, который будет обрабатывать исключения и (возможно) выбрасывать его снова, в случае повторного выброса важно знать, что при таком выбросе трассировка будет сохранена(то же самое исключение будет возбуждено повторно):
А при таком - нет(на самом деле потеряется еще и TargetSite, будет возбуждено новое исключение):
Для того, чтобы просмотреть все необработанные исключения приложения можно подписаться на событие AppDomain'а - UnhandledException
Трассировка в данном случае проходит от места выброса исключения до обработчика, если же нужна полная трассировка до возникновения исключения:
В IL код записываются слова try и catch, также интересно организован блок finally:
Блок try{}catch{} вложен внутрь блока try{} finally{}, видимо в c# коде было сделано выпрямление чтобы не делать вложенность, но тем самым у оператора finally не та семантика, на самом же деле finally никак не зависит от catch и может использоваться без него.
Блок finally выполняется в коде практически всегда(если поток прерывается и при этом не генерируется ThreadAbortException - тогда не выполняется), удивительно, что даже если поток отменен (thread.Abort), то finally все-равно выполнится до конца перед остановкой потока.
Также catch завершает свою работу даже если поток остановлен.
Вот последовательность выполнения блоков в такой ситуации:
Базовый Exception имеет несколько интересных полей:
- string Message - существует ctor принимающий message, заполняется при выбросе исключения
- IDictionary Data( на самом деле IDictionary<object,object>) - существует ctor, который принимает его, словарик, который должен помочь коду получившему исключение извлечь больше информации о том, что случилось
- Exception InnerException - существует ctor, принимающий InnerException - нужен когда исключение генерируется при обработке другого исключения
- string StackTrace - трассировка от места возникновения исключения до обработчика
- TargetSite - метод в котором сгенерировано исключение
- using, foreach, lock, деструктор - везде используется сочетание try{}finally{}, это удобно потому что блок в finally выполнится практически в любом случае (кроме Envorement.FailFast)
в using'e и foreach - вызывается dispose у disposable Объекта и итератора соответсвенно
lock - выполнятся Monitor.Exit(), для освобождения ресурса
деструктор - вызывается base.Finalize();
При таком вызове исключение я методах оборачиваются в TargetInvocationException, а сами хранятся в InnerException, такой прием помогает явно на то, где было сгенерировано исключение(при использовании dynamic такой проблемы нет).
Лучше такой подход реализован в статических кторах, если статический ктор типа генерирует исключение, то оно оборачивается в TypeInitializationException, это особенно важно потому что статический ктор вызывается неявно при первом обращении к экземпляру такого типа
Рихтер считает, что вы должны полностью подавлять исключения только если разрабатываете приложение которое взаимодействует с пользователями, поскольку пользователи не должны видеть ошибок. При разработке библиотеки допустимо перехватывать исключение обрабатывать (например, закрывать файл) и снова генерировать такое же, сохраняя трассировку стека. Также он считает, что иногда в блоке catch стоит вернуть новое исключение вместо полученного, сохранив полученное в поле InnerException, для того, что пользователь библиотеки на своем уровне понял в чем именно проблема, без погружения в сложные технические детали(например, если исключение из-за того, что не удалось считать файл, а пользователь вообще не знает о реализации, то нужно сообщить ему только о том, что метод вызвать не удалось. По мнению Рихтера - метод должен быть готов к тому, что будет сгенерировано исключение и должен уметь обработать эту ситуацию, сообщив вызывающему коду, что произошло исключение
В c++ если при создании объекта произошло исключение, то должен быть вызван деструктор, тем самым компилятор должен генерировать код, обрабатывающий такое поведение. В управляемых языках можно просто понадеяться на сборщик мусора - мы разделили ответственность за создание(программист) и удаление объектов(сборщик мусора) - тем самым сделав этот процесс проще
По мнению Рихтера выброс и перехват исключения самый удобный способ обработки исключений и он предлагает использовать TryXXX методы только если подход с генерацией исключения бьет по производительности, это спорно, поскольку существует вероятность что вызывающий код не обработает исключение и получит exception на своей стороне. Можно заставить клиентов обрабатывать ошибки, например используя тип Result<TError,TSuccess> https://tproger.ru/translations/functional-sharp-4/
Если приложение имеет состояние, то важно, чтобы оно не могло испортиться во всех случаях. Особенно подлые случаи, когда exception происходит в блоках catch и finally. Есть такие виды exception'ов, от которых не получится в любом случае, например Out of memory
или Stack Over Flow
, но можно попытаться заранее обработать код, который может привести к OutOfMemory
(дописать про CER, RuntimeHelpers, ReliabilityContractAttribute)