Сколько нулевой проверки достаточно?

Каковы некоторые рекомендации, когда не необходимо проверить нуль?

Множество унаследованных кодов, над которыми я работал, с опозданием имеет ноль-проверку ad nauseam. Null проверяет тривиальные функции, проверяет null на вызовах API, которые указывают непустые возвраты и т.д. В некоторых случаях нулевые проверки являются разумными, но во многих местах нуль не является разумным ожиданием.

Я слышал несколько аргументов, начиная от "Нельзя доверять другому коду", чтобы "ВСЕГДА программа защищалась" до "Пока язык не гарантирует мне ненулевое значение, я всегда буду проверять". Я определенно согласен со многими из этих принципов до определенного момента, но я обнаружил, что чрезмерная проверка нулевого значения вызывает другие проблемы, которые обычно нарушают эти принципы. Является ли цепкая нулевая проверка действительно стоящей?

Часто я наблюдал коды с избыточной нулевой проверкой, чтобы быть на самом деле более низкого качества, а не более высокого качества. Большая часть кода, похоже, настолько сфокусирована на нулевых проверках, что разработчик потерял из виду другие важные качества, такие как читаемость, правильность или обработка исключений. В частности, я вижу, что много кода игнорирует исключение std:: bad_alloc, но выполняет нулевую проверку на new.

В С++ я это понимаю в некоторой степени из-за непредсказуемого поведения разыменования нулевого указателя; Нулевое разынение обрабатывается более изящно в Java, С#, Python и т.д. Я только что видел плохие примеры бдительной проверки нулевого кода или есть что-то в этом роде?

Этот вопрос предназначен для языкового агностика, хотя меня в основном интересуют С++, Java и С#.


Некоторые примеры нулевой проверки, которые я видел, которые кажутся чрезмерными, включают следующее:


Этот пример, по-видимому, относится к нестандартным компиляторам, поскольку спецификация С++ говорит, что неудавшийся новый генерирует исключение. Если вы явно не поддерживаете несогласованные компиляторы, имеет ли это смысл? Имеет ли это смысл в управляемом языке, таком как Java или С# (или даже С++/CLR)?

try {
   MyObject* obj = new MyObject(); 
   if(obj!=NULL) {
      //do something
   } else {
      //??? most code I see has log-it and move on
      //or it repeats what in the exception handler
   }
} catch(std::bad_alloc) {
   //Do something? normally--this code is wrong as it allocates
   //more memory and will likely fail, such as writing to a log file.
}

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

Если метод, который вы можете видеть и может обновлять (или кричать на разработчика, который несет ответственность), имеет контракт, нужно ли еще проверять нули?

//X is non-negative.
//Returns an object or throws exception.
MyObject* create(int x) {
   if(x<0) throw;
   return new MyObject();
}

try {
   MyObject* x = create(unknownVar);
   if(x!=null) {
      //is this null check really necessary?
   }
} catch {
   //do something
}

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

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

//Internal use only--non-public, not part of public API
//input must be non-null.
//returns non-negative value, or -1 if failed
int ParseType(String input) {
   if(input==null) return -1;
   //do something magic
   return value;
}

По сравнению с:

//Internal use only--non-public, not part of public API
//input must be non-null.
//returns non-negative value
int ParseType(String input) {
   assert(input!=null : "Input must be non-null.");
   //do something magic
   return value;
}

Ответ 1

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

Я всегда немного сомневаюсь в реализации особых случаев более полезной концепции. Проверка контрактов полезна, потому что он ловит ошибки программирования при первом пересечении границы API. Что такого особенного в отношении нулей, что означает, что они являются единственной частью контракта, который вы хотите проверить? Тем не менее,

В отношении проверки ввода:

null является особенным в Java: множество Java-API написано так, что null является единственным недопустимым значением, которое даже возможно передать в данный вызов метода. В таких случаях нулевая проверка "полностью проверяет" ввод, поэтому применяется полный аргумент в пользу проверки контракта.

В С++, с другой стороны, NULL является только одним из почти 2 ^ 32 (2 ^ 64 на новых архитектурах) недопустимыми значениями, которые может иметь параметр указателя, поскольку почти все адреса не относятся к объектам правильного типа. Вы не можете "полностью подтвердить" свой ввод, если у вас нет списка из всех объектов этого типа.

Затем возникает вопрос, является ли NULL достаточно распространенным недопустимым вводом для получения специального лечения, которое (foo *)(-1) не получает?

В отличие от Java, поля не получают автоматическую инициализацию до NULL, поэтому неинициализированное значение мусора столь же правдоподобно, как и NULL. Но иногда объекты С++ имеют элементы указателя, которые явно имеют NULL-значение, что означает "у меня еще нет". Если ваш вызывающий абонент делает это, тогда существует значительный класс ошибок программирования, которые могут быть диагностированы с помощью проверки NULL. Исключение может быть проще для их отладки, чем ошибка страницы в библиотеке, для которой у них нет источника. Поэтому, если вы не против раздувания кода, это может быть полезно. Но это ваш собеседник, о котором вы должны думать, а не о себе - это не защитное кодирование, потому что оно только "защищает" от NULL, а не от (foo *) (- 1).

Если NULL не является допустимым входом, вы можете рассмотреть возможность использования параметра по ссылке, а не указателя, но многие стили кодирования не одобряют неконстантные ссылочные параметры. И если вызывающий абонент передает вам * fooptr, где fooptr - NULL, тогда он никому не пригодился. То, что вы пытаетесь сделать, - это сжать немного дополнительной документации в подпись функции, в надежде, что ваш вызывающий абонент с большей вероятностью подумает: "Хм, может быть, здесь нет нуля?" когда они должны явно разыменовать его, чем если бы они просто передали его вам как указатель. Это доходит до сих пор, но, насколько это возможно, это может помочь.

Я не знаю С#, но я понимаю, что он, как Java, в этих ссылках гарантированно имеет допустимые значения (по крайней мере, в безопасном коде), но в отличие от Java, в которых не все типы имеют значение NULL. Поэтому я предполагаю, что нулевые проверки там редко стоят того: если вы находитесь в безопасном коде, не используйте тип с нулевым значением, если значение null не является допустимым, и если вы находитесь в небезопасном коде, то те же рассуждения применяются как в С++.

В отношении проверки вывода:

Аналогичная проблема возникает: в Java вы можете "полностью проверить" результат, зная его тип и что значение не равно нулю. В С++ вы не можете "полностью проверить" результат с помощью проверки NULL - все, что вы знаете, функция вернула указатель на объект в своем собственном стеке, который только что был размотан. Но если NULL является общим недопустимым возвратом из-за конструкций, обычно используемых автором кода вызываемого абонента, то проверка его поможет.

Во всех случаях:

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

В случае написания кода, который является переносимым для нестандартных реализаций С++, вместо кода в вопросе, который проверяет значение null и также улавливает исключение, у меня, вероятно, была бы такая функция:

template<typename T>
static inline void nullcheck(T *ptr) { 
    #if PLATFORM_TRAITS_NEW_RETURNS_NULL
        if (ptr == NULL) throw std::bad_alloc();
    #endif
}

Затем, как один из списка вещей, которые вы делаете при переносе в новую систему, вы правильно определяете PLATFORM_TRAITS_NEW_RETURNS_NULL (и, возможно, некоторые другие PLATFORM_TRAITS). Очевидно, вы можете написать заголовок, который делает это для всех компиляторов, о которых вы знаете. Если кто-то берет ваш код и компилирует его на нестандартной реализации на С++, о которой вы ничего не знаете, они принципиально самостоятельны по большим причинам, чем это, поэтому они должны будут сделать это сами.

Ответ 2

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

  • Если я пишу публичный API, который будет доступен другим, тогда я сделаю нулевые проверки всех ссылочных параметров.

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

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

Ответ 3

Если вы пишете код и его контракт, вы несете ответственность за его использование в соответствии с его контрактом и гарантируете, что договор верен. Если вы скажете "возвращает ненулевой" x, то вызывающий не должен проверять значение null. Если вместо этого указателя/указателя возникает исключение с нулевым указателем, это неверно ваш контракт.

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

Ответ 4

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

Итак, пока это хорошо в рамках проекта, где он поддерживается модульными тестами и т.д.:

internal void DoThis(Something thing)
{
    Debug.Assert(thing != null, "Arg [thing] cannot be null.");
    //...
}

в методе, в котором у вас нет контроля над тем, кто его называет, что-то вроде этого может быть лучше:

public void DoThis(Something thing)
{
    if (thing == null)
    {
        throw new ArgumentException("Arg [thing] cannot be null.");
    }
    //...
}

Ответ 5

Это зависит от ситуации. Остальная часть моего ответа предполагает С++.

  • Я никогда не проверяю возвращаемое значение нового поскольку все реализации, которые я использую throw bad_alloc при сбое. Если я см. устаревший тест для нового возвращения null в любом коде, над которым я работаю, я вырезать его и не беспокоить замените его чем-нибудь.
  • Если малозначительные стандарты кодирования запрещаю это, я утверждаю документально предпосылки. Сломанный код, который нарушает опубликованные потребности в контрактах немедленно выйти из строя и драматически.
  • Если нуль возникает из среды выполнения неудача, которая не была нарушена код, я бросаю. fopen failure и malloc (хотя я редко, если когда-либо использовать их в С++) упадет в эту категорию.
  • Я не пытаюсь оправиться от отказ в распределении. Bad_alloc получает пойман в main().
  • Если нулевой тест для объекта, который является соавтором моего класса, я переписываю код, чтобы взять его по ссылке.
  • Если соавтор действительно не может существуют, я использую Null Object шаблон проектирования для создания заполнитель для неудачных пути.

Ответ 6

Проверка NULL вообще зла, поскольку она добавляет небольшой отрицательный токен к тестируемости кода. С помощью проверок NULL везде вы не можете использовать метод "pass null", и он ударит вас при модульном тестировании. Лучше иметь unit test для метода, чем проверка нуля.

Ознакомьтесь с достойной презентацией по этой проблеме и модульным тестированием в целом Мишко Хевери в http://www.youtube.com/watch?v=wEhu57pih5w&feature=channel

Ответ 7

Старые версии Microsoft С++ (и, возможно, другие) не генерировали исключение для неудачных распределений через new, но возвращали NULL. Код, который должен был работать как в стандартном, так и в более ранних версиях, имел бы избыточную проверку, указанную в первом примере.

Было бы проще сделать все неудачные выделения, следуя одному и тому же пути:

if(obj==NULL)
    throw std::bad_alloc();

Ответ 8

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

Когда я работаю над другим кодом, я стараюсь убедиться, что знаю две вещи.
1. Что хотел программист 2. Почему они написали код так, как они делали

Для слежения за программистами типа A, возможно, это помогает.

Итак, "Сколько этого достаточно" заканчивается тем, что является социальным вопросом, а не техническим вопросом - нет согласованного способа его измерения.

(Он тоже меня заводит.)

Ответ 9

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

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

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

Ответ 10

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

Ответ 11

Я бы сказал, что это немного зависит от вашего языка, но я использую Resharper с С#, и это в основном выходит из этого способа сказать мне, что "эта ссылка может быть нулевой", и в этом случае я добавляю чек, если он сообщает я "это всегда будет правдой" для "if (null!= oMyThing & &....)", тогда я слушаю его, не проверяйте значение null.

Ответ 12

Следует ли проверять значение null или не сильно зависит от обстоятельств.

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

public MyObject MyMethod(object foo)
{
  if (foo == null)
  {
    throw new ArgumentNullException("foo");
  }

  // do whatever if foo was non-null
}

Ответ 13

Я проверяю только NULL, когда знаю, что делать, когда вижу NULL. "Знать, что делать" здесь означает "знать, как избежать краха" или "знать, что сказать пользователю, кроме места аварии". Например, если malloc() возвращает NULL, у меня обычно нет опции, кроме как прервать программу. С другой стороны, если fopen() возвращает NULL, я могу сообщить пользователю имя файла, которое не может быть открыто и может быть errno. И если find() возвращает end(), я обычно знаю, как продолжать без сбоев.

Ответ 14

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

Цель состоит в том, чтобы немедленно выявлять ошибки, а также документировать контракт в коде, который не лежит.

Ответ 15

Я не думаю, что это плохой код. Справедливое количество API Windows/Linux API возвращает NULL при неудачной ошибке. Поэтому, конечно, я проверяю на отказ в том, как указывает API. Обычно я пропускаю поток управления в модуль ошибок некоторого способа вместо дублирования кода обработки ошибок.

Ответ 16

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

Речь идет не только о NULL, функция должна проверять предварительные и пост-условия, если это возможно.

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

Кроме того, если исключение выходит из main, стек может не свернуться, предотвращая запуск деструкторов вообще (см. стандарт С++ для terminate()). Что может быть серьезным. Поэтому оставить bad_alloc непроверенным может быть более опасным, чем кажется.

Ошибка с помощью assert vs. fail с ошибкой времени выполнения - это совсем другая тема.

Проверка на NULL после new(), если стандартное поведение new() не было изменено, чтобы вернуть NULL, а не бросать, кажется устаревшим.

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

Ответ 17

Моя первая проблема с этим заключается в том, что это приводит к коду, который усеян нулевыми проверками и подобными. Это ущемляет читаемость, а id даже доходит до того, что он болит ремонтопригодность, потому что действительно легко забыть нулевую проверку, если вы пишете фрагмент кода, где определенная ссылка действительно никогда не должна быть нулевой. И вы просто знаете, что нулевые проверки будут отсутствовать в некоторых местах. Что на самом деле делает отладку сложнее, чем нужно. Если бы исходное исключение не было обнаружено и заменено ошибочным возвращаемым значением, тогда мы получили бы ценный объект исключения с информативной стопкой. Что дает вам нулевая проверка? Исключение NullReferenceException в фрагменте кода, который заставляет вас идти: wtf? эта ссылка никогда не должна быть нулевой!

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

Другая проблема с нулевыми проверками повсюду в том, что некоторые разработчики действительно не нашли времени, чтобы правильно подумать о реальной проблеме, когда они получают исключение NullReferenceException. Я действительно видел довольно много разработчиков, просто добавляя нулевую проверку над кодом, где произошло исключение NullReferenceException. Отлично, исключение больше не происходит! Ура! Мы можем вернуться домой! Умм... как бой "нет, ты не можешь, и ты заслужил локоть в лицо? Настоящая ошибка может больше не вызывать исключения, но теперь у вас, вероятно, отсутствует или ошибочное поведение... и не исключение! Что еще более болезненно и требует еще больше времени для отладки.

Ответ 18

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

В любом случае, проверка на null там, где это уместно, приводит к созданию более чистого кода. Я бы зашел так далеко, чтобы сказать, что никогда не назначать значения параметров параметров функции - следующий логический шаг. Чтобы идти еще дальше, возвращая пустые массивы и т.д., Где это необходимо, приводит к еще более чистым кодам. Приятно не беспокоиться о получении null, кроме случаев, когда они логически значимы. Нуль лучше, чем ошибки.

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