Estoy desarrollando una biblioteca destinada al lanzamiento público. Contiene varios métodos para operar en conjuntos de objetos: generar, inspeccionar, particionar y proyectar los conjuntos en nuevas formas. En caso de que sea relevante, es una biblioteca de clase C # que contiene extensiones de estilo LINQ IEnumerable
, que se lanzará como un paquete NuGet.
Algunos de los métodos en esta biblioteca pueden recibir parámetros de entrada insatisfactorios. Por ejemplo, en los métodos combinatorios, hay un método para generar todos los conjuntos de n elementos que se pueden construir a partir de un conjunto fuente de m elementos. Por ejemplo, dado el conjunto:
1, 2, 3, 4, 5
y pedir combinaciones de 2 produciría:
1, 2
1, 3
1, 4,
etc ...
5, 3
5, 4
Ahora, obviamente es posible pedir algo que no se puede hacer, como darle un conjunto de 3 elementos y luego pedir combinaciones de 4 elementos mientras configura la opción que dice que solo puede usar cada elemento una vez.
En este escenario, cada parámetro es válido individualmente :
- La colección de origen no es nula y contiene elementos.
- El tamaño solicitado de las combinaciones es un entero positivo distinto de cero
- El modo solicitado (use cada elemento solo una vez) es una opción válida
Sin embargo, el estado de los parámetros cuando se toman juntos causa problemas.
En este escenario, ¿esperaría que el método arroje una excepción (por ejemplo InvalidOperationException
) o devuelva una colección vacía? Cualquiera de los dos me parece válido:
- No puede producir combinaciones de n elementos a partir de un conjunto de m elementos donde n> m si solo se le permite usar cada elemento una vez, por lo tanto, esta operación puede considerarse imposible
InvalidOperationException
. - El conjunto de combinaciones de tamaño n que se pueden producir a partir de m elementos cuando n> m es un conjunto vacío; No se pueden producir combinaciones.
El argumento para un conjunto vacío
Mi primera preocupación es que una excepción evita el encadenamiento idiomático de métodos al estilo LINQ cuando se trata de conjuntos de datos que pueden tener un tamaño desconocido. En otras palabras, es posible que desee hacer algo como esto:
var result = someInputSet
.CombinationsOf(4, CombinationsGenerationMode.Distinct)
.Select(combo => /* do some operation to a combination */)
.ToList();
Si su conjunto de entrada es de tamaño variable, el comportamiento de este código es impredecible. Si .CombinationsOf()
arroja una excepción cuando someInputSet
tiene menos de 4 elementos, este código a veces fallará en tiempo de ejecución sin alguna verificación previa. En el ejemplo anterior, esta comprobación es trivial, pero si la está llamando a la mitad de una cadena más larga de LINQ, esto puede ser tedioso. Si devuelve un conjunto vacío, entonces result
estará vacío, con lo que puede estar perfectamente satisfecho.
El argumento para una excepción
Mi segunda preocupación es que devolver un conjunto vacío puede ocultar problemas: si está llamando a este método a la mitad de una cadena de LINQ y devuelve silenciosamente un conjunto vacío, entonces puede tener problemas algunos pasos más tarde o encontrarse con un vacío conjunto de resultados, y puede no ser obvio cómo sucedió eso dado que definitivamente tenía algo en el conjunto de entrada.
¿Qué esperarías y cuál es tu argumento?
fuente
Respuestas:
Devolver un conjunto vacío
Esperaría un conjunto vacío porque:
Hay 0 combinaciones de 4 números del conjunto de 3 cuando solo puedo usar cada número una vez
fuente
En caso de duda, pregunte a alguien más.
Su función ejemplo tiene una muy similar en Python:
itertools.combinations
. Vamos a ver cómo funciona:Y se siente perfectamente bien para mí. Esperaba un resultado que pudiera repetir y obtuve uno.
Pero, obviamente, si fueras a preguntar algo estúpido:
Entonces, diría que si todos sus parámetros se validan pero el resultado es un conjunto vacío, devuelve un conjunto vacío, no es el único que lo hace.
Como dijo @Bakuriu en los comentarios, esto también es lo mismo para una
SQL
consulta comoSELECT <columns> FROM <table> WHERE <conditions>
. Mientras<columns>
,<table>
,<conditions>
así se forman en forma y se refieren a los nombres existentes, se puede construir un conjunto de condiciones que se excluyen mutuamente. La consulta resultante simplemente no generaría filas en lugar de arrojar unInvalidConditionsError
.fuente
n
elementos en una colección para contar la cantidad de formas en que podemos elegirlos. Si en algún momento no quedan elementos al seleccionarlos, entonces no hay (0) forma de seleccionarn
elementos de dicha colección.1.0 / 0
, Python no.SELECT
sobre una tabla produce una excepción cuando el conjunto de resultados está vacío? (spoiler: no!) El "buen formato" de una consulta depende solo de la consulta en sí y de algunos metadatos (por ejemplo, ¿existe la tabla y tiene este campo (con este tipo)?) Una vez que la consulta está bien formada y la ejecuta, espera (posiblemente vacío) resultado válido o una excepción porque el "servidor" tuvo un problema (por ejemplo, el servidor de base de datos se cayó o algo así).En términos simples:
fuente
throw
o devolver un conjunto vacío ...Estoy de acuerdo con la respuesta de Ewan, pero quiero agregar un razonamiento específico.
Se trata de operaciones matemáticas, por lo que podría ser un buen consejo seguir con las mismas definiciones matemáticas. Desde un punto de vista matemático, el número de conjuntos r de un conjunto n (es decir, nCr ) está bien definido para todos r> n> = 0. Es cero. Por lo tanto, devolver un conjunto vacío sería el caso esperado desde un punto de vista matemático.
fuente
Encuentro una buena manera de determinar si se usa una excepción, es imaginar a las personas involucradas en la transacción.
Tomando la recuperación del contenido de un archivo como ejemplo:
Por favor, tráeme el contenido del archivo, "no existe.txt"
a. "Aquí está el contenido: una colección vacía de personajes"
si. "Erm, hay un problema, ese archivo no existe. ¡No sé qué hacer!"
Por favor, tráeme el contenido del archivo, "existe pero está vacío.
a. "Aquí está el contenido: una colección vacía de personajes"
si. "Erm, hay un problema, no hay nada en este archivo. ¡No sé qué hacer!"
Sin duda, algunos estarán en desacuerdo, pero para la mayoría de la gente, "Erm, hay un problema" tiene sentido cuando el archivo no existe y devuelve "una colección vacía de caracteres" cuando el archivo está vacío.
Entonces, aplicando el mismo enfoque a tu ejemplo:
Por favor, dame todas las combinaciones de 4 artículos para
{1, 2, 3}
a. No hay ninguno, aquí hay un conjunto vacío.
si. Hay un problema, no sé qué hacer.
Una vez más, "Hay un problema" tendría sentido si, por ejemplo,
null
se ofrecieran como el conjunto de elementos, pero "aquí hay un conjunto vacío" parece una respuesta sensata a la solicitud anterior.Si devolver un valor vacío enmascara un problema (por ejemplo, un archivo faltante, a
null
), generalmente se debe usar una excepción en su lugar (a menos que el idioma elegido admitaoption/maybe
tipos, a veces tienen más sentido). De lo contrario, devolver un valor vacío probablemente simplificará el costo y cumplirá mejor con el principio del mínimo asombro.fuente
Como es para una biblioteca de propósito general, mi instinto sería Dejar que el usuario final elija.
Al igual que tenemos
Parse()
y tenemosTryParse()
a nuestra disposición, podemos tener la opción de utilizar según la salida que necesitemos de la función. Pasaría menos tiempo escribiendo y manteniendo un contenedor de funciones para lanzar la excepción que discutiendo sobre la elección de una versión única de la función.fuente
Single()
eSingleOrDefault()
inmediatamente surgieron a la mente. Single arroja una excepción si hay cero o> 1 resultado, mientras que SingleOrDefault no arrojará, y en su lugar regresadefault(T)
. Quizás OP podría usarCombinationsOf()
yCombinationsOfOrThrow()
.Single
ySingleOrDefault
(oFirst
yFirstOrDefault
) son una historia completamente diferente, @RubberDuck. Retomando mi ejemplo de mi comentario anterior. Está perfectamente bien responder ninguno a la consulta "¿Qué existen incluso primos mayores que dos?" , ya que esta es una respuesta matemáticamente sólida y sensata. De todos modos, si se pregunta "¿Cuál es el primer primo incluso mayor que dos?" No hay una respuesta natural. Simplemente no puede responder la pregunta, porque está pidiendo un solo número. No es ninguno (no es un número único, sino un conjunto), no 0. Por lo tanto, lanzamos una excepción.Debe validar los argumentos proporcionados cuando se llama a su función. Y, de hecho, desea saber cómo manejar argumentos no válidos. El hecho de que varios argumentos dependan unos de otros, no compensa el hecho de que valides los argumentos.
Por lo tanto, votaría por la excepción ArgumentException que proporciona la información necesaria para que el usuario comprenda qué salió mal.
Como ejemplo, verifique la
public static TSource ElementAt<TSource>(this IEnumerable<TSource>, Int32)
función en Linq. Lo que arroja una ArgumentOutOfRangeException si el índice es menor que 0 o mayor o igual que el número de elementos en la fuente. Por lo tanto, el índice se valida con respecto a los enumerables proporcionados por la persona que llama.fuente
new[] { 1, 2, 3 }.Skip(4).ToList();
, obtiene un conjunto vacío, lo que me hace pensar que devolver un conjunto vacío es quizás la mejor opción aquí.Debe hacer uno de los siguientes (aunque continúe lanzando constantemente problemas básicos como un número negativo de combinaciones):
Proporcione dos implementaciones, una que devuelva un conjunto vacío cuando las entradas juntas no tengan sentido y otra que arroje. Intenta llamarlos
CombinationsOf
yCombinationsOfWithInputCheck
. O lo que quieras. Puede revertir esto para que el que verifique la entrada sea el nombre más corto y el de la listaCombinationsOfAllowInconsistentParameters
.Para los métodos de Linq, devuelva el vacío
IEnumerable
en la premisa exacta que ha esbozado. Luego, agregue estos métodos de Linq a su biblioteca:Finalmente, use eso así:
o así:
Tenga en cuenta que se requiere que el método privado sea diferente del público para que se produzca el comportamiento de lanzamiento o acción cuando se crea la cadena linq en lugar de algún tiempo después cuando se enumera. Usted quiere que tirar de inmediato.
Sin embargo, tenga en cuenta que, por supuesto, tiene que enumerar al menos el primer elemento para determinar si hay algún elemento. Este es un inconveniente potencial que creo que se ve mitigado principalmente por el hecho de que cualquier futuro espectador puede razonar fácilmente que un
ThrowIfEmpty
método tiene que enumerar al menos un elemento, por lo que no debería sorprenderse al hacerlo. Pero nunca se sabe. Podrías hacer esto más explícitoThrowIfEmptyByEnumeratingAndReEmittingFirstItem
. Pero eso parece una exageración gigantesca.¡Creo que el # 2 es bastante bueno! Ahora el poder está en el código de llamada, y el próximo lector del código comprenderá exactamente lo que está haciendo, y no tendrá que lidiar con excepciones inesperadas.
fuente
Puedo ver argumentos para ambos casos de uso: una excepción es excelente si el código descendente espera conjuntos que contienen datos. Por otro lado, simplemente un conjunto vacío es genial si se espera esto.
Creo que depende de las expectativas de la persona que llama si se trata de un error o un resultado aceptable, por lo que transferiría la elección a la persona que llama. Tal vez introducir una opción?
.CombinationsOf(4, CombinationsGenerationMode.Distinct, Options.AllowEmptySets)
fuente
Hay dos enfoques para decidir si no hay una respuesta obvia:
Escriba el código asumiendo primero una opción, luego la otra. Considere cuál funcionaría mejor en la práctica.
Agregue un parámetro booleano "estricto" para indicar si desea que los parámetros se verifiquen estrictamente o no. Por ejemplo, Java
SimpleDateFormat
tiene unsetLenient
método para intentar analizar entradas que no coinciden completamente con el formato. Por supuesto, tendría que decidir cuál es el valor predeterminado.fuente
Según su propio análisis, devolver el conjunto vacío parece claramente correcto: incluso lo ha identificado como algo que algunos usuarios realmente desean y no ha caído en la trampa de prohibir algún uso porque no puede imaginar que los usuarios quieran usarlo alguna vez. de esa manera.
Si realmente crees que algunos usuarios pueden querer forzar retornos no vacíos, entonces dales una forma de pedir ese comportamiento en lugar de forzarlo a todos. Por ejemplo, podrías:
AssertNonempty
cheque que puedan poner en sus cadenas.fuente
Realmente depende de lo que sus usuarios esperan obtener. Por ejemplo (un poco no relacionado) si su código realiza la división, puede lanzar una excepción o retorno
Inf
oNaN
cuando divide por cero. Sin embargo, ninguno de los dos es correcto o incorrecto:Inf
a una biblioteca de Python, la gente te atacará por esconder erroresEn su caso, elegiría la solución que será menos sorprendente para los usuarios finales. Como está desarrollando una biblioteca que se ocupa de conjuntos, un conjunto vacío parece algo con lo que sus usuarios esperarían lidiar, por lo que devolverlo parece algo sensato. Pero puedo estar equivocado: tiene una comprensión mucho mejor del contexto que cualquier otra persona aquí, por lo que si espera que sus usuarios confíen en que el conjunto no siempre esté vacío, debe lanzar una excepción de inmediato.
Las soluciones que permiten al usuario elegir (como agregar un parámetro "estricto") no son definitivas, ya que reemplazan la pregunta original por una nueva equivalente: "¿Qué valor de
strict
debería ser el predeterminado?"fuente
Es una concepción común (en matemáticas) que cuando selecciona elementos sobre un conjunto, no puede encontrar ningún elemento y, por lo tanto, obtiene un conjunto vacío . Por supuesto, debe ser coherente con las matemáticas si sigue este camino:
Reglas comunes establecidas:
Tu pregunta es muy sutil:
Podría ser que la entrada de su función tenga que respetar un contrato : en ese caso, cualquier entrada no válida debería generar una excepción, eso es todo, la función no funciona bajo parámetros regulares.
Podría ser que la entrada de su función tenga que comportarse exactamente como un conjunto y, por lo tanto, debería poder devolver un conjunto vacío.
Ahora, si estuviera dentro de ti, iría al modo "Set", pero con un gran "PERO".
Suponga que tiene una colección que "por hipotesis" debería tener solo alumnas:
Ahora su colección ya no es un "conjunto puro", porque tiene un contrato y, por lo tanto, debe hacer cumplir su contrato con una excepción.
Cuando utiliza sus funciones de "conjunto" de una manera pura, no debe lanzar excepciones en el caso de un conjunto vacío, pero si tiene colecciones que ya no son "conjuntos puros", entonces debe lanzar excepciones cuando sea apropiado .
Siempre debe hacer lo que se siente más natural y consistente: para mí, un conjunto debe adherirse a las reglas establecidas, mientras que las cosas que no son conjuntos deben tener sus reglas correctamente pensadas.
En su caso, parece una buena idea hacer:
Pero no es realmente una buena idea, ahora tenemos un estado no válido que puede ocurrir en tiempo de ejecución en momentos aleatorios, sin embargo, eso está implícito en el problema, por lo que no podemos eliminarlo, seguro que podemos mover la excepción dentro de algún método de utilidad (es exactamente el mismo código, movido en diferentes lugares), pero eso está mal y básicamente lo mejor que puedes hacer es seguir las reglas establecidas .
De hecho, agregar nueva complejidad solo para mostrar que puede escribir métodos de consultas linq parece no valer para su problema , estoy bastante seguro de que si OP puede decirnos más sobre su dominio, probablemente podríamos encontrar el lugar donde realmente se necesita la excepción (si en absoluto, es posible que el problema no requiera ninguna excepción).
fuente
FemaleClass
menos que solo tenga hembras (no se puede crear públicamente, solo la clase Builder puede entregar una instancia), y posiblemente tenga al menos una hembra en ella . Entonces, su código en todo el lugar que tomaFemaleClass
como argumento no tiene que manejar excepciones porque el objeto está en el estado incorrecto.