¿Es mejor devolver una colección nula o vacía?

420

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?

Omu
fuente
55
Um, no del todo CPerkins. rads.stackoverflow.com/amzn/click/0321545613
3
@CPerkins - Sí, lo hay. Está claramente establecido en las propias pautas de diseño de .NET Framework de Microsoft. Vea la respuesta de RichardOD para los detalles.
Greg Beech
51
SOLO si el significado es "No puedo calcular los resultados" debería devolver nulo. Null nunca debe tener la semántica de "vacío", solo "perdido" o "desconocido". Más detalles en mi artículo sobre el tema: blogs.msdn.com/ericlippert/archive/2009/05/14/…
Eric Lippert
3
Bozho: "any" es una gran cantidad de idiomas. ¿Qué pasa con Common Lisp, donde la lista vacía es exactamente igual al valor nulo? :-)
Ken
99
En realidad, el "duplicado" se trata de métodos que devuelven un objeto, no una colección. Es un escenario diferente con diferentes respuestas.
GalacticCowboy

Respuestas:

499

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 nullal 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.

Ryan Lundy
fuente
24
Estoy de acuerdo con Will, pero creo que "siempre" es un poco excesivo. Mientras que una colección vacía puede significar "0 elementos", devolver Nulo podría significar "ninguna colección en absoluto", por ejemplo. si está analizando HTML, buscando un <ul> con id = "foo", <ul id = "foo"> </ul> podría devolver una colección vacía; si no hay <ul> con id = "foo", un retorno nulo sería mejor (a menos que desee manejar este caso con una excepción)
Patonza
30
no siempre se trata de si "puede devolver fácilmente una matriz vacía", sino de si una matriz vacía puede ser engañosa en el contexto actual. Una matriz vacía en realidad significa algo, al igual que nulo. Decir que siempre debe devolver una matriz vacía en lugar de nulo, es casi tan equivocado como decir que un método booleano siempre debe devolver verdadero. Ambos valores posibles transmiten un significado.
David Hedlund
31
En realidad, debería preferir devolver System.Linq.Enumerable.Empty <Foo> () en lugar de un nuevo Foo [0]. Es más explícito y le ahorra una asignación de memoria (al menos, en mi implementación de .NET instalada).
Trillian
44
@Will: OP: "devuelve una colección nula o vacía para un método que tiene una colección como tipo de retorno". Creo que en este caso si es IEnumerableo ICollectionno 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.
Matthijs Wessels
77
Relacionado (artículo de CodeProject): ¿Es realmente mejor 'devolver una lista vacía en lugar de nula'?
sampathsris
154

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 .

RichardOD
fuente
33
+1. Siempre es una buena práctica seguir las pautas de diseño del marco a menos que tenga una MUY buena razón para no hacerlo.
@ Will, absolutamente. Eso es lo que sigo. Nunca he encontrado ninguna necesidad de hacer otra cosa
RichardOD
Sí, eso es lo que sigues. Pero en los casos en que las API en las que confía no lo siguen, está 'atrapado';)
Bozho
@ Bozho- yeap. Su respuesta proporciona algunas buenas respuestas de esos casos marginales.
RichardOD
90

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;
  • su API (contrato) podría obligarlo a regresar null.

Algunos ejemplos concretos:

  • un componente de la interfaz de usuario (de una biblioteca fuera de su control), podría representar una tabla vacía si se pasa una colección vacía, o ninguna tabla, si se pasa nulo.
  • en un objeto a XML (JSON / lo que sea), donde nullsignificaría que falta el elemento, mientras que una colección vacía generaría una redundancia (y posiblemente incorrecta)<collection />
  • está utilizando o implementando una API que establece explícitamente que se debe devolver / pasar nulo
Bozho
fuente
44
@ Bozho- algunos ejemplos interesantes aquí.
RichardOD
1
Vería un poco tu punto, aunque no creo que debas construir tu código alrededor de otra falla y, por definición, 'nulo' en c # nunca significa algo específico. El valor nulo se define como "sin información" y, por lo tanto, decir que contiene información específica es un oximorón. Es por eso que las pautas de .NET establecen que debe devolver un conjunto vacío si el conjunto está realmente vacío. volver nulo es decir: "No sé a dónde fue el conjunto esperado"
Rune FS
13
no, significa "no hay conjunto" en lugar de "el conjunto no tiene elementos"
Bozho
Solía ​​creer eso, luego tuve que escribir mucho TSQL y aprendí que no siempre es así, jeje.
1
La parte con Object-To-Xml es perfecta
Mihai Caracostea
36

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.

Jeffrey L Whitledge
fuente
1
Técnicamente no creo que esto devuelva una colección vacía.
FryGuy
3
@FryGuy - No, no lo hace. Devuelve un objeto Enumerable, cuyo método GetEnumerator () devuelve un Enumerator que está (por analagia con una colección) vacío. Es decir, el método MoveNext () del enumerador siempre devuelve falso. Llamar al método de ejemplo no devuelve nulo, ni devuelve un objeto Enumerable cuyo método GetEnumerator () devuelve nulo.
Jeffrey L Whitledge
31

Vacío es mucho más amigable para el consumidor.

Hay un método claro de hacer un enumerable vacío:

Enumerable.Empty<Element>()
George Polevoy
fuente
2
Gracias por el buen truco. Estaba creando instancias de una Lista vacía <T> () como un idiota, pero esto se ve mucho más limpio y probablemente también sea un poco más eficiente.
Jacobs Data Solutions el
18

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.

Arrendajo
fuente
44
¿Por qué es necesario combinar las respuestas "sin respuesta" y "cero" en el mismo valor de retorno? En cambio, haga que el método devuelva "cualquier hospitalización previa en los últimos cinco años", y tenga un método separado que pregunte, "¿alguna vez se completó la lista de hospitalizaciones anteriores?". Eso supone que hay una diferencia entre una lista completa sin hospitalizaciones previas y una lista no completada.
John Saunders
2
Pero si devuelve nulo, ¡ya está poniendo una carga adicional en la persona que llama de todos modos! La persona que llama tiene que verificar cada valor de retorno por nulo, una horrible violación de DRY. Si desea indicar "la persona que llamó no respondió la pregunta" por separado, es más sencillo crear una llamada de método adicional para indicar el hecho. O eso, o use un tipo de colección derivada con una propiedad adicional de DeclinedToAnswer. La regla de no devolver nunca nulo no es arbitraria en absoluto, es el Principio de Menos Sorpresa. Además, el tipo de retorno de su método debe significar lo que su nombre dice que hace. Nulo casi seguro que no.
jammycakes
2
Asume que su getHospitalizationList solo se llama desde un lugar y / o que todas las personas que llaman desearán distinguir entre los casos "sin respuesta" y "cero". No habrá haber casos (casi con toda seguridad una mayoría), donde la persona que llama no tiene que hacer esa distinción, y por lo que ellos están obligando a añadir cheques nulos en los lugares donde esto no debería ser necesario. Esto agrega un riesgo significativo a su base de código porque las personas pueden olvidarse fácilmente de hacerlo, y debido a que hay muy pocas razones legítimas para devolver nulo en lugar de una colección, esto será mucho más probable.
jammycakes
3
RE el nombre: Ningún nombre de función puede describir completamente lo que hace la función a menos que sea tan larga como la función y, por lo tanto, sea muy poco práctica. Pero, en cualquier caso, si la función devuelve una lista vacía cuando no se dio respuesta, entonces, por el mismo razonamiento, ¿no debería llamarse "getHospitalizationListOrEmptyListIfNoAnswer"? Pero realmente, ¿insistiría en que la función Java Reader.read se renombra como readCharacterOrReturnMinusOneOnEndOfStream? ¿Que ResultSet.getInt realmente debería ser "getIntOrZeroIfValueWasNull"? Etc.
Jay
44
RE cada llamada quiere distinguir: Bueno, sí, supongo que, o al menos que el autor de una llamada debe tomar una decisión consciente de que no le importa. Si la función devuelve una lista vacía de "no sabe" y las personas que llaman tratan ciegamente este "ninguno", podría dar resultados muy inexactos. Imagine si la función fuera "getAllergicReactionToMedicationList". Un programa que trata ciegamente "la lista no se ingresó" como "el paciente no tiene reacciones alérgicas conocidas" podría literalmente causar la muerte de un paciente. Obtendrá resultados similares, aunque menos dramáticos, en muchos otros sistemas. ...
Jay
10

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 nullpuede ser útil para comunicar que no hay suficientes datos disponibles para decir cómo debería ser la lista que se devolverá.

David Hedlund
fuente
66
En el ejemplo que da, parece que el método probablemente debería arrojar una excepción si no puede cumplir con la solicitud en lugar de simplemente devolver nulo. Las excepciones son mucho más útiles para diagnosticar problemas que los nulos.
Greg Beech
bueno, sí, en el ejemplo de la bandeja de entrada, un nullvalor 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.
David Hedlund
6

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.

Codificador kármico
fuente
8
La eficiencia casi nunca debería ser un factor al considerar la corrección del diseño de una API. En algunos casos muy específicos, como las primitivas gráficas, puede ser así, pero cuando se trata de listas y la mayoría de las otras cosas de alto nivel, lo dudo mucho.
Greg Beech
De acuerdo con Greg, especialmente dado que el código que el usuario de API tiene que escribir para compensar esta "optimización" puede ser más ineficiente que si se usara un mejor diseño en primer lugar.
Craig Stuntz
De acuerdo, y en la mayoría de los casos simplemente no vale la pena la optimización. Las listas vacías son prácticamente gratuitas con la administración de memoria moderna.
Jason Baker
6

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.

Dan
fuente
4

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.

Larry Watanabe
fuente
4

Yo diría que eso nullno 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.

Jason Baker
fuente
4

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.

manuel aldana
fuente
3

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 ListCustomerAccounty en FindAlllugar 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.

Muthu Ganapathy Nathan
fuente
2

Tuvimos esta discusión entre el equipo de desarrollo en el trabajo hace una semana más o menos, y casi unánimemente fuimos por la recolección vacía. Una persona quería devolver nulo por la misma razón que Mike especificó anteriormente.

Henric
fuente
2

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).

mothis
fuente
2

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());
        }
    }
}
George Polevoy
fuente
2

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 nullen general. No estoy de acuerdo con la afirmación de que undefinedes otra null, pero aún vale la pena leerla. Y explica por qué debería evitarlo nully no solo en el caso de que lo haya pedido. La esencia es que, nullen cualquier idioma, es un caso especial. Tienes que pensar nullcomo una excepción. undefinedes 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.

ceving
fuente
1

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).

dthal
fuente