Некоторые (анти) паттерны использования 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 и нашел несколько интересных тем, но, по-видимому, точных дубликатов нет.

Во-первых, главный вопрос, который вызвал мой вопрос сегодня:

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;
законный вид Looks для я (эти случаи не должны происходить в производстве), что вы думаете? (кроме советов «нет переключателя, используйте OO», которые здесь неактуальны).

И, наконец, есть ли другие полезные варианты использования или надоедливые ошибки, которые я пропустил здесь? (вероятно!)

15.12.2008 15:42:30
Почему последний вопрос? Куда-то собираешься?
David Koelle 15.12.2008 15:54:17
Хорошо, я колебался между наконец и по крайней мере и ошибочно исправил первый вариант ... :-) Спасибо, я исправляю это.
PhiLho 15.12.2008 16:30:28
«по крайней мере» имеет еще меньше смысла
Dan Dyer 16.12.2008 00:09:24
Doh! ^ _ ^ Я не говорю по-французски красиво. :-D
PhiLho 16.12.2008 11:57:33
4 ОТВЕТА

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

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

3
15.12.2008 15:58:47

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

Утверждения следует использовать в закрытых методах для проверки постусловий и, возможно, предусловий. Например:

List returnListOfSize(int size) {
    // complex list creation
    assert list.size == size;
}

Часто с помощью умной стратегии обработки ошибок можно обойтись.

2
15.12.2008 15:59:02

Я использую assertне только для проверки параметров, но также используется для проверки потоков. Каждый раз, когда я делаю свинг, я пишу assert почти в каждом методе, чтобы пометить «Я должен выполняться только в рабочем потоке / AWTThread». (Я думаю, что Sun должен сделать это для нас.) Из-за модели потоков Swing, она НЕ МОЖЕТ завершиться с ошибкой (и может произойти случайный сбой), если вы получите доступ к Swi API из потока, не являющегося пользовательским интерфейсом. Довольно сложно разобраться во всех этих проблемах без утверждения.

Другой пример, который я могу представить - это проверить вложенный JAR-ресурс. Вы можете иметь английское исключение, а не NPE.


РЕДАКТИРОВАТЬ: еще один пример; проверка блокировки объекта. Если я знаю, что собираюсь использовать вложенный синхронизированный блок, или когда я собираюсь исправить тупик, я использую Thread.holdLock (Object), чтобы гарантировать, что я не получу блокировки в обратном порядке.


РЕДАКТИРОВАТЬ (2): Если вы уверены, что какой-то блок кода никогда не будет достигнут, вы можете написать

throw new AssertionError("You dead");

скорее тогда

assert false:"I am lucky";

Например, если вы переопределяете «equals (Object)» на изменяемый объект, переопределите hashCode () с помощью AssertionError, если вы считаете, что он никогда не будет ключом. Эта практика предлагается в некоторых книгах. Я не повредит производительности (так как она никогда не должна достигать).

2
15.12.2008 16:37:33
в вашем последнем примере, не лучше ли выбросить UnsupportedOperationException из hashCode ()?
James 24.05.2013 12:26:49

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

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

Утверждения являются исполняемыми предположениями. Я использую утверждения, чтобы высказать свои убеждения о текущем состоянии программы. Например, такие вещи, как «я предполагаю, что n здесь положительно» , или «я предполагаю, что список содержит здесь только один элемент» .

9
16.12.2008 00:20:40