Проверка нулевого параметра в 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>
SLaks
источник
5

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

AD.Net
источник
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;
Мухаммад Фарук Халид
источник