Controllo dei parametri nulli in C #


88

In C #, ci sono buone ragioni (oltre a un messaggio di errore migliore) per aggiungere controlli null dei parametri a ogni funzione in cui null non è un valore valido? Ovviamente, il codice che usa s genererà comunque un'eccezione. E tali controlli rendono il codice più lento e difficile da mantenere.

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

  // Use s
}

13
Dubito seriamente che un semplice controllo nullo rallenterà il tuo codice di un importo significativo (o addirittura misurabile).
Heinzi

Bene, dipende da cosa fa "Usa s". Se tutto ciò che fa è "return s.SomeField", il controllo aggiuntivo probabilmente rallenterà il metodo di un ordine di grandezza.
kaalus

11
@kaalus: Probabilmente? Probabilmente non lo taglia nel mio libro. Dove sono i tuoi dati di prova? E quanto è probabile che un tale cambiamento sia il collo di bottiglia della tua applicazione in primo luogo?
Jon Skeet

@ Jon: hai ragione non ho dati concreti qui. Se sviluppi per Windows Phone o Xbox in cui l'inlining di JIT sarà influenzato da questo "se" extra e dove il branching è molto costoso, potresti diventare paranoico.
kaalus

3
@kaalus: Quindi regola il tuo stile di codice in quei casi molto specifici dopo aver dimostrato che fa una differenza significativa o usa Debug.Assert (di nuovo, solo in quelle situazioni, vedi la risposta di Eric) in modo che i controlli siano attivi solo in alcune build.
Jon Skeet

Risposte:


167

Sì, ci sono buone ragioni:

  • Identifica esattamente ciò che è nullo, che potrebbe non essere ovvio da un file NullReferenceException
  • Fa fallire il codice su input non valido anche se qualche altra condizione significa che il valore non è dereferenziato
  • Fa sì che l'eccezione si verifichi prima che il metodo possa avere altri effetti collaterali che potresti raggiungere prima della prima dereferenziazione
  • Significa che puoi essere sicuro che se passi il parametro a qualcos'altro, non stai violando il loro contratto
  • Documenta i requisiti del tuo metodo (usare Code Contracts è ancora meglio per quello, ovviamente)

Ora per quanto riguarda le tue obiezioni:

  • È più lento : hai scoperto che questo è effettivamente il collo di bottiglia nel tuo codice o stai indovinando? I controlli di nullità sono molto rapidi e nella stragrande maggioranza dei casi non saranno il collo di bottiglia
  • Rende il codice più difficile da mantenere : penso il contrario. Penso che sia più facile usare il codice in cui è chiarito se un parametro può essere nullo o meno e dove sei sicuro che tale condizione sia applicata.

E per la tua affermazione:

Ovviamente, il codice che usa s genererà comunque un'eccezione.

Veramente? Ritenere:

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

Questo usa s, ma non genera un'eccezione. Se non è valido per sessere nullo, e questo indica che qualcosa non va, un'eccezione è il comportamento più appropriato qui.

Ora dove metti quei controlli di convalida degli argomenti è una questione diversa. Puoi decidere di fidarti di tutto il codice all'interno della tua classe, quindi non preoccuparti dei metodi privati. Potresti decidere di fidarti del resto del tuo assembly, quindi non preoccuparti dei metodi interni. Quasi certamente dovresti convalidare gli argomenti a favore dei metodi pubblici.

Una nota a margine: l'overload del costruttore a parametro singolo di ArgumentNullExceptiondovrebbe essere solo il nome del parametro, quindi il tuo test dovrebbe essere:

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

In alternativa puoi creare un metodo di estensione, consentendo a un po 'più terser:

s.ThrowIfNull("s");

Nella mia versione del metodo di estensione (generico), faccio in modo che restituisca il valore originale se non è nullo, permettendoti di scrivere cose come:

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

Puoi anche avere un sovraccarico che non prende il nome del parametro, se non te ne preoccupi troppo.


8
+1 per il metodo di estensione. Ho fatto la stessa identica cosa, incluse altre convalide come ThrowIfEmptysuICollection
Davy8

5
Perché penso che sia più difficile da mantenere: come con ogni meccanismo manuale che richiede ogni volta l'intervento del programmatore, questo è soggetto a errori e omissioni e ingombra il codice. La sicurezza instabile è peggio dell'assenza di sicurezza.
kaalus

8
@kaalus: applichi lo stesso atteggiamento ai test? "I miei test non individueranno tutti i possibili bug, quindi non ne scriverò nessuno"? Quando i meccanismi di sicurezza fanno il lavoro, ti rendono più facile trovare il problema e ridurre l'impatto altrove (catturando il problema in precedenza). Se ciò accade 9 volte su 10, è comunque meglio di quanto accada 0 volte su 10 ...
Jon Skeet

2
@DoctorOreo: non uso Debug.Assert. È ancora più importante rilevare gli errori nella produzione (prima che rovinino dati reali) che nello sviluppo.
Jon Skeet

3
Ora con C # 6.0 possiamo persino usare throw new ArgumentNullException(nameof(s))
hrzafer

52

Sono d'accordo con Jon, ma aggiungerei una cosa a questo.

Il mio atteggiamento su quando aggiungere controlli nulli espliciti si basa su queste premesse:

  • Ci dovrebbe essere un modo per i tuoi unit test di esercitare ogni affermazione in un programma.
  • throwle dichiarazioni sono dichiarazioni .
  • La conseguenza di una ifè una dichiarazione .
  • Pertanto, dovrebbe esserci un modo per esercitare throwinif (x == null) throw whatever;

Se non è possibile eseguire tale istruzione, non può essere testata e deve essere sostituita con Debug.Assert(x != null);.

Se esiste un modo possibile per eseguire quell'istruzione, scrivi l'istruzione, quindi scrivi uno unit test che lo eserciti.

È particolarmente importante che i metodi pubblici di tipo pubblico controllino i loro argomenti in questo modo; non hai idea di quale follia faranno i tuoi utenti. Dai loro il "hey testa di ossa, stai sbagliando!" eccezione il prima possibile.

I metodi privati ​​dei tipi privati, al contrario, hanno molte più probabilità di trovarsi nella situazione in cui controlli gli argomenti e possono avere una forte garanzia che l'argomento non sia mai nullo; utilizzare un'asserzione per documentare tale invariante.


23

Lo uso da un anno ormai:

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

È un oneliner e il discard ( _) significa che non ci sono allocazioni inutili.


8

Senza un ifcontrollo esplicito , può essere molto difficile capire cosa fosse nullse non si possiede il codice.

Se ottieni un NullReferenceExceptiondal profondo di una libreria senza codice sorgente, è probabile che tu abbia molti problemi a capire cosa hai sbagliato.

Questi ifcontrolli non renderanno il tuo codice notevolmente più lento.


Notare che il parametro per il ArgumentNullExceptioncostruttore è un nome di parametro, non un messaggio.
Il tuo codice dovrebbe essere

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

Ho scritto uno snippet di codice per renderlo più semplice:

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

Potresti voler dare un'occhiata ai contratti di codice se hai bisogno di un modo migliore per assicurarti di non ottenere alcun oggetto nullo come parametro.


2

Il vantaggio principale è che sei esplicito con i requisiti del tuo metodo fin dall'inizio. Ciò rende chiaro agli altri sviluppatori che lavorano sul codice che è veramente un errore per un chiamante inviare un valore nullo al tuo metodo.

Il controllo interromperà anche l'esecuzione del metodo prima che venga eseguito qualsiasi altro codice. Ciò significa che non dovrai preoccuparti delle modifiche apportate dal metodo che vengono lasciate incompiute.


2

Salva un po 'di debug, quando si colpisce quell'eccezione.

ArgumentNullException afferma esplicitamente che era "s" che era null.

Se non hai quel controllo e lasci che il codice esploda, ottieni una NullReferenceException da una riga non identificata in quel metodo. In una build di rilascio non ottieni i numeri di riga!


0

Codice originale:

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

  // Use s
}

Riscrivilo come:

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

Il motivo per riscrivere utilizzando nameofè che consente un refactoring più semplice. Se il nome della variabile scambia, verranno aggiornati anche i messaggi di debug, mentre se si codifica semplicemente il nome della variabile, alla fine sarà obsoleto quando gli aggiornamenti vengono effettuati nel tempo. È una buona pratica utilizzata nell'industria.


-2
int i = Age ?? 0;

Quindi per il tuo esempio:

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

O:

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

O:

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

O ternario:

int i = age.HasValue ? age.Value : 0;
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.