Когда я должен использовать Debug.Assert()?

Я являюсь профессиональным инженером-программистом около года, закончив со степенью бакалавра. Некоторое время я знал об утверждениях в C++ и C, но понятия не имел, что они существовали в C# и.NET вообще до недавнего времени.

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

Должен ли я начать использовать Asserts в нашем производственном коде? И если да, то когда его использование является наиболее подходящим? Будет ли больше смысла делать

Debug.Assert(val != null);

или же

if ( val == null )
    throw new exception();

20 ответов

Решение

В отладке приложений Microsoft .NET 2.0 у Джона Роббинса есть большой раздел с утверждениями. Его основные моменты:

  1. Утверждай либерально. Вы никогда не можете иметь слишком много утверждений.
  2. Утверждения не заменяют исключения. Исключения охватывают то, что требует ваш код; утверждения охватывают вещи, которые он принимает.
  3. Хорошо написанное утверждение может рассказать вам не только о том, что произошло и где (как исключение), но и почему.
  4. Сообщение об исключении часто может быть загадочным, требуя от вас работы в обратном направлении через код для воссоздания контекста, который вызвал ошибку. Утверждение может сохранить состояние программы в момент возникновения ошибки.
  5. Утверждения дублируются как документация, сообщая другим разработчикам, от каких предположений зависит ваш код.
  6. Диалоговое окно, появляющееся при сбое утверждения, позволяет подключить к процессу отладчик, чтобы вы могли обходить стек, как если бы вы установили точку останова.

PS: Если вам понравился Code Complete, я рекомендую дополнить его этой книгой. Я купил его, чтобы узнать, как использовать WinDBG и файлы дампа, но первая половина содержит советы, которые помогут избежать ошибок в первую очередь.

Положил Debug.Assert() везде в коде, где вы хотите, есть проверки работоспособности для обеспечения инвариантов. Когда вы компилируете сборку Release (т.е. нет DEBUG константа компилятора), вызовы Debug.Assert() будут удалены, чтобы они не влияли на производительность.

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

FWIW ... я считаю, что мои публичные методы имеют тенденцию использовать if () { throw; } шаблон, чтобы убедиться, что метод вызывается правильно. Мои частные методы имеют тенденцию использовать Debug.Assert(),

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

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

Из полного кода

8 Защитное программирование

8.2 Утверждения

Утверждение - это код, который используется во время разработки - обычно подпрограмма или макрос - который позволяет программе проверять себя во время работы. Когда утверждение верно, это означает, что все работает, как ожидалось. Если значение равно false, это означает, что в коде обнаружена непредвиденная ошибка. Например, если система предполагает, что файл информации о клиенте никогда не будет содержать более 50 000 записей, программа может содержать утверждение о том, что число записей меньше или равно 50 000. Пока количество записей меньше или равно 50 000, утверждение будет молчать. Однако, если он встречает более 50 000 записей, он громко "утверждает", что в программе есть ошибка.

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

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

(...)

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

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

На вашем месте я бы сделал:

Debug.Assert(val != null);
if ( val == null )
    throw new exception();

Или чтобы избежать повторной проверки состояния

if ( val == null )
{
    Debug.Assert(false,"breakpoint if val== null");
    throw new exception();
}

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

Это, конечно, увеличивает накладные расходы на ваш производственный исполняемый файл.

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

Вы можете переопределить это поведение, удалив DefaultTraceListener: посмотрите документацию для Trace.Listeners в MSDN.

В итоге,

  • Используйте Debug.Assert свободно, чтобы помочь выявить ошибки в сборках Debug.

  • Если вы используете Trace.Assert в режиме пользовательского интерфейса, вы, вероятно, захотите удалить DefaultTraceListener, чтобы избежать смущения пользователей.

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

Утверждения используются для обнаружения ошибки программиста (вашей), а не ошибки пользователя. Их следует использовать только в том случае, если нет никаких шансов, что пользователь может вызвать утверждение. Например, если вы пишете API, утверждения не должны использоваться для проверки того, что аргумент не является нулевым в любом методе, который может вызвать пользователь API. Но его можно использовать в закрытом методе, не являющемся частью вашего API, чтобы утверждать, что ВАШ код никогда не передает нулевой аргумент, если это не предполагается.

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

Короче

Asserts используются для охранников и для проверки дизайна по условиям контракта, а именно:

  • Asserts должен быть только для отладочных и непроизводственных сборок. Утверждения обычно игнорируются компилятором в сборках Release.
  • Asserts может проверить на наличие ошибок / неожиданных условий, которые находятся под контролем вашей системы
  • Asserts НЕ являются механизмом для проверки первой строки пользовательского ввода или бизнес-правил
  • Asserts не следует использовать для обнаружения непредвиденных условий среды (которые находятся вне контроля кода), например, нехватка памяти, сбой сети, сбой базы данных и т. д. Несмотря на то, что они встречаются редко, эти условия следует ожидать (и код вашего приложения не может исправить такие проблемы, как аппаратный сбой или истощение ресурсов). Как правило, генерируются исключения - ваше приложение может либо предпринять корректирующие действия (например, повторить попытку базы данных или выполнить сетевую операцию, попытаться освободить кэшированную память), либо изящно прервать работу, если исключение не может быть обработано.
  • Неудачное утверждение должно быть фатальным для вашей системы - то есть, в отличие от исключения, не пытайтесь ловить или обрабатывать неудачные Asserts - ваш код работает на неожиданной территории. Следы стека и аварийные дампы могут использоваться, чтобы определить, что пошло не так.

Утверждения имеют огромное преимущество:

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

... Более детально

Debug.Assert выражает условие, которое было принято о состоянии оставшейся частью блока кода в элементе управления программы. Это может включать в себя состояние предоставленных параметров, состояние членов экземпляра класса или то, что возврат от вызова метода находится в его контрактном / разработанном диапазоне. Как правило, утверждения должны приводить к аварийному завершению потока / процесса / программы со всей необходимой информацией (отслеживание стека, аварийный дамп и т. Д.), Поскольку они указывают на наличие ошибки или неучтенного условия, для которого не было разработано (то есть не пытаться поймать или обрабатывать ошибки подтверждения), с одним возможным исключением того, что само утверждение может нанести больше ущерба, чем ошибка (например, авиадиспетчеры не хотят YSOD, когда самолет идет на подводной лодке, хотя это спорный вопрос, следует ли развертывать отладочную сборку на производство...)

Когда вы должны использовать Asserts? - В любой точке системы, или API библиотеки, или службы, где входные данные для функции или состояния класса считаются действительными (например, когда проверка уже была произведена при вводе данных пользователем на уровне представления системы, бизнес и Классы уровня данных обычно предполагают, что проверки нуля, проверки диапазона, проверки длины строки и т. д. на входе уже выполнены). - общий Assert проверки включают, где неверное предположение может привести к разыменованию нулевого объекта, делителю нуля, числовому переполнению или арифметическому переполнению даты и общепринятым / не предназначенным для поведения (например, если 32-разрядное целое число использовалось для моделирования возраста человека, оно было бы разумно Assert что возраст на самом деле составляет от 0 до 125 или около того - значения от -100 до 10^10 не были рассчитаны).

Контракты.Net Code
В.Net Stack кодовые контракты могут использоваться в дополнение или в качестве альтернативы использованию Debug.Assert, Контракты на кодирование могут дополнительно формализовать проверку состояния и могут помочь в обнаружении нарушений допущений во время компиляции (или вскоре после этого, если они запускаются в качестве фоновой проверки в IDE).

Доступны проверки по контракту (DBC):

  • Contract.Requires - Договорные условия
  • Contract.Ensures - Контрактные условия
  • Invariant - выражает предположение о состоянии объекта во всех точках его жизненного цикла.
  • Contract.Assumes - Устанавливает статическую проверку при вызове методов, не оформленных по контракту.

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

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

Для меня это говорит о том, что, используя Debug.Asserts, вы откладываете проблему кому-то другому, решаете проблему самостоятельно. Если что-то должно быть, а это не так, бросьте.

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

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

Debug.Assert(true);

Потому что он проверяет то, что вы уже предположили, правда. Например:

public static void ConsumeEnumeration<T>(this IEnumerable<T> source)
{
  if(source != null)
    using(var en = source.GetEnumerator())
      RunThroughEnumerator(en);
}
public static T GetFirstAndConsume<T>(this IEnumerable<T> source)
{
  if(source == null)
    throw new ArgumentNullException("source");
  using(var en = source.GetEnumerator())
  {
    if(!en.MoveNext())
      throw new InvalidOperationException("Empty sequence");
    T ret = en.Current;
    RunThroughEnumerator(en);
    return ret;
  }
}
private static void RunThroughEnumerator<T>(IEnumerator<T> en)
{
  Debug.Assert(en != null);
  while(en.MoveNext());
}

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

В первом случае проблем нет.

Во втором случае есть проблема с вызывающим кодом - он не должен был вызывать GetFirstAndConsume с нулем, поэтому он получает исключение обратно.

В третьем случае есть проблема с этим кодом, потому что он уже должен был проверить, что en != null прежде чем он когда-либо был вызван, так что это не так, это ошибка. Или, другими словами, это должен быть код, который теоретически можно оптимизировать для Debug.Assert(true), так en != null всегда должен быть true!

Согласно стандарту IDesign, вы должны

Утверждай каждое предположение. В среднем каждая пятая строка является утверждением.

using System.Diagnostics;

object GetObject()
{...}

object someObject = GetObject();
Debug.Assert(someObject != null);

В качестве отказа от ответственности я должен отметить, что я не нашел практичным реализовать этот IRL. Но это их стандарт.

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

Учитывая ваш пример проверки на нулевое значение, если это API для внутреннего использования, я мог бы использовать утверждение. Если бы он был в публичном API, я бы определенно использовал явные check и throw.

Цитата, взятая у прагматичного программиста: от подмастерье до мастера

Оставьте утверждения включенными

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

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

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

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

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

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

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

1) Что-то, что я не видел, упоминалось здесь, это дополнительное концептуальное покрытие, которое Asserts может обеспечить во время автоматизированного тестирования В качестве простого примера:

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

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

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

2) Кроме того, некоторые тесты просты в написании, но дорогостоящи и не нужны, учитывая первоначальные предположения. Например:

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

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

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

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

Если ядро String.Find Операция заявляет, что вернет -1 когда критерии поиска не найдены, вы можете безопасно выполнить одну операцию, а не три. Однако, если он действительно вернулся -2у вас может не быть разумных действий. Было бы бесполезным заменить более простой расчет на тот, который тестирует отдельно для -1 ценность и неоправданна в большинстве сред выпуска, чтобы засорять ваш код тестами, гарантирующими, что основные библиотеки работают должным образом. В этом случае утверждения идеальны.

Вы должны всегда использовать второй подход (выбрасывая исключения).

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

Я прочитал ответы здесь, и я подумал, что должен добавить важное различие. Есть два очень разных способа использования утверждений. Одним из них является временное ярлык разработчика для "Это не должно действительно происходить, поэтому, если это действительно так, дайте мне знать, чтобы я мог решить, что делать", вроде условной точки останова, для случаев, когда ваша программа может продолжить работу. Другой способ - использовать в вашем коде допущения о допустимых состояниях программы.

В первом случае утверждения даже не должны быть в конечном коде. Вы должны использовать Debug.Assert во время разработки, и вы можете удалить их, если / когда больше не нужны. Если вы хотите оставить их или забыть удалить их, не проблема, так как они не будут иметь никакого значения в компиляциях Release.

Но во втором случае утверждения являются частью кода. Они, ну, утверждают, что ваши предположения верны, и также документируют их. В этом случае вы действительно хотите оставить их в коде. Если программа находится в недопустимом состоянии, ее нельзя продолжать. Если бы вы не могли позволить себе хит производительности, вы бы не использовали C#. С одной стороны, было бы полезно иметь возможность подключить отладчик, если это произойдет. С другой стороны, вы не хотите, чтобы трассировка стека появлялась у ваших пользователей, и, возможно, более важно, что вы не хотите, чтобы они могли ее игнорировать. Кроме того, если он находится в сервисе, он всегда будет игнорироваться. Следовательно, в производстве правильным поведением было бы создание исключения и использование обычной обработки исключений вашей программы, которая могла бы показать пользователю приятное сообщение и записать подробности.

Trace.Assert имеет идеальный способ достичь этого. Он не будет удален в работе и может быть настроен для разных слушателей с помощью app.config. Поэтому для разработки подходит стандартный обработчик, а для производства вы можете создать простой TraceListener, как показано ниже, который генерирует исключение и активирует его в файле конфигурации производства.

using System.Diagnostics;

public class ExceptionTraceListener : DefaultTraceListener
{
    [DebuggerStepThrough]
    public override void Fail(string message, string detailMessage)
    {
        throw new AssertException(message);
    }
}

public class AssertException : Exception
{
    public AssertException(string message) : base(message) { }
}

И в файле конфигурации производства:

<system.diagnostics>
  <trace>
    <listeners>
      <remove name="Default"/>
      <add name="ExceptionListener" type="Namespace.ExceptionTraceListener,AssemblyName"/>
    </listeners>
  </trace>
 </system.diagnostics>

Вы должны использовать Debug.Assert для проверки на логические ошибки в ваших программах. Компилятор может информировать вас только о синтаксических ошибках. Таким образом, вы должны определенно использовать операторы Assert для проверки на логические ошибки. Как, например, тестирование программы, которая продает автомобили, которые только синие BMW должны получить 15% скидку. Компилятор может ничего вам не сказать, если ваша программа логически верна в выполнении этого, но оператор assert может.

Я не знаю, как это происходит в C# и.NET, но в C assert() будет работать, только если он скомпилирован с -DDEBUG - конечный пользователь никогда не увидит assert(), если он скомпилирован без. Это только для разработчиков. Я использую его очень часто, иногда легче отслеживать ошибки.

Я бы не использовал их в производственном коде. Бросай исключения, лови и логи.

Также нужно быть осторожным в asp.net, так как assert может появиться на консоли и заморозить запрос (ы).

Другие вопросы по тегам