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


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

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

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

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

в C++ я понимаю это в некоторой степени из-за непредсказуемого поведения разыменования нулевого указателя; разыменование null обрабатывается более изящно в 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.
}

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

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

//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
}

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

(очевидно, что на вашем публичном 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 63

18 ответов:

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

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

по вопросу проверки ввода:

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

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

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

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

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

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

по вопросу проверки вывода:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

(Это тоже сводит меня с ума.)

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

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

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

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

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

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

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

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

  // do whatever if foo was non-null
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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