Esa es una pregunta general (pero estoy usando C #), ¿cuál es la mejor manera (mejor práctica), ¿devuelve una colección nula o vacía para un método que tiene una colección como tipo de retorno?
Esa es una pregunta general (pero estoy usando C #), ¿cuál es la mejor manera (mejor práctica), ¿devuelve una colección nula o vacía para un método que tiene una colección como tipo de retorno?
Respuestas:
Colección vacía Siempre.
Esto apesta:
if(myInstance.CollectionProperty != null)
{
foreach(var item in myInstance.CollectionProperty)
/* arrgh */
}
Se considera una práctica recomendada NUNCA regresar null
al devolver una colección o enumerable. SIEMPRE devuelve una enumeración / colección vacía. Evita las tonterías antes mencionadas y evita que su auto sea incitado por compañeros de trabajo y usuarios de sus clases.
Cuando hable de propiedades, siempre configure su propiedad una vez y olvídela
public List<Foo> Foos {public get; private set;}
public Bar() { Foos = new List<Foo>(); }
En .NET 4.6.1, puede condensar esto bastante:
public List<Foo> Foos { get; } = new List<Foo>();
Cuando se habla de métodos que devuelven enumerables, puede devolver fácilmente un enumerable vacío en lugar de null
...
public IEnumerable<Foo> GetMyFoos()
{
return InnerGetFoos() ?? Enumerable.Empty<Foo>();
}
Usar Enumerable.Empty<T>()
puede verse como más eficiente que devolver, por ejemplo, una nueva colección o matriz vacía.
IEnumerable
o ICollection
no importa tanto. De todos modos, si seleccionas algo de tipo ICollection
, también regresan null
... Me gustaría que devolvieran una colección vacía, pero me encontré con ellos regresando null
, así que pensé en mencionarlo aquí. Yo diría que el valor predeterminado de una colección de enumerables es vacío, no nulo. No sabía que era un tema tan delicado.
De las Directrices de diseño del marco, segunda edición (pág. 256):
NO devuelva valores nulos de propiedades de colección o de métodos que devuelven colecciones. Devuelve una colección vacía o una matriz vacía en su lugar.
Aquí hay otro artículo interesante sobre los beneficios de no devolver nulos (estaba tratando de encontrar algo en el blog de Brad Abram, y él lo vinculaba al artículo).
Editar: como Eric Lippert ha comentado la pregunta original, también me gustaría vincular a su excelente artículo .
Depende de su contrato y su caso concreto . En general , es mejor devolver colecciones vacías , pero a veces ( raramente ):
null
podría significar algo más específico;null
.Algunos ejemplos concretos:
null
significaría que falta el elemento, mientras que una colección vacía generaría una redundancia (y posiblemente incorrecta)<collection />
Hay otro punto que aún no se ha mencionado. Considere el siguiente código:
public static IEnumerable<string> GetFavoriteEmoSongs()
{
yield break;
}
El lenguaje C # devolverá un enumerador vacío al llamar a este método. Por lo tanto, para ser coherente con el diseño del lenguaje (y, por lo tanto, con las expectativas del programador), debe devolverse una colección vacía.
Vacío es mucho más amigable para el consumidor.
Hay un método claro de hacer un enumerable vacío:
Enumerable.Empty<Element>()
Me parece que debería devolver el valor semánticamente correcto en su contexto, sea lo que sea. Una regla que dice "siempre devolver una colección vacía" me parece un poco simplista.
Supongamos que, en un sistema para un hospital, tenemos una función que se supone que devuelve una lista de todas las hospitalizaciones anteriores de los últimos 5 años. Si el cliente no ha estado en el hospital, tiene sentido devolver una lista vacía. Pero, ¿qué pasa si el cliente deja en blanco esa parte del formulario de admisión? Necesitamos un valor diferente para distinguir "lista vacía" de "sin respuesta" o "no sé". Podríamos lanzar una excepción, pero no es necesariamente una condición de error, y no necesariamente nos saca del flujo normal del programa.
A menudo me han frustrado los sistemas que no pueden distinguir entre cero y ninguna respuesta. He tenido varias veces que un sistema me ha pedido que ingrese algún número, ingreso cero y recibo un mensaje de error que me dice que debo ingresar un valor en este campo. Acabo de hacerlo: ¡ingresé cero! Pero no aceptará cero porque no puede distinguirlo de ninguna respuesta.
Respuesta a Saunders:
Sí, supongo que hay una diferencia entre "La persona no respondió la pregunta" y "La respuesta fue cero". Ese fue el punto del último párrafo de mi respuesta. Muchos programas no pueden distinguir "no sabe" de en blanco o cero, lo que me parece una falla potencialmente grave. Por ejemplo, estaba comprando una casa hace aproximadamente un año. Fui a un sitio web de bienes raíces y había muchas casas listadas con un precio inicial de $ 0. A mí me pareció bastante bueno: ¡regalan estas casas gratis! Pero estoy seguro de que la triste realidad era que simplemente no habían ingresado el precio. En ese caso, usted puede decir: "Bueno, OBVIAMENTE cero significa que no ingresaron el precio, nadie va a regalar una casa gratis". Pero el sitio también enumeró los precios promedio de venta y solicitud de casas en varias ciudades. No puedo evitar preguntarme si el promedio no incluyó los ceros, lo que da un promedio incorrectamente bajo para algunos lugares. es decir, cuál es el promedio de $ 100,000; $ 120,000; y "no sabe"? Técnicamente, la respuesta es "no sé". Lo que probablemente realmente queremos ver es $ 110,000. Pero lo que probablemente obtendremos es $ 73,333, lo cual sería completamente incorrecto. Además, ¿qué pasaría si tuviéramos este problema en un sitio donde los usuarios pueden realizar pedidos en línea? (Es improbable para el sector inmobiliario, pero estoy seguro de que lo ha visto para muchos otros productos). ¿Realmente queremos que el "precio no especificado aún" se interprete como "gratis"? dando así un promedio incorrectamente bajo para algunos lugares. es decir, cuál es el promedio de $ 100,000; $ 120,000; y "no sabe"? Técnicamente, la respuesta es "no sé". Lo que probablemente realmente queremos ver es $ 110,000. Pero lo que probablemente obtendremos es $ 73,333, lo cual sería completamente incorrecto. Además, ¿qué pasaría si tuviéramos este problema en un sitio donde los usuarios pueden realizar pedidos en línea? (Es improbable para el sector inmobiliario, pero estoy seguro de que lo ha visto para muchos otros productos). ¿Realmente queremos que el "precio no especificado aún" se interprete como "gratis"? dando así un promedio incorrectamente bajo para algunos lugares. es decir, cuál es el promedio de $ 100,000; $ 120,000; y "no sabe"? Técnicamente, la respuesta es "no sé". Lo que probablemente realmente queremos ver es $ 110,000. Pero lo que probablemente obtendremos es $ 73,333, lo cual sería completamente incorrecto. Además, ¿qué pasaría si tuviéramos este problema en un sitio donde los usuarios pueden realizar pedidos en línea? (Es improbable para el sector inmobiliario, pero estoy seguro de que lo ha visto para muchos otros productos). ¿Realmente queremos que el "precio aún no especificado" se interprete como "gratis"? lo cual estaría completamente mal. Además, ¿qué pasaría si tuviéramos este problema en un sitio donde los usuarios pueden realizar pedidos en línea? (Es improbable para el sector inmobiliario, pero estoy seguro de que lo ha visto para muchos otros productos). ¿Realmente queremos que el "precio aún no especificado" se interprete como "gratis"? lo cual estaría completamente mal. Además, ¿qué pasaría si tuviéramos este problema en un sitio donde los usuarios pueden realizar pedidos en línea? (Es improbable para el sector inmobiliario, pero estoy seguro de que lo ha visto para muchos otros productos). ¿Realmente queremos que el "precio aún no especificado" se interprete como "gratis"?
RE tiene dos funciones separadas, un "¿hay alguna?" y un "si es así, ¿qué es?" Sí, ciertamente podrías hacer eso, pero ¿por qué quieres hacerlo? Ahora el programa de llamadas tiene que hacer dos llamadas en lugar de una. ¿Qué sucede si un programador no llama al "ninguno"? y va directamente al "¿qué es?" ? ¿El programa devolverá un cero erróneo? ¿Lanzar una excepción? ¿Devuelve un valor indefinido? Crea más código, más trabajo y más errores potenciales.
El único beneficio que veo es que le permite cumplir con una regla arbitraria. ¿Hay alguna ventaja en esta regla que haga que valga la pena obedecerla? Si no, ¿por qué molestarse?
Respuesta a Jammycakes:
Considere cómo se vería el código real. Sé que la pregunta decía C #, pero discúlpeme si escribo Java. Mi C # no es muy agudo y el principio es el mismo.
Con un retorno nulo:
HospList list=patient.getHospitalizationList(patientId);
if (list==null)
{
// ... handle missing list ...
}
else
{
for (HospEntry entry : list)
// ... do whatever ...
}
Con una función separada:
if (patient.hasHospitalizationList(patientId))
{
// ... handle missing list ...
}
else
{
HospList=patient.getHospitalizationList(patientId))
for (HospEntry entry : list)
// ... do whatever ...
}
En realidad, es una línea o dos códigos menos con el retorno nulo, por lo que no es más una carga para la persona que llama, es menos.
No veo cómo crea un problema SECO. No es que tengamos que ejecutar la llamada dos veces. Si siempre quisiéramos hacer lo mismo cuando la lista no existe, tal vez podríamos empujar el manejo hacia la función get-list en lugar de que la persona que llama lo haga, por lo que poner el código en la persona que llama sería una violación SECA. Pero casi seguro que no siempre queremos hacer lo mismo. En las funciones donde debemos tener la lista para procesar, una lista que falta es un error que bien podría detener el procesamiento. Pero en una pantalla de edición, seguramente no queremos detener el procesamiento si aún no han ingresado datos: queremos dejar que ingresen datos. Por lo tanto, el manejo de "no list" debe hacerse al nivel de la persona que llama de una forma u otra. Y si hacemos eso con un retorno nulo o una función separada, no hay diferencia para el principio más grande.
Claro, si la persona que llama no comprueba nulo, el programa podría fallar con una excepción de puntero nulo. Pero si hay una función separada "got any" y la persona que llama no llama a esa función sino que llama ciegamente a la función "get list", ¿qué sucede? Si arroja una excepción o falla, bueno, eso es más o menos lo que sucedería si devuelve nulo y no lo verifica. Si devuelve una lista vacía, eso está mal. No puede distinguir entre "Tengo una lista con cero elementos" y "No tengo una lista". Es como devolver cero por el precio cuando el usuario no ingresó ningún precio: simplemente está mal.
No veo cómo ayuda un atributo adicional a la colección. La persona que llama todavía tiene que verificarlo. ¿Cómo es eso mejor que verificar nulo? Una vez más, lo peor que puede pasar es que el programador olvide verificarlo y dar resultados incorrectos.
Una función que devuelve nulo no es una sorpresa si el programador está familiarizado con el concepto de nulo que significa "no tiene un valor", que creo que cualquier programador competente debería haber escuchado, ya sea que piense que es una buena idea o no. Creo que tener una función separada es más un problema "sorpresa". Si un programador no está familiarizado con la API, cuando ejecuta una prueba sin datos, descubrirá rápidamente que a veces recupera un valor nulo. Pero, ¿cómo descubriría la existencia de otra función a menos que se le ocurriera que podría existir tal función y verificara la documentación, y la documentación esté completa y sea comprensible? Preferiría tener una función que siempre me dé una respuesta significativa, en lugar de dos funciones que debo conocer y recordar llamar a ambas.
Si una colección vacía tiene sentido semánticamente, eso es lo que prefiero devolver. Devolver una colección vacía para GetMessagesInMyInbox()
comunicar "realmente no tiene ningún mensaje en su bandeja de entrada", mientras que devolver null
puede ser útil para comunicar que no hay suficientes datos disponibles para decir cómo debería ser la lista que se devolverá.
null
valor seguramente no parece razonable, estaba pensando en términos más generales al respecto. Las excepciones también son excelentes para comunicar el hecho de que algo ha salido mal, pero si los "datos insuficientes" a los que se hace referencia son perfectamente esperados, entonces lanzar una excepción sería un mal diseño. Estoy pensando en un escenario en el que es perfectamente posible y no hay ningún error para que el método a veces no pueda calcular una respuesta.
Devolver nulo podría ser más eficiente, ya que no se crea ningún objeto nuevo. Sin embargo, a menudo también requeriría unnull
verificación (o manejo de excepciones).
Semánticamente null
y una lista vacía no significa lo mismo. Las diferencias son sutiles y una opción puede ser mejor que la otra en casos específicos.
Independientemente de su elección, documente para evitar confusiones.
Se podría argumentar que el razonamiento detrás del Patrón de objetos nulos es similar a uno a favor de devolver la colección vacía.
Depende de la situación. Si es un caso especial, devuelva nulo. Si la función simplemente devuelve una colección vacía, entonces, obviamente, devolver eso está bien. Sin embargo, devolver una colección vacía como un caso especial debido a parámetros no válidos u otras razones NO es una buena idea, ya que está enmascarando una condición de caso especial.
En realidad, en este caso, generalmente prefiero lanzar una excepción para asegurarme de que REALMENTE no se ignore :)
Decir que hace que el código sea más robusto (al devolver una colección vacía) ya que no tienen que manejar la condición nula es malo, ya que simplemente está enmascarando un problema que debe ser manejado por el código de llamada.
Yo diría que eso null
no es lo mismo que una colección vacía y debe elegir cuál representa mejor lo que está devolviendo. En la mayoría de los casosnull
es nada (excepto en SQL). Una colección vacía es algo, aunque sea algo vacío.
Si tiene que elegir uno u otro, diría que debe tender a una colección vacía en lugar de nula. Pero hay momentos en que una colección vacía no es lo mismo que un valor nulo.
Piense siempre a favor de sus clientes (que están usando su API):
Devolver 'nulo' muy a menudo causa problemas con los clientes que no manejan las verificaciones nulas correctamente, lo que provoca una NullPointerException durante el tiempo de ejecución. He visto casos en los que la falta de verificación nula forzó un problema de producción prioritario (un cliente usó foreach (...) en un valor nulo). Durante las pruebas, el problema no se produjo porque los datos operados eran ligeramente diferentes.
Me gusta dar explicaciones aquí, con un ejemplo adecuado.
Considere un caso aquí ...
int totalValue = MySession.ListCustomerAccounts()
.FindAll(ac => ac.AccountHead.AccountHeadID
== accountHead.AccountHeadID)
.Sum(account => account.AccountValue);
Aquí considere las funciones que estoy usando.
1. ListCustomerAccounts() // User Defined
2. FindAll() // Pre-defined Library Function
Puedo usar fácilmente ListCustomerAccount
y en FindAll
lugar 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: Dado que AccountValue no lo es null
, la función Sum () no regresará null
. Por lo tanto, puedo usarla directamente.
Devolver una colección vacía es mejor en la mayoría de los casos.
La razón de esto es la conveniencia de la implementación de la persona que llama, un contrato consistente y una implementación más fácil.
Si un método devuelve nulo para indicar un resultado vacío, la persona que llama debe implementar un adaptador de comprobación nulo además de la enumeración. Este código se duplica en varias personas que llaman, así que ¿por qué no poner este adaptador dentro del método para que pueda reutilizarse?
Un uso válido de nulo para IEnumerable puede ser una indicación de resultado ausente o un fallo de la operación, pero en este caso se deben considerar otras técnicas, como lanzar una excepción.
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());
}
}
}
Lo llamo mi error de mil millones de dólares ... En ese momento, estaba diseñando el primer sistema de tipografía integral para referencias en un lenguaje orientado a objetos. Mi objetivo era asegurar que todo uso de referencias debería ser absolutamente seguro, con una verificación realizada automáticamente por el compilador. Pero no pude resistir la tentación de poner una referencia nula, simplemente porque era muy fácil de implementar. Esto ha llevado a innumerables errores, vulnerabilidades y fallas en el sistema, lo que probablemente ha causado miles de millones de dólares de dolor y daños en los últimos cuarenta años. - Tony Hoare, inventor de ALGOL W.
Vea aquí para una tormenta de mierda elaborada null
en general. No estoy de acuerdo con la afirmación de que undefined
es otra null
, pero aún vale la pena leerla. Y explica por qué debería evitarlo null
y no solo en el caso de que lo haya pedido. La esencia es que, null
en cualquier idioma, es un caso especial. Tienes que pensar null
como una excepción. undefined
es diferente en ese sentido, ese código que trata con comportamientos indefinidos es en la mayoría de los casos solo un error C y la mayoría de los otros lenguajes también tienen un comportamiento indefinido, pero la mayoría de ellos no tienen un identificador para eso en el lenguaje.
Desde la perspectiva de la gestión de la complejidad, un objetivo principal de ingeniería de software, queremos evitar la propagación innecesaria la complejidad ciclomática a los clientes de una API. Devolver un valor nulo al cliente es como devolverle el costo de complejidad ciclomática de otra rama de código.
(Esto corresponde a una carga de prueba unitaria. Debería escribir una prueba para el caso de devolución nulo, además del caso de devolución de colección vacía).