Некоторые (анти-) шаблоны при использовании assert (Java и другие)

Наконец, у меня есть вопрос спросить о переполнении стека!: -)

Основная цель для Java, но я считаю, что это в основном языковой агностик: если у вас нет встроенного assert, вы всегда можете имитировать его.

Я работаю в компании, продающей набор программных продуктов, написанных на Java. Код старый, по крайней мере, начиная с Java 1.3, и в некоторых местах он показывает... Это большая база кода, около 2 миллионов строк, поэтому мы не можем реорганизовать все сразу.
В последнее время мы переключили последние версии с синтаксиса Java 1.4 и JVM на Java 1.6, сделав консервативное использование некоторых новых функций, таких как assert (мы использовали макрос DEBUG.ASSERT - я знаю, что assert был введен в 1.4 но мы не использовали его раньше), generics (только типизированные коллекции), foreach loop, перечисления и т.д.

Я все еще немного зелёный об использовании 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/С++. И мне даже нравится (исходя из того же фона)! Так подавайте нам в суд.)
Во-первых, форма assert() происходит от преобразования вызовов DEBUG.ASSERT(). Мне не нравятся дополнительные круглые скобки, поскольку assert - это языковая конструкция, а не (уже не здесь) вызов функции. Мне тоже не нравится return (foo);:-)
Далее, утверждения не проверяют здесь для инвариантов, они скорее используются в качестве защитников от плохих значений. Но, как я понимаю, они бесполезны здесь: утверждение будет генерировать исключение, даже не документированное со строкой-компаньоном, и только если утверждения включены. Поэтому, если у нас есть опция -ea, у нас просто есть утверждение, которое вызывается вместо обычного NullPointerException. Это не похоже на первостепенное преимущество, так как в любом случае мы исключаем неконтролируемые исключения на самом высоком уровне.
Правильно ли я полагаю, что мы можем избавиться от них и жить с этим (т.е. позволить Java поднимать такое исключенное исключение)? (или, конечно, тест против нулевого значения, если это возможно, что делается в других места).

Боковое примечание: следует ли мне утверждать в приведенном выше фрагменте, я бы сделал это против значения ci, а не против геттера: даже если большинство геттеров оптимизировано/включено, мы не можем быть уверены, поэтому нам следует избегать двойного вызова.

Кто-то сказал в последнем упоминаемом потоке, что публичные методы должны использовать тесты против значений параметров (использование публичного API), а частные методы должны полагаться только на утверждения. Хороший совет.
Теперь оба метода должны проверять другой источник данных: внешний вход. То есть. данные, поступающие от пользователя, из базы данных, например, из какого-либо файла или из сети.
В нашем коде я вижу утверждения против этих значений. Я всегда меняю их на реальный тест, поэтому они действуют даже с отключенными утверждениями: они не являются инвариантами и должны быть надлежащим образом обработаны.
Я вижу только одно возможное исключение, где ввод считается постоянным, например, таблица базы данных, заполненная константами, используемыми в отношениях: программа будет разбита, если эта таблица будет изменена, но соответствующий код не будет обновлен.
Вы видите другие исключения?

Еще одно относительно частое использование, которое я вижу, что кажется ОК: по умолчанию для коммутатора или в конце серии else if тестирование всех возможных значений (эти случаи датируются до нашего использования перечислений!), там часто assert false : "Unexpected value for stuff: " + stuff;
Выглядит законно для меня (эти случаи не должны происходить в производстве), что вы думаете? (помимо "без коммутатора, используйте OO", которые здесь неактуальны).

И, наконец, есть ли какие-нибудь другие полезные примеры использования или раздражающие gotchas, которые я пропустил здесь? (Возможно!)

Ответ 1

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

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

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

Ответ 2

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

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

Ответ 3

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

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

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

Часто использование умной стратегии обработки ошибок может быть обойдено.

Ответ 4

Я использую assert не только для проверки параметров, но и для проверки потоков. Каждый раз, когда я делаю качели, я пишу утверждение почти по каждому методу, чтобы отметить "Я должен выполняться только в рабочем потоке /AWTThread ". (Я думаю, что Sun должна это сделать для нас.) Из-за модели Threading Swing она НЕ МОЖЕТ выйти из строя (и случайно сбой), если вы получаете доступ к swing api из потока, отличного от UI. Трудно найти всю эту проблему без assert.

Другим примером, который я могу вообразить, является проверка включенного ресурса JAR. У вас может быть исключение на английском языке, а не NPE.


EDIT: Другой пример; проверка блокировки объектов. Если я знаю, что я собираюсь использовать вложенный синхронизированный блок, или когда я собираюсь исправить тупик, я использую Thread.holdLock(Object) чтобы я не получил блокировки в обратном порядке.


EDIT (2): Если вы уверены, что какой-то блок кода никогда не будет доступен, вы можете написать

throw new AssertionError("You dead");

а не

assert false:"I am lucky";

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