Debug.Assert против выброса исключения

94

Я прочитал множество статей (и пару других подобных вопросов, которые были опубликованы на StackOverflow) о том, как и когда использовать утверждения, и я хорошо их понял. Но все же я не понимаю, какая мотивация должна побуждать меня использовать Debug.Assertвместо простого исключения. Я имею в виду, что в .NET ответ по умолчанию на неудачное утверждение - «остановить мир» и показать пользователю окно сообщения. Хотя такое поведение можно изменить, я считаю, что это очень раздражает и излишне, а вместо этого я мог бы просто выбросить подходящее исключение. Таким образом, я мог легко записать ошибку в журнал приложения непосредственно перед тем, как выбросить исключение, и, кроме того, мое приложение не обязательно зависает.

Итак, почему я должен использовать Debug.Assertвместо простого исключения , если это вообще возможно? Размещение утверждения там, где его не должно быть, может вызвать все виды «нежелательного поведения», поэтому, с моей точки зрения, я действительно ничего не добьюсь, используя утверждение вместо того, чтобы генерировать исключение. Вы согласны со мной, или я что-то здесь упускаю?

Примечание. Я полностью понимаю, в чем разница «теоретически» (отладка и выпуск, шаблоны использования и т. Д.), Но, как я это вижу, мне было бы лучше выбросить исключение, чем выполнять утверждение. Поскольку, если в производственном выпуске обнаруживается ошибка, я все равно хотел бы, чтобы «утверждение» провалилось (в конце концов, «накладные расходы» смехотворно малы), поэтому мне лучше вместо этого выбросить исключение.


Изменить: как я это вижу, если утверждение не удалось, это означает, что приложение вошло в какое-то поврежденное, неожиданное состояние. Так зачем мне продолжать казнь? Не имеет значения, работает ли приложение на отладочной или выпускной версии. То же самое касается обоих

Сообщество
источник
1
Что касается вещей, о которых вы говорите: «если в производственном выпуске обнаружена ошибка, я все равно хотел бы, чтобы« утверждение »не сработало», исключения - это то, что вы должны использовать
Том Нейланд
1
Производительность - единственная причина. Постоянная проверка всего нуля может снизить скорость, хотя это может быть совершенно незаметно. Это в основном для случаев, которые никогда не должны происходить, например, если вы знаете, что уже проверили null в предыдущей функции, нет смысла тратить циклы на повторную проверку. Debug.assert эффективно действует как последний шанс проинформировать вас.
rolls

Ответы:

177

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

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

  • ВСЕГДА должна быть возможность создать тестовый пример, который выполняет данный оператор throw. Если создать такой тестовый пример невозможно, значит, в вашей программе есть путь к коду, который никогда не выполняется, и его следует удалить как мертвый код.

  • НИКОГДА не должно быть возможности создать тестовый пример, который вызывает срабатывание утверждения. Если утверждение срабатывает, либо код неверен, либо утверждение неверно; так или иначе, что-то нужно изменить в коде.

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

Эрик Липперт
источник
16
Проблема с утверждениями в том, что их нет в производственной сборке. Невыполнение предполагаемого условия означает, что ваша программа вошла в зону неопределенного поведения, и в этом случае ответственная программа должна остановить выполнение как можно скорее (раскрутка стека также несколько опасна, в зависимости от того, насколько строгим вы хотите добиться). Да, утверждения обычно невозможно запустить, но вы не знаете, что возможно, когда что-то выходит из-под контроля. То, что вы считали невозможным, может произойти в производственной среде, и ответственная программа должна обнаруживать нарушенные предположения и действовать незамедлительно.
kizzx2 05
2
@ kizzx2: Итак, сколько невозможных исключений на строку производственного кода вы пишете?
Эрик Липперт
4
@ kixxx2: это C #, поэтому вы можете сохранять утверждения в производственном коде с помощью Trace.Assert. Вы даже можете использовать файл app.config, чтобы перенаправить производственные утверждения в текстовый файл, а не грубо по отношению к конечному пользователю.
HTTP 410,
3
@AnorZaken: Ваша точка зрения иллюстрирует недостаток дизайна в исключениях. Как я уже отмечал в другом месте, исключения - это (1) фатальные катастрофы, (2) упрямые ошибки, которые никогда не должны происходить, (3) ошибки проектирования, когда исключение используется для обозначения неисключительного состояния, или (4) неожиданные экзогенные условия. . Почему все эти четыре совершенно разные вещи представлены исключениями? Если бы я был моим druthers, boneheaded «нулевой был разыменовываются» исключения не будет catchable вообще . Это никогда не бывает правильно, и он должен завершить вашу программу, прежде чем она нанесет больше вреда . Они должны быть больше похожи на утверждения.
Эрик Липперт
2
@Backwards_Dave: ложные утверждения - это плохо, а не истинные. Утверждения позволяют выполнять дорогостоящие проверки, которые вы не хотели бы запускать в производственной среде. И если утверждение нарушается в производственной среде, что должен делать конечный пользователь?
Эрик Липперт
17

Утверждения используются для проверки понимания мира программистом. Утверждение должно потерпеть неудачу только в том случае, если программист сделал что-то не так. Например, никогда не используйте утверждение для проверки ввода пользователя.

Утверждает тест на условия, которые «не могут произойти». Исключения составляют условия, которые «не должно происходить, но происходят».

Утверждения полезны, потому что во время сборки (или даже во время выполнения) вы можете изменить их поведение. Например, часто в сборках релизов утверждения даже не проверяются, потому что они создают ненужные накладные расходы. Этого также следует опасаться: ваши тесты могут даже не выполняться.

Если вы используете исключения вместо утверждений, вы теряете некоторую ценность:

  1. Код более подробный, поскольку для тестирования и выдачи исключения требуется как минимум две строки, а для утверждения - только одна.

  2. Ваш тестовый код и код выброса будут всегда выполняться, в то время как утверждения могут быть скомпилированы.

  3. Вы теряете связь с другими разработчиками, потому что утверждения имеют другое значение, чем код продукта, который проверяет и бросает. Если вы действительно тестируете программное утверждение, используйте assert.

Подробнее здесь: http://nedbatchelder.com/text/assert.html

Нед Батчелдер
источник
Если этого «не может быть», то зачем писать утверждение. Разве это не лишнее? Если это действительно может произойти, но не должно, то разве это не то же самое, что «не должно происходить, но должно произойти» для исключений?
Дэвид Клемпфнер
2
«Не может произойти» заключено в кавычки по какой-то причине: это может произойти, только если программист сделал что-то не так в другой части программы. Утверждение - это проверка на ошибки программиста.
Нед Батчелдер
@NedBatchelder Термин " программист" немного двусмысленен, когда вы разрабатываете библиотеку. Верно ли, что эти «невозможные ситуации» должны быть невозможны для пользователя библиотеки, но могут быть возможны, если автор библиотеки допустил ошибку?
Бруно Целль,
12

РЕДАКТИРОВАТЬ: В ответ на редактирование / примечание, которое вы сделали в своем сообщении: похоже, что использование исключений - это правильный выбор, а не утверждения для того типа вещей, которые вы пытаетесь выполнить. Я думаю, что ваш мысленный камень преткновения состоит в том, что вы рассматриваете исключения и утверждения для достижения той же цели, и поэтому вы пытаетесь выяснить, какое из них было бы «правильным» для использования. Хотя может быть некоторое совпадение в том, как могут использоваться утверждения и исключения, не путайте, что для них это разные решения одной и той же проблемы - это не так. Утверждения и исключения имеют свои собственные цели, сильные и слабые стороны.

Я собирался напечатать ответ своими словами, но это лучше отражает концепцию, чем я:

C # Station: утверждения

Использование операторов assert может быть эффективным способом обнаружения логических ошибок программы во время выполнения, и тем не менее их легко отфильтровать из производственного кода. После завершения разработки затраты времени выполнения этих избыточных тестов на ошибки кодирования могут быть устранены просто путем определения символа препроцессора NDEBUG [который отключает все утверждения] во время компиляции. Однако не забывайте, что код, помещенный в само утверждение, будет опущен в производственной версии.

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

* the condition should never be false if the code is correct,
* the condition is not so trivial so as to obviously be always true, and
* the condition is in some sense internal to a body of software.

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

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

Том Нейланд
источник
Я все это понимаю. Но дело в том, что то же утверждение, которое вы пометили жирным шрифтом, также относится к исключениям. Таким образом, как я это вижу, вместо утверждения я мог бы просто выбросить исключение (поскольку, если «ситуация, которая никогда не должна возникать» действительно произойдет в развернутой версии, я все равно хотел бы получить информацию об этом [плюс, приложение может войти в поврежденное состояние, поэтому мне подходит исключение, возможно, я не захочу продолжать нормальный поток выполнения)
1
Утверждения следует использовать для инвариантов; исключения следует использовать, когда, скажем, что-то не должно быть нулевым, но оно будет (например, параметр метода).
Эд С.
Я предполагаю, что все сводится к тому, насколько оборонительно вы хотите кодировать.
Нед Батчелдер,
Я согласен, для того, что вам нужно, исключения - это лучший способ. Вы сказали, что хотели бы: сбои, обнаруженные в производственной среде, возможность регистрировать информацию об ошибках, управление потоком выполнения и т. Д. Эти три вещи заставляют меня думать, что вам нужно выбросить некоторые исключения.
Tom Neyland
7

Я думаю, что (надуманный) практический пример может помочь пролить свет на разницу:

(адаптировано из расширения MoreLinq Batch )

// 'public facing' method
public int DoSomething(List<string> stuff, object doohickey, int limit) {

    // validate user input and report problems externally with exceptions

    if(stuff == null) throw new ArgumentNullException("stuff");
    if(doohickey == null) throw new ArgumentNullException("doohickey");
    if(limit <= 0) throw new ArgumentOutOfRangeException("limit", limit, "Should be > 0");

    return DoSomethingImpl(stuff, doohickey, limit);
}

// 'developer only' method
private static int DoSomethingImpl(List<string> stuff, object doohickey, int limit) {

    // validate input that should only come from other programming methods
    // which we have control over (e.g. we already validated user input in
    // the calling method above), so anything using this method shouldn't
    // need to report problems externally, and compilation mode can remove
    // this "unnecessary" check from production

    Debug.Assert(stuff != null);
    Debug.Assert(doohickey != null);
    Debug.Assert(limit > 0);

    /* now do the actual work... */
}

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

Drzaus
источник
Разве ваши 3 утверждения не являются полностью лишними? Их параметры невозможно оценить как ложные.
Дэвид Клемпфнер
1
В том-то и дело - утверждения нужны для того, чтобы задокументировать то, что невозможно. Почему ты бы так поступил? Потому что у вас может быть что-то вроде ReSharper, которое предупреждает вас внутри метода DoSomethingImpl, что «вы можете разыменовать здесь null», и вы хотите сказать ему: «Я знаю, что делаю, это никогда не может быть нулевым». Это также признак для некоторых более поздних программистов, которые могут не сразу понять связь между DoSomething и DoSomethingImpl, особенно если они разделены сотнями строк.
Марсель Попеску
4

Еще один самородок из Code Complete :

«Утверждение - это функция или макрос, который громко жалуется, если предположение неверно. Используйте утверждения для документирования предположений, сделанных в коде, и для исключения неожиданных условий ...

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

Далее он добавляет несколько рекомендаций о том, что следует и не следует утверждать.

С другой стороны, исключения:

«Используйте обработку исключений, чтобы привлечь внимание к неожиданным случаям. Исключительные случаи следует обрабатывать таким образом, чтобы они были очевидны во время разработки и могли быть восстановлены при выполнении производственного кода».

Если у вас нет этой книги, купите ее.

Эндрю Коуэнховен
источник
2
Книгу прочитал, отлично. Однако .. вы не ответили на мой вопрос :)
Вы правы, я не ответил. Мои ответы - нет, я не согласен с вами. Утверждения и исключения - это разные животные, о которых говорилось выше, и некоторые другие опубликованные здесь ответы.
Эндрю Коуэнховен,
0

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

Мез
источник
первое частичное предложение верно, остальное в целом - плохая идея: утверждения - это предположения и отсутствие проверки (как указано выше), включение отладки в выпуске действительно не вариант.
Марк Виттке,
0

Используйте утверждения для вещей, которые ВОЗМОЖНЫ, но не должны происходить (если бы это было невозможно, зачем бы вы ставили утверждение?).

Разве это не похоже на случай использования Exception? Почему бы вам использовать утверждение вместо Exception?

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

Обычно перед вами нет кода, Exceptionкоторый гарантирует, что он не будет брошен.

Почему хорошо, что Debug.Assert()компилируется в prod? Если вы хотите узнать об этом во время отладки, разве вы не хотели бы знать об этом в продукте?

Вы хотите этого только во время разработки, потому что, обнаружив Debug.Assert(false)ситуации, вы пишете код, чтобы гарантировать, что Debug.Assert(false)это больше не повторится. После завершения разработки, если вы обнаружили Debug.Assert(false)ситуации и исправили их, их Debug.Assert()можно безопасно скомпилировать, поскольку теперь они являются избыточными.

Дэвид Клемпфнер
источник
0

Предположим, вы являетесь членом довольно большой команды и несколько человек работают над одной и той же общей базой кода, включая перекрытие классов. Вы можете создать метод, который вызывается несколькими другими методами, и, чтобы избежать конфликта блокировок, вы не добавляете к нему отдельную блокировку, а скорее «предполагаете», что он был ранее заблокирован вызывающим методом с определенной блокировкой. Например, Debug.Assert (RepositoryLock.IsReadLockHeld || RepositoryLock.IsWriteLockHeld); Другие разработчики могут пропустить комментарий, в котором говорится, что вызывающий метод должен использовать блокировку, но они не могут это игнорировать.

Дэниел
источник