Excepción vs conjunto de resultados vacío cuando las entradas son técnicamente válidas, pero no son satisfactorias

107

Estoy desarrollando una biblioteca destinada al lanzamiento público. Contiene varios métodos para operar con 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 en IEnumerable , que se lanzará como un paquete NuGet.

Algunos de los métodos en esta biblioteca pueden recibir parámetros de entrada no satisfactorios. Por ejemplo, en los métodos combinatorios, hay un método para generar todos los conjuntos de elementos n que se pueden construir a partir de un conjunto de origen de elementos m . 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 se configura la opción que dice que solo se puede usar cada elemento una vez.

En este escenario, cada parámetro es individualmente válido:

  • 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 (usar 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 arrojara una excepción (por ejemplo, InvalidOperationException ) o devolviera una colección vacía? Cualquiera de los dos me parece válido:

  • No puede producir combinaciones de elementos n a partir de un conjunto de elementos m donde n > m si solo puedes usar cada elemento una vez, por lo que esta operación puede considerarse imposible, por lo tanto, InvalidOperationException .
  • El conjunto de combinaciones de tamaño n que se pueden producir a partir de los elementos de m 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 impide el encadenamiento idiomático de los métodos del 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() lanza una excepción cuando someInputSet tiene menos de 4 elementos, entonces este código a veces fallará en el tiempo de ejecución sin una comprobación previa. En el ejemplo anterior, esta comprobación es trivial, pero si lo está llamando a la mitad de una cadena más larga de LINQ, esto puede resultar 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 llama a este método hasta la mitad de una cadena de LINQ y regresa silenciosamente un conjunto vacío, es posible que tenga problemas algunos pasos más adelante o se encuentre con un conjunto de resultados vacío, y puede que no sea obvio cómo sucedió, dado que definitivamente tuvo algo en el conjunto de entrada.

¿Qué esperaría usted y cuál es su argumento al respecto?

    
pregunta anaximander 30.11.2016 - 13:19
fuente

13 respuestas

144

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

    
respondido por el Ewan 30.11.2016 - 14:54
fuente
79

En caso de duda, pregunte a otra persona.

Su función de ejemplo tiene una muy similar en Python: itertools.combinations . Veamos cómo funciona:

>>> import itertools
>>> input = [1, 2, 3, 4, 5]
>>> list(itertools.combinations(input, 2))
[(1, 2), (1, 3), (1, 4), (1, 5), (2, 3), (2, 4), (2, 5), (3, 4), (3, 5), (4, 5)]
>>> list(itertools.combinations(input, 5))
[(1, 2, 3, 4, 5)]
>>> list(itertools.combinations(input, 6))
[]

Y me parece perfectamente bien. Esperaba un resultado que pudiera repetir y obtuve uno.

Pero, obviamente, si tuvieras que preguntar algo estúpido:

>>> list(itertools.combinations(input, -1))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: r must be non-negative

Por lo que diría, si todos los parámetros se validan pero el resultado es un conjunto vacío devuelve un conjunto vacío, no eres el único que lo está haciendo.

Como lo dice @ Bakuriu en los comentarios, esto también es lo mismo para una consulta SQL como SELECT <columns> FROM <table> WHERE <conditions> . Siempre que <columns> , <table> , <conditions> estén bien formados y se refieran a nombres existentes, puede crear un conjunto de condiciones que se excluyan entre sí. La consulta resultante simplemente no generaría filas en lugar de lanzar un InvalidConditionsError .

    
respondido por el Mathias Ettinger 30.11.2016 - 14:55
fuente
71

En términos sencillos:

  • Si hay un error, debe generar una excepción. Eso puede implicar hacer las cosas en pasos en lugar de hacerlo en una sola llamada encadenada para saber exactamente dónde ocurrió el error.
  • Si no hay ningún error pero el conjunto resultante está vacío, no haga una excepción, devuelva el conjunto vacío. Un conjunto vacío es un conjunto válido.
respondido por el Tulains Córdova 30.11.2016 - 13:32
fuente
52

Estoy de acuerdo con la respuesta de Ewan pero quiero agregar un razonamiento específico.

Estás tratando con 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 r -sets de un n -set (es decir, nCr ) está bien definido para todos los 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.

    
respondido por el sigy 30.11.2016 - 15:36
fuente
23

Encuentro que una buena manera de determinar si usar una excepción, es imaginando a las personas involucradas en la transacción.

Tomando como ejemplo la recuperación del contenido de un archivo:

  1. Por favor, tráeme el contenido del archivo, "no existe.txt"

    a. "Aquí está el contenido: una colección de caracteres vacía"

    b. "Erm, hay un problema, ese archivo no existe. ¡No sé qué hacer!"

  2. Por favor, tráeme el contenido del archivo, "existe pero está vacío.txt"

    a. "Aquí está el contenido: una colección de caracteres vacía"

    b. "Erm, hay un problema, no hay nada en este archivo. ¡No sé qué hacer!"

Sin duda, algunos no estarán de acuerdo, pero para la mayoría de la gente, "Erm, hay un problema" tiene sentido cuando el archivo no existe y devuelve "una colección de caracteres vacía" cuando el archivo está vacío.

Entonces aplique el mismo enfoque a su ejemplo:

  1. Por favor, dame todas las combinaciones de 4 elementos para {1, 2, 3}

    a. No hay ninguno, aquí hay un conjunto vacío.

    b. Hay un problema, no sé qué hacer.

Una vez más, "hay un problema" tendría sentido si, por ejemplo, se ofreciera null como 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, un null ), entonces generalmente se debe usar una excepción en su lugar (a menos que su idioma elegido admita option/maybe tipos, a veces tienen más sentido). De lo contrario, devolver un valor vacío probablemente simplificará el costo y cumple mejor con el principio de menos asombro.

    
respondido por el David Arno 30.11.2016 - 14:48
fuente
10

Como se trata de una biblioteca de propósito general, mi instinto sería Permitir que el usuario final elija.

Al igual que tenemos Parse() y TryParse() disponibles, podemos tener la opción de lo que usamos dependiendo de qué salida necesitamos de la función. Pasaría menos tiempo escribiendo y manteniendo un envoltorio de función para lanzar la excepción que discutiendo sobre la elección de una única versión de la función.

    
respondido por el James Snell 30.11.2016 - 15:36
fuente
4

Debe validar los argumentos proporcionados cuando se llama a su función. Y, de hecho, desea saber cómo manejar los argumentos no válidos. El hecho de que varios argumentos dependan unos de otros, no compensa el hecho de que usted valide los argumentos.

Por lo tanto, votaría por la excepción ArgumentException que proporciona la información necesaria para que el usuario entienda lo que salió mal.

Como ejemplo, verifique la Función public static TSource ElementAt<TSource>(this IEnumerable<TSource>, Int32) en linq. Que lanza una excepción 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 en relación con los enumerables proporcionados por la persona que llama.

    
respondido por el sbecker 30.11.2016 - 15:04
fuente
2

Puedo ver los argumentos para ambos casos de uso; una excepción es excelente si el código descendente espera conjuntos que contengan datos. Por otro lado, simplemente un conjunto vacío es excelente si se espera esto.

Creo que depende de las expectativas de la persona que llama si esto es un error o un resultado aceptable, por lo que transferiría la opción a la persona que llama. ¿Tal vez introducir una opción?

.CombinationsOf(4, CombinationsGenerationMode.Distinct, Options.AllowEmptySets)

    
respondido por el Christian Sauer 30.11.2016 - 14:44
fuente
2

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, SimpleDateFormat de Java tiene un método setLenient para intentar analizar las entradas que no coinciden completamente con el formato. Por supuesto, tendrías que decidir cuál es el valor predeterminado.

respondido por el JollyJoker 30.11.2016 - 15:35
fuente
2

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 han caído en la trampa de prohibir algún uso porque no puede imaginar que los usuarios quieran alguna vez para usarlo de esa manera.

Si realmente sientes que algunos usuarios pueden querer forzar devoluciones no vacías, entonces dales una manera de preguntar por ese comportamiento en lugar de forzarlo a todos. Por ejemplo, usted podría:

  • Conviértalo en una opción de configuración en cualquier objeto que realice la acción para el usuario.
  • Conviértalo en un indicador que el usuario puede pasar a la función de forma opcional.
  • Proporcione un cheque AssertNonempty que puedan poner en sus cadenas.
  • Haga dos funciones, una que afirma que no está vacía y otra que no.
respondido por el Hurkyl 01.12.2016 - 03:55
fuente
2

Debes realizar una de las siguientes acciones (aunque continúes generando problemas básicos, como un número negativo de combinaciones):

  1. Proporcione dos implementaciones, una que devuelve un conjunto vacío cuando las entradas juntas no tienen sentido, y una que se lanza. Intenta llamarlos CombinationsOf y CombinationsOfWithInputCheck . O lo que quieras. Puede revertir esto para que el control de entrada sea el nombre más corto y el de la lista sea CombinationsOfAllowInconsistentParameters .

  2. Para los métodos Linq, devuelva el IEnumerable vacío en la premisa exacta que ha descrito. Luego, agregue estos métodos Linq a su biblioteca:

    public static class EnumerableExtensions {
       public static IEnumerable<T> ThrowIfEmpty<T>(this IEnumerable<T> input) {
          return input.IfEmpty<T>(() => {
             throw new InvalidOperationException("An enumerable was unexpectedly empty");
          });
       }
    
       public static IEnumerable<T> IfEmpty<T>(
          this IEnumerable<T> input,
          Action callbackIfEmpty
       ) {
          var enumerator = input.GetEnumerator();
          if (!enumerator.MoveNext()) {
             // Safe because if this throws, we'll never run the return statement below
             callbackIfEmpty();
          }
          return EnumeratePrimedEnumerator(enumerator);
       }
    
       private static IEnumerable<T> EnumeratePrimedEnumerator<T>(
          IEnumerator<T> primedEnumerator
       ) {
          yield return primedEnumerator.Current;
          while (primedEnumerator.MoveNext()) {
             yield return primedEnumerator.Current;
          }
       }
    }
    

    Finalmente, usa eso así:

    var result = someInputSet
       .CombinationsOf(4, CombinationsGenerationMode.Distinct)
       .ThrowIfEmpty()
       .Select(combo => /* do some operation to a combination */)
       .ToList();
    

    o como esto:

    var result = someInputSet
       .CombinationsOf(4, CombinationsGenerationMode.Distinct)
       .IfEmpty(() => _log.Warning(
          $@"Unexpectedly received no results when creating combinations for {
             nameof(someInputSet)}"
       ))
       .Select(combo => /* do some operation to a combination */)
       .ToList();
    

    Tenga en cuenta que el método privado que es diferente de los públicos es necesario para que se produzca el comportamiento de lanzamiento o acción cuando se crea la cadena de linq en lugar de algún tiempo después cuando se enumera. Usted quiere que se tire de inmediato.

    Sin embargo, tenga en cuenta que, por supuesto, debe enumerar al menos el primer elemento para determinar si hay algún elemento. Este es un inconveniente potencial que creo que se mitiga principalmente por el hecho de que cualquier futuro espectador puede razonar fácilmente que un método ThrowIfEmpty tiene para enumerar al menos un elemento, por lo que no ser sorprendido por hacerlo al hacerlo. Pero nunca se sabe. Podrías hacer esto más explícito ThrowIfEmptyByEnumeratingAndReEmittingFirstItem . 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 siguiente lector del código entenderá exactamente lo que está haciendo y no tendrá que lidiar con excepciones inesperadas.

    
respondido por el ErikE 01.12.2016 - 21:47
fuente
1

Realmente depende de lo que sus usuarios esperan obtener. Por ejemplo (un tanto no relacionado) si su código realiza una división, puede lanzar una excepción o devolver Inf o NaN cuando se divide por cero. Sin embargo, ninguno es correcto o incorrecto:

  • si devuelves Inf en una biblioteca de Python, la gente te atacará por ocultar errores
  • si genera un error en una biblioteca de Matlab, la gente lo asaltará por no poder procesar los datos con valores perdidos

En su caso, elegiría la solución que sea menos sorprendente para los usuarios finales. Dado que está desarrollando una biblioteca que trata con conjuntos, un conjunto vacío parece algo que los usuarios esperan tratar, por lo que devolverlo parece algo sensato. Pero puedo estar equivocado: tiene una mejor comprensión del contexto que cualquier otra persona aquí, por lo que si espera que sus usuarios confíen en que el conjunto no esté siempre 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? "

    
respondido por el Dmitry Grigoryev 01.12.2016 - 11:48
fuente
0

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, debes ser coherente con las matemáticas si sigues este camino:

Reglas de configuración comunes:

  • Set.Foreach (predicado); // siempre devuelve true para conjuntos vacíos
  • Set.Exists (predicado); // siempre devuelve falso para conjuntos vacíos

Su 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, la función no funciona con 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 en ti, seguiría el camino "Set", pero con un gran "PERO".

Suponga que tiene una colección que "por hipotesis" debería tener solo estudiantes mujeres:

class FemaleClass{

    FemaleStudent GetAnyFemale(){
        var femaleSet= mySet.Select( x=> x.IsFemale());
        if(femaleSet.IsEmpty())
            throw new Exception("No female students");
        else
            return femaleSet.Any();
    }
}

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 use sus funciones de "conjunto" de una manera pura, no debe lanzar excepciones en caso de que esté vacío, pero si tiene colecciones que no son más "conjuntos puros", debería lanzar excepciones donde corresponda .

Siempre debes hacer lo que parezca más natural y coherente: para mí, un conjunto debería adherirse a las reglas establecidas, mientras que las cosas que no son conjuntos deberían tener sus propias reglas de pensamiento.

En su caso, parece una buena idea:

List SomeMethod( Set someInputSet){
    var result = someInputSet
        .CombinationsOf(4, CombinationsGenerationMode.Distinct)
        .Select(combo => /* do some operation to a combination */)
        .ToList();

    // the only information here is that set is empty => there are no combinations

    // BEWARE! if 0 here it may be invalid input, but also a empty set
    if(result.Count == 0)  //Add: "&&someInputSet.NotEmpty()"

    // we go a step further, our API require combinations, so
    // this method cannot satisfy the API request, then we throw.
         throw new Exception("you requsted impossible combinations");

    return result;
}

Pero no es realmente una buena idea, ahora tenemos un estado no válido que puede ocurrir en el tiempo de ejecución en momentos aleatorios, sin embargo, eso está implícito en el problema, por lo que no podemos eliminarlo, estamos seguros de que podemos mover la excepción dentro de algunos método de utilidad (es exactamente el mismo código, movido en diferentes lugares), pero eso es incorrecto y básicamente lo mejor que puedes hacer se adhiere a las reglas regulares establecidas .

El hecho de agregar una nueva complejidad solo para mostrar que puede escribir métodos de consultas de linq parece que no vale la pena por su problema , estoy bastante seguro de que si OP nos puede dar más información sobre su dominio, probablemente podríamos encontrarlo el lugar donde realmente se necesita la excepción (si es posible, es posible que el problema no requiera ninguna excepción).

    
respondido por el GameDeveloper 01.12.2016 - 13:20
fuente

Lea otras preguntas en las etiquetas