Некоторые (анти) паттерны использования assert (Java и др.)
Наконец, у меня есть вопрос к переполнению стека!:-)
Основная цель для Java, но я считаю, что это в основном не зависит от языка: если у вас нет нативного утверждения, вы всегда можете смоделировать его.
Я работаю в компании, продающей набор программного обеспечения, написанного на Java. Код устарел, по крайней мере, начиная с Java 1.3, и в некоторых местах он показывает... Это большая база кода, около 2 миллионов строк, поэтому мы не можем все сразу же реорганизовать.
Недавно мы переключили последние версии с синтаксиса Java 1.4 и JVM на Java 1.6, что позволило консервативно использовать некоторые новые функции, такие как assert
(мы использовали макрос DEBUG.ASSERT - я знаю, assert
был представлен в 1.4, но мы не использовали его ранее), дженерики (только типизированные коллекции), цикл foreach, перечисления и т. д.
Я все еще немного зелен в использовании assert, хотя я прочитал несколько статей на эту тему. Тем не менее, некоторые обычаи, которые я вижу, оставляют меня в замешательстве, причиняя боль моему здравому смыслу... ^_^ Поэтому я подумал, что мне следует задать несколько вопросов, чтобы понять, прав ли я, если я хочу что-то исправить, или это идет вразрез с обычной практикой. Я многословен, поэтому я обдумал вопросы, для тех, кто любит бегать.
Для справки я искал assert java в SO и нашел несколько интересных тем, но, по-видимому, точных дубликатов нет.
- Как избежать "!= Null" операторов в Java? и сколько проверки нуля достаточно? являются весьма актуальными, потому что многие утверждения мы просто проверяем, если переменная равна нулю. В некоторых местах в нашем коде есть использование нулевого объекта (например, возвращение
new String[0]
) но не всегда. Мы должны жить с этим, по крайней мере, для поддержки унаследованного кода. - Некоторые хорошие ответы также в утверждениях Java недостаточно.
- О, и это указывает на причину, что когда я должен использовать Debug.Assert()? Вопрос тоже связан (хорошая функция для уменьшения дубликатов!).
Во-первых, главный вопрос, который вызвал мой вопрос сегодня:
SubDocument aSubDoc = documents.GetAt( i );
assert( aSubDoc != null );
if ( aSubDoc.GetType() == GIS_DOC )
{
continue;
}
assert( aSubDoc.GetDoc() != null );
ContentsInfo ci = (ContentsInfo) aSubDoc.GetDoc();
(Да, мы используем соглашения по стилю / коду MS C/C++. И мне даже это нравится (исходя из того же фона)! Так что подайте в суд на нас.)
Во-первых, assert()
форма происходит от преобразования DEBUG.ASSERT()
звонки. Мне не нравятся дополнительные скобки, так как assert является языковой конструкцией, а не (здесь больше не) вызовом функции. Мне тоже не нравится return (foo);
:-)
Далее, утверждения здесь не проверяются на инварианты, они скорее используются для защиты от плохих значений. Но, насколько я понимаю, они здесь бесполезны: assert выдаст исключение, даже не задокументированное со строкой-компаньоном, и только если утверждения включены. Так что если у нас есть -ea
вариант, мы просто выбрасываем утверждение вместо обычного исключения NullPointerException. Это не похоже на первостепенное преимущество, так как в любом случае мы ловим непроверенные исключения на самом высоком уровне.
Правильно ли я полагаю, что мы можем избавиться от них и жить с этим (т. Е. Позволить Java вызвать такое непроверенное исключение)? (или, конечно, если возможно, проверьте нулевое значение, что делается в других местах).
Примечание: если мне придется утверждать в приведенном выше фрагменте кода, я сделаю это по отношению к значению ci, а не к получателю: даже если большинство получателей оптимизированы / встроены, мы не можем быть уверены, поэтому нам следует избегать вызова дважды.
Кто-то сказал, в последнем потоке, на который ссылаются, что публичные методы должны использовать тесты со значениями параметров (использование публичного API), а приватные методы должны полагаться на утверждения. Хороший совет.
Теперь оба вида методов должны проверять другой источник данных: внешний ввод. То есть. например, данные, поступающие от пользователя, из базы данных, из какого-либо файла или из сети.
В нашем коде я вижу утверждения против этих значений. Я всегда изменяю их на настоящий тест, поэтому они действуют даже при отключенных утверждениях: они не являются инвариантами и должны обрабатываться надлежащим образом.
Я вижу только одно возможное исключение, когда ввод предполагается постоянным, например, таблица базы данных, заполненная константами, используемыми в отношениях: программа сломалась бы, если эта таблица была изменена, но соответствующий код не был обновлен.
Вы видите другие исключения?
Я вижу еще одно относительно частое использование, которое кажется нормальным: по умолчанию для переключателя или в конце серии else if
тестируя все возможные значения (эти случаи датируются еще до нашего использования перечислений!), часто возникает assert false : "Unexpected value for stuff: " + stuff;
Выглядит законно для меня (эти случаи не должны происходить в производстве), как вы думаете? (кроме советов "нет переключателя, используйте OO", которые здесь неактуальны).
И, наконец, есть ли другие полезные варианты использования или надоедливые ошибки, которые я пропустил здесь? (наверное!)
4 ответа
Правило номер один - избегать побочных эффектов в утверждениях. Другими словами, код должен вести себя идентично с отключенными утверждениями, как и при включенных утверждениях, а не при сбое (очевидно, что ошибочные утверждения изменят поведение, поскольку вызовут ошибку).
Правило номер два - не использовать утверждения для обязательных проверок. Их можно отключить (или, вернее, не включить). Для проверки параметров не закрытых методов используйте IllegalArgumentException.
Утверждения являются исполняемыми предположениями. Я использую утверждения, чтобы высказать свои убеждения о текущем состоянии программы. Например, такие вещи, как "я предполагаю, что n здесь положительно", или "я предполагаю, что список содержит здесь только один элемент".
Я использую assert
, не только для проверки параметров, но также используется для проверки потоков. Каждый раз, когда я выполняю качание, я пишу assert почти в каждом методе, чтобы отметить "Я должен выполняться только в рабочем потоке /AWTThread". (Я думаю, что Sun должен сделать это для нас.) Из-за модели потоков Swing, она НЕ МОЖЕТ завершиться с ошибкой (и может произойти случайный сбой), если вы обращаетесь к Swi-API из потока не-UI. Довольно сложно разобраться во всех этих проблемах без утверждения.
Другой пример, который я могу представить - это проверить вложенный JAR-ресурс. Вы можете иметь английское исключение, а не NPE.
РЕДАКТИРОВАТЬ: еще один пример; проверка блокировки объекта. Если я знаю, что собираюсь использовать вложенный синхронизированный блок, или когда я собираюсь исправить тупик, я использую Thread.holdLock(Object), чтобы гарантировать, что я не получу блокировки в обратном порядке.
РЕДАКТИРОВАТЬ (2): Если вы уверены, что какой-то блок кода никогда не будет достигнут, вы можете написать
throw new AssertionError("You dead");
скорее тогда
assert false:"I am lucky";
Например, если вы переопределяете "equals(Object)" на изменяемый объект, переопределите hashCode() с помощью AssertionError, если вы считаете, что он никогда не будет ключом. Эта практика предлагается в некоторых книгах. Я не повредит производительности (так как она никогда не должна достигать).
Вы затронули многие причины, по которым я считаю, что в целом следует избегать утверждений. если вы не работаете с кодовой базой, где использование утверждений имеет очень строгие правила, вы очень быстро попадете в ситуацию, когда вы никогда не сможете отключить утверждения, и в этом случае вы могли бы просто использовать обычные логические тесты.
Итак, моя рекомендация - пропустить утверждения. не вставляйте дополнительные проверки нулевого указателя, где язык сделает это за вас. однако, если указатель не может быть разыменован некоторое время, хорошей идеей будет предварительная проверка нуля. Кроме того, всегда используйте реальные исключения для случаев, которые должны "никогда" не случиться (последнее, если ветвь или случай переключения по умолчанию), не используйте "assert false". если вы используете утверждение, есть вероятность, что кто-то может его отключить, и если ситуация действительно произойдет, все будет по-настоящему запутано.
Я рекомендую проверять параметры в общедоступных (API) методах и генерировать исключение IllegalArgumentException, если параметры недопустимы. Здесь нет утверждений, поскольку пользователь API должен получить правильную ошибку (сообщение).
Утверждения должны использоваться в закрытых методах для проверки постусловий и, возможно, предусловий. Например:
List returnListOfSize(int size) {
// complex list creation
assert list.size == size;
}
Часто, используя умную стратегию обработки ошибок, можно обойтись.