Pull to refresh

Comments 44

Вот пока вы не показали в вашем примере, как, собственно, правильно делать сериализацию для дополнительных полей, описывающих состояние исключения, это все особого смысла не имеет.
Вы правы — показать, как выполнять сериализацию дополнительных полей, стоит. Сегодня постараюсь дополнить статью. Спасибо.
>>Тогда как System.Exception является общим классом для всех user-defined exceptions, то System.ApplicationException определяет исключения, возникающие на уровне конкретного приложения.

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

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

Это из Троелсена — «Язык программирования C# 5.0 и платформа .NET 4.5».
Внимание, вопрос: от какого базового типа должен наследоваться Exception, бросаемый прикладной библиотекой общего пользования?
Процитирую CLR via C#
Специалисты Microsoft хотели сделать тип System.Exception базовым для
всех исключений, а два других типа, System.SystemException и System.ApplicationException,
стали бы его непосредственными потомками. Кроме того, исключения,
вброшенные CLR, стали бы производными от типа SystemException, в то время
как исключения, появившиеся в приложениях, должны были наследовать от
ApplicationException. Это дало бы возможность написать блок catch, перехватывающий
как все СLR-исключения, так и все исключения приложений.
Однако на практике это правило соблюдается не полностью; некоторые
исключения являются прямыми потомками типа Exception (IsolatedStorageException),
некоторые СLR-исключения наследуют от типа ApplicationException (TargetinvocationException),
а некоторые исключения приложений — от типа
SystemException (FormatException). Из-за этой путаницы типы SystemException
и ApplicationException не несут никакой особой смысловой нагрузки. В настоящее
время в Microsoft подумывают вообще убрать их из иерархии классов
исключений, но это невозможно, так как приведет к нарушению работы уже
имеющихся приложений, в которых используются эти классы.
Сам никогда не вникал, но имхается, что только для того чтобы определить, откуда прилетел кирпич: из системы (.NET) или из приклада.
upd: о, выше Троелсеном подтвердили.
Внимание, вопрос: от какого базового типа должен наследоваться Exception, бросаемый прикладной библиотекой общего пользования?
Думаю, от System.Exception, так как исключение возникает на уровне библиотеки.
… и как отличить, прилетело исключение из системы, или из прикладной библиотеки?
… и что делать, если часть приложения (выкидывающего ApplicationException), решили вынести в отдельную библиотеку? Менять AppException на простой Exception опасно, так что-то может сломаться.
Чтобы код класса пользовательского исключения соответствовал рекомендациям .NET, нужно придерживаться следующих правил

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

По каждому пункту обоснования добавлю. Спасибо.
Философский вопрос: а разве факт того, что пользователь не найден является исключительной ситуацией? Судя по приведённому коду, программа вполне может продолжить работу в штатном режиме. Это я к тому, что не стоит, наверное, использовать исключения там, где они по сути не нужны.
Тут все зависит от целей создания исключения. Если, допустим, в лог нужно записать, что возникала ситуация, когда пользователь не был найден — то генерация исключения и его перехват в catch позволит решить эту задачу.
Исключения может быть создано только с одной целью — для информирования об исключительной ситуации. Запись в лог сама по себе не может быть целью исключения.
Мне кажется, можно долго спорить о том, что является, а что не является исключительной ситуацией. Думаю, тут все остается на усмотрение разработчика — если он считает, что не найденный в системе пользователь достоин своего отдельного исключения, то так и должно быть :)
Вопрос не в том, что является или нет исключительной ситуацией, а в том, для чего используется Exception.
В этом случае можно использовать тип функции int и возвращать 0 или 1 если выполнение корректно или с ошибкой.

Но в конечном счете что быстрее вернуть 1 или перехватить Exception

И что проще для написания и поддержки
В этом случае можно использовать тип функции int и возвращать 0 или 1 если выполнение корректно или с ошибкой.

Мы вообще про C# говорим?
Тогда твой вариант возвратить из функции информацию о отсутствующем пользователе.
без использования ref или out параметр тоже не будем, т.к. тоже отдает с++.

Итого остается только Exception, либо я что то пров этой жизни :)
Вообще-то, вариантов ровно два. Либо метод (функций в C# нет) предполагает, что пользователь есть, а его отсутствие — нештатная ситуация. Тогда мы кидаем Exception. Либо же метод предполагает, что пользователя может и не быть, тогда ему неплохо бы называться Try..., и он возвращает bool.
Про метод который я назвал функцией согласен, ошибка терминологии.

А про Try… тогда кейс использования как например у bool int.TryParse(string input, out int N);
И если вернул true то в N точно есть валидное значение.

Вы это имели в виду?
Да, только надо понимать, что у обсуждаемого метода нет возвращаемого значения.
Это имеет значение? Ну конкретно эта программа может работать, другая не может это как то влияет на то как правильно описывать пользовательские иссключения?
Статья же называется «Как правильно...» а не «Где правильно» ?:)
Зная «как правильно», но не зная «где правильно» неопытный читатель подвергнется искушению использовать исключения не по назначению. На мой взгляд логичнее было рассмотреть ситуации, когда недоступен удалённый сервис, например — это в 99.9% случаев является исключительной ситуацией не позволяющей продолжить дальнейшее исполнение программы.
Помимо этого, данный конкретный случай не обязует реализовывать кастомные исключения. Даже если отсутствие пользователя с заданным именем подразумевает исключительную ситуацию, тут вполне применимо System.ArgumentException. Ведь, если это исключение (невозможно обработать внутри класса работы с именами пользователей) это скорее всего «косяк» вызывающей стороны и нужно понять, откуда вызывающая сторона получила такое значение аргумента.
Если быть совсем точным, ArgumentOutOfRangeException.
Думаю, что всё-таки ArgumentOutOfRangeException лучше здесь не использовать, хотя я не нашел явных практик использования ArgumentOutOfRangeException для данного случая(и документация тоже на самом деле не противоречит такому использованию), однако мне кажется, что ArgumentOutOfRangeException следует применять для типов:
1. У которых есть отношение порядка(например int, double)
2. У которых количество возможных экземпляров ограничено(например enum или ограниченный список строковых констант)

Опять же, это мое личное мнение.
В данном случае, возможно, лучше использовать, или просто ArgumentException, или собственное исключение унаследованное от ArgumentException.
Вкусовщина (справедливости ради, и с вашей, и с моей стороны).
Почему же все статьи начинающиеся с «Это статья предназначена для новичков....» де-факто можно относить к шлаку и пользы, что новичкам, либо еще кому-нибудь от нее не будет.
Прежде всего, конечно же, надо было начинать с изложения официальной точки зрения Design Guidelines for Exceptions плюс Best Practices for Exceptions. Также очень рекомендую рассуждения Vexing exceptions от Eric Lippert (создателя компилятора C#). Без этого получилось слишком много сомнительной «отсебятины».

По поводу указанных требований «класс должен быть помечен атрибутом [System.Serializable]» и «класс должен определять конструктор для поддержки сериализации типа». Это некоторый пережиток прошлого и в современных библиотеках, которые делают как Portable Class Libraries, они неактуальны.

В примере, описываемом в начале статьи, не нужно создавать своё исключение, тут идеально подходит InvalidOperationException, которое будет означать неверную операцию «редактирование несуществующего пользователя».

По поводу упоминающегося в комментариях ArgumentException. Это исключение (и все производные от него) соверешенно особенное, оно означает нарушение контракта. Оно никогда не должно возникать при выполнении программ. Его возникновение должно быть 100% предсказуемо до вызова метода, на основании значений параметров и свойств объекта согласно контракту метода. Это исключение означает неправильно написанную программу.
По поводу упоминающегося в комментариях ArgumentException. Это исключение (и все производные от него) соверешенно особенное, оно означает нарушение контракта. Оно никогда не должно возникать при выполнении программ. Его возникновение должно быть 100% предсказуемо до вызова метода, на основании значений параметров и свойств объекта согласно контракту метода. Это исключение означает неправильно написанную программу.

В частности, ArgumentException (будучи design error) не должен ловиться пользователем библиотеки; при возбуждении этого исключения нужно менять вызывающий код, добавить какую-то проверку.
Его возникновение должно быть 100% предсказуемо до вызова метода, на основании значений параметров и свойств объекта согласно контракту метода.

Любопытная логика. Очевидно, что корректно ли кидать ArgumentOutOfRangeException при попытке достать несуществующий (находящийся за пределами строки) символ. Почему тогда некорректно кидать его же при попытке достать несуществующего пользователя?

Можете дать ссылку на то место, где говорится, что ArgumentException и его производные используются только для формально определимых контрактов?
корректно ли кидать ArgumentOutOfRangeException при попытке достать несуществующий (находящийся за пределами строки) символ. Почему тогда некорректно кидать его же при попытке достать несуществующего пользователя?

Находится ли символ за пределами строки (или ключ в словаре, etc) — может быть проверено не прибегая к механизму исключений, просто if'ом. Ошибка несуществующего элемента в этом случае — ошибка времени разработки, так как разработчик забыл сделать проверку.

Напротив, существование пользователя (или файла на диске, etc) в общем случае не может быть проверено как «if (UserExists(...))», потому что между проверкой и обращением ситуация может измениться (обновиться база данных, или файловая система). В этом случае отсутствие пользователя является ошибкой времени выполнения, вполне объективная исключительная ситуация, которая не может быть обработана статически (изменением кода вызова).
А как же многопоточные системы, в которых состояние словаря может измениться между проверкой и получением значения?
А как же многопоточные системы, в которых состояние словаря может измениться между проверкой и получением значения?

На эту тему надо смотреть раздел Thread Safety в статье MSDN Library, посвящённой классу словаря (Dictionary, ConcurrentDictionary, etc).
Спасибо, как работать со словарями в многопоточных системах — я знаю. Но важно то, что без дополнительных действий (например, блокировки) состояние словаря может измениться между проверкой и получением значения, как следствие, не вполне очевидно, это ошибка времени разработки или времени выполнения.

И с другой стороны, в реальном LOB-приложении то, что объект не найден по ключу на определенном уровне означает ровно то, что уровнем выше разработчик забыл поставить проверку на наличие (или доступность) объекта (и заблокировать его, если у нас многопользовательская система).
Но важно то, что без дополнительных действий (например, блокировки) состояние словаря может измениться между проверкой и получением значения, как следствие, не вполне очевидно, это ошибка времени разработки или времени выполнения.

Это ошибка времени разработки, так как это нарушение контракта — в упомянутом разделе Thread Safety сказано, что Dictionary не гарантирует корректную работу без внешней синхронизации. Обрабатываться эта ошибка должна не динамически (ловим KeyNotFoundException), а статически (меняем код так, чтоб не было неправомерного использования в многопоточной ситуации, для которой класс не предназначен; добавляем проверки — TryGetValue или if).
И вот тут мы возвращаемся обратно к LOB-приложению.

Предположим, у меня есть некий репозиторий пользователей с двумя методами: CanGetUser(id) и GetUser(id). Чем эта ситуация отличается от ситуации со словарем (учитывая, что репозиторий точно так же можно положить под ту или иную синхронизацию)?
Если API гарантирует, что CanUserId(id) достаточно для последующего бессбойного вызова GetUser(id), то можно кидать UsageException (в случае C# это производное от ArgumentException). То есть трактовать ошибку можно как нарушение контракта — пользователь не сделал проверку существования перед обращением. Не вижу никаких противоречий.
Противоречий нет, есть очередная серая зона «а можно ли тут бросить ArgumentException».
Sign up to leave a comment.

Articles