¿Debo aceptar colecciones vacías en mis métodos que las repiten?

40

Tengo un método donde toda la lógica se realiza dentro de un bucle foreach que itera sobre el parámetro del método:

public IEnumerable<TransformedNode> TransformNodes(IEnumerable<Node> nodes)
{
    foreach(var node in nodes)
    {
        // yadda yadda yadda
        yield return transformedNode;
    }
}

En este caso, enviar una colección vacía da como resultado una colección vacía, pero me pregunto si eso no es prudente.

Mi lógica aquí es que si alguien llama a este método, entonces tienen la intención de pasar datos, y solo pasarían una colección vacía a mi método en circunstancias erróneas.

¿Debería detectar este comportamiento y lanzar una excepción, o es la mejor práctica devolver la colección vacía?

Nick Udell
fuente
43
¿Está seguro de que su suposición "si alguien llama a este método, entonces tiene la intención de pasar datos" es correcta? Tal vez solo están pasando lo que tienen a mano para trabajar con el resultado.
SpaceTrucker
77
Por cierto: su método de "transformación" generalmente se llama "mapa", aunque en .NET (y SQL, que es de donde obtuvo el nombre .NET), comúnmente se llama "select". "Transformar" es lo que C ++ lo llama, pero ese no es un nombre común que uno pueda reconocer.
Jörg W Mittag
25
Tiraría si la colección es nullpero no si está vacía.
CodesInChaos
21
@NickUdell: paso colecciones vacías en mi código todo el tiempo. Es más fácil que el código se comporte de la misma manera que escribir una lógica de bifurcación especial para casos en los que no puedo agregar nada a mi colección antes de continuar con ella.
theMayer
77
Si marca y arroja un error si la colección está vacía, entonces está obligando a la persona que llama a verificar también y no pasar una colección vacía a su método. Las validaciones deben realizarse en (y solo en) los límites del sistema, si las colecciones vacías le molestan, ya debería haberlas verificado mucho antes de que llegara a cualquier método de utilidad. Ahora tiene dos cheques redundantes.
Lie Ryan

Respuestas:

175

Los métodos de utilidad no deberían arrojar colecciones vacías. Sus clientes API lo odiarían por ello.

Una colección puede estar vacía; una "colección que no debe estar vacía" es conceptualmente una cosa mucho más difícil de trabajar.

La transformación de una colección vacía tiene un resultado obvio: la colección vacía. (Incluso puede guardar algo de basura devolviendo el parámetro en sí).

Hay muchas circunstancias en las que un módulo mantiene listas de cosas que pueden o no estar llenas de algo. Tener que verificar el vacío antes de cada llamada transformes molesto y tiene el potencial de convertir un algoritmo simple y elegante en un desastre feo.

Los métodos de utilidad siempre deben esforzarse por ser liberales en sus entradas y conservadores en sus salidas.

Por todas estas razones, por el amor de Dios, maneje la colección vacía correctamente. Nada es más exasperante que un módulo auxiliar que cree que sabe lo que quiere mejor que usted.

Kilian Foth
fuente
14
+1 - (más si pudiera). Incluso podría llegar a unirme también nulla la colección vacía. Las funciones con limitaciones implícitas son un dolor.
Telastyn
66
"No, no deberías" ... ¿no deberías aceptarlos o no lanzar una excepción?
Ben Aaronson
16
@Andy: sin embargo, esos no serían métodos de utilidad . Serían métodos comerciales o algún comportamiento concreto similar.
Telastyn
38
No creo que reciclar la colección vacía para una devolución sea una buena idea. Solo está ahorrando un poquito de memoria; y podría provocar un comportamiento inesperado en la persona que llama. Ejemplo ligeramente inventado: Populate1(input); output1 = TransformNodes(input); Populate2(input); output2 = TransformNodes(input); si Populate1 dejara la colección vacía y la devolvieras para la primera llamada TransformNodes, output1 y input serían la misma colección, y cuando se llamaba a Populaten2 si ponía nodos en la colección, terminarías con tu segundo conjunto de entrada en salida2.
Dan Neely
66
@Andy Aun así, si el argumento nunca debe estar vacío, si es un error no tener datos para procesar, creo que es responsabilidad de la persona que llama verificar esto, cuando está compilando ese argumento. No solo mantiene este método más elegante, sino que significa que uno puede generar un mejor mensaje de error (mejor contexto) y generarlo de una manera rápida y sin fallas. No tiene sentido que una clase aguas abajo verifique los invariantes de la persona que llama ...
Andrzej Doyle
26

Veo dos preguntas importantes que determinan la respuesta a esto:

  1. ¿Puede su función devolver algo significativo y lógico cuando se pasa una colección vacía (incluyendo nulo )?
  2. ¿Cuál es el estilo de programación general en esta aplicación / biblioteca / equipo? (Específicamente, ¿cómo estás FP?)

1. Retorno significativo

Esencialmente, si puede devolver algo significativo, no arroje una excepción. Deje que la persona que llama lidie con el resultado. Entonces, si tu función ...

  • Cuenta el número de elementos en la colección, devuelve 0. Eso fue fácil.
  • Busca elementos que coincidan con criterios particulares, devuelve una colección vacía. Por favor no arrojes nada. La persona que llama puede tener una gran colección de colecciones, algunas de las cuales están vacías, otras no. La persona que llama quiere elementos coincidentes en cualquier colección. Las excepciones solo dificultan la vida de la persona que llama.
  • Está buscando el mayor / menor / mejor ajuste a los criterios de la lista. Ups Dependiendo de la pregunta de estilo, puede lanzar una excepción aquí o puede regresar nulo . Odio nulo (en gran medida una persona FP), pero podría decirse que es más significativo aquí y le permite reservar excepciones para errores inesperados dentro de su propio código. Si la persona que llama no verifica nulo, de todos modos se producirá una excepción bastante clara. Pero se lo dejas a ellos.
  • Pide el enésimo elemento de la colección o el primer / último n elementos. Este es el mejor caso para una excepción y el que tiene menos probabilidades de causar confusión y dificultad para la persona que llama. Aún se puede presentar un caso para nulo si usted y su equipo están acostumbrados a verificarlo, por todas las razones dadas en el punto anterior, pero este es el caso más válido hasta ahora para lanzar una excepción DudeYou Know YouShouldCheckTheSizeFirst . Si su estilo es más funcional, entonces anule o lea mi respuesta de Estilo .

En general, mi sesgo de FP me dice "devolver algo significativo" y nulo puede tener un significado válido en este caso.

2. Estilo

¿Su código general (o el código del proyecto o el código del equipo) favorece un estilo funcional? Si no, se esperarán y se tratarán excepciones. En caso afirmativo, considere devolver un Tipo de opción . Con un tipo de opción, puede devolver una respuesta significativa o Ninguno / Nada . En mi tercer ejemplo de arriba, Nothing sería una buena respuesta en estilo FP. El hecho de que la función devuelva señales de tipo Opción claramente a la persona que llama que una respuesta significativa puede no ser posible y que la persona que llama debe estar preparada para lidiar con eso. Siento que le da a la persona que llama más opciones (si perdona el juego de palabras).

F # es donde todos los niños del fresco .Net hacen este tipo de cosas, pero C # hace apoyar este estilo.

tl; dr

Mantenga excepciones para errores inesperados en su propia ruta de código, entrada no totalmente previsible (y legal) de otra persona.

itsbruce
fuente
"Mejor ajuste", etc.: puede tener un método que encuentre el objeto de mejor ajuste en una colección que coincida con algún criterio. Ese método tendría el mismo problema para las colecciones no vacías, porque nada podría ajustarse al criterio, por lo que no debe preocuparse por las selecciones vacías.
gnasher729
1
No, es por eso que dije "mejor ajuste" y no "coincide"; la frase implica la aproximación más cercana, no una coincidencia exacta. Las opciones más grandes / más pequeñas deberían haber sido una pista. Si solo se ha pedido el "mejor ajuste", cualquier colección con 1 o más miembros debería poder devolver un miembro. Si solo hay un miembro, ese es el mejor ajuste.
itsbruce
1
Devolver "nulo" en la mayoría de los casos es peor que lanzar una excepción. Sin embargo, devolver una colección vacía es significativo.
Ian
1
No si tiene que devolver un solo artículo (min / max / head / tail). En cuanto a nulo, como dije, lo odio (y prefiero los idiomas que no lo tienen), pero cuando un idioma lo tiene, debe ser capaz de lidiar con él y codificarlo. Dependiendo de las convenciones de codificación locales, puede ser apropiado.
itsbruce
Ninguno de sus ejemplos tiene nada que ver con una entrada vacía. Son solo casos especiales de situaciones en las que la respuesta podría ser "nada". Lo que a su vez debería responder a la pregunta del OP sobre cómo "manejar" la colección vacía.
djechlin
18

Como siempre, depende.

¿ Importa que la colección esté vacía?
La mayoría del código de manejo de colecciones probablemente diría "no"; una colección puede tener cualquier número de elementos, incluido cero.

Ahora, si tiene algún tipo de colección donde es "inválido" no tener elementos, entonces ese es un nuevo requisito y tiene que decidir qué hacer al respecto.

Pide prestada alguna lógica de prueba del mundo de la base de datos: prueba para cero elementos, un elemento y dos elementos. Eso atiende a los casos más importantes (eliminar cualquier condición de unión cartesiana o interna mal formada).

Phill W.
fuente
Y si no es válido no tener elementos en una colección, entonces idealmente el argumento no sería una clase java.util.Collectionpersonalizada, sino una com.foo.util.NonEmptyCollectionclase personalizada , que pueda preservar constantemente esta invariante y evitar que comience a un estado no válido.
Andrzej Doyle
3
Esta pregunta está etiquetada [c #]
Nick Udell
@NickUdell ¿C # no admite programación orientada a objetos o el concepto de espacios de nombres anidados? TIL
John Dvorak
2
Lo hace. Mi comentario fue una aclaración, ya que Andrzej debe haber estado confundido (¿por qué más se esforzarían por especificar el espacio de nombres para un idioma que esta pregunta no hace referencia?).
Nick Udell
-1 por enfatizar el "depende" más que el "casi seguro que sí". No es broma: comunicar lo incorrecto hace daño aquí.
djechlin
11

Como cuestión de buen diseño, acepte tantas variaciones en sus entradas como sea práctico o posible. Solo se deben generar excepciones cuando (se presenta una entrada inaceptable O se producen errores inesperados durante el procesamiento) Y, como resultado, el programa no puede continuar de una manera predecible .

En este caso, debe esperarse que se presente una colección vacía, y su código debe manejarla (lo que ya hace). Sería una violación de todo lo que es bueno si su código arrojó una excepción aquí. Esto sería similar a multiplicar 0 por 0 en matemáticas. Es redundante, pero absolutamente necesario para que funcione como lo hace.

Ahora, al argumento de colección nula. En este caso, una colección nula es un error de programación: el programador olvidó asignar una variable. Este es un caso en el que podría lanzarse una excepción, porque no puede procesar eso significativamente en una salida, e intentar hacerlo introduciría un comportamiento inesperado. Esto sería similar a dividir entre cero en matemáticas: no tiene ningún sentido.

theMayer
fuente
Su declaración audaz es un consejo extremadamente malo en general. Creo que es cierto aquí, pero debes explicar qué tiene de especial esta circunstancia. (Es un mal consejo en general porque promueve fallas silenciosas).
djechlin
@AAA: claramente no estamos escribiendo un libro sobre cómo diseñar software aquí, pero no creo que sea un mal consejo si realmente entiendes lo que estoy diciendo. Mi punto es que si la entrada incorrecta produce una salida ambigua o arbitraria, debe lanzar una excepción. En casos donde la salida es totalmente predecible (como es el caso aquí), lanzar una excepción sería arbitrario. La clave es nunca tomar decisiones arbitrarias en su programa, ya que de ahí proviene el comportamiento impredecible.
theMayer
Pero es tu primera oración y la única resaltada ... nadie entiende lo que estás diciendo todavía. (No estoy tratando de ser demasiado agresivo aquí, solo una de las cosas que estamos aquí para hacer es aprender a escribir y comunicarme y creo que la pérdida de precisión en un punto clave es dañina).
djechlin
10

La solución correcta es mucho más difícil de ver cuando solo está mirando su función de forma aislada. Considere su función como parte de un problema mayor . Una posible solución a ese ejemplo se ve así (en Scala):

input.split("\\D")
.filterNot (_.isEmpty)
.map (_.toInt)
.filter (x => x >= 1000 && x <= 9999)

Primero, divide la cadena por no dígitos, filtra las cadenas vacías, convierte las cadenas en enteros y luego filtra para mantener solo los números de cuatro dígitos. Su función podría estar map (_.toInt)en la tubería.

Este código es bastante sencillo porque cada etapa de la tubería solo maneja una cadena vacía o una colección vacía. Si coloca una cadena vacía al principio, obtendrá una lista vacía al final. No tiene que detenerse y verificar nullo una excepción después de cada llamada.

Por supuesto, eso supone que una lista de salida vacía no tiene más de un significado. Si necesita diferenciar entre una salida vacía causada por una entrada vacía y una causada por la propia transformación, eso cambia completamente las cosas.

Karl Bielefeldt
fuente
+1 para un ejemplo extremadamente efectivo (faltan las otras buenas respuestas).
djechlin
2

Esta pregunta es en realidad sobre excepciones. Si lo mira de esa manera e ignora la colección vacía como un detalle de implementación, la respuesta es sencilla:

1) Un método debe arrojar una excepción cuando no puede continuar: no puede realizar la tarea designada o devolver el valor apropiado.

2) Un método debe detectar una excepción cuando puede continuar a pesar de la falla.

Por lo tanto, su método auxiliar no debería ser "útil" y generar una excepción a menos que no pueda hacer su trabajo con una colección vacía. Deje que la persona que llama determine si se pueden manejar los resultados.

Si devuelve una colección vacía o nula, es un poco más difícil pero no mucho: las colecciones anulables deben evitarse si es posible. El propósito de una colección anulable sería indicar (como en SQL) que no tiene la información; una colección de niños, por ejemplo, podría ser nula si no sabe si alguien tiene alguna, pero usted no Sé que no lo hacen. Pero si eso es importante por alguna razón, probablemente valga una variable adicional para rastrearlo.

jmoreno
fuente
1

El método se nombra TransformNodes. En el caso de una colección vacía como entrada, recuperar una colección vacía es natural e intuitivo, y tiene un sentido matemático perfecto.

Si el método fue nombrado Max y diseñado para devolver el elemento máximo, entonces sería natural lanzar NoSuchElementExceptionuna colección vacía, ya que el máximo de nada no tiene sentido matemático.

Si el método fue nombrado JoinSqlColumnNamesy diseñado para devolver una cadena donde los elementos están unidos por una coma para usar en consultas SQL, entonces tendría sentido lanzar IllegalArgumentExceptionuna colección vacía, ya que el llamador eventualmente obtendría un error SQL de todos modos si usara la cadena en una consulta SQL directamente sin más comprobaciones, y en lugar de buscar una cadena vacía devuelta, realmente debería haber verificado la colección vacía.

Detener el daño continuo a Mónica
fuente
Maxde nada es a menudo negativo infinito.
djechlin
0

Retrocedamos y usemos un ejemplo diferente que calcule la media aritmética de una matriz de valores.

Si la matriz de entrada está vacía (o nula), ¿puede cumplir razonablemente la solicitud de la persona que llama? No. ¿Cuáles son tus opciones? Bueno, podrías:

  • presente / devolver / lanzar un error. usando la convención de su base de código para esa clase de error.
  • documentar que se devolverá un valor como cero
  • documento de que se devolverá un valor no válido designado (por ejemplo, NaN)
  • documentar que se devolverá un valor mágico (p. ej., un mínimo o máximo para el tipo o algún valor indicativo de esperanza)
  • declara que el resultado no está especificado
  • declarar que la acción no está definida
  • etc.

Les digo que les den el error si le han dado una entrada no válida y la solicitud no se puede completar. Me refiero a un error grave desde el primer día para que entiendan los requisitos de su programa. Después de todo, su función no está en condiciones de responder. Si la operación pudiera fallar (por ejemplo, copiar un archivo), entonces su API debería darles un error con el que puedan lidiar.

De modo que eso puede definir cómo su biblioteca maneja las solicitudes con formato incorrecto y las solicitudes que pueden fallar.

Es muy importante que su código sea coherente en la forma en que maneja estas clases de errores.

La siguiente categoría es decidir cómo su biblioteca maneja las solicitudes sin sentido. Volviendo a un ejemplo similar a la suya - Vamos a utilizar una función que determina si existe un archivo en una ruta: bool FileExistsAtPath(String). Si el cliente pasa una cadena vacía, ¿cómo maneja este escenario? ¿Qué tal una matriz vacía o nula pasada void SaveDocuments(Array<Document>)? Decida su biblioteca / base de código, y sea ​​coherente. Considero que estos casos son errores y prohíbo que los clientes realicen solicitudes sin sentido al marcarlos como errores (a través de una aserción). Algunas personas resistirán fuertemente esa idea / acción. Encuentro esta detección de errores muy útil. Es muy bueno para localizar problemas en los programas, con buena ubicación para el programa infractor. Los programas son mucho más claros y correctos (considere la evolución de su base de código), y no graben ciclos dentro de funciones que no hacen nada. El código es más pequeño / más limpio de esta manera, y las comprobaciones generalmente se envían a los lugares donde se puede presentar el problema.

justin
fuente
66
Para su último ejemplo, no es el trabajo de esa función determinar lo que no tiene sentido. Por lo tanto, una cadena vacía debe devolver falso. De hecho, ese es el enfoque exacto que toma File.Exists .
theMayer
@ rmayer06 es su trabajo. la función referenciada permite cadenas nulas y vacías, busca caracteres no válidos en la cadena, realiza el truncamiento de la cadena, puede que necesite hacer llamadas al sistema de archivos, puede que necesite consultar el entorno, etc. esas 'conveniencias' a menudo redundantes en ejecución tienen un alto costo, y definitivamente difuminan las líneas de corrección (IMO). He visto muchos if (!FileExists(path)) { ...create it!!!... }errores, muchos de los cuales se detectarían antes de cometer, si las líneas de corrección no fueran borrosas.
justin
2
Estoy de acuerdo con usted, si el nombre de la función fuera File.ThrowExceptionIfPathIsInvalid, pero ¿quién en su sano juicio llamaría a tal función?
theMayer
Un promedio de 0 elementos ya devolverá NaN o arrojará una excepción de división por cero, solo por cómo se define la función. Un archivo con un nombre que posiblemente no puede existir, o no existirá o ya provocará un error. No hay una razón convincente para manejar estos casos especialmente.
cHao
Incluso se podría argumentar que el concepto completo de verificar la existencia de un archivo antes de leerlo o escribirlo, de todos modos es defectuoso. (Hay una condición de carrera inherente). Es mejor seguir adelante e intentar abrir el archivo, para leerlo o con configuraciones de solo creación si está escribiendo. Ya fallará si el nombre de archivo no es válido o no existe (o si existe, en el caso de la escritura).
cHao
-3

Como regla general, una función estándar debería poder aceptar la lista más amplia de entradas y dar retroalimentación sobre ella, hay muchos ejemplos en los que los programadores usan funciones de formas que el diseñador no había planeado, teniendo esto en cuenta, creo que la función debería poder aceptar no solo colecciones vacías, sino también una amplia gama de tipos de entrada y devolver comentarios con gracia, ya sea incluso un objeto de error de cualquier operación realizada en la entrada ...

Clemente Mark-Aaba
fuente
Es absolutamente incorrecto para el diseñador suponer que siempre se pasará una colección y pasar directamente a iterar sobre dicha colección, se debe hacer una verificación para asegurarse de que el argumento recibido se ajusta al tipo de datos esperado ...
Clement Mark-Aaba
3
"amplia gama de tipos de entrada" parece irrelevante aquí. La pregunta está etiquetada c # , un lenguaje fuertemente tipado. Es decir, compilador garantiza que la entrada es una colección
mosquito
Amablemente "regla de oro" de Google, mi respuesta fue una advertencia generalizada de que, como programador, usted deja espacio para sucesos imprevistos o erróneos, uno de estos puede pasar erróneamente por argumentos de función ...
Clement Mark-Aaba
1
Esto es incorrecto o tautológico. writePaycheckToEmployeeno debería aceptar un número negativo como entrada ... pero si "retroalimentación" significa "hace algo que podría suceder después", entonces sí, cada función hará algo que haga después.
djechlin