Перевірка нульових параметрів у C #


85

У C # чи є якісь вагомі причини (крім кращого повідомлення про помилку) для додавання перевірки null параметрів до кожної функції, де null не є допустимим значенням? Очевидно, що код, який використовує s, все одно видасть виняток. І такі перевірки роблять код повільнішим та важчим у обслуговуванні.

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

  // Use s
}

12
Я серйозно сумніваюся, що проста перевірка нуля сповільнить ваш код на значну (або навіть вимірювану) величину.
Heinzi

Ну, це залежить від того, що робить "Use s". Якщо все, що він робить, це "повернення s.SomeField", тоді додаткова перевірка, ймовірно, сповільнить метод на порядок.
kaalus

10
@kaalus: Напевно? Можливо, це не вирізано в моїй книзі. Де ваші тестові дані? І наскільки ймовірно, що така зміна спочатку буде вузьким місцем вашої заявки?
Джон Скіт,

@ Джон: ти маєш рацію, у мене тут немає надійних даних. Якщо ви розробляєте для Windows Phone або Xbox, де на вбудовування JIT вплине це зайве "якщо", і де розгалуження дуже дороге, ви можете стати параноїком.
kaalus

3
@kaalus: Тож відрегулюйте свій стиль коду в тих дуже конкретних випадках, переконавшись, що це суттєво впливає, або використовуйте Debug.Assert (знову ж таки, лише в тих ситуаціях, див. відповідь Еріка), щоб перевірки були включені лише в певних збірках.
Jon Skeet

Відповіді:


163

Так, є вагомі причини:

  • Він точно визначає, що є нульовим, що може бути не очевидним із NullReferenceException
  • Це робить код невдалим при неправильному введенні, навіть якщо якась інша умова означає, що значення не розменоване
  • Це робить виняток ще до того, як метод може мати будь-які інші побічні ефекти, до яких ви могли б дістатись до першого переорієнтування
  • Це означає, що ви можете бути впевнені, що якщо передати параметр у щось інше, ви не порушуєте їхній контракт
  • Він документує вимоги вашого методу (використання кодових контрактів навіть краще, ніж звичайно)

Щодо ваших заперечень:

  • Це повільніше : Ви виявили, що це насправді є вузьким місцем у вашому коді, або ви здогадуєтесь? Перевірка недійсності відбувається дуже швидко, і в переважній більшості випадків вони не будуть вузьким місцем
  • Це ускладнює підтримку коду : я думаю навпаки. Я думаю, що простіше використовувати код там, де чітко зрозуміло, чи може параметр нульовий чи ні, і де ви впевнені, що ця умова виконується.

І на ваше твердження:

Очевидно, що код, який використовує 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. Ще важливіше виявити помилки у виробництві (до того, як вони зіпсують реальні дані), ніж у розробці.
Джон Скіт,

2
Тепер з C # 6.0 ми навіть можемо використовувати throw new ArgumentNullException(nameof(s))
hrzafer

52

Я погоджуюся з Джоном, але я додав би до цього одне.

Моє ставлення до того, коли додавати явні перевірки на нуль, базується на таких передумовах:

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

Якщо немає ніякої можливості для цього оператор буде виконуватися , то він не може бути перевірений і повинен бути замінений Debug.Assert(x != null);.

Якщо існує можливий спосіб виконання цього твердження, напишіть його, а потім напишіть модульний тест, який його виконує.

Це особливо важливо , щоб державні методи громадських типів перевірити свої аргументи таким чином; Ви навіть не уявляєте, яку божевільну штуку збираються робити ваші користувачі. Дайте їм "привіт вам, кісточко, ви робите це неправильно!" виняток якомога швидше.

Навпаки, приватні методи приватних типів набагато частіше потрапляють у ситуацію, коли ви керуєте аргументами і можете мати надійну гарантію того, що аргумент ніколи не буде нульовим; використовувати твердження для документування такого інваріанта.


22

Я використовую це вже рік:

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

Це однолінійний, і відкинути ( _) означає, що немає зайвого розподілу.


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

Головна перевага полягає в тому, що ви з самого початку чітко відповідаєте вимогам свого методу. Це дає зрозуміти іншим розробникам, що працюють над кодом, що виклик справді помилково надсилає нульове значення до вашого методу.

Перевірка також зупинить виконання методу до виконання будь-якого іншого коду. Це означає, що вам не доведеться турбуватися про модифікації, внесені незавершеним методом.


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));
}

[Редагувати] Причина перезапису з використанням nameofполягає в тому, що це дозволяє спростити рефакторинг. Якщо ім'я вашої змінної sколи-небудь змінюється, тоді повідомлення про налагодження також будуть оновлені, тоді як якщо ви просто вкажете ім'я змінної, тоді воно з часом буде застарілим, коли оновлення з часом будуть зроблені. Це хороша практика, яка використовується в промисловості.


Якщо ви пропонуєте змінити, поясніть, чому. Також переконайтеся, що ви відповідаєте на запитання, яке задають.
CodeCaster

-1
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.