Разработка по контракту с использованием утверждений или исключений? [закрыто]

123

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

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

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

См. Соответствующий вопрос здесь

Андреас BuyKX
источник
3
Суть проектирования по контракту заключается в том, что вам не нужно (и, возможно, не следует) проверять предварительные условия во время выполнения. Вы проверяете ввод перед тем, как передать его в метод с предварительными условиями, таким образом вы уважаете свой конец контракта. Если ввод недействителен или нарушает ваш конец контракта, программа, как правило, все равно завершится с ошибкой в ​​обычном порядке действий (который вы хотите).
void.pointer 09
Хороший вопрос, но я думаю, вам действительно стоит изменить принятый ответ (как показывают голоса)!
DaveFar
Я знаю, что навсегда, но должен ли этот вопрос действительно иметь тег c ++? Я искал этот ответ, чтобы использовать его на другом языке (Delpih), и я не могу представить себе язык, в котором есть исключения и утверждения, которые не следовали бы тем же правилам. (Все еще изучаю руководство по переполнению стека.)
Эрик Джи,
Очень сжато ответ дается в ответе : «Другими словами, исключения адрес надежности вашего приложения во время утверждения решения его правильности.»
Шмуэль Левин

Ответы:

39

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

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

coppro
источник
4
Утверждения полезны, по крайней мере, в тех случаях, когда проверка правильности будет либо неэффективной, либо неэффективной для правильной реализации.
Casebash
89
Смысл утверждений не в том, чтобы исправить ошибки, а в том, чтобы предупредить программиста. Сохранять их включенными в выпускных сборках бесполезно по этой причине: что бы вы получили, если бы запускали assert? Разработчик не сможет вмешаться и отладить его. Утверждения являются вспомогательным средством отладки, они не заменяют исключения (и исключения не заменяют утверждения). Исключения предупреждают программу об ошибке. Assert предупреждает разработчика.
jalf
12
Но утверждение следует использовать, когда внутренние данные были повреждены после исправления - если утверждение срабатывает, вы не можете делать никаких предположений о состоянии программы, потому что это означает, что что-то / неправильно /. Если утверждение сработало, вы не можете предполагать, что какие-либо данные действительны. Вот почему релизная сборка должна утверждаться - не для того, чтобы сообщить программисту, в чем проблема, а для того, чтобы программа могла завершиться и не рисковать более серьезными проблемами. Программа должна просто делать все возможное, чтобы облегчить восстановление позже, когда данным можно будет доверять.
coppro
5
@jalf, хотя вы не можете добавить ловушку в свой отладчик в сборках релиза, вы можете использовать ведение журнала, чтобы разработчики видели информацию, относящуюся к неудачному утверждению. В этом документе ( martinfowler.com/ieeeSoftware/failFast.pdf ) Джим Шор отмечает: «Помните, что ошибка, возникающая на сайте клиента, прошла через процесс тестирования. У вас, вероятно, возникнут проблемы с ее воспроизведением. Эти ошибки труднее всего найти, и хорошо сформулированное утверждение, объясняющее проблему, может сэкономить вам дни усилий ".
StriplingWarrior
5
Лично я предпочитаю asserts для разработки контрактных подходов. Исключения являются защитными и выполняют проверку аргументов внутри функции. Кроме того, в предварительных условиях dbc не сказано: «Я не буду работать, если вы используете значения вне рабочего диапазона», но «Я не гарантирую, что предоставлю правильный ответ, но все же могу». Утверждения предоставляют разработчику обратную связь о том, что они вызывают функцию с нарушением условия, но не мешают им использовать ее, если они считают, что знают лучше. Нарушение может привести к возникновению исключений, но я считаю это другим.
Matt_JD
194

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

Дима
источник
как насчет сериализации / десериализации в разных модулях / приложениях и, в конечном итоге, рассинхронизации? Я имею в виду, что со стороны читателя это всегда моя ошибка, если я пытаюсь понять вещи неправильно, поэтому я склонен использовать утверждения, но, с другой стороны, у меня есть внешние данные, которые в конечном итоге могут изменить формат без уведомления.
Слава
Если данные внешние, следует использовать исключения. В этом конкретном случае вам, вероятно, также следует перехватить эти исключения и обработать их разумным способом, а не просто позволить вашей программе умереть. Кроме того, мой ответ - практическое правило, а не закон природы. :) Так что нужно рассматривать каждый случай индивидуально.
Дима
Если ваша функция f (int * x) содержит в себе строку x-> len, тогда f (v), где доказано, что v имеет значение null, гарантированно выйдет из строя. Более того, если еще раньше на v было доказано, что оно равно нулю, но доказано, что вызывается f (v), вы получаете логическое противоречие. Это то же самое, что иметь a / b, где b в конечном итоге оказывается равным 0. В идеале такой код не должен компилироваться. Отключение проверок предположений совершенно глупо, если только проблема не связана со стоимостью проверок, потому что это скрывает место, где было нарушено предположение. Он должен быть как минимум зарегистрирован. В любом случае у вас должен быть план перезапуска при сбое.
Роб
22

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

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

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

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

Гед Бирн
источник
6

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

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

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

DJClayworth
источник
5

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

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

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

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

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

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

См. Также: Когда утверждения должны оставаться в производственном коде?

Даниэль Даранас
источник
1
Ваша точка зрения определенно верна. В SO не указан конкретный язык - в случае C # стандартным утверждением является System.Diagnostics.Debug.Assert, который не работает только при отладочной сборке и будет удален во время компиляции в сборке Release.
yoyo
2

Был большой поток, касающийся включения / отключения утверждений в сборках релизов на comp.lang.c ++. Moderated, и если у вас есть несколько недель, вы можете увидеть, насколько разные мнения по этому поводу. :)

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

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

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

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

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

Ричард Корден
источник
2

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

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

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

Йоханнес Шауб - litb
источник
1

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

asserts - это уведомления типа «эй, программист, это не работает», они напоминают вам об ошибках, которые вы бы не заметили, когда они произошли.

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

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

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

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

gbjbaanb
источник
0

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

jdmichal
источник
0

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

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

Майк Элкинс
источник
0

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

Я полюбил функции отчетов об ошибках в 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;
}
Indiv
источник
Я не думаю, что видел в вопросе OP что-нибудь, связанное с C ++. Я считаю, что это не следует включать в ваш ответ.
ForceMagic 07
@ForceMagic: Вопрос был с тегом C ++ в 2008 году, когда я опубликовал этот ответ, и на самом деле тег C ++ был удален всего 5 часов назад. Тем не менее, код иллюстрирует концепцию, не зависящую от языка.
индив
0

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

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

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

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

Casebash
источник
0

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

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

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

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

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

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

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

herohuyongtao
источник
-1

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

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

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

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

assert(value>0);
if(value<=0) throw new ArgumentOutOfRangeException("value");
//do stuff
Rik
источник