Se le funzioni devono eseguire controlli nulli prima di eseguire il comportamento previsto è questa cattiva progettazione?


67

Quindi non so se questo è un buon o cattivo design del codice, quindi ho pensato che sarebbe meglio chiedere.

Creo spesso metodi che eseguono l'elaborazione di dati che coinvolgono classi e spesso eseguo molti controlli nei metodi per assicurarmi di non avere riferimenti null o altri errori prima d'ora.

Per un esempio molto semplice:

// fields and properties
private Entity _someEntity;
public Entity SomeEntity => _someEntity;

public void AssignEntity(Entity entity){
    _someEntity = entity;
}
public void SetName(string name)
{
    if (_someEntity == null) return; //check to avoid null ref
    _someEntity.Name = name;
    label.SetText(_someEntity.Name);
}

Così come puoi vedere sto controllando ogni volta null. Ma il metodo non dovrebbe avere questo controllo?

Ad esempio, il codice esterno dovrebbe ripulire i dati in anticipo, quindi i metodi non devono essere convalidati come di seguito:

 if(entity != null) // this makes the null checks redundant in the methods
 {
     Manager.AssignEntity(entity);
     Manager.SetName("Test");
 }

In breve, i metodi dovrebbero "convalidare i dati" e quindi eseguire la loro elaborazione sui dati o dovrebbero essere garantiti prima di chiamare il metodo e se non riesci a convalidare prima di chiamare il metodo dovrebbe generare un errore (o intercettare errore)?


7
Cosa succede quando qualcuno non riesce a fare il controllo null e SetName genera comunque un'eccezione?
Robert Harvey,

5
Cosa succede se in realtà voglio che SomeEntity sia Null? Decidi tu cosa vuoi, o someEntity può essere Null, oppure no, e poi scrivi il tuo codice di conseguenza. Se vuoi che non sia mai Null, puoi sostituire i controlli con assert.
gnasher729,

5
Puoi guardare i Confini di Gary Bernhardt parlare di una chiara divisione tra i dati di sanificazione (ad esempio, controllo di null, ecc.) In un guscio esterno e di avere un sistema di core interno che non deve preoccuparsi di quella roba - e fare semplicemente il suo lavoro.
Margarciaisaia,

1
Con C # 8 non dovresti potenzialmente preoccuparti delle eccezioni di riferimento null con le giuste impostazioni attivate: blogs.msdn.microsoft.com/dotnet/2017/11/15/…
JᴀʏMᴇᴇ

3
Questo è uno dei motivi per cui il team del linguaggio C # vuole introdurre tipi di riferimento nulli (e non nulli) -> github.com/dotnet/csharplang/issues/36
Mafii

Risposte:


168

Il problema con il tuo esempio di base non è il controllo null, è il fallimento silenzioso .

Gli errori di puntatore / riferimento nulli, il più delle volte, sono errori del programmatore. Gli errori del programmatore sono spesso meglio risolti fallendo immediatamente e ad alta voce. Esistono tre modi generali per affrontare il problema in questa situazione:

  1. Non preoccuparti di controllare e lascia che il runtime generi l'eccezione nullpointer.
  2. Controllare e generare un'eccezione con un messaggio migliore del messaggio NPE di base.

Una terza soluzione è più lavoro ma è molto più robusta:

  1. Struttura la tua classe in modo che sia praticamente impossibile _someEntitytrovarsi in uno stato non valido. Un modo per farlo è quello di sbarazzarsi di AssignEntitye richiederlo come parametro per l'istanza. Anche altre tecniche di iniezione di dipendenza possono essere utili.

In alcune situazioni, ha senso controllare la validità di tutti gli argomenti delle funzioni prima di qualsiasi lavoro stiate facendo, e in altri ha senso dire al chiamante che sono responsabili di assicurarsi che i loro input siano validi e non di verificare. Quale estremità dello spettro stai dipendendo dipenderà dal tuo dominio problematico. La terza opzione ha un vantaggio significativo in quanto è possibile far sì che il compilatore imponga che il chiamante faccia tutto correttamente.

Se la terza opzione non è un'opzione, secondo me non importa davvero fino a quando non fallisce silenziosamente. Se il controllo di null non provoca l'esplosione istantanea del programma, va bene, ma se invece corrompe silenziosamente i dati per causare problemi lungo la strada, allora è meglio controllarli e gestirli subito e lì.


10
@aroth: qualsiasi progetto di qualsiasi cosa, incluso il software, avrà dei vincoli. L'atto stesso di progettare è selezionare dove vanno quei vincoli, e non è mia responsabilità né dovrebbe essere che accetto qualsiasi input e ci si aspetti che faccia qualcosa di utile. So se nullè sbagliato o non valido perché nella mia ipotetica libreria ho definito i tipi di dati e ho definito ciò che è valido e ciò che non lo è. Lo chiami "imposizione forzata" ma indovina un po ': è quello che fa ogni libreria di software esistente. Ci sono momenti in cui null è un valore valido, ma non è "sempre".
whatsisname

4
"che il tuo codice è rotto perché non gestisce nullcorrettamente" - Questo è un fraintendimento di cosa significhi una corretta gestione. Per la maggior parte dei programmatori Java, significa generare un'eccezione per qualsiasi input non valido. Usando @ParametersAreNonnullByDefault, significa anche per ogni null, a meno che l'argomento non sia contrassegnato come @Nullable. Fare qualsiasi altra cosa significa allungare il percorso fino a quando non soffia finalmente altrove e quindi allungare anche il tempo sprecato nel debug. Bene, ignorando i problemi, potresti ottenere che non soffia mai, ma un comportamento scorretto è quindi abbastanza garantito.
maaartinus,

4
@aroth Caro Signore, odio lavorare con qualsiasi codice tu scriva. Le esplosioni sono infinitamente migliori del fare qualcosa di insensato, che è l'unica altra opzione per input non validi. Le eccezioni mi costringono, il chiamante, a decidere quale sia la cosa giusta da fare in quei casi, quando il metodo non può indovinare ciò che intendevo. Le eccezioni controllate e l'estensione non necessaria Exceptionsono dibattiti completamente indipendenti. (Sono d'accordo che entrambi sono problematici.) Per questo esempio specifico, nullovviamente non ha senso qui se lo scopo della classe è invocare metodi sull'oggetto.
jpmc26

3
"il tuo codice non dovrebbe forzare le assunzioni nel mondo del chiamante" - È impossibile non forzare le assunzioni al chiamante. Ecco perché dobbiamo rendere queste ipotesi il più esplicite possibile.
Diogo Kollross,

3
@aroth il tuo codice è rotto perché passa il mio codice a null quando dovrebbe passare qualcosa che ha Name come proprietà. Qualsiasi comportamento diverso dall'esplosione è una sorta di versione backdoor del mondo bizzarro di IMHO di digitazione dinamica.
mbrig

56

Poiché _someEntitypuò essere modificato in qualsiasi fase, ha senso testarlo ogni volta che SetNameviene chiamato. Dopotutto, avrebbe potuto cambiare dall'ultima volta in cui è stato chiamato quel metodo. Tuttavia, tieni presente che il codice in SetNamenon è thread-safe, quindi puoi eseguire quel controllo in un thread, averlo _someEntityimpostato su nullun altro e quindi il codice verrà visualizzato NullReferenceExceptioncomunque.

Un approccio a questo problema è quindi quello di diventare ancora più difensivo ed eseguire una delle seguenti operazioni:

  1. creare una copia locale _someEntitye fare riferimento a tale copia tramite il metodo,
  2. usa un lock around _someEntityper assicurarti che non cambi mentre il metodo viene eseguito,
  3. usa a try/catched esegui la stessa azione su qualsiasi NullReferenceExceptioncosa si verifichi all'interno del metodo come faresti nel controllo null iniziale (in questo caso, nopun'azione).

Ma quello che dovresti davvero fare è fermarti e porti la domanda: devi davvero consentire _someEntitydi essere sovrascritto? Perché non impostarlo una volta, tramite il costruttore. In questo modo, devi sempre e solo fare quel controllo null una volta:

private readonly Entity _someEntity;

public Constructor(Entity someEntity)
{
    if (someEntity == null) throw new ArgumentNullException(nameof(someEntity));
    _someEntity = someEntity;
}

public void SetName(string name)
{
    _someEntity.Name = name;
    label.SetText(_someEntity.Name);
}

Se possibile, questo è l'approccio che consiglierei di adottare in tali situazioni. Se non riesci a tenere presente la sicurezza dei thread quando scegli come gestire eventuali null.

Come commento bonus, ritengo che il tuo codice sia strano e potrebbe essere migliorato. Tutto di:

private Entity _someEntity;
public Entity SomeEntity => _someEntity;

public void AssignEntity(Entity entity){
    _someEntity = entity;
}

può essere sostituito con:

public Entity SomeEntity { get; set; }

Che ha le stesse funzionalità, salvo per poter impostare il campo tramite il setter proprietà, piuttosto che un metodo con un nome diverso.


5
Perché questo risponde alla domanda? La domanda riguarda il controllo null e si tratta praticamente di una revisione del codice.
IEatBagels

17
@TopinFrassi spiega che l'attuale approccio al controllo null non è thread-safe. Tocca poi su altre tecniche di programmazione difensiva per aggirare questo. Quindi si conclude con il suggerimento di utilizzare l'immutabilità per evitare la necessità di avere quella programmazione difensiva in primo luogo. E come bonus aggiuntivo, offre consigli su come strutturare meglio il codice.
David Arno,

23

Ma il metodo non dovrebbe avere questo controllo?

Questa è la tua scelta

Creando un publicmetodo, offri al pubblico l'opportunità di chiamarlo. Ciò comporta sempre un contratto implicito su come chiamare quel metodo e cosa aspettarsi quando lo si fa. Questo contratto può includere (o meno) " sì, uhhm, se passi nullcome valore di parametro, esploderà proprio in faccia ". Altre parti di quel contratto potrebbero essere " oh, BTW: ogni volta che due thread eseguono questo metodo contemporaneamente, un gattino muore ".

Quale tipo di contratto vuoi progettare dipende da te. Le cose importanti sono

  • creare un'API utile, coerente e coerente

  • per documentarlo bene, specialmente per quei casi limite.

Quali sono i casi probabili in cui viene chiamato il tuo metodo?


Bene, la sicurezza dei thread è l'eccezione, non la regola, in caso di accesso simultaneo. Pertanto, l'uso dello stato nascosto / globale è documentato, così come la concorrenza è comunque sicura.
Deduplicatore,

14

Le altre risposte sono buone; Vorrei estenderli facendo alcuni commenti sui modificatori dell'accessibilità. Innanzitutto, cosa fare se nullnon è mai valido:

  • i metodi pubblici e protetti sono quelli in cui non si controlla il chiamante. Dovrebbero lanciare quando superato null. In questo modo addestrate i vostri chiamanti a non passarvi mai nulla, perché vorrebbero che il loro programma non si arrestasse in modo anomalo.

  • interni e privati metodi sono quelli in cui si fa controllare il chiamante. Dovrebbero affermare quando viene passato null; probabilmente si arresteranno in seguito più tardi, ma almeno hai prima ottenuto l'asserzione e un'opportunità per entrare nel debugger. Ancora una volta, vuoi addestrare i tuoi chiamanti a chiamarti correttamente facendogli male quando non lo fanno.

Ora, cosa fai se potrebbe essere valido per il passaggio di un null? Eviterei questa situazione con il modello di oggetti null . Cioè: crea un'istanza speciale del tipo e richiedi che venga utilizzata ogni volta che è necessario un oggetto non valido . Ora sei tornato nel piacevole mondo del lancio / asserzione di ogni uso di null.

Ad esempio, non vuoi essere in questa situazione:

class Person { ... }
...
class ExpenseReport { 
  public void SubmitReport(Person approver) { 
    if (approver == null) ... do something ...

e sul sito di chiamata:

// I guess this works but I don't know what it means
expenses.SubmitReport(null); 

Perché (1) stai insegnando ai tuoi chiamanti che null è un buon valore e (2) non è chiaro quale sia la semantica di quel valore. Piuttosto, fai questo:

class ExpenseReport { 
  public static readonly Person ApproverNotRequired = new Person();
  public static readonly Person ApproverUnknown = new Person();
  ...
  public void SubmitReport(Person approver) { 
    if (approver == null) throw ...
    if (approver == ApproverNotRequired) ... do something ...
    if (approver == ApproverUnknown) ... do something ...

E ora il sito di chiamata è simile al seguente:

expenses.SubmitReport(ExpenseReport.ApproverNotRequired);

e ora il lettore del codice sa cosa significa.


Meglio ancora, non avere una catena di ifs per gli oggetti null ma piuttosto usare il polimorfismo per farli comportare correttamente.
Konrad Rudolph,

@KonradRudolph: In effetti, questa è spesso una buona tecnica. Mi piace usarlo per le strutture di dati, in cui EmptyQueuecreo un tipo che genera quando viene rimosso dalla coda, e così via.
Eric Lippert,

@EricLippert - Perdonami perché sto ancora imparando qui, ma supponi che se la Personclasse fosse immutabile e non contenesse un costruttore a argomento zero, come costruiresti la tua ApproverNotRequiredo le tue ApproverUnknownistanze? In altre parole, quali valori passeresti al costruttore?

2
Adoro imbatterti nelle tue risposte / commenti su SE, sono sempre perspicaci. Non appena ho visto affermare i metodi interni / privati ​​ho fatto scorrere verso il basso aspettandomi che fosse una risposta di Eric Lippert, non sono rimasto deluso :)
bob esponja

4
Ragazzi, state pensando troppo all'esempio specifico che ho dato. Si intendeva far passare rapidamente l'idea del modello di oggetti null. Non intendeva essere un esempio di buona progettazione in un sistema di automazione di documenti e buste paga. In un sistema reale, rendere "approvatore" il tipo "persona" è probabilmente una cattiva idea perché non corrisponde al processo aziendale; potrebbe non esserci alcun approvatore, potresti averne bisogno diversi e così via. Come noto spesso quando rispondo a domande in quel dominio, ciò che vogliamo veramente è un oggetto politico . Non rimanere bloccato nell'esempio.
Eric Lippert,

8

Le altre risposte sottolineano che il tuo codice può essere ripulito per non aver bisogno di un controllo nullo dove lo hai, tuttavia, per una risposta generale su cosa può essere un utile controllo null per considerare il seguente codice di esempio:

public static class Foo {
    public static void Frob(Bar a, Bar b) {
        if (a == null) throw new ArgumentNullException(nameof(a));
        if (b == null) throw new ArgumentNullException(nameof(b));

        a.Something = b.Something;
    }
}

Questo ti dà un vantaggio per la manutenzione. Se si verifica mai un difetto nel codice in cui qualcosa passa un oggetto null al metodo, otterrai informazioni utili dal metodo su quale parametro era null. Senza i controlli, otterrai una NullReferenceException sulla linea:

a.Something = b.Something;

E (dato che la proprietà Something è un tipo di valore) a o b potrebbero essere nulli e non si avrà modo di sapere quale dalla traccia dello stack. Con i controlli, saprai esattamente quale oggetto era nullo, il che può essere enormemente utile quando si tenta di eliminare un difetto.

Vorrei notare che il modello nel tuo codice originale:

if (x == null) return;

Può avere i suoi usi in determinate circostanze, può anche (possibilmente più spesso) darti un ottimo modo per mascherare i problemi sottostanti nella tua base di codice.


4

No per niente, ma dipende.

Esegui un controllo di riferimento null solo se vuoi reagire.

Se si esegue un controllo di riferimento null e si genera un'eccezione controllata , si forza l'utente del metodo a reagire su di esso e gli si consente di recuperare. Il programma funziona ancora come previsto.

Se non si esegue un controllo di riferimento null (o si genera un'eccezione non selezionata), è possibile che venga generato un controllo non controllato NullReferenceException, che di solito non viene gestito dall'utente del metodo e che potrebbe addirittura chiudere l'applicazione. Ciò significa che la funzione è ovviamente completamente fraintesa e quindi l'applicazione è difettosa.


4

Qualcosa di un'estensione alla risposta di @ null, ma nella mia esperienza, eseguo controlli null in ogni caso.

Una buona programmazione difensiva è l'atteggiamento secondo cui si codifica la routine corrente isolatamente, supponendo che l'intero resto del programma stia cercando di bloccarlo. Quando stai scrivendo un codice mission critical in cui il fallimento non è un'opzione, questo è praticamente l'unico modo per andare.

Ora, come gestisci effettivamente un nullvalore, dipende molto dal tuo caso d'uso.

Se nullè un valore accettabile, ad es. Uno qualsiasi dei parametri dal secondo al quinto alla routine MSVC _splitpath (), quindi gestirlo silenziosamente è il comportamento corretto.

Se nullnon dovesse mai accadere quando si chiama la routine in questione, vale a dire nel caso del PO il contratto API AssignEntity()deve essere stato chiamato con un valido, Entityquindi lanciare un'eccezione o altrimenti fallire assicurando di fare molto rumore nel processo.

Non preoccuparti mai del costo di un controllo nullo, sono sporchi a buon mercato se accadono e con i compilatori moderni, l'analisi del codice statico può e li elimina completamente se il compilatore determina che non accadrà.


O evitarlo del tutto (48 minuti e 29 secondi: "Mai e poi mai usare un puntatore nullo in nessun punto del programma" ).
Peter Mortensen,
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.