c# - practices - ¿Es mejor devolver la colección nula o vacía?
params comments c# (18)
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 devolución?
Lo llamo mi error de mil millones de dólares ... En ese momento, estaba diseñando el primer sistema de tipo completo para referencias en un lenguaje orientado a objetos. Mi objetivo era garantizar que todo uso de referencias fuera absolutamente seguro, con la 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, que probablemente hayan causado un dolor y un daño de mil millones de dólares en los últimos cuarenta años. - Tony Hoare, inventor de ALGOL W.
Vea here para una elaborada tormenta de mierda sobre null
en general. No estoy de acuerdo con la afirmación de que undefined
es otro null
, pero todavía vale la pena leerlo. Y explica, por qué debe evitar el null
en absoluto y no solo en el caso que ha solicitado. La esencia es que ese null
es en cualquier idioma un caso especial. Tienes que pensar en null
como excepción. undefined
es diferente de esa manera, ese código que trata con un comportamiento indefinido es, en la mayoría de los casos, solo un error. C y la mayoría de los otros idiomas también tienen un comportamiento indefinido, pero la mayoría de ellos no tienen un identificador para eso en el idioma.
Colección vacía. Si está utilizando C #, se supone que maximizar los recursos del sistema no es esencial. Si bien es menos eficiente, devolver la Colección vacía es mucho más conveniente para los programadores involucrados (por la razón que Will describió anteriormente).
Colección vacía. Siempre.
Esto apesta:
if(myInstance.CollectionProperty != null)
{
foreach(var item in myInstance.CollectionProperty)
/* arrgh */
}
Se considera una mejor práctica NUNCA devolver un null
al devolver una colección o enumerable. SIEMPRE devuelve una enumerable / colección vacía. Previene las tonterías mencionadas anteriormente, y evita que su automóvil sea manipulado por compañeros de trabajo y usuarios de sus clases.
Cuando se habla de propiedades, siempre establezca su propiedad una vez y olvídese.
public List<Foo> Foos {public get; private set;}
public Bar() { Foos = new List<Foo>(); }
En .NET 4.6.1, puedes 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 en lugar de null
...
public IEnumerable<Foo> GetMyFoos()
{
return InnerGetFoos() ?? Enumerable.Empty<Foo>();
}
El uso de Enumerable.Empty<T>()
puede verse como más eficiente que devolver, por ejemplo, una nueva colección o matriz vacía.
De las Pautas de Diseño del Marco de la 2ª Edición (pág. 256):
NO devuelva valores nulos de las propiedades de colección o de los 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 intentando encontrar algo en el blog de Brad Abram, y él lo vinculó al artículo).
Edit - como Eric Lippert ha comentado ahora a la pregunta original, también me gustaría blogs.msdn.com/ericlippert/archive/2009/05/14/… .
Depende de la situación. Si es un caso especial, devuelve nulo. Si la función simplemente devuelve una colección vacía, es obvio que devolverla 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, normalmente prefiero lanzar una excepción para asegurarme de que REALMENTE no se ignora :)
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á ocultando un problema que debe ser manejado por el código que llama.
Depende de su contrato y de su caso concreto . En general , es mejor devolver las colecciones vacías , pero a veces ( rara vez ):
-
null
puede significar algo más específico; - su API (contrato) podría forzarle a devolver el
null
.
Algunos ejemplos concretos:
- un componente de la interfaz de usuario (de una biblioteca fuera de su control), podría estar representando una tabla vacía si se pasa una colección vacía, o no se muestra ninguna tabla, si se pasa un valor nulo.
- en un Objeto a XML (JSON / lo que sea), donde
null
significaría que falta el elemento, mientras que una colección vacía representaría un redundante (y posiblemente incorrecto)<collection />
- está utilizando o implementando una API que declara explícitamente que el valor nulo se debe devolver / pasar
Desde la perspectiva de la gestión de la complejidad, un objetivo principal de la ingeniería de software, queremos evitar propagar una complejidad ciclomática innecesaria a los clientes de una API. Devolver un nulo al cliente es como devolverles el costo de complejidad ciclomática de otra rama de código.
(Esto corresponde a una carga de prueba de unidad. Necesitaría escribir una prueba para el caso de devolución nula, además del caso de devolución de colección vacía).
Devolver nulo podría ser más eficiente, ya que no se crea ningún nuevo objeto. Sin embargo, a menudo también requeriría una comprobación null
(o manejo de excepciones).
Semánticamente, null
y una lista vacía no significan 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, documéntelo para evitar confusiones.
Devolver una colección vacía es mejor en la mayoría de los casos.
La razón de ello es la conveniencia de la implementación de la persona que llama, el contrato consistente y la implementación más fácil.
Si un método devuelve un valor 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 luego se duplica en varias personas que llaman, así que ¿por qué no colocar este adaptador dentro del método para poder reutilizarlo?
Un uso válido de nulo para IEnumerable puede ser una indicación de un resultado ausente o un error de 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 .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://.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());
}
}
}
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, las expectativas del programador) se debe devolver una colección vacía.
Me gusta dar explicaciones aquí, con el 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 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 es null
, la función Sum () no devolverá null
. Por lo tanto, puedo usarlo directamente.
Me parece que deberías devolver el valor semánticamente correcto en el contexto, cualquiera que sea. Una regla que dice "siempre devolver una colección vacía" me parece un poco simplista.
Supongamos que en, digamos, un sistema para un hospital, tenemos una función que debe devolver una lista de todas las hospitalizaciones anteriores durante los últimos 5 años. Si el cliente no ha estado en el hospital, tiene sentido devolver una lista vacía. Pero, ¿y si el cliente dejó 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, yo ingrese cero y recibo un mensaje de error que me indica que debo ingresar un valor en este campo. Acabo de hacer: entré en cero! Pero no aceptará cero porque no puede distinguirlo de ninguna respuesta.
Responder 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 sé" de blanco o cero, lo que me parece un defecto potencialmente grave. Por ejemplo, yo estaba comprando una casa hace aproximadamente un año. Fui a un sitio web de bienes raíces y había muchas casas en la lista con un precio de venta de $ 0. Me sonó bastante bien: ¡están regalando estas casas gratis! Pero estoy seguro de que la triste realidad era que simplemente no habían ingresado en el precio. En ese caso, puede decir: "Bueno, OBVIAMENTE, cero significa que no ingresaron en el precio; nadie va a regalar una casa de forma gratuita". Pero el sitio también enumeró los precios promedio de venta y venta 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 sé"? Técnicamente la respuesta es "no sé". Lo que probablemente queremos ver es $ 110,000. Pero lo que probablemente obtendremos es $ 73,333, lo que sería completamente incorrecto. Además, ¿qué pasaría si tuviéramos este problema en un sitio donde los usuarios pueden hacer pedidos en línea? (Es poco probable que se trate de bienes raíces, pero estoy seguro de que lo ha visto en muchos otros productos). ¿Querríamos realmente que "el precio no especificado todavía" 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é querrías hacerlo? Ahora el programa de llamadas tiene que hacer dos llamadas en lugar de una. ¿Qué sucede si un programador no llama al "alguna"? y va directo al "¿qué es?" ? ¿El programa devolverá un cero equivocado? Lanzar una excepción? ¿Devolver 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 el esfuerzo de obedecerla? Si no, ¿para qué molestarse?
Responder a Jammycakes:
Considere cómo se vería el código real. Sé que la pregunta dice C #, pero disculpe si escribo Java. Mi C # no es muy fuerte 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 un código de línea o dos 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 DRY. 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 presionar el manejo hacia abajo a la función de obtener lista en lugar de que la persona que llama lo haga, y así colocar el código en la persona que llama sería una violación SECA. Pero casi seguro que no queremos hacer siempre lo mismo. En las funciones donde debemos tener la lista para procesar, una lista faltante 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 que ingresen datos. Por lo tanto, el manejo de "no lista" debe realizarse en el nivel de la persona que llama de una u otra manera. Y si lo hacemos con un retorno nulo o una función separada no hace ninguna diferencia al principio más grande.
Claro, si la persona que llama no comprueba si es 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 lanza una excepción o falla de otra manera, bueno, eso es más o menos lo que pasaría si devolviera el valor nulo y no lo verificara. 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: es simplemente incorrecto.
No veo cómo ayuda adjuntar un atributo adicional a la colección. La persona que llama todavía tiene que comprobarlo. ¿Cómo es eso mejor que comprobar si hay nulo? Nuevamente, lo peor que podría pasar es que el programador se olvide de verificarlo y dé 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 tener 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 nulo. Pero, ¿cómo descubriría la existencia de otra función a menos que se le ocurriera que podría haber tal función y verifica la documentación, y la documentación es completa y comprensible? Preferiría tener una función que siempre me dé una respuesta significativa, en lugar de dos funciones que debo conocer y recordar para llamar a ambas.
Piense siempre a favor de sus clientes (que están usando su api):
Devolver ''nulo'' muy a menudo genera problemas con los clientes que no manejan las comprobaciones nulas correctamente, lo que provoca una NullPointerException durante el tiempo de ejecución. He visto casos en los que una verificación nula faltante forzó un problema de producción prioritario (un cliente usó foreach (...) en un valor nulo). Durante la prueba, el problema no ocurrió, porque los datos operados eran ligeramente diferentes.
Se podría argumentar que el razonamiento detrás del Patrón de objeto nulo es similar a uno a favor de devolver la colección vacía.
Si una colección vacía tiene sentido semánticamente, eso es lo que prefiero devolver. Al devolver una colección vacía para GetMessagesInMyInbox()
comunica "realmente no tiene ningún mensaje en su bandeja de entrada", mientras que devolver un null
puede ser útil para comunicar que no hay suficientes datos disponibles para decir qué aspecto debería tener la lista que debe ser devuelta.
Tuvimos esta discusión entre el equipo de desarrollo en el trabajo hace una semana, y casi por unanimidad fuimos a la colección vacía. Una persona quería devolver el valor nulo por la misma razón que Mike especificó anteriormente.
Vacío es mucho más amigable para el consumidor.
Hay un método claro de hacer un enumerable vacío:
Enumerable.Empty<Element>()
Yo diría que null
no es lo mismo que una colección vacía y debería elegir cuál representa mejor lo que está devolviendo. En la mayoría de los casos, null
no es nada (excepto en SQL). Una colección vacía es algo, aunque algo vacío.
Si tiene que elegir uno u otro, yo diría que debería tender hacia una colección vacía en lugar de nula. Pero hay ocasiones en que una colección vacía no es lo mismo que un valor nulo.