È irragionevole aspettarsi che Any () * non * lanci un'eccezione di riferimento nulla?

41

Quando crei un metodo di estensione puoi, ovviamente, chiamarlo su null . Ma, a differenza di una chiamata al metodo di istanza, chiamandola su null non hai per lanciare una NullReferenceException - > devi controllare e lanciare manualmente.

Per l'implementazione del metodo di estensione Linq Any() Microsoft ha deciso che dovrebbero lanciare un ArgumentNullException ( link ).

Mi irrita dover scrivere if( myCollection != null && myCollection.Any() )

Mi sbaglio, come cliente di questo codice, per aspettarmi che ad es. ((int[])null).Any() dovrebbe restituire false ?

    
posta thisextendsthat 18.09.2018 - 17:09
fonte

13 risposte

152

Ho una borsa con cinque patate in essa. Ci sono .Any() di patate nel sacchetto?

"," dici. <= true

Prendo tutte le patate fuori e le mangio. Ci sono .Any() di patate nel sacchetto?

" No ," dici. <= false

Incenerisco completamente la borsa in un incendio. Ci sono% di% di patate nella borsa ora?

" Non ci sono borse ." .Any()

    
risposta data 18.09.2018 - 18:39
fonte
52

Innanzitutto, sembra che il codice sorgente genererà ArgumentNullException , non NullReferenceException .

Detto questo, in molti casi sai già che la tua raccolta non è nullo, perché questo codice viene chiamato solo dal codice che sa che la raccolta esiste già, quindi non dovrai mettere il controllo Null dentro molto spesso. Ma se non sai che esiste, allora ha senso controllare prima di chiamare Any() su di esso.

Am I wrong, as a client of this code, to expect that e.g. ((int[])null).Any() should return false?

Sì. La domanda che Any() risponde è "questa raccolta contiene elementi?" Se questa raccolta non esiste, allora la domanda stessa è priva di senso; non può né contenere né contenere nulla, perché non esiste.

    
risposta data 18.09.2018 - 17:23
fonte
23

Null significa informazione mancante, non nessun elemento.

Potresti considerare più ampiamente evitando null - ad esempio, usa uno degli enumerabili vuoti incorporati per rappresentare una raccolta senza elementi invece di null.

Se si restituisce null in alcune circostanze, è possibile modificarlo per restituire la raccolta vuota. (Altrimenti, se stai trovando i valori nulli restituiti dai metodi di libreria (non i tuoi), è sfortunato, e li avrei avvolti per normalizzarli.)

Vedi anche link

    
risposta data 18.09.2018 - 17:59
fonte
13

A parte la sintassi null-condizionale , esiste un'altra tecnica per alleviare questo problema: non lasciare che la tua variabile rimane sempre null .

Considera una funzione che accetta una raccolta come parametro. Se per gli scopi della funzione, null e vuoto sono equivalenti, puoi assicurarti che non contenga mai null all'inizio:

public MyResult DoSomething(int a, IEnumerable<string> words)
{
    words = words ?? Enumerable.Empty<string>();

    if (!words.Any())
    {
        ...

Puoi fare lo stesso quando recuperi le collezioni da qualche altro metodo:

var words = GetWords() ?? Enumerable.Empty<string>();

(Si noti che nei casi in cui si ha il controllo su una funzione come GetWords e null è equivalente alla raccolta vuota, è preferibile semplicemente restituire la raccolta vuota in primo luogo.)

Ora puoi eseguire qualsiasi operazione sulla tua collezione. Ciò è particolarmente utile se è necessario eseguire molte operazioni che potrebbero fallire quando la raccolta è null e nei casi in cui si ottiene lo stesso risultato eseguendo il loop o interrogando un'enumerabile vuota, si consentirà di eliminare completamente le condizioni if .

    
risposta data 19.09.2018 - 05:32
fonte
12

Am I wrong, as a client of this code, to expect that e.g. ((int[])null).Any() should return false?

Sì, semplicemente perché sei in C # e quel comportamento è ben definito e documentato.

Se si stesse creando la propria libreria o se si stesse utilizzando una lingua diversa con una cultura delle eccezioni diversa, sarebbe più ragionevole aspettarsi false.

Personalmente ritengo che restituire false sia un approccio più sicuro che rende il tuo programma più robusto, ma almeno è discutibile.

    
risposta data 18.09.2018 - 18:52
fonte
10

Se i controlli null ripetuti ti infastidiscono, puoi creare il tuo metodo di estensione 'IsNullOrEmpty ()', per rispecchiare il metodo String con quel nome e avvolgere sia il controllo null che la chiamata .Any () in un chiamata singola.

Altrimenti, la soluzione, citata da @ 17 di 26 in un commento sotto la tua domanda, è più corta del metodo 'standard', e ragionevolmente chiara a chiunque abbia familiarità con la nuova sintassi null-condizionale.

if(myCollection?.Any() == true)
    
risposta data 18.09.2018 - 19:59
fonte
8

Am I wrong, as a client of this code, to expect that e.g. ((int[])null).Any() should return false?

Se ti preoccupi delle aspettative, devi pensare alle intenzioni.

null significa qualcosa di molto diverso da Enumerable.Empty<T>

Come menzionato nella risposta di Erik Eidt , c'è una differenza di significato tra null e una raccolta vuota .

Diamo prima un'occhiata a come dovrebbero essere usati.

Il libro < em> Linee guida per la progettazione di framework: convenzioni, idiomi e schemi per le librerie .NET riutilizzabili, 2a edizione scritte da architetti Microsoft Krzysztof Cwalina e Brad Abrams afferma la seguente best practice:

X DO NOT return null values from collection properties or from methods returning collections. Return an empty collection or an empty array instead.

Considera la tua chiamata a un metodo che alla fine ottiene i dati da un database: se ricevi un array vuoto o Enumerable.Empty<T> questo significa semplicemente che lo spazio campione è vuoto, cioè il tuo risultato è un set vuoto . La ricezione di null in questo contesto, tuttavia, significherebbe uno stato di errore .

Nella stessa linea di pensiero di l'analogia della patata di Dan Wilson , ha senso fare domande sui tuoi dati anche se è un set vuoto . Ma ha senso molto meno , se non ci sono set .

    
risposta data 19.09.2018 - 10:08
fonte
3

Ci sono molte risposte che spiegano perché null e empty sono differenti e abbastanza opinioni che provano a spiegare sia perché dovrebbero essere trattati in modo diverso oppure no. Comunque stai chiedendo:

Am I wrong, as a client of this code, to expect that e.g. ((int[])null).Any() should return false?

È un'aspettativa perfettamente ragionevole . Sei come giusto come qualcun altro che difende il comportamento corrente. Sono d'accordo con la filosofia dell'attuazione corrente, ma i fattori guida non sono - solo - basati su considerazioni fuori dal contesto.

Dato che Any() senza predicato è essenzialmente Count() > 0 , cosa ti aspetti da questo snippet?

List<int> list = null;
if (list.Count > 0) {}

O un generico:

List<int> list = null;
if (list.Foo()) {}

Suppongo che tu aspetti NullReferenceException .

  • Any() è un metodo di estensione, dovrebbe integrarsi facilmente con l'oggetto esteso , quindi lanciare un'eccezione è la cosa meno sorprendente.
  • Non tutti i linguaggi .NET supportano i metodi di estensione.
  • Puoi sempre chiamare Enumerable.Any(null) e ti aspetti sicuramente ArgumentNullException . È lo stesso metodo e deve essere coerente con - probabilmente - quasi TUTTO il resto del Framework.
  • L'accesso a un oggetto null è un errore di programmazione , il framework non dovrebbe imporre null come valore magico . Se la usi in questo modo, è tua responsabilità occupartene.
  • È supponente , pensi in un modo e io penso in un altro modo. Il framework dovrebbe essere il più libero possibile.
  • Se hai un caso speciale, devi essere coerente : devi prendere decisioni sempre più altamente motivate su tutti gli altri metodi di estensione: se Count() sembra una decisione facile allora Where() non è. Che dire di Max() ? Genera un'eccezione per un elenco EMPTY, non dovrebbe essere lanciato anche per un null uno?

Cosa hanno fatto i progettisti di libreria prima che LINQ introducesse metodi espliciti quando null è un valore valido (ad esempio String.IsNullOrEmpty() ) quindi DEVE essere coerente con la filosofia di progettazione esistente . Detto questo, anche se è piuttosto semplice scrivere, due metodi EmptyIfNull() e EmptyOrNull() potrebbero essere utili.

    
risposta data 19.09.2018 - 12:43
fonte
1

Jim dovrebbe lasciare le patate in ogni sacchetto. Altrimenti lo ucciderò.

Jim ha una borsa con cinque patate in essa. Ci sono .Any() di patate nel sacchetto?

"Sì", dici. < = true

Ok, così Jim vive questa volta.

Jim prende fuori tutte le patate e le mangia. Ci sono. () Patate nel sacchetto?

"No", dici. < = falso

Tempo di uccidere Jim.

Jim incenerisce completamente la borsa in un incendio. Ci sono. () Patate nel sacchetto adesso?

"Non c'è nessuna borsa". < = ArgumentNullException

Jim dovrebbe vivere o morire? Beh, non ce l'aspettavamo, quindi ho bisogno di una sentenza. Permettere a Jim di farla franca o no?

Puoi utilizzare le annotazioni per segnalare che non stai sopportando alcun tipo di shenanigans nullo in questo modo.

public bool Any( [NotNull] List bag ) 

Ma la tua catena di strumenti deve supportarlo. Il che significa che probabilmente continuerai a scrivere assegni.

    
risposta data 21.09.2018 - 01:52
fonte
0

Se ti infastidisce così tanto, ti suggerisco un semplice metodo di estensione.

static public IEnumerable<T> NullToEmpty<T>(this IEnumerable<T> source)
{
    return (source == null) ? Enumerable.Empty<T>() : source;
}

Ora puoi fare questo:

List<string> list = null;
var flag = list.NullToEmpty().Any( s => s == "Foo" );

... e il flag verrà impostato su false .

    
risposta data 20.09.2018 - 04:52
fonte
0

Questa è una domanda sui metodi di estensione di C # e sulla loro filosofia di progettazione, quindi penso che il modo migliore per rispondere a questa domanda sia citare La documentazione di MSDN ai fini dei metodi di estensione :

Extension methods enable you to "add" methods to existing types without creating a new derived type, recompiling, or otherwise modifying the original type. Extension methods are a special kind of static method, but they are called as if they were instance methods on the extended type. For client code written in C#, F# and Visual Basic, there is no apparent difference between calling an extension method and the methods that are actually defined in a type.

In general, we recommend that you implement extension methods sparingly and only when you have to. Whenever possible, client code that must extend an existing type should do so by creating a new type derived from the existing type. For more information, see Inheritance.

When using an extension method to extend a type whose source code you cannot change, you run the risk that a change in the implementation of the type will cause your extension method to break.

If you do implement extension methods for a given type, remember the following points:

  • An extension method will never be called if it has the same signature as a method defined in the type.
  • Extension methods are brought into scope at the namespace level. For example, if you have multiple static classes that contain extension methods in a single namespace named Extensions, they will all be brought into scope by the using Extensions; directive.

Per riassumere, i metodi di estensione sono progettati per aggiungere metodi di istanza a un tipo particolare, anche quando gli sviluppatori non possono farlo direttamente. E poiché i metodi di istanza sempre sovrascrivono i metodi di estensione se presenti (se chiamati usando la sintassi del metodo di istanza), questo dovrebbe solo essere eseguito se non è possibile aggiungere direttamente un metodo o estendere la classe . *

In altre parole, un metodo di estensione dovrebbe agire proprio come farebbe un metodo di istanza, poiché potrebbe finire per essere reso un metodo di istanza da qualche client. E perché un metodo di istanza dovrebbe essere lanciato se l'oggetto su cui è chiamato è null , quindi dovrebbe essere il metodo di estensione.

* Come nota a margine, questa è esattamente la situazione che i progettisti di LINQ hanno affrontato: quando è stato rilasciato C # 3.0, c'erano già milioni di client che utilizzavano System.Collections.IEnumerable e System.Collections.Generic.IEnumerable<T> , sia nelle loro raccolte che in foreach cicli. Queste classi hanno restituito IEnumerator oggetti che avevano solo i due metodi Current e MoveNext , quindi l'aggiunta di eventuali ulteriori metodi di istanza richiesti, come Count , Any , ecc., Infrangerebbe questi milioni di client. Quindi, per fornire questa funzionalità (soprattutto perché può essere implementata in termini di Current e MoveNext con relativa facilità), l'hanno rilasciato come metodi di estensione, che possono essere applicati a qualsiasi istanza IEnumerable esistente esistente e può anche essere implementato dalle classi in modi più efficienti. Se i progettisti di C # decidessero di rilasciare LINQ il primo giorno, sarebbero stati forniti come metodi di esempio di IEnumerable e probabilmente avrebbero progettato un qualche tipo di sistema per fornire implementazioni di interfaccia predefinite di tali metodi.

    
risposta data 21.09.2018 - 02:31
fonte
0

Penso che il punto saliente qui sia che restituendo false invece di lanciare un'eccezione, stai offuscando informazioni che potrebbero essere rilevanti per futuri lettori / modificatori del tuo codice.

Se è possibile che l'elenco sia nullo, suggerirei di avere un percorso logico separato per quello, altrimenti in futuro qualcuno potrebbe aggiungere una logica basata su elenchi (come un add) al else {} del tuo if, risultando in un'eccezione indesiderata che non avevano motivo di prevedere.

Readability and maintainability briscola 'Devo scrivere una condizione extra' ogni volta.

    
risposta data 21.09.2018 - 08:28
fonte
0

Come regola generale, scrivo la maggior parte del mio codice per supporre che il chiamante è responsabile di non consegnarmi dati nulli, principalmente per i motivi delineati in Dì No a Null (e altri post simili). Il concetto di null spesso non è ben compreso e, per questo motivo, è consigliabile inizializzare le variabili in anticipo, per quanto possibile. Supponendo che tu stia lavorando con un'API ragionevole, idealmente non dovresti mai ottenere un valore nullo, quindi non dovresti mai controllare nulla. Il punto nullo, come hanno affermato altre risposte, è quello di assicurarsi di avere un oggetto valido (ad esempio un "sacchetto") con cui lavorare prima di continuare. Questa non è una funzionalità di Any , ma una caratteristica della lingua stessa. Non è possibile eseguire la maggior parte delle operazioni su un oggetto nullo, perché non esiste. È come se ti chiedessi di andare al negozio con la mia macchina, tranne che non possiedo una macchina, quindi non hai modo di usare la mia auto per andare al negozio. È assurdo eseguire un'operazione su qualcosa che letteralmente non esiste.

Esistono casi pratici per l'utilizzo di null, ad esempio quando non hai letteralmente a disposizione le informazioni richieste (ad es. se ti ho chiesto quale sia la mia età, la scelta più probabile per te per rispondere a questo punto è "I don 'so', che è ciò che è un valore nullo). Tuttavia, nella maggior parte dei casi, dovresti almeno sapere se le tue variabili sono inizializzate o meno. E se non lo fai, ti consiglierei di stringere il tuo codice. La prima cosa che faccio in qualsiasi programma o funzione è inizializzare un valore prima di usarlo. È raro che debba controllare i valori nulli, perché posso garantire che le mie variabili non siano nulle. È una buona abitudine entrare e più frequentemente ti ricordi di inizializzare le tue variabili, meno frequentemente hai bisogno di verificare la nullità.

    
risposta data 22.09.2018 - 23:12
fonte

Leggi altre domande sui tag