Por que o loop foreach do .NET lança NullRefException quando a coleção é nula?


231

Então, eu frequentemente corro nessa situação ... onde Do.Something(...)retorna uma coleção nula, assim:

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

Em seguida, tento usar esta coleção da seguinte maneira:

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

Estou curioso, por que um loop foreach não pode operar em uma coleção nula? Parece lógico para mim que 0 iterações seriam executadas com uma coleção nula ... em vez disso, lança a NullReferenceException. Alguém sabe por que isso pode ser?

Isso é chato, pois estou trabalhando com APIs que não são claras sobre o que exatamente retornam, então acabo em if (someCollection != null)todos os lugares ...

Edit: Obrigado a todos por explicar que foreachusa GetEnumeratore, se não houver um enumerador para obter, o foreach falhará. Acho que estou perguntando por que o idioma / tempo de execução não pode ou não fará uma verificação nula antes de pegar o enumerador. Parece-me que o comportamento ainda estaria bem definido.


1
Algo parece errado em chamar uma matriz de coleção. Mas talvez eu seja apenas velha escola.
Robaticus

Sim, eu concordo ... Eu nem sei ao certo por que tantos métodos nesta base de código retornam matrizes x_x
Polaris878

4
Suponho que, pelo mesmo raciocínio, seria bem definido que todas as instruções em C # se tornassem no-ops quando recebessem um nullvalor. Você está sugerindo isso apenas para foreachloops ou outras declarações?
Ken

7
@ Ken ... Eu estou pensando laços apenas foreach, porque para mim parece evidente para o programador que nada iria acontecer se a coleção está vazia ou inexistente
Polaris878

Respostas:


251

Bem, a resposta curta é "porque foi assim que os designers do compilador o projetaram". Realisticamente, porém, seu objeto de coleção é nulo; portanto, não há como o compilador fazer com que o enumerador faça um loop pela coleção.

Se você realmente precisar fazer algo assim, tente o operador coalescente nulo:

int[] array = null;

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

3
Por favor, desculpe minha ignorância, mas isso é eficiente? Isso não resulta em uma comparação em cada iteração?
user919426

20
Eu não acredito nisso. Observando a IL gerada, o loop é posterior à comparação é nula.
Robaticus 30/03

10
Santo necro ... Às vezes, é necessário olhar para a IL para ver o que o compilador está fazendo para descobrir se há alguma eficiência. O usuário919426 perguntou se fez a verificação para cada iteração. Embora a resposta possa ser óbvia para algumas pessoas, não é óbvia para todos, e fornecer a dica de que olhar para a IL lhe dirá o que o compilador está fazendo, ajuda as pessoas a pescar por si mesmas no futuro.
Robaticus

2
@Robaticus (até por que mais tarde) a IL parece isso porque, porque a especificação diz isso. A expansão do açúcar sintático (também conhecido como foreach) é avaliar a expressão no lado direito de "in" e chamar GetEnumeratoro resultado
Rune FS

2
@RuneFS - exatamente. Compreender a especificação ou olhar para o IL é uma maneira de descobrir o "porquê". Ou para avaliar se duas abordagens C # diferentes se resumem à mesma IL. Esse foi, essencialmente, o meu argumento para Shimmy acima.
Robaticus

148

Um foreachloop chama o GetEnumeratormétodo
Se a coleção for null, essa chamada de método resulta em a NullReferenceException.

É uma prática recomendada retornar uma nullcoleção; seus métodos devem retornar uma coleção vazia.


7
Concordo, coleções vazias devem sempre ser devolvido ... No entanto, eu não escrevi esses métodos :)
Polaris878

19
@Polaris, operador coalescente nulo para o resgate! int[] returnArray = Do.Something() ?? new int[] {};
precisa saber é o seguinte

2
Ou: ... ?? new int[0].
Ken

3
+1 Como a dica de retornar coleções vazias em vez de nulas. Obrigado.
Galilyou

1
Eu discordo de uma má prática: veja ⇒ se uma função falhar, ela pode retornar uma coleção vazia - é uma chamada ao construtor, alocação de memória e talvez um monte de código a ser executado. Ou você pode simplesmente retornar «null» → obviamente existe apenas um código para retornar e um código muito curto para verificar é o argumento «null». É apenas uma performance.
Hi-Angel

47

Há uma grande diferença entre uma coleção vazia e uma referência nula a uma coleção.

Quando você usa foreachinternamente, isso está chamando o método GetEnumerator () do IEnumerable . Quando a referência é nula, isso gera essa exceção.

No entanto, é perfeitamente válido ter um IEnumerableou vazio IEnumerable<T>. Nesse caso, o foreach não "iterará" sobre nada (já que a coleção está vazia), mas também não será lançada, pois esse é um cenário perfeitamente válido.


Editar:

Pessoalmente, se você precisar contornar isso, recomendo um método de extensão:

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

Você pode simplesmente ligar para:

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

3
Sim, mas por que não faz uma verificação nula antes de obter o enumerador?
precisa saber é o seguinte

12
@ Polaris878: Porque nunca foi projetado para ser usado com uma coleção nula. Isso é bom para a IMO - já que uma referência nula e uma coleção vazia devem ser tratadas separadamente. Se você quiser resolver este problema, existem maneiras .. .I'll edição para mostrar uma outra opção ...
Reed Copsey

1
@ Polaris878: Eu sugiro reformular sua pergunta: "Por que o tempo de execução deve fazer uma verificação nula antes de obter o enumerador?"
Reed Copsey

Acho que estou perguntando "por que não?" lol parece que o comportamento ainda estaria bem definida
Polaris878

2
@ Polaris878: Acho que da maneira como penso, retornar nulo para uma coleção é um erro. Do jeito que está agora, o tempo de execução oferece uma exceção significativa nesse caso, mas é fácil contornar (por exemplo: acima) se você não gosta desse comportamento. Se o compilador escondeu isso de você, você perderia a verificação de erros em tempo de execução, mas não haveria maneira de "desligar" ...
Reed Copsey

12

Ele está sendo respondido há muito tempo, mas tentei fazer isso da seguinte maneira para evitar exceção de ponteiro nulo e pode ser útil para alguém que usa o operador de verificação nula de C #?

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

@kjbartel venceu você por mais de um ano (em " stackoverflow.com/a/32134295/401246 "). ;) Essa é a melhor solução, porque não: a) envolve a degradação do desempenho (mesmo quando não null) generalizando todo o loop no LCD de Enumerable(como ??seria o uso ), b) exige a adição de um método de extensão a cada projeto, ec) exigir que evite null IEnumerables (Pffft! Puh-LEAZE! SMH.) para começar.
Tom

10

Outro método de extensão para contornar isso:

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

Consuma de várias maneiras:

(1) com um método que aceite T:

returnArray.ForEach(Console.WriteLine);

(2) com uma expressão:

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

(3) com um método anônimo de múltiplas linhas

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

Apenas falta um parêntese de fechamento no terceiro exemplo. Caso contrário, código bonito que pode ser estendido ainda mais de maneiras interessantes (para loops, reversão, salto, etc). Obrigado por compartilhar.
Lara

Obrigado por um código tão maravilhoso, mas eu não entendia os primeiros métodos, por que você passar Console.WriteLine como parâmetro, embora a sua impressão a matriz elements.but não entendi
Ajay Singh

@AjaySingh Console.WriteLineé apenas um exemplo de método que utiliza um argumento (an Action<T>). Os itens 1, 2 e 3 estão mostrando exemplos de passagem de funções para o .ForEachmétodo de extensão.
Jay

A resposta de @ kjbartel (em " stackoverflow.com/a/32134295/401246 " é a melhor solução, porque ela não envolve: a) envolve a degradação do desempenho de (mesmo quando não null) generalizar todo o loop no LCD de Enumerable(como o uso ??seria ), b) exigem a adição de um método de extensão a cada projeto ou c) exigem que evite null IEnumerables (Pffft! Puh-LEAZE! SMH.) para começar com (cuz, nullsignifica N / A, enquanto lista vazia significa, é aplicável. atualmente, bem, vazio !, ou seja, um Empl. poderia ter comissões N / A para não vendas ou vazias para vendas).
Tom

5

Basta escrever um método de extensão para ajudá-lo:

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

Porque uma coleção nula não é a mesma coisa que uma coleção vazia. Uma coleção vazia é um objeto de coleção sem elementos; uma coleção nula é um objeto inexistente.

Aqui está algo para tentar: Declarar duas coleções de qualquer tipo. Inicialize um normalmente para que fique vazio e atribua o outro ao valor null. Em seguida, tente adicionar um objeto às duas coleções e veja o que acontece.


3

É culpa de Do.Something(). A melhor prática aqui seria retornar uma matriz de tamanho 0 (isso é possível) em vez de um nulo.


2

Porque nos bastidores o foreachadquire um enumerador, equivalente a este:

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

2
tão? Por que não pode simplesmente verificar se é nulo primeiro e pular o loop? AKA, exatamente o que é mostrado nos métodos de extensão? A questão é: é melhor usar o padrão para pular o loop se nulo ou lançar uma exceção? Eu acho que é melhor pular! Parece provável que os contêineres nulos devem ser ignorados, e não repetidos, pois os loops devem fazer algo se o contêiner não for nulo.
AbstractDissonance

@AbstractDissonance Você pode argumentar o mesmo com todas as nullreferências, por exemplo, ao acessar membros. Normalmente, isso é um erro e, se não for, é simples o suficiente para lidar com isso, por exemplo, com o método de extensão que outro usuário forneceu como resposta.
Lucero

1
Acho que não. O foreach deve operar sobre a coleção e é diferente de referenciar um objeto nulo diretamente. Embora se possa argumentar o mesmo, aposto que se você analisasse todo o código do mundo, a maioria dos loops de foreach teria verificações nulas de algum tipo à sua frente, apenas para ignorar o loop quando a coleção for "nula" (que é portanto tratado da mesma forma que vazio). Eu não acho que alguém considere fazer loop sobre uma coleção nula como algo que eles querem e preferiria simplesmente ignorar o loop se a coleção for nula. Talvez seja melhor usar um foreach (var x em C).
AbstractDissonance

O ponto que estou tentando enfatizar é que ele cria um pouco de lixo no código, já que é preciso verificar todas as vezes sem uma boa razão. As extensões, é claro, funcionam, mas um recurso de idioma pode ser adicionado para evitar essas coisas sem muito problema. (principalmente, acho que o método atual produz bugs ocultos, pois o programador pode esquecer de colocar a verificação e, portanto, uma exceção ... porque ele espera que a verificação ocorra em outro lugar antes do loop ou está pensando que foi pré-inicializado (o que ele pode ou mudaram) Mas em qualquer causa, o comportamento seria o mesmo que se vazio..
AbstractDissonance

@AbstractDissonance Bem, com algumas análises estáticas adequadas, você sabe onde poderia ter nulos e onde não. Se você obtiver um nulo onde não espera um, é melhor falhar em vez de ignorar silenciosamente os problemas IMHO (no espírito de falhar rapidamente ). Portanto, sinto que esse é o comportamento correto.
Lucero

1

Eu acho que a explicação de por que a exceção é lançada é muito clara com as respostas fornecidas aqui. Quero apenas complementar o modo como costumo trabalhar com essas coleções. Porque, algumas vezes, eu uso a coleção mais de uma vez e tenho que testar se nula todas as vezes. Para evitar isso, faço o seguinte:

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

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

Dessa forma, podemos usar a coleção o quanto quisermos sem temer a exceção e não poluiremos o código com declarações condicionais excessivas.

Usar o operador de verificação nula ?.também é uma ótima abordagem. Mas, no caso de matrizes (como o exemplo na pergunta), ele deve ser transformado em Lista antes:

    int[] returnArray = DoSomething();

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

2
Convertendo para uma lista apenas para ter acesso ao ForEachmétodo é uma das coisas que eu odeio em uma base de código.
huysentruitw

Eu concordo ... eu evito isso o máximo possível. :(
Alielson Piffer

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

dr["ID"] = (!Object.Equals(item["ID"], null)) ? item["ID"].ToString() : string.Empty;
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.