Nullparameterprüfung in C #

84

Gibt es in C # gute Gründe (außer einer besseren Fehlermeldung), jeder Funktion, bei der null kein gültiger Wert ist, Parameter-Null-Prüfungen hinzuzufügen? Offensichtlich löst der Code, der s verwendet, trotzdem eine Ausnahme aus. Und solche Überprüfungen machen die Code langsamer und schwieriger zu warten.

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

  // Use s
}
Kaalus
quelle
11
Ich bezweifle ernsthaft, dass eine einfache Nullprüfung Ihren Code um einen signifikanten (oder sogar messbaren) Betrag verlangsamt.
Heinzi
Nun, es kommt darauf an, was "Use s" macht. Wenn alles, was es tut, "return s.SomeField" ist, wird die zusätzliche Prüfung die Methode wahrscheinlich um eine Größenordnung verlangsamen.
Kaalus
10
@kaalus: Wahrscheinlich? Vermutlich schneidet es nicht in mein Buch. Wo sind deine Testdaten? Und wie wahrscheinlich ist es, dass eine solche Änderung überhaupt den Engpass Ihrer Bewerbung darstellt?
Jon Skeet
@ Jon: Du hast recht, ich habe hier keine harten Daten. Wenn Sie für Windows Phone oder Xbox entwickeln, bei denen das JIT-Inlining von diesem zusätzlichen "Wenn" betroffen ist und bei denen die Verzweigung sehr teuer ist, werden Sie möglicherweise ziemlich paranoid.
Kaalus
3
@kaalus: Passen Sie also Ihren Codestil in diesen sehr spezifischen Fällen an, nachdem Sie bewiesen haben, dass er einen signifikanten Unterschied macht, oder verwenden Sie Debug.Assert (wiederum nur in diesen Situationen, siehe Erics Antwort), sodass die Überprüfungen nur in bestimmten Builds aktiviert sind.
Jon Skeet

Antworten:

161

Ja, es gibt gute Gründe:

  • Es identifiziert genau, was null ist, was aus a möglicherweise nicht ersichtlich ist NullReferenceException
  • Der Code schlägt bei ungültiger Eingabe fehl, auch wenn eine andere Bedingung bedeutet, dass der Wert nicht dereferenziert wird
  • Die Ausnahme tritt auf, bevor die Methode andere Nebenwirkungen haben kann, die Sie möglicherweise vor der ersten Dereferenzierung erreichen
  • Sie können also sicher sein, dass Sie nicht gegen den Vertrag verstoßen, wenn Sie den Parameter an etwas anderes übergeben
  • Es dokumentiert die Anforderungen Ihrer Methode (die Verwendung von Code-Verträgen ist dafür natürlich noch besser)

Nun zu Ihren Einwänden:

  • Es ist langsamer : Haben Sie festgestellt, dass dies tatsächlich der Engpass in Ihrem Code ist, oder raten Sie? Nullitätsprüfungen sind sehr schnell und in den allermeisten Fällen nicht der Engpass
  • Es macht es schwieriger, den Code zu pflegen : Ich denke das Gegenteil. Ich denke, es ist einfacher , Code zu verwenden, bei dem klargestellt wird, ob ein Parameter null sein kann oder nicht, und bei dem Sie sicher sind, dass diese Bedingung durchgesetzt wird.

Und für Ihre Behauptung:

Offensichtlich löst der Code, der s verwendet, trotzdem eine Ausnahme aus.

"Ja wirklich?" Erwägen:

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

Das nutzt s, aber es löst keine Ausnahme aus. Wenn es ungültig ist s, null zu sein, und dies darauf hinweist, dass etwas nicht stimmt, ist eine Ausnahme hier das am besten geeignete Verhalten.

Nun ist es eine andere Sache, wo Sie diese Überprüfungen der Argumentvalidierung platzieren. Sie können sich dafür entscheiden, dem gesamten Code in Ihrer eigenen Klasse zu vertrauen, ohne sich um private Methoden zu kümmern. Sie können sich dafür entscheiden, dem Rest Ihrer Baugruppe zu vertrauen, ohne sich um interne Methoden zu kümmern. Sie sollten mit ziemlicher Sicherheit die Argumente für öffentliche Methoden validieren.

Eine Randnotiz: Die Einzelparameter-Konstruktorüberladung von ArgumentNullExceptionsollte nur der Parametername sein, daher sollte Ihr Test sein:

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

Alternativ können Sie eine Erweiterungsmethode erstellen, die Folgendes zulässt:

s.ThrowIfNull("s");

In meiner Version der (generischen) Erweiterungsmethode wird der ursprüngliche Wert zurückgegeben, wenn er nicht null ist. So können Sie Folgendes schreiben:

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

Sie können auch eine Überladung haben, die den Parameternamen nicht annimmt, wenn Sie sich nicht zu sehr darum kümmern.

Jon Skeet
quelle
8
+1 für die Erweiterungsmethode. Ich habe genau das Gleiche getan, einschließlich anderer Validierungen wie ThrowIfEmptyamICollection
Davy8
5
Warum ich denke, dass es schwieriger zu warten ist: Wie bei jedem manuellen Mechanismus, bei dem jedes Mal ein Eingreifen des Programmierers erforderlich ist, ist dies anfällig für Fehler und Auslassungen und führt zu Unordnung im Code. Flockige Sicherheit ist schlechter als gar keine Sicherheit.
Kaalus
8
@kaalus: Wenden Sie die gleiche Einstellung auf das Testen an? "Meine Tests werden nicht alle möglichen Fehler auffangen, deshalb schreibe ich keine"? Wenn die Sicherheitsmechanismen zu tun Arbeit, werden sie es einfacher machen , das Problem zu finden und die Auswirkungen an anderer Stelle reduzieren (durch das Problem früher zu kontrollieren). Wenn das 9 mal von 10 passiert, ist das immer noch besser als 0 mal von 10 ...
Jon Skeet
2
@ DoctorOreo: Ich benutze nicht Debug.Assert. Es ist noch wichtiger, Fehler in der Produktion (bevor sie echte Daten vermasseln) zu erkennen als in der Entwicklung.
Jon Skeet
2
Jetzt mit C # 6.0 können wir sogar verwenden throw new ArgumentNullException(nameof(s))
hrzafer
52

Ich stimme Jon zu, aber ich würde noch eins hinzufügen.

Meine Einstellung, wann explizite Nullprüfungen hinzugefügt werden sollen, basiert auf folgenden Prämissen:

  • Es sollte eine Möglichkeit für Ihre Komponententests geben, jede Aussage in einem Programm auszuführen.
  • throwAussagen sind Aussagen .
  • Die Folge von a ifist eine Aussage .
  • Daher sollte es eine Möglichkeit sein , die Ausübung throwinif (x == null) throw whatever;

Wenn es keine Möglichkeit für die Anweisung ausgeführt werden , dann kann es nicht getestet werden und sollte ersetzt werden Debug.Assert(x != null);.

Wenn es eine Möglichkeit gibt, diese Anweisung auszuführen, schreiben Sie die Anweisung und dann einen Komponententest, der sie ausführt.

Es ist besonders wichtig, dass öffentliche Methoden öffentlicher Typen ihre Argumente auf diese Weise überprüfen. Sie haben keine Ahnung, was Ihre Benutzer verrückt machen werden. Gib ihnen das "Hey du Bonehead, du machst es falsch!" Ausnahme so schnell wie möglich.

Private Methoden privater Typen sind dagegen viel wahrscheinlicher in der Situation, in der Sie die Argumente steuern, und können eine starke Garantie dafür haben, dass das Argument niemals null ist. Verwenden Sie eine Behauptung, um diese Invariante zu dokumentieren.

Eric Lippert
quelle
20

Ich benutze das jetzt seit einem Jahr:

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

Es ist ein Oneliner, und discard ( _) bedeutet, dass keine unnötige Zuordnung erfolgt.

Galdin
quelle
8

Ohne eine explizite ifÜberprüfung kann es sehr schwierig sein, herauszufinden, was warnull wenn Sie den Code nicht besitzen.

Wenn Sie eine bekommen NullReferenceException aus der Tiefe einer Bibliothek ohne Quellcode erhalten, haben Sie wahrscheinlich große Probleme, herauszufinden, was Sie falsch gemacht haben.

Diese ifÜberprüfungen machen Ihren Code nicht merklich langsamer.


Beachten Sie, dass der Parameter für den ArgumentNullExceptionKonstruktor ein Parametername und keine Nachricht ist.
Ihr Code sollte sein

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

Ich habe ein Code-Snippet geschrieben, um dies zu vereinfachen:

<?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
quelle
5

Vielleicht möchten Sie einen Blick auf Codeverträge werfen, wenn Sie eine bessere Möglichkeit benötigen, um sicherzustellen, dass Sie keine Nullobjekte als Parameter erhalten.

AD.Net
quelle
2

Der Hauptvorteil besteht darin, dass Sie von Anfang an explizit mit den Anforderungen Ihrer Methode umgehen. Dies macht anderen Entwicklern, die an dem Code arbeiten, klar, dass es für einen Aufrufer wirklich ein Fehler ist, einen Nullwert an Ihre Methode zu senden.

Die Überprüfung stoppt auch die Ausführung der Methode, bevor ein anderer Code ausgeführt wird. Das heißt, Sie müssen sich keine Sorgen machen, dass Änderungen an der Methode vorgenommen werden, die noch nicht abgeschlossen sind.

Justin Niessner
quelle
2

Es erspart einige Fehlerbehebung, wenn Sie diese Ausnahme treffen.

Die ArgumentNullException gibt explizit an, dass "s" null war.

Wenn Sie diese Prüfung nicht haben und den Code blasen lassen, erhalten Sie eine NullReferenceException von einer nicht identifizierten Zeile in dieser Methode. In einem Release-Build erhalten Sie keine Zeilennummern!

Hans Keing
quelle
0

Originalcode:

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

  // Use s
}

Schreiben Sie es um als:

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

[Bearbeiten] Der Grund für das Umschreiben mit nameofist, dass es ein einfacheres Refactoring ermöglicht. Wenn sich der Name Ihrer Variablen sjemals ändert, werden auch die Debugging-Meldungen aktualisiert. Wenn Sie jedoch nur den Namen der Variablen fest codieren, ist diese möglicherweise veraltet, wenn im Laufe der Zeit Aktualisierungen vorgenommen werden. Dies ist eine gute Praxis in der Industrie.

Asher Girlande
quelle
Wenn Sie eine Änderung vorschlagen, erklären Sie, warum. Stellen Sie außerdem sicher, dass Sie die gestellte Frage beantworten.
CodeCaster
-1
int i = Age ?? 0;

Also für Ihr Beispiel:

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

Oder:

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

Oder:

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

Oder ternär:

int i = age.HasValue ? age.Value : 0;
Muhammad Farooq Khalid
quelle