Сколько проверки нуля достаточно? - PullRequest
66 голосов
/ 19 ноября 2008

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

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

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

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

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

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


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


Этот пример, кажется, учитывает нестандартные компиляторы, поскольку спецификация C ++ говорит, что неудачный новый вызов вызывает исключение. Если вы явно не поддерживаете несовместимые компиляторы, имеет ли это смысл? Имеет ли это смысл любой в управляемом языке, таком как Java или C # (или даже C ++ / 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's 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;
}

Ответы [ 18 ]

35 голосов
/ 19 ноября 2008

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

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

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

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

19 голосов
/ 21 ноября 2008

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

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

По вопросу подтверждения ввода:

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

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

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

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

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

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

По теме проверки выходных данных:

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

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

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

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

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). Очевидно, вы можете написать заголовок, который делает это для всех известных вам компиляторов. Если кто-то возьмет ваш код и скомпилирует его в нестандартной реализации C ++, о которой вы ничего не знаете, он будет принципиально сам по более важным причинам, чем этот, поэтому ему придется делать это самостоятельно.

10 голосов
/ 19 ноября 2008

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

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

7 голосов
/ 19 ноября 2008

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

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

Частично это зависит от того, как используется код - например, если это метод, доступный только в рамках проекта или общедоступного 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.");
    }
    //...
}
4 голосов
/ 19 ноября 2008

Проверка NULL в целом - это зло, поскольку она добавляет небольшой отрицательный токен в тестируемость кода. С NULL-проверками везде, где вы не можете использовать технику «pass null», и это ударит вас при модульном тестировании. Для этого метода лучше выполнить модульный тест, чем нулевую.

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

3 голосов
/ 19 ноября 2008

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

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

if(obj==NULL)
    throw std::bad_alloc();
2 голосов
/ 19 ноября 2008

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

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

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

2 голосов
/ 21 ноября 2008

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

2 голосов
/ 20 ноября 2008

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

...