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?
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?
Respostas:
Coleção vazia. Sempre.
Isso é péssimo:
if(myInstance.CollectionProperty != null)
{
foreach(var item in myInstance.CollectionProperty)
/* arrgh */
}
É uma prática recomendada NUNCA retornar null
ao 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.
IEnumerable
ou ICollection
não importa tanto. De qualquer forma, se você selecionar algo do tipo, ICollection
eles 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.
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 .
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;null
.Alguns exemplos concretos:
null
significaria que o elemento está ausente, enquanto uma coleção vazia renderizaria um redundante (e possivelmente incorreto)<collection />
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.
Vazio é muito mais amigável ao consumidor.
Existe um método claro de criar um enumerável vazio:
Enumerable.Empty<Element>()
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.
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 null
pode ser útil para comunicar que dados insuficientes estão disponíveis para dizer como deve ser a lista que pode ser retornada.
null
valor 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.
Retornar nulo pode ser mais eficiente, pois nenhum novo objeto é criado. No entanto, também costuma exigir uma null
verificação (ou tratamento de exceção).
Semanticamente, null
e 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.
Pode-se argumentar que o raciocínio por trás do Null Object Pattern é semelhante a um favor de retornar a coleção vazia.
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.
Eu diria que null
nã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, null
nã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.
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.
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 ListCustomerAccount
e, em FindAll
vez 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.
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());
}
}
}
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 null
em 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 null
e não apenas no caso solicitado. A essência é que, null
em qualquer idioma, é um caso especial. Você tem que pensar null
como 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.
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.)