Проверка нулевого параметра в C #


88

Существуют ли в C # какие-либо веские причины (кроме более точного сообщения об ошибке) для добавления проверки нулевого параметра к каждой функции, где значение null не является допустимым значением? Очевидно, что код, использующий s, в любом случае вызовет исключение. И такие проверки делают код медленнее и труднее поддерживать.

void f(SomeType s)
{
  if (s == null)
  {
    throw new ArgumentNullException("s cannot be null.");
  }

  // Use s
}

13
Я серьезно сомневаюсь, что простая проверка нуля замедлит ваш код на значительную (или даже измеримую) величину.
— Heinzi

Ну, это зависит от того, что делает "Use s". Если все, что он делает, это «return s.SomeField», то дополнительная проверка, вероятно, замедлит работу метода на порядок.
— kaalus

11
@kaalus: Наверное? Наверное, не в моей книге. Где ваши тестовые данные? И насколько вероятно, что такое изменение станет узким местом вашего приложения?
— Джон Скит

@Jon: ты прав, у меня нет точных данных. Если вы разрабатываете для Windows Phone или Xbox, где на встраивание JIT будет влиять это дополнительное «если» и где ветвление очень дорого, вы можете стать совершенно параноиком.
— kaalus

3
@kaalus: Поэтому измените стиль кода в этих очень конкретных случаях после того, как докажете, что он имеет существенное значение, или используйте Debug.Assert (опять же, только в этих ситуациях, см. ответ Эрика), чтобы проверки были включены только в определенных сборках.
— Джон Скит

Ответы:


167

Да, на то есть веские причины:

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

Теперь что касается ваших возражений:

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

И для вашего утверждения:

Очевидно, что код, использующий s, в любом случае вызовет исключение.

В самом деле? Рассматривать:

void f(SomeType s)
{
  // Use s
  Console.WriteLine("I've got a message of {0}", s);
}

Это используется s, но не вызывает исключения. Если sзначение null недопустимо , и это указывает на то, что что-то не так, исключение является наиболее подходящим поведением здесь.

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

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

if (s == null)
{
  throw new ArgumentNullException("s");
}

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

s.ThrowIfNull("s");

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

this.name = name.ThrowIfNull("name");

У вас также может быть перегрузка, которая не принимает имя параметра, если вас это не слишком беспокоит.


8
+1 для метода расширения. Я сделал ту же самую вещь, включая другие проверки , такие как ThrowIfEmptyнаICollection
— Davy8

5
Почему мне кажется, что его сложнее поддерживать: как и любой ручной механизм, который каждый раз требует вмешательства программиста, он подвержен ошибкам и упущениям и загромождает код. Нестабильная безопасность хуже, чем ее отсутствие.
— kaalus

8
@kaalus: Вы так же относитесь к тестированию? «Мои тесты не поймают все возможные ошибки, поэтому писать не буду»? Когда механизмы безопасности делают работу, они будут делать это легче найти проблему и снизить влияние других (ловя проблему ранее). Если это произойдет в 9 случаях из 10, это все равно лучше, чем в 0 раз из 10 ...
— Джон Скит,

2
@DoctorOreo: не использую Debug.Assert. Еще важнее выявлять ошибки в продакшене (до того, как они испортят реальные данные), чем в разработке.
— Джон Скит

3
Теперь с C # 6.0 мы можем даже использовать throw new ArgumentNullException(nameof(s))
— hrzafer

52

Я согласен с Джоном, но я бы добавил к этому одну вещь.

Мое отношение к тому, когда следует добавлять явные нулевые проверки, основано на следующих предпосылках:

  • У ваших модульных тестов должен быть способ проверять каждое выражение в программе.
  • throwзаявления есть заявления .
  • Следствием an ifявляется заявление .
  • Следовательно, должен быть способ тренировки throwвif (x == null) throw whatever;

Если нет возможности выполнить этот оператор, его нельзя проверить, и его следует заменить на Debug.Assert(x != null);.

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

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

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


23

Пользуюсь вот этим уже год:

_ = s ?? throw new ArgumentNullException(nameof(s));

Это единичная строка, и discard ( _) означает, что ненужного выделения памяти нет.


8

Без явной ifпроверки может быть очень сложно выяснить, что было, nullесли вы не владеете кодом.

Если вы извлечете NullReferenceExceptionиз глубины библиотеки без исходного кода, у вас, вероятно, будет много проблем с выяснением того, что вы сделали не так.

Эти ifпроверки не сделают ваш код заметно медленнее.


Обратите внимание, что параметр ArgumentNullExceptionконструктора - это имя параметра, а не сообщение.
Ваш код должен быть

if (s == null) throw new ArgumentNullException("s");

Я написал фрагмент кода, чтобы упростить задачу:

<?xml version="1.0" encoding="utf-8" ?>
<CodeSnippets  xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
    <CodeSnippet Format="1.0.0">
        <Header>
            <Title>Check for null arguments</Title>
            <Shortcut>tna</Shortcut>
            <Description>Code snippet for throw new ArgumentNullException</Description>
            <Author>SLaks</Author>
            <SnippetTypes>
                <SnippetType>Expansion</SnippetType>
                <SnippetType>SurroundsWith</SnippetType>
            </SnippetTypes>
        </Header>
        <Snippet>
            <Declarations>
                <Literal>
                    <ID>Parameter</ID>
                    <ToolTip>Paremeter to check for null</ToolTip>
                    <Default>value</Default>
                </Literal>
            </Declarations>
            <Code Language="csharp"><![CDATA[if ($Parameter$ == null) throw new ArgumentNullException("$Parameter$");
        $end$]]>
            </Code>
        </Snippet>
    </CodeSnippet>
</CodeSnippets>

5

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


2

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

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


2

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

ArgumentNullException явно заявляет, что это было «s», которое было пустым.

Если у вас нет этой проверки и вы позволите коду взорваться, вы получите исключение NullReferenceException из какой-то неопознанной строки в этом методе. В сборке выпуска вы не получаете номеров строк!


0

Исходный код:

void f(SomeType s)
{
  if (s == null)
  {
    throw new ArgumentNullException("s cannot be null.");
  }

  // Use s
}

Перепишите его как:

void f(SomeType s)
{
  if (s == null) throw new ArgumentNullException(nameof(s));
}

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


-2
int i = Age ?? 0;

Итак, для вашего примера:

if (age == null || age == 0)

Или же:

if (age.GetValueOrDefault(0) == 0)

Или же:

if ((age ?? 0) == 0)

Или тройной:

int i = age.HasValue ? age.Value : 0;
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.