Проектирование по контракту с использованием утверждений или исключений?


при программировании по контракту функция или метод сначала проверяет, выполнены ли ее предварительные условия, прежде чем начать работать над своими обязанностями, не так ли? Два наиболее известных способа сделать эти проверки на assert и exception.

  1. утверждать не только в режиме отладки. Чтобы убедиться, что крайне важно (unit) проверить все отдельные предварительные условия контракта, чтобы увидеть, действительно ли они терпят неудачу.
  2. исключение не удается в режиме отладки и выпуска. Это преимущество в том, что тестируемое поведение отладки идентично поведению выпуска, но оно влечет за собой снижение производительности среды выполнения.

какой из них вы считаете предпочтительным?

смотрите освобожденный вопрос здесь

14 116

14 ответов:

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

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

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

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

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

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

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

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

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

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

не совсем верно, что " assert терпит неудачу только в режиме отладки."

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

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

следует ли всегда оставлять проверки предварительных условий включенными? Это зависит от. Это зависит от тебя. Универсального ответа не существует. Если вы делаете программное обеспечение для банка, может быть, лучше прервать выполнение с тревожным сообщением, чем перевести $1,000,000 вместо $1,000. Но что, если вы программируете игру? Возможно, вам нужна вся скорость, которую вы можете получить, и если кто-то получает 1000 очков вместо 10 из-за ошибки, которую предварительные условия не поймали (потому что они не включены), неудача.

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

подведем итог вы можете иметь утверждения и по-прежнему получать исключения автоматически, если вы оставьте их включенными - по крайней мере, в Eiffel. Я думаю сделать то же самое в C++ вам нужно ввести его себе.

Читайте также: когда утверждения должны оставаться в производственном коде?

там был огромный thread относительно включения / отключения утверждений в релизе строится на comp.ленг.с.++модератор, который, если у вас есть несколько недель, вы можете увидеть, насколько разные мнения по этому поводу. :)

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

  1. умереть с каким-то сбоем типа ОС, в результате чего вызов прервать. (Без утверждения)
  2. умереть с помощью прямого вызова для прерывания. (С утверждением)

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

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

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

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

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

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

вы спрашиваете о разнице между ошибками времени разработки и времени выполнения.

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

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

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

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

наконец, я бы не использовал ни один - коды ошибок намного превосходят ошибки, которые, как вы думаете, будут происходить регулярно. :)

Я предпочитаю второй. Хотя ваши тесты могут работать нормально,Мерфи говорит, что что-то неожиданно пойдет не так. Таким образом, вместо получения исключения при фактическом ошибочном вызове метода вы в конечном итоге отслеживаете исключение NullPointerException (или эквивалентное) на 10 кадров стека глубже.

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

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

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

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

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

#ifdef DEBUG
#define RETURN_IF_FAIL(expr)      do {                      \
 if (!(expr))                                           \
 {                                                      \
     fprintf(stderr,                                        \
        "file %s: line %d (%s): precondition `%s' failed.", \
        __FILE__,                                           \
        __LINE__,                                           \
        __PRETTY_FUNCTION__,                                \
        #expr);                                             \
     ::print_stack_trace(2);                                \
     return;                                                \
 };               } while(0)
#define RETURN_VAL_IF_FAIL(expr, val)  do {                         \
 if (!(expr))                                                   \
 {                                                              \
    fprintf(stderr,                                             \
        "file %s: line %d (%s): precondition `%s' failed.",     \
        __FILE__,                                               \
        __LINE__,                                               \
        __PRETTY_FUNCTION__,                                    \
        #expr);                                                 \
     ::print_stack_trace(2);                                    \
     return val;                                                \
 };               } while(0)
#else
#define RETURN_IF_FAIL(expr)
#define RETURN_VAL_IF_FAIL(expr, val)
#endif

Если мне нужна проверка выполнения аргументы, я бы сделал это:

char *doSomething(char *ptr)
{
    RETURN_VAL_IF_FAIL(ptr != NULL, NULL);  // same as assert(ptr != NULL), but returns NULL if it fails.
                                            // Goes away when debug off.

    if( ptr != NULL )
    {
       ...
    }

    return ptr;
}

Я попытался синтезировать несколько других ответов здесь с моими собственными взглядами.

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

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

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

см. также этот вопрос:

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

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

Я обычно бросаю исключение в if-оператор, чтобы взять на себя роль утверждения в случае, если они отключены

assert(value>0);
if(value<=0) throw new ArgumentOutOfRangeException("value");
//do stuff

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

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

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

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

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

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

PS: вы можете проверить аналогичный вопрос:Исключение Против Утверждения.