Дизайн по контракту с использованием утверждений или исключений?
При программировании по контракту функция или метод сначала проверяет, выполнены ли ее предварительные условия, прежде чем приступать к выполнению своих обязанностей, не так ли? Два наиболее важных способа выполнения этих проверок - assert
и exception
.
- assert не работает только в режиме отладки. Чтобы убедиться, что важно (единица) проверить все отдельные условия контракта, чтобы убедиться, что они действительно не работают.
- исключение не работает в режиме отладки и выпуска. Это имеет то преимущество, что проверенное поведение отладки идентично поведению релиза, но оно несет в себе ограничение производительности во время выполнения.
Какой, по вашему мнению, предпочтительнее?
См. выпущенный вопрос здесь
Ответы
Ответ 1
Отключение assert в сборках релизов похоже на высказывание "У меня никогда не будет никаких проблем в сборке релиза", что часто бывает не так. Поэтому утверждение не должно быть отключено в сборке релиза. Но вы не хотите, чтобы сбой сборки выпуска происходил каждый раз при возникновении ошибок, не так ли?
Поэтому используйте исключения и используйте их хорошо. Используйте хорошую, сплошную иерархию исключений и убедитесь, что вы поймаете, и вы можете поместить крючок на исключение, бросая в ваш отладчик, чтобы поймать его, а в режиме освобождения вы можете компенсировать ошибку, а не сразу. Это более безопасный способ пойти.
Ответ 2
Правило большого пальца состоит в том, что вы должны использовать утверждения, когда пытаетесь поймать свои собственные ошибки и исключения, когда пытаетесь поймать ошибки других людей. Другими словами, вы должны использовать исключения, чтобы проверить предварительные условия для общедоступных функций API и всякий раз, когда вы получаете какие-либо данные, которые являются внешними для вашей системы. Вы должны использовать утверждения для функций или данных, которые являются внутренними для вашей системы.
Ответ 3
Принцип, которым я следую, следующий: если ситуацию можно избежать с помощью кодирования, тогда используйте утверждение. В противном случае используйте исключение.
Утверждения предназначены для обеспечения соблюдения Контракта. Контракт должен быть справедливым, чтобы клиент был в состоянии обеспечить его соблюдение. Например, вы можете указать в контракте, что URL-адрес должен быть действительным, поскольку правила о том, что является и не является допустимым URL-адресом, известны и согласованы.
Исключения для ситуаций, которые находятся вне контроля как клиента, так и сервера. Исключение означает, что что-то пошло не так, и нет ничего, что можно было бы сделать, чтобы избежать этого. Например, сетевое подключение находится вне контроля приложений, поэтому ничего не может быть сделано для предотвращения сетевой ошибки.
Я хотел бы добавить, что различие Assertion/Exception не является лучшим способом подумать об этом. То, о чем вы действительно хотите думать, это контракт и то, как он может быть применен. В приведенном выше примере URL-адреса лучше всего иметь класс, который инкапсулирует URL-адрес и является либо нулевым, либо действительным URL-адресом. Это преобразование строки в URL-адрес, который обеспечивает выполнение контракта, и исключение генерируется, если оно недействительно. Метод с параметром URL гораздо яснее, чем метод с параметром String и утверждение, которое указывает URL-адрес.
Ответ 4
Утверждения предназначены для того, чтобы поймать что-то, что разработчик сделал не так (не только вы - еще один разработчик в вашей команде). Если разумно, что ошибка пользователя может создать это условие, то это должно быть исключение.
Также подумайте о последствиях. Утверждение обычно отключает приложение. Если есть реальное ожидание того, что условие может быть восстановлено, вероятно, вы должны использовать исключение.
С другой стороны, если проблема может быть только из-за ошибки программиста, то используйте assert, потому что вы хотите узнать об этом как можно скорее. Исключение можно поймать и обработать, и вы никогда не узнаете об этом. И да, вы должны отключить утверждения в коде выпуска, потому что вы хотите, чтобы приложение восстановилось, если есть хоть малейшая вероятность. Даже если состояние вашей программы глубоко нарушено, пользователь может просто сохранить свою работу.
Ответ 5
Неверно, что "assert не работает только в режиме отладки".
В объектно-ориентированном программном обеспечении, второе издание Бертран Майер, автор оставляет дверь открытой для проверки предусловий в режиме выпуска. В этом случае то, что происходит, когда утверждение терпит неудачу, заключается в том, что... возникает исключение нарушения утверждения! В этом случае восстановление ситуации невозможно: можно было бы сделать что-то полезное, и оно должно автоматически генерировать отчет об ошибке и в некоторых случаях перезапускать приложение.
Мотивация заключается в том, что предварительные условия обычно более дешевы для тестирования, чем инварианты и постусловия, и что в некоторых случаях правильность и "безопасность" в сборке релизов важнее скорости. т.е. для многих приложений скорость не является проблемой, но надежность (способность программы вести себя безопасно, когда ее поведение неверно, т.е. когда контракт нарушен).
Должны ли вы всегда оставлять проверки предварительного условия включенными? Это зависит. Это вам. Нет универсального ответа. Если вы создаете программное обеспечение для банка, было бы лучше прервать выполнение с тревожным сообщением, чем передать 1 000 000 долларов вместо 1000 долларов США. Но что, если вы программируете игру? Может быть, вам нужна вся скорость, которую вы можете получить, и если кто-то получит 1000 очков вместо 10 из-за ошибки, которую предпосылки не поймали (потому что они не включены), тяжелая удача.
В обоих случаях вы должны в идеале уловить эту ошибку во время тестирования, и вы должны выполнить значительную часть своего тестирования с включенными утверждениями. То, что обсуждается здесь, - это то, что является лучшей политикой для тех редких случаев, когда предпосылки не работают в производственном коде в сценарии, который ранее не был обнаружен из-за неполного тестирования.
Подводя итог, , вы можете иметь утверждения и получать автоматически исключения, если оставить их включенными - по крайней мере, в Eiffel. Я думаю, что сделать то же самое в С++, вам нужно ввести его самостоятельно.
См. также: Когда должны сохраняться утверждения в производственном коде?
Ответ 6
Существовал огромный thread в отношении включения/отключения утверждений в версиях сборки на comp.lang.С++. moderated, что если у вас есть несколько недель, вы можете увидеть, насколько разнообразны мнения по этому поводу.:)
В отличие от coppro, я считаю, что если вы не уверены, что утверждение может быть отключено в сборке релиза, то это не должно было быть утверждением. Утверждения предназначены для защиты от прерывания программных инвариантов. В таком случае, что касается клиента вашего кода, будет один из двух возможных результатов:
- Откажитесь от какого-либо сбоя типа ОС, в результате чего вызов прекратите. (Без утверждений)
- Устранить с помощью прямого вызова для прерывания. (С утверждением)
Для пользователя нет никакой разницы, однако, возможно, что утверждения добавляют ненужную стоимость производительности в коде, который присутствует в подавляющем большинстве запусков, где код не терпит неудачу.
Ответ на вопрос на самом деле зависит гораздо больше от того, кем будут клиенты API. Если вы пишете библиотеку, предоставляющую API, то вам нужен какой-то механизм для уведомления ваших клиентов о том, что они неправильно использовали API. Если вы не предоставите две версии библиотеки (одна с утверждениями, одна без), то утверждать, что это очень маловероятно.
Лично, однако, я не уверен, что пойдет и на исключения из этого дела. Исключения лучше подходят для того, где может быть подходящая форма восстановления. Например, возможно, вы пытаетесь выделить память. Когда вы поймаете исключение 'std:: bad_alloc', можно освободить память и повторить попытку.
Ответ 7
Я изложил свой взгляд на состояние вопроса здесь: Как вы проверяете внутреннее состояние объекта?. Как правило, утверждайте свои претензии и бросайте за нарушение другими. Для отключения утверждений в сборках выпуска вы можете:
- Отключить утверждения для дорогих проверок (например, проверить, упорядочен ли диапазон)
- Включить тривиальные проверки (например, проверить нулевой указатель или логическое значение)
Конечно, в сборках релизов неудачные утверждения и неперехваченные исключения должны обрабатываться иначе, чем в отладочных сборках (где он мог бы просто вызвать std:: abort). Напишите журнал ошибки где-нибудь (возможно, в файл), сообщите клиенту, что произошла внутренняя ошибка. Клиент сможет отправить вам файл журнала.
Ответ 8
вы спрашиваете о различии между ошибками времени разработки и времени выполнения.
утверждает, что это "эй программист, это сломанные" уведомления, они там, чтобы напомнить вам об ошибках, которые вы бы не заметили, когда они произошли.
исключения - это уведомления "эй, пользователь, что-то не так" (очевидно, вы можете закодировать, чтобы их поймать, чтобы пользователь никогда не рассказывал), но они созданы во время выполнения, когда пользователь Joe использует приложение.
Итак, если вы думаете, что можете получить все свои ошибки, используйте только исключения. Если вы думаете, что не можете использовать исключения. Вы все равно можете использовать отладочные подтверждения, чтобы исключить количество исключений.
Не забывайте, что многие из предварительных условий будут предоставлены пользователями, поэтому вам понадобится хороший способ сообщить пользователю, что его данные не были хорошими. Для этого вам часто нужно возвращать данные об ошибках в стек вызовов до битов, с которыми он взаимодействует. Если вы не используете n-уровнее, то вам не пригодится то, что вам нужно.
Наконец, я бы не использовал ни один из них - коды ошибок намного превосходят ошибки, которые, по вашему мнению, будут возникать регулярно.:)
Ответ 9
Я предпочитаю второй. Хотя ваши тесты, возможно, выполнялись нормально, Murphy говорит, что что-то неожиданное пойдет не так. Таким образом, вместо того, чтобы получать исключение при фактическом вызове ошибочного метода, вы в конечном итоге трассируете NullPointerException (или эквивалент) 10 кадров стека глубже.
Ответ 10
См. также этот вопрос:
В некоторых случаях утверждения запрещены при создании для выпуска. Вы можете не иметь контроля над этим (иначе вы могли бы строить с утверждениями on), поэтому было бы неплохо сделать это так.
Проблема с "корректировкой" входных значений заключается в том, что вызывающий не получить то, что они ожидают, и это может привести к проблемам или даже сбои в полностью разных частях программы, что позволяет отлаживать кошмар.
Обычно я делаю исключение в if-statement, чтобы взять на себя роль утверждения в случае, если они отключены.
assert(value>0);
if(value<=0) throw new ArgumentOutOfRangeException("value");
//do stuff
Ответ 11
Предыдущие ответы верны: используйте исключения для общедоступных функций API. Единственный раз, когда вы, возможно, захотите согнуть это правило, - это когда калькулятор стоит дорого. В этом случае вы можете поместить его в assert.
Если вы считаете, что нарушение этого предположения, скорее всего, сохранит его как исключение или реорганизует предварительное условие.
Ответ 12
Вы должны использовать оба. Подтверждения для вашего удобства как разработчика. Исключения улавливают то, что вы пропустили или не ожидали во время выполнения.
Я полюбил функции отчетов об ошибках 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;
}
Ответ 13
Я попытался синтезировать несколько других ответов здесь с моими собственными представлениями.
Используйте утверждения для случаев, когда вы хотите отключить его в процессе производства, заблуждаясь, чтобы оставить их. Единственная реальная причина отключения в производстве, но не в разработке, - это ускорить работу программы. В большинстве случаев эта скорость не будет значительной, но иногда код является критическим по времени или тест является дорогостоящим. Если код критически важный, тогда исключения могут быть лучше всего, несмотря на замедление.
Если существует реальная вероятность восстановления, используйте исключение, поскольку утверждения не предназначены для восстановления. Например, код редко предназначен для восстановления от ошибок программирования, но он предназначен для восстановления таких факторов, как сбои сети или заблокированные файлы. Ошибки не должны рассматриваться как исключения просто для того, чтобы быть вне контроля программиста. Скорее, предсказуемость этих ошибок по сравнению с ошибками кодирования делает их более любезными для восстановления.
Re аргумент, что легче отлаживать утверждения: трассировка стека из правильно названного исключения так же легко читается, как утверждение. Хороший код должен улавливать только определенные типы исключений, поэтому исключения не должны оставаться незамеченными из-за того, что их поймали. Однако, я думаю, что Java иногда заставляет вас поймать все исключения.
Ответ 14
Эмпирическое правило для меня заключается в том, что использовать выражения assert, чтобы найти внутренние ошибки и исключения для внешних ошибок. Вы можете извлечь большую пользу из следующего обсуждения Грега из здесь.
Ассемблерные выражения используются для поиска ошибок программирования: либо ошибок в самой программе, либо ошибок в ее соответствующей реализации. Условие assert подтверждает, что программа остается в определенном состоянии. "Определенное состояние" - это в основном тот, который согласуется с программными предположениями. Обратите внимание, что "определенное состояние" для программы не обязательно должно быть "идеальным состоянием" или даже "обычным состоянием" или даже "полезным состоянием", но более важным для этого важного момента позже.
Чтобы понять, как утверждения вписываются в программу, рассмотрите процедуру в программа на С++, предназначенная для разыменования указателя. Теперь следует проверять, является ли указатель NULL перед разыменованием или должен ли он утверждать, что указатель не является NULL, а затем идти вперед и разыгрывать его независимо?
Я предполагаю, что большинство разработчиков захотят сделать то и другое, добавить assert, но также проверить указатель на значение NULL, чтобы не сбой если заявленное условие терпит неудачу. На поверхности, выполняя как тест и проверка может показаться самым мудрым решением
В отличие от утвержденных условий, обработка программных ошибок (исключений) относится не к к ошибкам в программе, но к входам, которые программа получает из своих Окружающая среда. Это часто "ошибки" для кого-то, например пользователя попытка входа в учетную запись без ввода пароля. А также даже если ошибка может помешать успешному завершению работы программы задачи, нет сбоя программы. Программа не может войти в систему пользователя без пароля из-за внешней ошибки - ошибка на пользователе часть. Если обстоятельства были разными, и пользователь напечатал правильный пароль, и программа не смогла его распознать; то хотя результат все равно будет таким же, неудача теперь будет принадлежать программы.
Цель обработки ошибок (исключений) - в два раза. Первое - это общение пользователю (или другому клиенту), что ошибка ввода программы и что это значит. Вторая цель - восстановить приложение после обнаружения ошибки, в четко определенное состояние. Заметка что сама программа не ошибается в этой ситуации. Конечно, программа может находиться в неидеальном состоянии или даже в состоянии, в котором может ничего полезного, но нет ошибки программирования. Напротив, поскольку состояние восстановления ошибки является ожидаемым с помощью программы дизайн, это тот, который может обрабатывать программа.
PS: вы можете проверить аналогичный вопрос: Except Vs Assertion.