¿Por qué el bucle foreach de .NET arroja NullRefException cuando la colección es nula?

231

Así que con frecuencia me encuentro con esta situación ... donde Do.Something(...)devuelve una colección nula, así:

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

Entonces, trato de usar esta colección así:

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

Solo tengo curiosidad, ¿por qué un bucle foreach no puede funcionar en una colección nula? Me parece lógico que 0 iteraciones se ejecuten con una colección nula ... en su lugar, arroja un NullReferenceException. Alguien sabe por qué esto podría ser?

Esto es molesto ya que estoy trabajando con API que no tienen claro exactamente lo que devuelven, así que termino en if (someCollection != null)todas partes ...

Editar: Gracias a todos por explicar esos foreachusos GetEnumeratory si no hay un enumerador para obtener, el foreach fallará. Supongo que estoy preguntando por qué el lenguaje / tiempo de ejecución no puede o no hará una comprobación nula antes de tomar el enumerador. Me parece que el comportamiento aún estaría bien definido.

Polaris878
fuente
1
Algo se siente mal al llamar a una matriz una colección. Pero tal vez solo soy de la vieja escuela.
Robaticus
Sí, estoy de acuerdo ... Ni siquiera estoy seguro de por qué tantos métodos en esta base de código devuelven matrices x_x
Polaris878
44
Supongo que por el mismo razonamiento estaría bien definido que todas las declaraciones en C # se conviertan en no-ops cuando se les dé un nullvalor. ¿Sugieres esto solo para foreachbucles u otras declaraciones?
Ken
77
@ Ken ... Estoy pensando en los bucles foreach, porque para mí es evidente para el programador que no pasaría nada si la colección está vacía o no existe
Polaris878

Respuestas:

251

Bueno, la respuesta corta es "porque así lo diseñaron los diseñadores del compilador". Sin embargo, de manera realista, su objeto de colección es nulo, por lo que no hay forma de que el compilador haga que el enumerador recorra la colección.

Si realmente necesita hacer algo como esto, pruebe el operador de fusión nula:

int[] array = null;

foreach (int i in array ?? Enumerable.Empty<int>())
{
   System.Console.WriteLine(string.Format("{0}", i));
}
Robaticus
fuente
3
Disculpe mi ignorancia, pero ¿es esto eficiente? ¿No resulta en una comparación en cada iteración?
user919426
20
No lo creo Mirando la IL generada, el bucle está después de la comparación nula.
Robaticus
10
Holy necro ... A veces tienes que mirar el IL para ver qué está haciendo el compilador para descubrir si hay algún golpe de eficiencia. User919426 había preguntado si realizó la verificación para cada iteración. Aunque la respuesta puede ser obvia para algunas personas, no lo es para todos, y dar la pista de que mirar el IL le dirá qué está haciendo el compilador, ayuda a las personas a pescar por sí mismas en el futuro.
Robaticus
2
@Robaticus (incluso por qué más tarde), el IL parece ese porque porque la especificación lo dice. La expansión del azúcar sintáctico (también conocido como foreach) es evaluar la expresión en el lado derecho de "in" e invocar GetEnumeratorel resultado
Rune FS
2
@RuneFS - exactamente. Comprender la especificación o mirar el IL es una forma de descubrir el "por qué". O para evaluar si dos enfoques diferentes de C # se reducen a la misma IL. Ese fue, esencialmente, mi punto de Shimmy arriba.
Robaticus
148

Un foreachbucle llama al GetEnumeratormétodo.
Si la colección es null, esta llamada al método da como resultado a NullReferenceException.

Es una mala práctica devolver una nullcolección; sus métodos deberían devolver una colección vacía en su lugar.

SLaks
fuente
77
Estoy de acuerdo, las colecciones vacías siempre deben devolverse ... sin embargo, no escribí estos métodos :)
Polaris878
19
@Polaris, ¡operador nulo que se une al rescate! int[] returnArray = Do.Something() ?? new int[] {};
JSB ձոգչ
2
O: ... ?? new int[0].
Ken
3
+1 Como la punta de devolver colecciones vacías en lugar de nulas. Gracias.
Galilyou
1
No estoy de acuerdo con una mala práctica: vea ⇒ si una función falló, podría devolver una colección vacía: es una llamada al constructor, la asignación de memoria y quizás un montón de código para ejecutar. O bien, podría devolver "nulo" → obviamente, solo hay un código para devolver y un código muy breve para verificar es que el argumento es "nulo". Es solo una actuación.
Hola Ángel
47

Hay una gran diferencia entre una colección vacía y una referencia nula a una colección.

Cuando usa foreach, internamente, esto llama al método GetEnumerator () de IEnumerable . Cuando la referencia es nula, esto generará esta excepción.

Sin embargo, es perfectamente válido tener un IEnumerableo vacío IEnumerable<T>. En este caso, foreach no "iterará" sobre nada (ya que la colección está vacía), pero tampoco lanzará, ya que este es un escenario perfectamente válido.


Editar:

Personalmente, si necesita solucionar esto, le recomiendo un método de extensión:

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

Entonces puede simplemente llamar:

foreach (int i in returnArray.AsNotNull())
{
    // do some more stuff
}
Reed Copsey
fuente
3
Sí, pero ¿por qué no hace una verificación nula antes de obtener el enumerador?
Polaris878
12
@ Polaris878: Porque nunca fue diseñado para usarse con una colección nula. Esto es, en mi opinión, algo bueno, ya que una referencia nula y una colección vacía deben tratarse por separado. Si quiere evitar esto, hay maneras ... Lo editaré para mostrar otra opción ...
Reed Copsey
1
@ Polaris878: Sugeriría volver a redactar su pregunta: "¿Por qué DEBERÍA el tiempo de ejecución hacer una verificación nula antes de obtener el enumerador?"
Reed Copsey
Supongo que estoy preguntando "¿por qué no?" jajaja parece que el comportamiento todavía estaría bien definido
Polaris878
2
@ Polaris878: Supongo que, según mi opinión, devolver un valor nulo para una colección es un error. Tal como está ahora, el tiempo de ejecución le ofrece una excepción significativa en este caso, pero es fácil de solucionar (es decir, arriba) si no le gusta este comportamiento. Si el compilador te ocultara esto, perderías la comprobación de errores en tiempo de ejecución, pero no habría forma de "desactivarlo" ...
Reed Copsey
12

Se está respondiendo desde hace mucho tiempo, pero he intentado hacer esto de la siguiente manera para evitar una excepción de puntero nulo y puede ser útil para alguien que usa el operador de verificación nula de C #.

     //fragments is a list which can be null
     fragments?.ForEach((obj) =>
        {
            //do something with obj
        });
Devesh
fuente
@kjbartel lo superó en más de un año (en " stackoverflow.com/a/32134295/401246 "). ;) Esta es la mejor solución, ya que no: a) implica la degradación del rendimiento de (incluso cuando no null) generalizar todo el ciclo a la pantalla LCD de Enumerable(como se ??usaría), b) requiere agregar un Método de Extensión a cada Proyecto, y c) requieren evitar null IEnumerables (Pffft! Puh-LEAZE! SMH.) para empezar.
Tom
10

Otro método de extensión para evitar esto:

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

Consumir de varias maneras:

(1) con un método que acepta T:

returnArray.ForEach(Console.WriteLine);

(2) con una expresión:

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

(3) con un método anónimo multilínea

int toCompare = 10;
returnArray.ForEach(i =>
{
    var thisInt = i;
    var next = i++;
    if(next > 10) Console.WriteLine("Match: {0}", i);
});
Arrendajo
fuente
Solo falta un paréntesis de cierre en el 3er ejemplo. De lo contrario, un código hermoso que se puede extender aún más de maneras interesantes (para bucles, marcha atrás, saltos, etc.). Gracias por compartir.
Lara
Gracias por un código tan maravilloso, pero no entendí los primeros métodos, por qué pasas console.writeline como parámetro, aunque está imprimiendo los elementos de la matriz. Pero no entendí
Ajay Singh
@AjaySingh Console.WriteLinees solo un ejemplo de un método que toma un argumento (an Action<T>). Los elementos 1, 2 y 3 muestran ejemplos de pasar funciones al .ForEachmétodo de extensión.
Jay
La respuesta de @kjbartel (en " stackoverflow.com/a/32134295/401246 " es la mejor solución, ya que no: a) implica la degradación del rendimiento de (incluso cuando no null) generalizando todo el bucle a la pantalla LCD de Enumerable(como se ??usaría) ), b) requieren agregar un Método de Extensión a cada Proyecto, o c) requieren evitar null IEnumerables (Pffft! Puh-LEAZE! SMH.) para comenzar con (cuz, nullsignifica N / A, mientras que la lista vacía significa que es aplicable, pero es actualmente, bueno, ¡ vacío !, es decir, un Empl. podría tener Comisiones que son N / A para no ventas o vacías para Ventas).
Tom
5

Simplemente escriba un método de extensión para ayudarlo:

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);
      }
   }
}
BFree
fuente
5

Porque una colección nula no es lo mismo que una colección vacía. Una colección vacía es un objeto de colección sin elementos; Una colección nula es un objeto inexistente.

Aquí hay algo para probar: declara dos colecciones de cualquier tipo. Inicialice uno normalmente para que esté vacío y asigne al otro el valor null. Luego intente agregar un objeto a ambas colecciones y vea qué sucede.

PINCHAZO
fuente
3

Es culpa de Do.Something(). La mejor práctica aquí sería devolver una matriz de tamaño 0 (que es posible) en lugar de un valor nulo.

Henk Holterman
fuente
2

Porque detrás de escena foreachadquiere un enumerador, equivalente a esto:

using (IEnumerator<int> enumerator = returnArray.getEnumerator()) {
    while (enumerator.MoveNext()) {
        int i = enumerator.Current;
        // do some more stuff
    }
}
Lucero
fuente
2
¿entonces? ¿Por qué no puede simplemente verificar si es nulo primero y omitir el ciclo? AKA, ¿qué se muestra exactamente en los métodos de extensión? La pregunta es, ¿es mejor omitir el bucle por defecto si es nulo o lanzar una excepción? ¡Creo que es mejor saltar! Parece probable que los contenedores nulos deben omitirse en lugar de repetirse, ya que los bucles deben hacer algo SI el contenedor no es nulo.
AbstractDissonance
@AbstractDissonance Podría argumentar lo mismo con todas las nullreferencias, por ejemplo, al acceder a los miembros. Por lo general, este es un error, y si no lo es, es lo suficientemente simple como para manejar esto, por ejemplo, con el método de extensión que otro usuario ha proporcionado como respuesta.
Lucero
1
No lo creo. El foreach está destinado a operar sobre la colección y es diferente a hacer referencia a un objeto nulo directamente. Si bien uno podría argumentar lo mismo, apuesto a que si analizara todo el código del mundo, la mayoría de los bucles foreach tienen controles nulos de algún tipo frente a ellos solo para omitir el bucle cuando la colección es "nula" (que es por lo tanto, se trata igual que vacío). No creo que nadie considere el bucle sobre una colección nula como algo que desean y preferiría simplemente ignorar el bucle si la colección es nula. Tal vez, más bien, podría usarse un foreach? (Var x en C).
AbstractDissonance
El punto que estoy tratando principalmente de hacer es que crea un poco de basura en el código ya que uno tiene que verificar cada vez sin una buena razón. Las extensiones, por supuesto, funcionan, pero se podría agregar una función de lenguaje para evitar estas cosas sin mucho problema. (principalmente, creo que el método actual produce errores ocultos ya que el programador puede olvidar poner el cheque y, por lo tanto, una excepción ... porque espera que el cheque ocurra en otro lugar antes del ciclo o está pensando que fue preinicializado (que puede o puede haber cambiado) Pero en cualquier causa, el comportamiento sería el mismo que si está vacío..
AbstractDissonance
@AbstractDissonance Bueno, con un análisis estático adecuado sabes dónde podrías tener nulos y dónde no. Si obtiene un valor nulo donde no espera uno, es mejor fallar en lugar de ignorar silenciosamente los problemas en mi humilde opinión (en el espíritu de fallar rápidamente ). Por lo tanto, siento que este es el comportamiento correcto.
Lucero
1

Creo que la explicación de por qué se produce una excepción es muy clara con las respuestas proporcionadas aquí. Solo deseo complementar con la forma en que suelo trabajar con estas colecciones. Porque, algunas veces, uso la colección más de una vez y tengo que probar si es nula cada vez. Para evitar eso, hago lo siguiente:

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

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

De esta manera, podemos usar la colección todo lo que queramos sin temor a la excepción y no contaminamos el código con declaraciones condicionales excesivas.

Usar el operador de verificación nula ?.también es un gran enfoque. Pero, en caso de matrices (como el ejemplo en la pregunta), debe transformarse en Lista antes:

    int[] returnArray = DoSomething();

    returnArray?.ToList().ForEach((i) =>
    {
        // do some more stuff
    });
Alielson Piffer
fuente
2
La conversión a una lista solo para tener acceso al ForEachmétodo es una de las cosas que odio en una base de código.
huysentruitw
Estoy de acuerdo ... lo evito tanto como sea posible. :(
Alielson Piffer
-2
SPListItem item;
DataRow dr = datatable.NewRow();

dr["ID"] = (!Object.Equals(item["ID"], null)) ? item["ID"].ToString() : string.Empty;
Naveen Baabu K
fuente