Perché .NET foreach loop genera NullRefException quando la raccolta è nulla?


231

Quindi mi imbatto spesso in questa situazione ... dove Do.Something(...)restituisce una raccolta nulla, in questo modo:

int[] returnArray = Do.Something(...);

Quindi, provo a usare questa raccolta in questo modo:

foreach (int i in returnArray)
{
    // do some more stuff
}

Sono solo curioso, perché un ciclo foreach non può operare su una collezione null? Mi sembra logico che 0 iterazioni vengano eseguite con una collezione null ... invece genera a NullReferenceException. Qualcuno sa perché questo potrebbe essere?

Questo è fastidioso poiché sto lavorando con API che non sono chiare su esattamente cosa restituiscono, quindi finisco if (someCollection != null)ovunque ...

Modifica: grazie a tutti per aver spiegato che foreachusa GetEnumeratore se non c'è un enumeratore da ottenere, la foreach fallirebbe. Immagino che sto chiedendo perché la lingua / runtime non possano o non facciano un controllo nullo prima di afferrare l'enumeratore. Mi sembra che il comportamento sarebbe ancora ben definito.


1
Qualcosa non va nel chiamare un array una collezione. Ma forse sono solo vecchia scuola.
Robaticus,

Sì, sono d'accordo ... Non sono nemmeno sicuro del motivo per cui così tanti metodi in questa base di codice restituiscono array x_x
Polaris878

4
Suppongo che con lo stesso ragionamento sarebbe ben definito che tutte le istruzioni in C # diventino no-op quando viene dato un nullvalore. Lo stai suggerendo anche solo per foreachloop o altre dichiarazioni?
Ken,

7
@ Ken ... Sto solo pensando a dei loop, perché per me sembra al programmatore che non succederebbe nulla se la collezione fosse vuota o inesistente
Polaris878,

Risposte:


251

Bene, la risposta breve è "perché è così che lo hanno progettato i progettisti del compilatore". Realisticamente, tuttavia, l'oggetto della raccolta è nullo, quindi non c'è modo per il compilatore di far scorrere l'enumeratore attraverso la raccolta.

Se hai davvero bisogno di fare qualcosa del genere, prova l'operatore di coalescenza null:

int[] array = null;

foreach (int i in array ?? Enumerable.Empty<int>())
{
   System.Console.WriteLine(string.Format("{0}", i));
}

3
Per favore, scusa la mia ignoranza, ma è efficace? Non si traduce in un confronto su ogni iterazione?
user919426

20
Io non ci credo. Osservando l'IL generato, il ciclo segue il confronto is null.
Robaticus,

10
Holy necro ... A volte devi guardare l'IL per vedere cosa sta facendo il compilatore per capire se ci sono hit di efficienza. L'utente 919426 aveva chiesto se ha eseguito il controllo per ogni iterazione. Sebbene la risposta possa essere ovvia per alcune persone, non è ovvio per tutti e fornire il suggerimento che guardare l'IL ti dirà cosa sta facendo il compilatore, aiuta le persone a pescare da sole in futuro.
Robaticus,

2
@Robaticus (anche perché in seguito) l'IL sembra tale perché la specifica lo dice. L'espansione dello zucchero sintattico (noto anche come foreach) consiste nel valutare l'espressione sul lato destro di "in" e invocare GetEnumeratoril risultato
Rune FS

2
@RuneFS - esattamente. Comprendere le specifiche o guardare l'IL è un modo per capire il "perché". O per valutare se due diversi approcci C # si riducono allo stesso IL. Questo era essenzialmente il mio punto su Shimmy sopra.
Robaticus,

148

Un foreachciclo chiama il GetEnumeratormetodo.
Se la raccolta è null, questa chiamata al metodo si traduce in a NullReferenceException.

È una cattiva pratica restituire una nullraccolta; i tuoi metodi dovrebbero invece restituire una raccolta vuota.


7
Sono d'accordo, le raccolte vuote dovrebbero sempre essere restituite ... tuttavia non ho scritto questi metodi :)
Polaris878,

19
@Polaris, operatore null coalescente in soccorso! int[] returnArray = Do.Something() ?? new int[] {};
JSB ձոգչ

2
Oppure: ... ?? new int[0].
Ken,

3
+1 Come la punta di restituire raccolte vuote invece di null. Grazie.
Galilyou,

1
Non sono d'accordo su una cattiva pratica: vedi ⇒ se una funzione fallisce potrebbe restituire una raccolta vuota: è una chiamata al costruttore, l'allocazione della memoria e forse un mucchio di un codice da eseguire. O potresti semplicemente restituire «null» → ovviamente c'è solo un codice da restituire e un codice molto corto da verificare è l'argomento «null». È solo uno spettacolo.
Ciao Angelo

47

C'è una grande differenza tra una raccolta vuota e un riferimento null a una raccolta.

Quando si utilizza foreach, internamente, si chiama il metodo GetEnumerator () di IEnumerable . Quando il riferimento è null, ciò solleverà questa eccezione.

Tuttavia, è perfettamente valido avere un vuoto IEnumerableo IEnumerable<T>. In questo caso, foreach non "itererà" su nulla (poiché la raccolta è vuota), ma non verrà generata, poiché si tratta di uno scenario perfettamente valido.


Modificare:

Personalmente, se hai bisogno di aggirare questo, ti consiglierei un metodo di estensione:

public static IEnumerable<T> AsNotNull<T>(this IEnumerable<T> original)
{
     return original ?? Enumerable.Empty<T>();
}

Quindi puoi semplicemente chiamare:

foreach (int i in returnArray.AsNotNull())
{
    // do some more stuff
}

3
Sì, ma PERCHÉ non cercare un controllo null prima di ottenere l'enumeratore?
Polaris878,

12
@ Polaris878: perché non è mai stato progettato per essere utilizzato con una raccolta null. Questo è, IMO, una buona cosa - dal momento che un riferimento null e una raccolta vuota dovrebbero essere trattati separatamente. Se vuoi aggirare questo problema, ci sono modi ... Modificherò per mostrare un'altra opzione ...
Reed Copsey,

1
@ Polaris878: Suggerirei di riformulare la tua domanda: "Perché DOVREBBE il runtime fare un controllo null prima di ottenere l'enumeratore?"
Reed Copsey,

Immagino che sto chiedendo "perché no?" lol sembra che il comportamento sarebbe ancora ben definito
Polaris878,

2
@ Polaris878: suppongo che, a mio avviso, restituire null per una raccolta sia un errore. Per come è adesso, il runtime ti dà un'eccezione significativa in questo caso, ma è facile aggirare (cioè: sopra) se non ti piace questo comportamento. Se il compilatore ti nascondesse questo, perderesti l'errore durante il runtime, ma non ci sarebbe modo di "spegnerlo" ...
Reed Copsey,

12

Si sta rispondendo molto tempo fa, ma ho provato a farlo nel modo seguente per evitare l'eccezione del puntatore null e può essere utile per qualcuno che utilizza l'operatore di controllo null C #.

     //fragments is a list which can be null
     fragments?.ForEach((obj) =>
        {
            //do something with obj
        });

@kjbartel ti ha battuto per oltre un anno (su " stackoverflow.com/a/32134295/401246 "). ;) Questa è la soluzione migliore, perché non: a) comporta un degrado delle prestazioni (anche quando non null) generalizzando l'intero loop sul display LCD di Enumerable(come usando ??sarebbe), b) richiede l'aggiunta di un metodo di estensione a ogni progetto, e c) richiedono di evitare null IEnumerables (Pffft! Puh-LEAZE! SMH.) per cominciare.
Tom,

10

Un altro metodo di estensione per aggirare questo:

public static void ForEach<T>(this IEnumerable<T> items, Action<T> action)
{
    if(items == null) return;
    foreach (var item in items) action(item);
}

Consumare in diversi modi:

(1) con un metodo che accetta T:

returnArray.ForEach(Console.WriteLine);

(2) con un'espressione:

returnArray.ForEach(i => UpdateStatus(string.Format("{0}% complete", i)));

(3) con un metodo anonimo multilinea

int toCompare = 10;
returnArray.ForEach(i =>
{
    var thisInt = i;
    var next = i++;
    if(next > 10) Console.WriteLine("Match: {0}", i);
});

Manca solo una parentesi di chiusura nel terzo esempio. Altrimenti, bellissimo codice che può essere esteso ulteriormente in modi interessanti (per loop, inversione, salto, ecc.). Grazie per la condivisione.
Lara,

Grazie per un codice così meraviglioso, ma non ho capito i primi metodi, perché passi console.writeline come parametro, anche se sta stampando gli elementi dell'array. Ma non ho capito
Ajay Singh,

@AjaySingh Console.WriteLineè solo un esempio di un metodo che accetta un argomento (an Action<T>). Le voci 1, 2 e 3 mostrano esempi di passaggio di funzioni al .ForEachmetodo di estensione.
Jay,

@ risposta di kjbartel (a " stackoverflow.com/a/32134295/401246 " è la soluzione migliore, perché non è così: a) comportano una riduzione delle prestazioni di (anche quando non null) generalizzare l'intero ciclo per il display LCD di Enumerable(come l'utilizzo ??farebbe ), b) richiedere l'aggiunta di un metodo di estensione a ogni progetto, oppure c) richiedere di evitare null IEnumerables (Pffft! Puh-LEAZE! SMH.) per iniziare con (cuz, nullsignifica N / A, mentre l'elenco vuoto significa, è appl. ma è attualmente, beh, vuoto !, ovvero un Empl. potrebbe avere Commissioni N / A per non Vendite o vuote per Vendite).
Tom,

5

Basta scrivere un metodo di estensione per aiutarti:

public static class Extensions
{
   public static void ForEachWithNull<T>(this IEnumerable<T> source, Action<T> action)
   {
      if(source == null)
      {
         return;
      }

      foreach(var item in source)
      {
         action(item);
      }
   }
}

5

Perché una raccolta nulla non è la stessa cosa di una raccolta vuota. Una raccolta vuota è un oggetto raccolta senza elementi; una raccolta nulla è un oggetto inesistente.

Ecco qualcosa da provare: dichiarare due raccolte di qualsiasi tipo. Inizializzane uno normalmente in modo che sia vuoto e assegna all'altro il valore null. Quindi prova ad aggiungere un oggetto ad entrambe le raccolte e guarda cosa succede.


3

È colpa di Do.Something(). La migliore procedura qui sarebbe quella di restituire un array di dimensioni 0 (che è possibile) invece di un valore nullo.


2

Perché dietro le quinte foreachacquisisce un enumeratore, equivalente a questo:

using (IEnumerator<int> enumerator = returnArray.getEnumerator()) {
    while (enumerator.MoveNext()) {
        int i = enumerator.Current;
        // do some more stuff
    }
}

2
così? Perché non può semplicemente controllare se è prima null e saltare il ciclo? AKA, esattamente cosa viene mostrato nei metodi di estensione? La domanda è: è meglio saltare di default il loop se null o generare un'eccezione? Penso che sia meglio saltare! Sembra probabile che i contenitori null debbano essere ignorati piuttosto che sottoposti a loop poiché i loop sono pensati per fare qualcosa SE il container non è null.
AbstractDissonance,

@AbstractDissonance Potresti argomentare lo stesso con tutti i nullriferimenti, ad esempio quando accedi ai membri. In genere si tratta di un errore e, in caso contrario, è abbastanza semplice gestirlo ad esempio con il metodo di estensione che un altro utente ha fornito come risposta.
Lucero,

1
Io non la penso così. Il foreach è pensato per operare sulla raccolta ed è diverso dal riferimento diretto a un oggetto null. Mentre uno potrebbe obiettare lo stesso, scommetto che se analizzi tutto il codice del mondo, avresti la maggior parte dei loop foreach con controlli null di qualche tipo davanti a loro solo per bypassare il loop quando la raccolta è "null" (che è quindi trattato come vuoto). Non penso che nessuno consideri il looping su una collezione null come qualcosa che vogliono e preferirebbe semplicemente ignorare il loop se la collezione è null. Forse, piuttosto, un foreach? (Var x in C) potrebbe essere usato.
AbstractDissonance,

Il punto che sto principalmente cercando di sottolineare è che crea un po 'di rifiuti nel codice poiché uno deve controllare ogni volta senza una buona ragione. Le estensioni, ovviamente, funzionano ma è possibile aggiungere una funzionalità linguistica per evitare queste cose senza troppi problemi. (principalmente penso che l'attuale metodo produca bug nascosti poiché il programmatore potrebbe dimenticare di mettere il controllo e quindi un'eccezione ... perché o si aspetta che il controllo si verifichi da qualche altra parte prima del ciclo o sta pensando che sia stato pre-inizializzato (che potrebbe o potrebbe essere cambiato). Ma in entrambe le cause, il comportamento sarebbe lo stesso di se vuoto.
AbstractDissonance

@AbstractDissonance Bene, con qualche corretta analisi statica sai dove potresti avere valori nulli e dove no. Se ottieni un valore nullo dove non te lo aspetti, è meglio fallire invece di ignorare silenziosamente i problemi IMHO (nello spirito del fallimento veloce ). Pertanto ritengo che questo sia il comportamento corretto.
Lucero,

1

Penso che la spiegazione del perché venga generata un'eccezione sia molto chiara con le risposte fornite qui. Vorrei solo integrare il modo in cui lavoro abitualmente con queste collezioni. Perché, alcune volte, utilizzo la raccolta più di una volta e devo verificare se ogni volta è null. Per evitare ciò, faccio quanto segue:

    var returnArray = DoSomething() ?? Enumerable.Empty<int>();

    foreach (int i in returnArray)
    {
        // do some more stuff
    }

In questo modo possiamo usare la raccolta quanto vogliamo senza temere l'eccezione e non inquiniamo il codice con dichiarazioni condizionali eccessive.

L'uso dell'operatore null check ?.è anche un ottimo approccio. Ma, nel caso di array (come nell'esempio nella domanda), dovrebbe essere trasformato in Elenco prima di:

    int[] returnArray = DoSomething();

    returnArray?.ToList().ForEach((i) =>
    {
        // do some more stuff
    });

2
La conversione in un elenco solo per avere accesso al ForEachmetodo è una delle cose che odio in una base di codice.
huysentruitw,

Sono d'accordo ... lo evito il più possibile. :(
Alielson Piffer,

-2
SPListItem item;
DataRow dr = datatable.NewRow();

dr["ID"] = (!Object.Equals(item["ID"], null)) ? item["ID"].ToString() : string.Empty;
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.