É melhor retornar uma coleção nula ou vazia?

420

Essa é uma pergunta geral (mas estou usando C #), qual é a melhor maneira (melhor prática), você retorna uma coleção nula ou vazia para um método que tem uma coleção como um tipo de retorno?

Omu
fonte
5
Hum, não é bem CPerkins. rads.stackoverflow.com/amzn/click/0321545613
3
@CPerkins - Sim, existe. É claramente indicado nas diretrizes de design da estrutura .NET da Microsoft. Veja a resposta de RichardOD para obter detalhes.
Greg Beech
51
SOMENTE se o significado for "Não consigo calcular os resultados", você deve retornar nulo. Nulo nunca deve ter a semântica de "vazio", apenas "ausente" ou "desconhecido". Mais detalhes no meu artigo sobre o assunto: blogs.msdn.com/ericlippert/archive/2009/05/14/…
Eric Lippert
3
Bozho: "any" é uma enorme quantidade de idiomas. E o Common Lisp, onde a lista vazia é exatamente igual ao valor nulo? :-) #
Ken
9
Na verdade, o "duplicado" é sobre métodos que retornam um objeto, não uma coleção. É um cenário diferente, com respostas diferentes.
GalacticCowboy

Respostas:

499

Coleção vazia. Sempre.

Isso é péssimo:

if(myInstance.CollectionProperty != null)
{
  foreach(var item in myInstance.CollectionProperty)
    /* arrgh */
}

É uma prática recomendada NUNCA retornar nullao retornar uma coleção ou enumerável. SEMPRE retorne um enumerável / coleção vazio. Isso evita o absurdo acima mencionado e evita que seu carro seja instigado por colegas de trabalho e usuários de suas classes.

Ao falar sobre propriedades, sempre defina sua propriedade uma vez e esqueça-a

public List<Foo> Foos {public get; private set;}

public Bar() { Foos = new List<Foo>(); }

No .NET 4.6.1, você pode condensar bastante isso:

public List<Foo> Foos { get; } = new List<Foo>();

Ao falar sobre métodos que retornam enumeráveis, você pode retornar facilmente um enumerável vazio em vez de null...

public IEnumerable<Foo> GetMyFoos()
{
  return InnerGetFoos() ?? Enumerable.Empty<Foo>();
}

O uso Enumerable.Empty<T>()pode ser visto como mais eficiente do que retornar, por exemplo, uma nova coleção ou matriz vazia.

Ryan Lundy
fonte
24
Eu concordo com Will, mas acho que "sempre" é um pouco excessivo. Enquanto uma coleção vazia pode significar "0 itens", retornar Null pode significar "nenhuma coleção" - por exemplo. se você estiver analisando HTML, procurando um <ul> with id = "foo", <ul id = "foo"> </ul> poderia retornar uma coleção vazia; se não houver <ul> com id = "foo" um retorno nulo seria melhor (a menos que você quer lidar com este caso com uma exceção)
Patonza
30
nem sempre é uma questão de se "você pode retornar facilmente uma matriz vazia", ​​mas se uma matriz vazia pode ou não ser enganosa no contexto atual. Uma matriz vazia realmente significa algo, assim como nulo. Dizer que você sempre deve retornar uma matriz vazia em vez de nula é quase tão equivocado quanto dizer que um método booleano deve sempre retornar verdadeiro. Ambos os valores possíveis transmitem um significado.
David Hedlund
31
Você realmente deve preferir retornar System.Linq.Enumerable.Empty <Foo> () em vez de um novo Foo [0]. É mais explícito e economiza uma alocação de memória (pelo menos na minha implementação .NET instalada).
31510 Trillian
4
@ Will: OP: "você retorna uma coleção nula ou vazia para um método que possui uma coleção como um tipo de retorno". Eu acho que, neste caso, é IEnumerableou ICollectionnão importa tanto. De qualquer forma, se você selecionar algo do tipo, ICollectioneles também retornarão null... Gostaria que eles retornassem uma coleção vazia, mas os encontrei retornando null, então pensei em mencionar aqui. Eu diria que o padrão de uma coleção de enumerável é vazio e não nulo. Eu não sabia que era um assunto tão sensível.
Matthijs Wessels
7
Relacionado (artigo do CodeProject): É realmente melhor 'retornar uma lista vazia em vez de nula'?
sampathsris
154

Nas Diretrizes de Design do Quadro, 2ª Edição (pág. 256):

NÃO retorne valores nulos das propriedades da coleção ou dos métodos que retornam coleções. Retorne uma coleção vazia ou uma matriz vazia.

Aqui está outro artigo interessante sobre os benefícios de não retornar nulos (eu estava tentando encontrar algo no blog de Brad Abram, e ele vinculou ao artigo).

Editar - como Eric Lippert comentou agora a pergunta original, eu também gostaria de vincular ao seu excelente artigo .

RichardOD
fonte
33
+1. É sempre uma boa prática seguir as diretrizes de design da estrutura, a menos que você tenha um motivo MUITO bom para não fazê-lo.
@ Will, absolutamente. É o que eu sigo. Eu nunca encontrei qualquer necessidade de fazer o contrário
RichardOD
Sim, é isso que você segue. Mas nos casos em que as APIs você dependem de não segui-lo, você está 'preso';)
Bozho
@ Bozho- yeap. Sua resposta fornece algumas boas respostas para esses casos adicionais.
RichardOD
90

Depende do seu contrato e do seu caso concreto . Geralmente , é melhor retornar coleções vazias , mas às vezes ( raramente ):

  • null pode significar algo mais específico;
  • sua API (contrato) pode forçá-lo a retornar null.

Alguns exemplos concretos:

  • um componente de interface do usuário (de uma biblioteca fora de seu controle) pode renderizar uma tabela vazia se uma coleção vazia for passada ou nenhuma tabela, se nulo for passado.
  • em um objeto para XML (JSON / qualquer que seja), onde nullsignificaria que o elemento está ausente, enquanto uma coleção vazia renderizaria um redundante (e possivelmente incorreto)<collection />
  • você está usando ou implementando uma API que afirma explicitamente que nulo deve ser retornado / passado
Bozho
fonte
4
@ Bozho- alguns exemplos interessantes aqui.
RichardOD
1
Eu meio que veria o seu ponto, apesar de não achar que você deva criar seu código em torno de outra falha e, por definição, 'null' em c # nunca significa algo específico. O valor nulo é definido como "sem informação" e, portanto, dizer que carrega informações específicas é um oximoro. É por isso que as diretrizes do .NET declaram que você deve retornar um conjunto vazio se o conjunto estiver realmente vazio. return null está dizendo: "Não sei para onde foi o conjunto esperado"
Rune FS
13
não, isso significa que "não existe um conjunto" em vez de "o conjunto não tem elementos"
Bozho
Eu acreditava nisso, então eu tive que escrever muito TSQL e aprendi que nem sempre é o caso, heheh.
1
A parte com o Object-To-Xml está no local
Mihai Caracostea 11/11
36

Há um outro ponto que ainda não foi mencionado. Considere o seguinte código:

    public static IEnumerable<string> GetFavoriteEmoSongs()
    {
        yield break;
    }

O idioma do C # retornará um enumerador vazio ao chamar esse método. Portanto, para ser consistente com o design da linguagem (e, portanto, com as expectativas do programador), uma coleção vazia deve ser retornada.

Jeffrey L Whitledge
fonte
1
Tecnicamente, não acho que isso retorne uma coleção vazia.
FryGuy
3
@FryGuy - Não, não. Ele retorna um objeto Enumerable, cujo método GetEnumerator () retorna um Enumerator que está (por analogia com uma coleção) vazio. Ou seja, o método MoveNext () do enumerador sempre retorna false. Chamar o método de exemplo não retorna nulo, nem retorna um objeto Enumerable cujo método GetEnumerator () retorna nulo.
Jeffrey L Whitledge
31

Vazio é muito mais amigável ao consumidor.

Existe um método claro de criar um enumerável vazio:

Enumerable.Empty<Element>()
George Polevoy
fonte
2
Obrigado pelo truque. Eu estava instanciando uma List <T> () vazia como um idiota, mas isso parece muito mais limpo e provavelmente é um pouco mais eficiente também.
Jacobs Data Solutions
18

Parece-me que você deve retornar o valor que é semanticamente correto no contexto, qualquer que seja. Uma regra que diz "sempre retorne uma coleção vazia" parece um pouco simplista para mim.

Suponha que, digamos, em um sistema para um hospital, tenhamos uma função que deve retornar uma lista de todas as hospitalizações anteriores nos últimos 5 anos. Se o cliente não estiver no hospital, faz sentido retornar uma lista vazia. Mas e se o cliente deixasse essa parte do formulário de entrada em branco? Precisamos de um valor diferente para distinguir "lista vazia" de "sem resposta" ou "não sei". Poderíamos lançar uma exceção, mas não é necessariamente uma condição de erro e não necessariamente nos afasta do fluxo normal do programa.

Muitas vezes fico frustrado com sistemas que não conseguem distinguir entre zero e nenhuma resposta. Já tive várias vezes em que um sistema me pediu para digitar algum número, digito zero e recebo uma mensagem de erro informando que devo inserir um valor nesse campo. Acabei de fazer: digitei zero! Mas não aceita zero porque não pode distingui-lo de nenhuma resposta.


Resposta a Saunders:

Sim, suponho que haja uma diferença entre "A pessoa não respondeu à pergunta" e "A resposta foi zero". Esse foi o ponto do último parágrafo da minha resposta. Muitos programas são incapazes de distinguir "não sei" de branco ou zero, o que me parece uma falha potencialmente séria. Por exemplo, eu estava comprando uma casa há um ano ou mais. Eu fui a um site imobiliário e havia muitas casas listadas com um preço inicial de US $ 0. Pareceu-me muito bom: eles estão dando essas casas de graça! Mas tenho certeza que a triste realidade era que eles simplesmente não haviam entrado no preço. Nesse caso, você pode dizer: "Bem, obviamente zero significa que eles não entraram no preço - ninguém vai dar uma casa de graça". Mas o site também listou os preços médios de compra e venda de casas em várias cidades. Não posso deixar de me perguntar se a média não incluiu os zeros, fornecendo uma média incorretamente baixa para alguns lugares. ou seja, qual é a média de US $ 100.000; US $ 120.000; e "não sei"? Tecnicamente, a resposta é "não sei". O que provavelmente queremos realmente ver é de US $ 110.000. Mas provavelmente receberemos 73.333 dólares, o que seria completamente errado. Além disso, e se tivéssemos esse problema em um site onde os usuários podem fazer pedidos on-line? (Improvável no setor imobiliário, mas tenho certeza de que você já viu isso em muitos outros produtos.) Queremos realmente que "o preço ainda não especificado" seja interpretado como "gratuito"? dando assim uma média incorretamente baixa para alguns lugares. ou seja, qual é a média de US $ 100.000; US $ 120.000; e "não sei"? Tecnicamente, a resposta é "não sei". O que provavelmente queremos realmente ver é de US $ 110.000. Mas provavelmente receberemos 73.333 dólares, o que seria completamente errado. Além disso, e se tivéssemos esse problema em um site onde os usuários podem fazer pedidos on-line? (Improvável no setor imobiliário, mas tenho certeza de que você já viu isso em muitos outros produtos.) Queremos realmente que "o preço ainda não especificado" seja interpretado como "gratuito"? dando assim uma média incorretamente baixa para alguns lugares. ou seja, qual é a média de US $ 100.000; US $ 120.000; e "não sei"? Tecnicamente, a resposta é "não sei". O que provavelmente queremos realmente ver é de US $ 110.000. Mas provavelmente receberemos 73.333 dólares, o que seria completamente errado. Além disso, e se tivéssemos esse problema em um site onde os usuários podem fazer pedidos on-line? (Improvável no setor imobiliário, mas tenho certeza de que você já viu isso em muitos outros produtos.) Queremos realmente que "o preço ainda não especificado" seja interpretado como "gratuito"? o que seria completamente errado. Além disso, e se tivéssemos esse problema em um site onde os usuários podem fazer pedidos on-line? (Improvável no setor imobiliário, mas tenho certeza de que você já viu isso em muitos outros produtos.) Queremos realmente que "o preço ainda não especificado" seja interpretado como "gratuito"? o que seria completamente errado. Além disso, e se tivéssemos esse problema em um site onde os usuários podem fazer pedidos on-line? (Improvável no setor imobiliário, mas tenho certeza de que você já viu isso em muitos outros produtos.) Queremos realmente que "o preço ainda não especificado" seja interpretado como "gratuito"?

RE tendo duas funções separadas, um "existe algum?" e um "se sim, o que é?" Sim, você certamente poderia fazer isso, mas por que você gostaria? Agora, o programa de chamada deve fazer duas chamadas em vez de uma. O que acontece se um programador não chamar o "any"? e vai direto para o "o que é isso?" ? O programa retornará um zero incorreto? Lançar uma exceção? Retornar um valor indefinido? Cria mais código, mais trabalho e mais erros em potencial.

O único benefício que vejo é que ele permite que você cumpra uma regra arbitrária. Existe alguma vantagem nessa regra que vale a pena obedecer? Se não, por que se preocupar?


Resposta a Jammycakes:

Considere como seria o código real. Eu sei que a pergunta dizia C #, mas com licença se eu escrever Java. Meu C # não é muito nítido e o princípio é o mesmo.

Com um retorno nulo:

HospList list=patient.getHospitalizationList(patientId);
if (list==null)
{
   // ... handle missing list ...
}
else
{
  for (HospEntry entry : list)
   //  ... do whatever ...
}

Com uma função separada:

if (patient.hasHospitalizationList(patientId))
{
   // ... handle missing list ...
}
else
{
  HospList=patient.getHospitalizationList(patientId))
  for (HospEntry entry : list)
   // ... do whatever ...
}

Na verdade, é uma linha ou dois códigos a menos com retorno nulo; portanto, não é mais oneroso para o chamador, é menos.

Não vejo como isso cria um problema SECO. Não é como se tivéssemos que executar a chamada duas vezes. Se sempre quiséssemos fazer a mesma coisa quando a lista não existe, talvez pudéssemos enviar a manipulação para a função get-list em vez de fazer com que o chamador fizesse isso; portanto, colocar o código no chamador seria uma violação DRY. Mas quase certamente não queremos sempre fazer a mesma coisa. Nas funções em que precisamos ter a lista para processar, uma lista ausente é um erro que pode interromper o processamento. Mas em uma tela de edição, certamente não queremos interromper o processamento se eles ainda não inseriram dados: queremos deixá-los inserir dados. Portanto, o manuseio de "nenhuma lista" deve ser feito no nível do chamador de uma maneira ou de outra. E se fazemos isso com um retorno nulo ou uma função separada não faz diferença para o princípio maior.

Claro, se o chamador não procurar nulo, o programa poderá falhar com uma exceção de ponteiro nulo. Mas se houver uma função separada "obteve alguma" e o chamador não a chama, mas chama cegamente a função "obter lista", o que acontece? Se isso gera uma exceção ou falha, bem, é praticamente o mesmo que aconteceria se retornasse nulo e não a checasse. Se retornar uma lista vazia, isso está errado. Você não conseguiu distinguir entre "Eu tenho uma lista com zero elementos" e "Eu não tenho uma lista". É como retornar zero pelo preço quando o usuário não inseriu nenhum preço: está errado.

Não vejo como anexar um atributo adicional à coleção. O chamador ainda precisa verificar. Como isso é melhor do que procurar por nulos? Novamente, a pior coisa que pode acontecer é que o programador esqueça de verificá-lo e dê resultados incorretos.

Uma função que retorna nulo não é uma surpresa se o programador estiver familiarizado com o conceito de nulo que significa "não tem valor", que eu acho que qualquer programador competente deveria ter ouvido falar, se ele acha que é uma boa ideia ou não. Eu acho que ter uma função separada é mais um problema de "surpresa". Se um programador não estiver familiarizado com a API, quando executar um teste sem dados, descobrirá rapidamente que às vezes recebe um nulo. Mas como ele descobriria a existência de outra função, a menos que lhe ocorresse a possibilidade de tal função e ele verifique a documentação, e a documentação é completa e compreensível? Eu preferiria ter uma função que sempre me dá uma resposta significativa, em vez de duas funções que tenho que conhecer e lembrar de chamar de ambas.

Jay
fonte
4
Por que é necessário combinar as respostas "sem resposta" e "zero" no mesmo valor de retorno? Em vez disso, o método retornou "qualquer hospitalização anterior nos últimos cinco anos" e tem um método separado que pergunta "a lista de hospitalizações anteriores já foi preenchida?". Isso pressupõe que há uma diferença entre uma lista preenchida com ausência de internações anteriores, e uma lista não preenchido.
John Saunders
2
Mas se você retornar nulo, já estará cobrando um encargo extra na pessoa que ligou! O chamador deve verificar se há nulo em todos os valores de retorno - uma violação DRY horrível. Se você deseja indicar "o chamador não respondeu à pergunta" separadamente, é mais fácil criar uma chamada de método extra para indicar o fato. Ou use um tipo de coleção derivado com uma propriedade extra de DeclinedToAnswer. A regra de nunca retornar nulo não é arbitrária, é o Princípio da Menos Surpresa. Além disso, o tipo de retorno do seu método deve significar o que o nome diz que ele faz. Nulo quase certamente não.
jammycakes
2
Você está assumindo que seu getHospitalizationList é chamado apenas de um local e / ou que todos os chamadores desejam distinguir entre os casos "sem resposta" e "zero". Não vai haver casos (quase certamente a maioria), onde o seu interlocutor não precisa fazer essa distinção, e assim você está forçando-os a adicionar verificações nulos em locais onde isso não deve ser necessário. Isso adiciona um risco significativo à sua base de código, porque as pessoas podem facilmente esquecer de fazê-lo - e porque há muito poucas razões legítimas para retornar nulo em vez de uma coleção, isso será muito mais provável.
Jammycakes
3
RE o nome: Nenhum nome de função pode descrever completamente o que a função faz, a menos que contenha a função e, portanto, impraticável. Mas, em qualquer caso, se a função retornar uma lista vazia quando nenhuma resposta foi dada, pelo mesmo raciocínio, ela não deveria ser chamada de "getHospitalizationListOrEmptyListIfNoAnswer"? Mas, na verdade, você insistiria que a função Java Reader.read fosse renomeada como readCharacterOrReturnMinusOneOnEndOfStream? Que ResultSet.getInt deve realmente ser "getIntOrZeroIfValueWasNull"? Etc.
Jay
4
RE toda chamada quer distinguir: Bem, sim, estou assumindo que, ou pelo menos, que o autor de um chamador tome uma decisão consciente de que ele não se importa. Se a função retornar uma lista vazia para "não sei" e os chamadores tratarem cegamente esse "nenhum", isso poderá gerar resultados seriamente imprecisos. Imagine se a função fosse "getAllergicReactionToMedicationList". Um programa que "lista cega tratada" não foi inserida "como" o paciente não tem reações alérgicas conhecidas "poderia literalmente resultar na morte de um paciente. Você obteria resultados semelhantes, se menos dramáticos, em muitos outros sistemas. ...
Jay
10

Se uma coleção vazia faz sentido semanticamente, é isso que eu prefiro retornar. Retornar uma coleção vazia para GetMessagesInMyInbox()comunicações "você realmente não tem nenhuma mensagem na sua caixa de entrada", enquanto retornar nullpode ser útil para comunicar que dados insuficientes estão disponíveis para dizer como deve ser a lista que pode ser retornada.

David Hedlund
fonte
6
No exemplo que você dá, parece que o método provavelmente lançará uma exceção se não puder atender à solicitação, em vez de apenas retornar nulo. Exceções são muito mais úteis para diagnosticar problemas do que nulos.
Greg Beech
Bem, sim, no exemplo da caixa de entrada, um nullvalor certamente não parece razoável, eu estava pensando em termos mais gerais sobre esse. Exceções também são ótimas para comunicar o fato de que algo deu errado, mas se os "dados insuficientes" mencionados forem perfeitamente esperados, lançando uma exceção, o design será ruim. Estou pensando em um cenário em que é perfeitamente possível e nenhum erro para o método às vezes não conseguir calcular uma resposta.
David Hedlund
6

Retornar nulo pode ser mais eficiente, pois nenhum novo objeto é criado. No entanto, também costuma exigir uma nullverificação (ou tratamento de exceção).

Semanticamente, nulle uma lista vazia não significam a mesma coisa. As diferenças são sutis e uma opção pode ser melhor que a outra em casos específicos.

Independentemente da sua escolha, documente-a para evitar confusão.

Karmic Coder
fonte
8
A eficiência quase nunca deve ser um fator ao considerar a correção do design de uma API. Em alguns casos muito específicos, como primitivos gráficos, pode ser que sim, mas ao lidar com listas e com a maioria das outras coisas de alto nível, duvido muito disso.
Greg Beech
Concorde com Greg, especialmente porque o código que o usuário da API precisa escrever para compensar essa "otimização" pode ser mais ineficiente do que se um design melhor fosse usado em primeiro lugar.
22610 Craig Stuntz
Concordou e, na maioria dos casos, simplesmente não vale a pena a otimização. Listas vazias são praticamente gratuitas com o gerenciamento de memória moderno.
Jason Baker
6

Pode-se argumentar que o raciocínio por trás do Null Object Pattern é semelhante a um favor de retornar a coleção vazia.

Dan
fonte
4

Depende da situação. Se for um caso especial, retorne nulo. Se a função retornar uma coleção vazia, é óbvio que retornar está correto. No entanto, retornar uma coleção vazia como um caso especial por causa de parâmetros inválidos ou por outros motivos NÃO é uma boa idéia, pois está ocultando uma condição de caso especial.

Na verdade, neste caso, eu geralmente prefiro lançar uma exceção para garantir que REALMENTE não seja ignorada :)

Dizer que isso torna o código mais robusto (retornando uma coleção vazia), pois eles não precisam lidar com a condição nula é ruim, pois simplesmente oculta um problema que deve ser tratado pelo código de chamada.

Larry Watanabe
fonte
4

Eu diria que nullnão é a mesma coisa que uma coleção vazia e você deve escolher qual deles representa melhor o que está retornando. Na maioria dos casos, nullnão há nada (exceto no SQL). Uma coleção vazia é algo, embora algo vazio.

Se você tiver que escolher um ou outro, eu diria que você deve tender para uma coleção vazia em vez de nula. Mas há momentos em que uma coleção vazia não é a mesma coisa que um valor nulo.

Jason Baker
fonte
4

Pense sempre a favor de seus clientes (que estão usando sua API):

O retorno de 'nulo' frequentemente causa problemas com os clientes que não lidam com verificações nulas corretamente, o que causa uma NullPointerException durante o tempo de execução. Vi casos em que essa verificação nula ausente forçou um problema de produção prioritário (um cliente usado foreach (...) em um valor nulo). Durante o teste, o problema não ocorreu, porque os dados operados eram ligeiramente diferentes.

manuel aldana
fonte
3

Eu gosto de explicar aqui, com exemplo adequado.

Considere um caso aqui ..

int totalValue = MySession.ListCustomerAccounts()
                          .FindAll(ac => ac.AccountHead.AccountHeadID 
                                         == accountHead.AccountHeadID)
                          .Sum(account => account.AccountValue);

Aqui considere as funções que estou usando ..

1. ListCustomerAccounts() // User Defined
2. FindAll()              // Pre-defined Library Function

Eu posso facilmente usar ListCustomerAccounte, em FindAllvez de.,

int totalValue = 0; 
List<CustomerAccounts> custAccounts = ListCustomerAccounts();
if(custAccounts !=null ){
  List<CustomerAccounts> custAccountsFiltered = 
        custAccounts.FindAll(ac => ac.AccountHead.AccountHeadID 
                                   == accountHead.AccountHeadID );
   if(custAccountsFiltered != null)
      totalValue = custAccountsFiltered.Sum(account => 
                                            account.AccountValue).ToString();
}

NOTA: Como o AccountValue não é null, a função Sum () não retornará null. Por isso, posso usá-lo diretamente.

Muthu Ganapathy Nathan
fonte
2

Tivemos essa discussão entre a equipe de desenvolvimento no trabalho há mais ou menos uma semana e, quase por unanimidade, fomos para a coleta vazia. Uma pessoa queria retornar nulo pelo mesmo motivo que Mike especificou acima.

Henric
fonte
2

Coleção Vazia. Se você estiver usando C #, a suposição é que maximizar os recursos do sistema não é essencial. Embora menos eficiente, o retorno da Coleção Vazia é muito mais conveniente para os programadores envolvidos (pelo motivo descrito acima).

mothis
fonte
2

Retornar uma coleção vazia é melhor na maioria dos casos.

O motivo disso é a conveniência da implementação do chamador, o contrato consistente e a implementação mais fácil.

Se um método retornar nulo para indicar resultado vazio, o responsável pela chamada deverá implementar um adaptador de verificação nula, além da enumeração. Esse código é duplicado em vários chamadores, por que não colocar esse adaptador dentro do método para que ele possa ser reutilizado.

Um uso válido de null para IEnumerable pode ser uma indicação de resultado ausente ou falha de operação, mas, neste caso, outras técnicas devem ser consideradas, como lançar uma exceção.

using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;

namespace StackOverflow.EmptyCollectionUsageTests.Tests
{
    /// <summary>
    /// Demonstrates different approaches for empty collection results.
    /// </summary>
    class Container
    {
        /// <summary>
        /// Elements list.
        /// Not initialized to an empty collection here for the purpose of demonstration of usage along with <see cref="Populate"/> method.
        /// </summary>
        private List<Element> elements;

        /// <summary>
        /// Gets elements if any
        /// </summary>
        /// <returns>Returns elements or empty collection.</returns>
        public IEnumerable<Element> GetElements()
        {
            return elements ?? Enumerable.Empty<Element>();
        }

        /// <summary>
        /// Initializes the container with some results, if any.
        /// </summary>
        public void Populate()
        {
            elements = new List<Element>();
        }

        /// <summary>
        /// Gets elements. Throws <see cref="InvalidOperationException"/> if not populated.
        /// </summary>
        /// <returns>Returns <see cref="IEnumerable{T}"/> of <see cref="Element"/>.</returns>
        public IEnumerable<Element> GetElementsStrict()
        {
            if (elements == null)
            {
                throw new InvalidOperationException("You must call Populate before calling this method.");
            }

            return elements;
        }

        /// <summary>
        /// Gets elements, empty collection or nothing.
        /// </summary>
        /// <returns>Returns <see cref="IEnumerable{T}"/> of <see cref="Element"/>, with zero or more elements, or null in some cases.</returns>
        public IEnumerable<Element> GetElementsInconvenientCareless()
        {
            return elements;
        }

        /// <summary>
        /// Gets elements or nothing.
        /// </summary>
        /// <returns>Returns <see cref="IEnumerable{T}"/> of <see cref="Element"/>, with elements, or null in case of empty collection.</returns>
        /// <remarks>We are lucky that elements is a List, otherwise enumeration would be needed.</remarks>
        public IEnumerable<Element> GetElementsInconvenientCarefull()
        {
            if (elements == null || elements.Count == 0)
            {
                return null;
            }
            return elements;
        }
    }

    class Element
    {
    }

    /// <summary>
    /// http://stackoverflow.com/questions/1969993/is-it-better-to-return-null-or-empty-collection/
    /// </summary>
    class EmptyCollectionTests
    {
        private Container container;

        [SetUp]
        public void SetUp()
        {
            container = new Container();
        }

        /// <summary>
        /// Forgiving contract - caller does not have to implement null check in addition to enumeration.
        /// </summary>
        [Test]
        public void UseGetElements()
        {
            Assert.AreEqual(0, container.GetElements().Count());
        }

        /// <summary>
        /// Forget to <see cref="Container.Populate"/> and use strict method.
        /// </summary>
        [Test]
        [ExpectedException(typeof(InvalidOperationException))]
        public void WrongUseOfStrictContract()
        {
            container.GetElementsStrict().Count();
        }

        /// <summary>
        /// Call <see cref="Container.Populate"/> and use strict method.
        /// </summary>
        [Test]
        public void CorrectUsaOfStrictContract()
        {
            container.Populate();
            Assert.AreEqual(0, container.GetElementsStrict().Count());
        }

        /// <summary>
        /// Inconvenient contract - needs a local variable.
        /// </summary>
        [Test]
        public void CarefulUseOfCarelessMethod()
        {
            var elements = container.GetElementsInconvenientCareless();
            Assert.AreEqual(0, elements == null ? 0 : elements.Count());
        }

        /// <summary>
        /// Inconvenient contract - duplicate call in order to use in context of an single expression.
        /// </summary>
        [Test]
        public void LameCarefulUseOfCarelessMethod()
        {
            Assert.AreEqual(0, container.GetElementsInconvenientCareless() == null ? 0 : container.GetElementsInconvenientCareless().Count());
        }

        [Test]
        public void LuckyCarelessUseOfCarelessMethod()
        {
            // INIT
            var praySomeoneCalledPopulateBefore = (Action)(()=>container.Populate());
            praySomeoneCalledPopulateBefore();

            // ACT //ASSERT
            Assert.AreEqual(0, container.GetElementsInconvenientCareless().Count());
        }

        /// <summary>
        /// Excercise <see cref="ArgumentNullException"/> because of null passed to <see cref="Enumerable.Count{TSource}(System.Collections.Generic.IEnumerable{TSource})"/>
        /// </summary>
        [Test]
        [ExpectedException(typeof(ArgumentNullException))]
        public void UnfortunateCarelessUseOfCarelessMethod()
        {
            Assert.AreEqual(0, container.GetElementsInconvenientCareless().Count());
        }

        /// <summary>
        /// Demonstrates the client code flow relying on returning null for empty collection.
        /// Exception is due to <see cref="Enumerable.First{TSource}(System.Collections.Generic.IEnumerable{TSource})"/> on an empty collection.
        /// </summary>
        [Test]
        [ExpectedException(typeof(InvalidOperationException))]
        public void UnfortunateEducatedUseOfCarelessMethod()
        {
            container.Populate();
            var elements = container.GetElementsInconvenientCareless();
            if (elements == null)
            {
                Assert.Inconclusive();
            }
            Assert.IsNotNull(elements.First());
        }

        /// <summary>
        /// Demonstrates the client code is bloated a bit, to compensate for implementation 'cleverness'.
        /// We can throw away the nullness result, because we don't know if the operation succeeded or not anyway.
        /// We are unfortunate to create a new instance of an empty collection.
        /// We might have already had one inside the implementation,
        /// but it have been discarded then in an effort to return null for empty collection.
        /// </summary>
        [Test]
        public void EducatedUseOfCarefullMethod()
        {
            Assert.AreEqual(0, (container.GetElementsInconvenientCarefull() ?? Enumerable.Empty<Element>()).Count());
        }
    }
}
George Polevoy
fonte
2

Eu chamo isso de meu erro de um bilhão de dólares ... Naquela época, eu estava projetando o primeiro sistema abrangente de tipos para referências em uma linguagem orientada a objetos. Meu objetivo era garantir que todo o uso de referências fosse absolutamente seguro, com a verificação realizada automaticamente pelo compilador. Mas não pude resistir à tentação de colocar uma referência nula, simplesmente porque era muito fácil de implementar. Isso levou a inúmeros erros, vulnerabilidades e falhas no sistema, que provavelmente causaram um bilhão de dólares de dor e danos nos últimos quarenta anos. - Tony Hoare, inventor da ALGOL W.

Veja aqui uma tempestade de merda elaborada nullem geral. Não concordo com a afirmação que undefinedé outra null, mas ainda vale a pena ler. E explica por que você deve evitar nulle não apenas no caso solicitado. A essência é que, nullem qualquer idioma, é um caso especial. Você tem que pensar nullcomo uma exceção. undefinedé diferente, pois o código que lida com o comportamento indefinido é, na maioria dos casos, apenas um bug. C e a maioria dos outros idiomas também têm comportamento indefinido, mas a maioria deles não tem identificador para isso no idioma.

ceving
fonte
1

Da perspectiva do gerenciamento da complexidade, um objetivo principal da engenharia de software, queremos evitar a propagação de complexidade ciclomática desnecessária para os clientes de uma API. Retornar um nulo para o cliente é como retornar o custo da complexidade ciclomática de outra ramificação de código.

(Isso corresponde a uma carga de teste de unidade. Você precisaria escrever um teste para o caso de retorno nulo, além do caso de retorno de coleção vazio.)

dthal
fonte