Sprawdzanie parametrów zerowych w C #

85

Czy w języku C # są jakieś dobre powody (inne niż lepszy komunikat o błędzie) do dodawania sprawdzeń wartości null parametrów do każdej funkcji, w której null nie jest prawidłową wartością? Oczywiście kod, który używa s, i tak zgłosi wyjątek. Takie kontrole powodują, że kod jest wolniejszy i trudniejszy w utrzymaniu.

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

  // Use s
}
kaalus
źródło
12
Poważnie wątpię, czy prosta kontrola zerowa spowolni twój kod o znaczną (lub nawet mierzalną) kwotę.
Heinzi
Cóż, to zależy, co robi „Use s”. Jeśli wszystko, co robi, to „return s.SomeField”, to dodatkowe sprawdzenie prawdopodobnie spowolni metodę o rząd wielkości.
kaalus
10
@kaalus: Prawdopodobnie? Prawdopodobnie nie umieściłem tego w mojej książce. Gdzie są twoje dane testowe? A jakie jest prawdopodobieństwo, że taka zmiana byłaby wąskim gardłem Twojej aplikacji?
Jon Skeet
@Jon: masz rację, nie mam tutaj twardych danych. Jeśli tworzysz dla Windows Phone lub Xbox, gdzie inlining JIT będzie miał wpływ na to dodatkowe „if” i gdzie rozgałęzianie jest bardzo kosztowne, możesz wpaść w paranoję.
kaalus
3
@kaalus: Więc dostosuj styl swojego kodu w tych bardzo konkretnych przypadkach po udowodnieniu, że robi to znaczącą różnicę, lub użyj Debug.Assert (ponownie, tylko w takich sytuacjach, zobacz odpowiedź Erica), aby testy były włączone tylko w niektórych kompilacjach.
Jon Skeet

Odpowiedzi:

163

Tak, istnieją dobre powody:

  • Identyfikuje dokładnie to, co jest zerowe, co może nie być oczywiste z pliku NullReferenceException
  • Sprawia, że ​​kod kończy się niepowodzeniem przy nieprawidłowym wejściu, nawet jeśli jakiś inny warunek oznacza, że ​​wartość nie jest wyłuskiwana
  • To sprawia, że ​​wyjątek występuje, zanim metoda może mieć jakiekolwiek inne skutki uboczne, które można osiągnąć przed pierwszym wyłuskiwaniem
  • Oznacza to, że możesz mieć pewność, że przekazując parametr do czegoś innego, nie naruszasz ich umowy
  • Dokumentuje wymagania metody (użycie Code Contracts jest oczywiście jeszcze lepsze)

A teraz co do twoich zastrzeżeń:

  • Jest wolniejszy : czy faktycznie jest to wąskie gardło w Twoim kodzie, czy też zgadujesz? Kontrole nieważności są bardzo szybkie iw zdecydowanej większości przypadków nie będą stanowić wąskiego gardła
  • Utrudnia to utrzymanie kodu : myślę odwrotnie. Myślę, że łatwiej jest używać kodu, w którym jest jasne, czy parametr może być zerowy, i gdzie masz pewność, że ten warunek jest wymuszony.

I dla twojego zapewnienia:

Oczywiście kod, który używa s, i tak zgłosi wyjątek.

Naprawdę? Rozważać:

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

To używa s, ale nie zgłasza wyjątku. Jeśli nieważne jest, saby mieć wartość null, a to wskazuje, że coś jest nie tak, wyjątek jest tutaj najbardziej odpowiednim zachowaniem.

Teraz gdzie można umieścić te weryfikację argument jest zupełnie inna sprawa. Możesz zdecydować się zaufać całemu kodowi z własnej klasy, więc nie przejmuj się prywatnymi metodami. Możesz zdecydować się zaufać reszcie zespołu, więc nie przejmuj się metodami wewnętrznymi. Prawie na pewno powinieneś zweryfikować argumenty dla metod publicznych.

Na marginesie: przeciążenie konstruktora jednoparametrowego ArgumentNullExceptionpowinno być tylko nazwą parametru, więc test powinien wyglądać następująco:

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

Alternatywnie możesz utworzyć metodę rozszerzającą, pozwalającą na nieco bardziej zwięzłe:

s.ThrowIfNull("s");

W mojej wersji (generycznej) metody rozszerzenia sprawiam, że zwraca oryginalną wartość, jeśli nie jest to null, pozwalając ci pisać takie rzeczy, jak:

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

Możesz także mieć przeciążenie, które nie przyjmuje nazwy parametru, jeśli nie przejmujesz się tym zbytnio.

Jon Skeet
źródło
8
+1 dla metody rozszerzenia. Zrobiłem dokładnie to samo, łącznie z inną weryfikacją, taką jak ThrowIfEmptynaICollection
Davy8
5
Dlaczego uważam, że jest trudniejszy w utrzymaniu: jak w przypadku każdego ręcznego mechanizmu, który za każdym razem wymaga interwencji programisty, jest on podatny na błędy i pominięcia oraz zaśmieca kod. Niestabilne bezpieczeństwo jest gorsze niż brak bezpieczeństwa.
kaalus
8
@kaalus: Czy masz takie samo podejście do testowania? „Moje testy nie wychwycą wszystkich możliwych błędów, więc nie napiszę żadnych”? Gdy mechanizmy zabezpieczające zrobić pracę, będą mogły łatwiej znaleźć problem i zmniejszyć wpływ gdzie indziej (łapiąc problemu wcześniej). Jeśli tak się stanie 9 razy na 10, to i tak lepiej niż miało to miejsce 0 razy na 10 ...
Jon Skeet
2
@DoctorOreo: Nie używam Debug.Assert. Jeszcze ważniejsze jest wyłapywanie błędów w produkcji (zanim zepsują one rzeczywiste dane) niż w rozwoju.
Jon Skeet,
2
Teraz z C # 6.0 możemy nawet używać throw new ArgumentNullException(nameof(s))
hrzafer
52

Zgadzam się z Jonem, ale dodałbym do tego jedną rzecz.

Moje podejście do tego, kiedy należy dodać jawne kontrole null, opiera się na następujących przesłankach:

  • Powinien istnieć sposób, aby testy jednostkowe sprawdzały każdą instrukcję w programie.
  • throwoświadczenia są oświadczeniami .
  • Konsekwencją ifjest stwierdzenie .
  • Dlatego powinien istnieć sposób na ćwiczenie throwinif (x == null) throw whatever;

Jeśli nie ma możliwości wykonania tej instrukcji, nie można jej przetestować i należy ją zastąpić Debug.Assert(x != null);.

Jeśli istnieje możliwy sposób wykonania tej instrukcji, napisz ją, a następnie napisz test jednostkowy, który ją sprawdza.

Jest to szczególnie ważne, że metody publiczne typach publicznych sprawdzić swoje argumenty w ten sposób; nie masz pojęcia, jakie szalone rzeczy będą robić Twoi użytkownicy. Daj im "hej ty głupcze, robisz to źle!" wyjątek tak szybko, jak to możliwe.

Natomiast metody prywatne typów prywatnych są dużo bardziej prawdopodobne w sytuacji, gdy kontrolujesz argumenty i mogą mieć silną gwarancję, że argument nigdy nie będzie zerowy; użyj potwierdzenia, aby udokumentować tę niezmienność.

Eric Lippert
źródło
23

Używam tego od roku:

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

To oneliner, a discard ( _) oznacza, że ​​nie ma niepotrzebnego przydziału.

galdin
źródło
8

Bez wyraźnego ifczeku, to może być bardzo trudne, aby dowiedzieć się, co było null, jeśli nie jesteś właścicielem kodu.

Jeśli otrzymasz plik NullReferenceExceptionz wnętrza biblioteki bez kodu źródłowego, prawdopodobnie będziesz mieć wiele problemów ze zrozumieniem, co zrobiłeś źle.

Te ifsprawdzenia nie spowalniają znacząco kodu.


Zauważ, że parametr dla ArgumentNullExceptionkonstruktora to nazwa parametru, a nie komunikat.
Twój kod powinien być

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

Napisałem fragment kodu, aby to ułatwić:

<?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
źródło
5

Możesz rzucić okiem na Kontrakty kodu, jeśli potrzebujesz ładniejszego sposobu, aby upewnić się, że nie otrzymujesz żadnych obiektów zerowych jako parametru.

AD.Net
źródło
2

Główną korzyścią jest to, że od samego początku jasno określasz wymagania swojej metody. To wyjaśnia innym programistom pracującym nad kodem, że wysyłanie wartości null do metody wywołującej jest naprawdę błędem.

Sprawdzenie zatrzyma również wykonanie metody przed wykonaniem jakiegokolwiek innego kodu. Oznacza to, że nie będziesz musiał martwić się o modyfikacje dokonane metodą, która nie została ukończona.

Justin Niessner
źródło
2

Oszczędza to trochę debugowania, kiedy trafisz na ten wyjątek.

ArgumentNullException wyraźnie stwierdza, że ​​to „s” ma wartość null.

Jeśli nie masz tego sprawdzania i nie pozwolisz, aby kod działał, otrzymasz NullReferenceException z jakiegoś niezidentyfikowanego wiersza w tej metodzie. W kompilacji wydania nie otrzymujesz numerów linii!

Hans Ke ing
źródło
0

Oryginalny kod:

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

  // Use s
}

Przepisz to jako:

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

[Edytuj] Powodem przepisywania za pomocą nameofjest to, że pozwala na łatwiejszą refaktoryzację. Jeśli nazwa zmiennej skiedykolwiek się zmieni, komunikaty debugowania również zostaną zaktualizowane, podczas gdy jeśli po prostu zakodujesz na stałe nazwę zmiennej, to ostatecznie stanie się nieaktualna, gdy aktualizacje będą dokonywane w czasie. To dobra praktyka stosowana w przemyśle.

Asher Garland
źródło
Jeśli sugerujesz zmianę, wyjaśnij dlaczego. Upewnij się również, że odpowiadasz na zadane pytanie.
CodeCaster
-1
int i = Age ?? 0;

Na przykład:

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

Lub:

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

Lub:

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

Lub trójskładnikowy:

int i = age.HasValue ? age.Value : 0;
Muhammad Farooq Khalid
źródło