¿Está bien usar excepciones como herramientas para "atrapar" errores temprano?

29

Utilizo excepciones para detectar problemas temprano. Por ejemplo:

public int getAverageAge(Person p1, Person p2){
    if(p1 == null || p2 == null)
        throw new IllegalArgumentException("One or more of input persons is null").
    return (p1.getAge() + p2.getAge()) / 2;
}

Mi programa nunca debería pasar nullen esta función. Nunca tengo la intención de hacerlo. Sin embargo, como todos sabemos, suceden cosas involuntarias en la programación.

Lanzar una excepción si se produce este problema, me permite detectarlo y solucionarlo, antes de que cause más problemas en otros lugares del programa. La excepción detiene el programa y me dice "aquí pasaron cosas malas, arréglenlo". En lugar de que esto se nullmueva, el programa causa problemas en otros lugares.

Ahora, tiene razón, en este caso nullsimplemente causaría un NullPointerExceptioninmediato, por lo que podría no ser el mejor ejemplo.

Pero considere un método como este, por ejemplo:

public void registerPerson(Person person){
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

En este caso, a nullcomo parámetro se pasaría por el programa y podría causar errores mucho más tarde, lo que será difícil de rastrear hasta su origen.

Cambiando la función así:

public void registerPerson(Person person){
    if(person == null) throw new IllegalArgumentException("Input person is null.");
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

Me permite detectar el problema mucho antes de que cause errores extraños en otros lugares.

Además, una nullreferencia como parámetro es solo un ejemplo. Podría haber muchos tipos de problemas, desde argumentos inválidos hasta cualquier otra cosa. Siempre es mejor detectarlos temprano.


Entonces mi pregunta es simple: ¿es esta una buena práctica? ¿Es bueno el uso de excepciones como herramientas para prevenir problemas? ¿Es esta una aplicación legítima de excepciones o es problemática?

Aviv Cohn
fuente
2
¿Puede el votante explicar qué está mal con esta pregunta?
Giorgio
2
"accidente temprano, accidente frecuente"
Bryan Chen
16
Eso es literalmente para lo que hay excepciones.
raptortech97
Tenga en cuenta que los contratos de codificación le permiten detectar muchos de estos errores de forma estática. Esto generalmente es superior a lanzar una excepción en tiempo de ejecución. El soporte y la efectividad de los contratos de codificación varía según el idioma.
Brian
3
Como está lanzando IllegalArgumentException, es redundante decir " Persona de entrada ".
Kevin Cline

Respuestas:

29

Sí, "fallar temprano" es un principio muy bueno, y esta es simplemente una forma posible de implementarlo. Y en los métodos que tienen que devolver un valor específico, en realidad no hay mucho más que pueda hacer para fallar deliberadamente: lanzar excepciones o desencadenar afirmaciones. Se supone que las excepciones señalan condiciones 'excepcionales', y la detección de un error de programación ciertamente es excepcional.

Kilian Foth
fuente
Fallar temprano es el camino a seguir si no hay forma de recuperarse de un error o condición. Por ejemplo, si necesita copiar un archivo y la ruta de origen o destino está vacía, debe lanzar una excepción de inmediato.
Michael Shopsin
También se conoce como "falla rápido"
keuleJ
8

Sí, lanzar excepciones es una buena idea. Tírelos temprano, tírelos a menudo, tírelos con entusiasmo.

Sé que hay un debate de "excepciones versus aserciones", con algunos tipos de comportamiento excepcional (específicamente, aquellos que se piensa que reflejan errores de programación) manejados por aserciones que pueden "compilarse" para el tiempo de ejecución en lugar de depurar / probar compilaciones. Pero la cantidad de rendimiento que se consume en algunas comprobaciones de corrección adicionales es mínima en el hardware moderno, y cualquier costo adicional es ampliamente compensado por el valor de tener resultados correctos e incorruptos. En realidad, nunca conocí una base de código de aplicación para la cual quisiera que se eliminaran (la mayoría) las verificaciones en tiempo de ejecución.

Estoy tentado a decir que no querría muchas comprobaciones adicionales y condicionales dentro de los lazos estrechos del código numéricamente intensivo ... pero en realidad es donde se generan muchos errores numéricos, y si no se detectan allí, se propagarán hacia afuera para efectuar todos los resultados. Entonces, incluso allí, vale la pena hacer los cheques. De hecho, algunos de los mejores y más eficaces algoritmos numéricos se basan en la evaluación de errores.

Un último lugar para ser muy consciente del código adicional es el código sensible a la latencia, donde los condicionales adicionales pueden causar paradas en la tubería. Entonces, en medio del sistema operativo, DBMS y otros núcleos de middleware, y manejo de protocolo / comunicación de bajo nivel. Pero, de nuevo, esos son algunos de los lugares en los que es más probable que se observen errores, y sus efectos (seguridad, corrección e integridad de los datos) son los más dañinos.

Una mejora que he encontrado es no lanzar solo excepciones de nivel base. IllegalArgumentExceptiones bueno, pero puede provenir esencialmente de cualquier parte. No se necesita mucho en la mayoría de los idiomas para agregar excepciones personalizadas. Para su módulo de manejo de personas, diga:

public class PersonArgumentException extends IllegalArgumentException {
    public MyException(String message) {
        super(message);
    }
}

Entonces cuando alguien ve un PersonArgumentException, está claro de dónde viene. Hay un acto de equilibrio sobre cuántas excepciones personalizadas desea agregar, porque no desea multiplicar entidades innecesariamente (Navaja de Occam). A menudo, solo unas pocas excepciones personalizadas son suficientes para indicar "¡este módulo no está obteniendo los datos correctos!" o "¡este módulo no puede hacer lo que se supone que debe hacer!" de una manera específica y personalizada, pero no tan precisa como para tener que volver a implementar toda la jerarquía de excepciones. A menudo llego a ese pequeño conjunto de excepciones personalizadas comenzando con excepciones de stock, luego escaneando el código y dándome cuenta de que "estos N lugares están generando excepciones de stock, pero se reducen a la idea de nivel superior de que no están obteniendo los datos ellos necesitan

Jonathan Eunice
fuente
I've never actually met an application codebase for which I'd want (most) checks removed at runtime. Entonces no has hecho código que sea crítico para el rendimiento. Estoy trabajando en algo en este momento que hace 37 millones de operaciones por segundo con afirmaciones compiladas y 42 millones sin ellas. Las aserciones no están allí para validar la entrada externa, están ahí para asegurarse de que el código sea correcto. Mis clientes están más que felices de tomar el aumento del 13% una vez que estoy satisfecho de que mis cosas no están rotas.
Blrfl
Debe evitarse una base de código con excepciones personalizadas porque si bien puede ayudarlo, no ayuda a otros que tienen que familiarizarse con ellos. Por lo general, si se encuentra extendiendo una excepción común y no agregando ningún miembro o funcionalidad, en realidad no la necesita. Incluso en tu ejemplo, PersonArgumentExceptionno es tan claro como IllegalArgumentException. Se sabe universalmente que este último se lanza cuando se pasa un argumento ilegal. De hecho, esperaría que se lanzara el primero si un Personestaba en un estado no válido para una llamada (Similar a InvalidOperationExceptionen C #).
Selali Adobor
Es cierto que si no tiene cuidado, puede activar la misma excepción desde otro lugar de la función, pero eso es un defecto del código, no de la naturaleza de las excepciones comunes. Y ahí es donde intervienen los rastros de depuración y apilamiento. Si está tratando de "etiquetar" sus excepciones, utilice las facilidades existentes que proporcionan las excepciones más comunes, como usar el constructor de mensajes (para eso está exactamente). Algunas excepciones incluso proporcionan a los constructores parámetros adicionales para obtener el nombre del argumento problemático, pero puede proporcionar la misma facilidad con un mensaje bien formado.
Selali Adobor
Admito que simplemente cambiar el nombre de una excepción común a una etiqueta privada de básicamente la misma excepción es un ejemplo débil, y rara vez vale la pena el esfuerzo. En la práctica trato de emitir excepciones personalizadas a un nivel semántico más alto, más íntimamente ligado a la intención del módulo.
Jonathan Eunice
He realizado un trabajo sensible al rendimiento en los ámbitos numérico / HPC y OS / middleware. Un aumento de rendimiento del 13% no es poca cosa. Podría desactivar algunos controles para obtenerlo, de la misma manera que un comandante militar podría pedir el 105% de la producción nominal del reactor. Pero he visto que "se ejecutará más rápido de esta manera" dado a menudo como una razón para desactivar los controles, las copias de seguridad y otras protecciones. Básicamente se trata de compensar la resistencia y la seguridad (en muchos casos, incluida la integridad de los datos) por un rendimiento adicional. Si eso vale la pena es una decisión judicial.
Jonathan Eunice
3

Fallar lo antes posible es excelente al depurar una aplicación. Recuerdo una falla de segmentación particular en un programa heredado de C ++: el lugar donde se detectó el error no tenía nada que ver con el lugar donde se introdujo (el puntero nulo se movió felizmente de un lugar a otro en la memoria antes de que finalmente causara un problema ) Los rastros de pila no pueden ayudarte en esos casos.

Entonces, sí, la programación defensiva es un enfoque realmente efectivo para detectar y corregir errores rápidamente. Por otro lado, puede exagerarse, especialmente con referencias nulas.

En su caso específico, por ejemplo: si alguna de las referencias es nula, se NullReferenceExceptionarrojará en la siguiente declaración, cuando intente obtener la edad de una persona. Realmente no necesita verificar las cosas usted mismo aquí: deje que el sistema subyacente detecte esos errores y arroje excepciones, por eso existen .

Para un ejemplo más realista, puede usar assertdeclaraciones, que:

  1. Son más cortos de escribir y leer:

        assert p1 : "p1 is null";
        assert p2 : "p2 is null";
    
  2. Están diseñados específicamente para su enfoque. En un mundo donde tiene tanto afirmaciones como excepciones, puede distinguirlas de la siguiente manera:

    • Las afirmaciones son para errores de programación ("/ * esto nunca debería suceder * /"),
    • Las excepciones son para casos de esquina (situaciones excepcionales pero probables)

Por lo tanto, exponer sus suposiciones sobre las entradas y / o el estado de su aplicación con aserciones le permite al próximo desarrollador comprender un poco más el propósito de su código.

Un analizador estático (por ejemplo, el compilador) también podría ser más feliz.

Finalmente, las aserciones se pueden eliminar de la aplicación implementada utilizando un solo conmutador. Pero en términos generales, no espere mejorar la eficiencia con eso: las verificaciones de afirmaciones en tiempo de ejecución son insignificantes.

volcado de memoria
fuente
1

Hasta donde sé, diferentes programadores prefieren una solución u otra.

La primera solución generalmente se prefiere porque es más concisa, en particular, no tiene que verificar la misma condición una y otra vez en diferentes funciones.

Encuentro la segunda solución, por ejemplo

public void registerPerson(Person person){
    if(person == null) throw new IllegalArgumentException("Input person is null.");
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

más sólido porque

  1. Detecta el error lo antes posible, es decir, cuando registerPerson()se llama, no cuando se lanza una excepción de puntero nulo en algún lugar de la pila de llamadas. La depuración se vuelve mucho más fácil: todos sabemos qué tan lejos puede viajar un valor no válido a través del código antes de que se manifieste como un error.
  2. Disminuye el acoplamiento entre funciones: registerPerson()no hace suposiciones sobre qué otras funciones terminarán usando el personargumento y cómo lo usarán: la decisión que nulles un error se toma e implementa localmente.

Entonces, especialmente si el código es bastante complejo, tiendo a preferir este segundo enfoque.

Giorgio
fuente
1
Dar la razón ("La persona de entrada es nula") también ayuda a comprender el problema, a diferencia de cuando falla algún método oscuro en una biblioteca.
Florian F
1

En general, sí, es una buena idea "fallar temprano". Sin embargo, en su ejemplo específico, lo explícito IllegalArgumentExceptionno proporciona una mejora significativa sobre un NullReferenceException- porque ambos objetos operados ya se entregan como argumentos para la función.

Pero veamos un ejemplo ligeramente diferente.

class PersonCalculator {
    PersonCalculator(Person p) {
        if (p == null) throw new ArgumentNullException("p");
        _p = p;
    }

    void Calculate() {
        // Do something to p
    }
}

Si no hubiera ningún argumento que verificara en el constructor, obtendría un NullReferenceExceptional llamar Calculate.

Pero el código roto no era ni la Calculatefunción ni el consumidor de la Calculatefunción. El fragmento de código roto fue el código que intenta construir PersonCalculatorcon un valor nulo Person, de modo que es donde queremos que ocurra la excepción.

Si eliminamos esa comprobación de argumento explícito, tendrá que averiguar por qué NullReferenceExceptionocurrió un error cuando Calculatese llamó al. Y rastrear por qué el objeto se construyó con una nullpersona puede ser complicado, especialmente si el código que construye la calculadora no está cerca del código que realmente llama a la Calculatefunción.

Pete
fuente
0

No en los ejemplos que das.

Como dices, lanzar explícitamente no te gana mucho cuando vas a obtener una excepción poco después. Muchos argumentarán que tener una excepción explícita con un buen mensaje es mejor, aunque no estoy de acuerdo. En escenarios prelanzados, el seguimiento de la pila es lo suficientemente bueno. En escenarios posteriores al lanzamiento, el sitio de la llamada a menudo puede proporcionar mejores mensajes que dentro de la función.

La segunda forma es proporcionar demasiada información a la función. Esa función no necesariamente debe saber que las otras funciones arrojarán una entrada nula. Incluso si lanzan una entrada nula ahora , se vuelve muy problemático refactorizar si eso deja de ser así, ya que la verificación nula se extiende por todo el código.

Pero en general, debe tirar temprano una vez que determine que algo salió mal (respetando DRY). Sin embargo, estos quizás no sean buenos ejemplos de eso.

Telastyn
fuente
-3

En su función de ejemplo, preferiría que no hiciera verificaciones y simplemente permitiera NullReferenceExceptionque sucediera.

Por un lado, no tiene sentido pasar un nulo allí de todos modos, así que resolveré el problema de inmediato basándome en el lanzamiento de a NullReferenceException.

Dos, es que si cada función arroja excepciones ligeramente diferentes en función de qué tipo de entradas obviamente incorrectas se han proporcionado, entonces muy pronto tendrá funciones que podrían arrojar 18 tipos diferentes de excepciones, y muy pronto se encontrará diciendo que eso también mucho trabajo con el que lidiar, y simplemente suprimir todas las excepciones de todos modos.

No hay nada que pueda hacer realmente para ayudar a la situación de un error en tiempo de diseño en su función, así que deje que ocurra la falla sin cambiarla.

como se llame
fuente
He estado trabajando durante algunos años sobre la base de un código que se basa en este principio: los punteros simplemente se desreferencian cuando es necesario y casi nunca se comparan con NULL y se manejan explícitamente. La depuración de este código siempre ha sido muy lenta y problemática, ya que las excepciones (o volcados del núcleo) ocurren mucho más tarde en el punto donde se encuentra el error lógico. Perdí literalmente semanas buscando ciertos errores. Por esta razón, prefiero el estilo de fallar lo antes posible, al menos para el código complejo donde no es inmediatamente obvio de dónde proviene una excepción de puntero nulo.
Giorgio
"Por un lado, no tiene sentido pasar un valor nulo de todos modos, así que resolveré el problema inmediatamente basándome en lanzar una NullReferenceException": No necesariamente, como lo ilustra el segundo ejemplo de Prog.
Giorgio
"Dos, es que si cada función arroja excepciones ligeramente diferentes en función de qué tipo de entradas obviamente incorrectas se han proporcionado, entonces muy pronto tendrá funciones que podrían arrojar 18 tipos diferentes de excepciones ...": en este caso usted puede usar la misma excepción (incluso NullPointerException) en todas las funciones: lo importante es lanzar temprano, no lanzar una excepción diferente de cada función.
Giorgio
Para eso están las RuntimeExceptions. Cualquier condición que "no debería suceder" pero que impida que su código funcione debería generar una. No necesita declararlos en la firma del método. Pero debe atraparlos en algún momento e informar que la función de la acción solicitada falló.
Florian F
1
La pregunta no especifica un idioma, por lo que confiar, por ejemplo, RuntimeExceptionno es necesariamente una suposición válida.