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.