¿Por qué no usar excepciones como flujo de control regular?

192

Para evitar todas las respuestas estándar que podría haber buscado en Google, proporcionaré un ejemplo que todos pueden atacar a voluntad.

C # y Java (y muchos otros) tienen muchos tipos de comportamiento de 'desbordamiento' que no me gusta en absoluto (por type.MaxValue + type.SmallestValue == type.MinValueejemplo, por ejemplo int.MaxValue + 1 == int.MinValue).

Pero, visto mi naturaleza viciosa, agregaré un insulto a esta lesión al expandir este comportamiento a, digamos un DateTimetipo Anulado . (Sé que DateTimeestá sellado en .NET, pero por el bien de este ejemplo, estoy usando un pseudo lenguaje que es exactamente como C #, excepto por el hecho de que DateTime no está sellado).

El Addmétodo anulado :

/// <summary>
/// Increments this date with a timespan, but loops when
/// the maximum value for datetime is exceeded.
/// </summary>
/// <param name="ts">The timespan to (try to) add</param>
/// <returns>The Date, incremented with the given timespan. 
/// If DateTime.MaxValue is exceeded, the sum wil 'overflow' and 
/// continue from DateTime.MinValue. 
/// </returns>
public DateTime override Add(TimeSpan ts) 
{
    try
    {                
        return base.Add(ts);
    }
    catch (ArgumentOutOfRangeException nb)
    {
        // calculate how much the MaxValue is exceeded
        // regular program flow
        TimeSpan saldo = ts - (base.MaxValue - this);
        return DateTime.MinValue.Add(saldo)                         
    }
    catch(Exception anyOther) 
    {
        // 'real' exception handling.
    }
}

Por supuesto, un if podría resolver esto con la misma facilidad, pero el hecho es que no puedo ver por qué no se pueden usar excepciones (lógicamente, puedo ver que cuando el rendimiento es un problema que en ciertos casos se deben evitar las excepciones) )

Creo que en muchos casos son más claros que las estructuras if y no rompen ningún contrato que el método esté haciendo.

En mi humilde opinión, la reacción de "Nunca los use para el flujo regular del programa" que todo el mundo parece tener no está tan mal construida como la fuerza de esa reacción puede justificar.

¿O me equivoco?

He leído otras publicaciones que tratan todo tipo de casos especiales, pero mi punto es que no hay nada de malo si ambos son:

  1. Claro
  2. Honra el contrato de tu método

Dispararme.

Peter
fuente
2
+1 Siento lo mismo. Además del rendimiento, la única buena razón para evitar excepciones para el flujo de control es cuando el código de la persona que llama será mucho más legible con valores de retorno.
44
es el: return -1 si algo sucedió, return -2 si algo más, etc ... realmente más legible que las excepciones?
kender
1
Es triste que uno tenga una reputación negativa por decir la verdad: que su ejemplo no podría haber sido escrito con declaraciones if. (Esto no quiere decir que sea correcto / completo.)
Ingo
8
Yo diría que lanzar una excepción a veces puede ser su única opción. Tengo, por ejemplo, un componente comercial que inicializa su estado interno dentro de su constructor consultando la base de datos. Hay momentos en que no hay datos apropiados disponibles en la base de datos. Lanzar una excepción dentro del constructor es la única forma de cancelar efectivamente la construcción del objeto. Esto se establece claramente en el contrato (Javadoc en mi caso) de la clase, por lo que no tengo ningún problema en que el código del cliente pueda (y deba) detectar esa excepción al crear el componente y continuar desde allí.
Stefan Haberl
1
Como formuló una hipótesis, es responsabilidad de usted también citar pruebas / razones que lo corroboren. Para empezar, mencione una razón por la cual su código es superior a una ifdeclaración mucho más breve y autodocumentada . Encontrarás esto muy difícil. En otras palabras: su premisa es defectuosa y, por lo tanto, las conclusiones que saca de ella son erróneas.
Konrad Rudolph

Respuestas:

169

¿Alguna vez ha intentado depurar un programa que genera cinco excepciones por segundo en el curso normal de operación?

Yo tengo.

El programa era bastante complejo (era un servidor de cálculo distribuido), y una ligera modificación en un lado del programa podría romper fácilmente algo en un lugar totalmente diferente.

Ojalá pudiera haber lanzado el programa y esperar a que ocurran excepciones, pero hubo alrededor de 200 excepciones durante el inicio en el curso normal de las operaciones

Mi punto: si usa excepciones para situaciones normales, ¿cómo ubica situaciones inusuales (es decir, excepcionales )?

Por supuesto, hay otras razones importantes para no usar demasiado las excepciones, especialmente en cuanto al rendimiento.

Brann
fuente
13
Ejemplo: cuando depuro un programa .net, lo ejecuto desde Visual Studio y le pido a VS que rompa todas las excepciones. Si confía en las excepciones como un comportamiento esperado, ya no puedo hacerlo (ya que se rompería 5 veces / seg), y es mucho más complicado localizar la parte problemática del código.
Brann
15
+1 por señalar que no desea crear un pajar de excepción en el que encontrar una aguja excepcional real.
Grant Wagner
16
no recibo esta respuesta en absoluto, creo que la gente no entiende aquí. No tiene nada que ver con la depuración, sino con el diseño. Este es un razonamiento circular en su forma pura, me temo. Y su punto es realmente además de la pregunta como se dijo antes
Peter
11
@Peter: La depuración sin romper excepciones es difícil, y atrapar todas las excepciones es doloroso si hay muchas de ellas por diseño. Creo que un diseño que dificulta la depuración está roto parcialmente (en otras palabras, el diseño TIENE algo que ver con la depuración, OMI)
Brann
77
Incluso ignorando el hecho de que la mayoría de las situaciones que quiero depurar no corresponden a excepciones lanzadas, la respuesta a su pregunta es: "por tipo", por ejemplo, le diré a mi depurador que capture solo AssertionError o StandardError o algo que sí lo haga. corresponden a cosas malas que suceden. Si tiene problemas con eso, ¿cómo se registra? ¿No se registra por nivel y clase, precisamente para poder filtrarlos? ¿Crees que también es una mala idea?
Ken
157

Las excepciones son básicamente gotodeclaraciones no locales con todas las consecuencias de este último. El uso de excepciones para el control de flujo viola un principio de mínimo asombro , hace que los programas sean difíciles de leer (recuerde que los programas están escritos primero para programadores).

Además, esto no es lo que los vendedores de compiladores esperan. Esperan que rara vez se produzcan excepciones, y generalmente dejan que el throwcódigo sea bastante ineficiente. Lanzar excepciones es una de las operaciones más caras en .NET.

Sin embargo, algunos lenguajes (especialmente Python) usan excepciones como construcciones de control de flujo. Por ejemplo, los iteradores generan una StopIterationexcepción si no hay más elementos. Incluso las construcciones de lenguaje estándar (como for) se basan en esto.

Anton Gogolev
fuente
11
¡oye, las excepciones no son asombrosas! Y se contradice a sí mismo cuando dice "es una mala idea" y luego dice "pero es una buena idea en Python".
hasen
66
Todavía no estoy convencido en absoluto: 1) La eficiencia estaba fuera de la cuestión, muchos programas que no son bacht no podrían importarle menos (por ejemplo, la interfaz de usuario) 2) Asombroso: como dije, es sorprendente porque no se usa, pero la pregunta sigue siendo: ¿por qué no usar id en primer lugar? Pero, como esta es la respuesta
Peter
44
+1 En realidad, me alegra que hayas señalado la distinción entre Python y C #. No creo que sea una contradicción. Python es mucho más dinámico y la expectativa de usar excepciones de esta manera está integrada en el lenguaje. También es parte de la cultura EAFP de Python. No sé qué enfoque es conceptualmente más puro o más coherente, pero me gusta la idea de escribir código que haga lo que otros esperan que haga, lo que significa diferentes estilos en diferentes idiomas.
Mark E. Haase
13
A diferencia de goto, por supuesto, las excepciones interactúan correctamente con su pila de llamadas y con el alcance léxico, y no dejan la pila o los ámbitos en un lío.
Lukas Eder
44
En realidad, la mayoría de los proveedores de máquinas virtuales esperan excepciones y las manejan de manera eficiente. Como señala @LukasEder, las excepciones son completamente diferentes a goto en que están estructuradas.
Marcin
29

Mi regla de oro es:

  • Si puede hacer algo para recuperarse de un error, obtenga excepciones
  • Si el error es muy común (por ejemplo, el usuario intentó iniciar sesión con la contraseña incorrecta), utilice valores de retorno
  • Si no puede hacer nada para recuperarse de un error, déjelo sin atrapar (o póngalo en su receptor principal para hacer un apagado semi-agraciado de la aplicación)

El problema que veo con excepciones es desde un punto de vista puramente sintáctico (estoy bastante seguro de que la sobrecarga de rendimiento es mínima). No me gustan los try-blocks por todas partes.

Toma este ejemplo:

try
{
   DoSomeMethod();  //Can throw Exception1
   DoSomeOtherMethod();  //Can throw Exception1 and Exception2
}
catch(Exception1)
{
   //Okay something messed up, but is it SomeMethod or SomeOtherMethod?
}

.. Otro ejemplo podría ser cuando necesita asignar algo a un controlador usando una fábrica, y esa fábrica podría lanzar una excepción:

Class1 myInstance;
try
{
   myInstance = Class1Factory.Build();
}
catch(SomeException)
{
   // Couldn't instantiate class, do something else..
}
myInstance.BestMethodEver();   // Will throw a compile-time error, saying that myInstance is uninitalized, which it potentially is.. :(

Entonces, personalmente, creo que debería mantener excepciones para condiciones de error raras (sin memoria, etc.) y usar valores de retorno (clases de valor, estructuras o enumeraciones) para hacer su verificación de errores.

Espero haber entendido su pregunta correcta :)

cwap
fuente
44
re: Su segundo ejemplo: ¿por qué no simplemente poner la llamada a BestMethodEver dentro del bloque try, después de Build? Si Build () arroja una excepción, no se ejecutará y el compilador está contento.
Blorgbeard sale el
2
Sí, eso probablemente sea con lo que terminarás, pero considera un ejemplo más complejo en el que el tipo myInstance puede lanzar excepciones ... Y otras instancias en el alcance del método también pueden hacerlo. Terminarás con muchos bloques anidados de prueba / captura :(
cwap
Debe hacer la traducción de excepción (a un tipo de excepción apropiado para el nivel de abstracción) en su bloque catch. FYI: Se supone que "Multi-catch" va a Java 7.
jasonnerothin
FYI: en C ++, puede poner varias capturas después de intentar capturar diferentes excepciones.
RobH
2
Para el software retráctil, debe detectar todas las excepciones. Al menos coloque un cuadro de diálogo que explique que el programa debe cerrarse, y aquí hay algo incomprensible que puede enviar en un informe de error.
David Thornley
25

Una primera reacción a muchas respuestas:

estás escribiendo para los programadores y el principio del mínimo asombro

¡Por supuesto! Pero un if simplemente no está más claro todo el tiempo.

No debería ser sorprendente, por ejemplo: divide (1 / x) catch (divisionByZero) es más claro que ninguno para mí (en Conrad y otros). El hecho de que no se espere este tipo de programación es puramente convencional y, de hecho, sigue siendo relevante. Quizás en mi ejemplo un if sería más claro.

Pero DivisionByZero y FileNotFound para el caso son más claros que ifs.

Por supuesto, si es menos eficiente y necesita un trillón de tiempo por segundo, debería evitarlo, pero aún no he leído ninguna buena razón para evitar el diseño general.

En cuanto al principio de menor asombro: hay un peligro de razonamiento circular aquí: supongamos que toda una comunidad usa un mal diseño, ¡este diseño será esperado! Por lo tanto, el principio no puede ser un grial y debe considerarse cuidadosamente.

excepciones para situaciones normales, ¿cómo ubica situaciones inusuales (es decir, excepcionales)?

En muchas reacciones algo. como este brilla a través. Solo atraparlos, ¿no? Su método debe ser claro, estar bien documentado y cumplir con su contrato. No entiendo esa pregunta, debo admitir.

Depuración de todas las excepciones: lo mismo, eso solo se hace a veces porque el diseño para no usar excepciones es común. Mi pregunta fue: ¿por qué es común en primer lugar?

Peter
fuente
1
1) ¿Siempre revisas xantes de llamar 1/x? 2) ¿Envuelve cada operación de división en un bloque try-catch para atrapar DivideByZeroException? 3) ¿De qué lógica pones el bloque catch para recuperarte DivideByZeroException?
Lightman
1
Excepto DivisionByZero y FileNotFound son malos ejemplos, ya que son casos excepcionales que deben tratarse como excepciones.
0x6C38
No hay nada "excesivamente excepcional" sobre un archivo no encontrado de la manera que las personas "anti-excepción" aquí están promocionando. openConfigFile();puede ser seguido por un FileNotFound capturado con { createDefaultConfigFile(); setFirstAppRun(); }FileNotFound excepción manejado con gracia; sin bloqueo, hagamos que la experiencia del usuario final sea mejor, no peor. Usted puede decir "¿Pero qué pasa si esta no fue realmente la primera carrera y la obtienen siempre?" ¡Al menos la aplicación se ejecuta cada vez y no se bloquea cada inicio! En un 1 a 10 "esto es terrible": "primera ejecución" cada inicio = 3 o 4, bloquee cada inicio = 10.
Loduwijk
Tus ejemplos son excepciones. No, no siempre verificas xantes de llamar 1/x, porque generalmente está bien. El caso excepcional es el caso donde no está bien. No estamos hablando de hacer temblar la tierra aquí, pero por ejemplo, para un número entero básico dado al azar x, solo 1 de cada 4294967296 no lograría hacer la división. Eso es excepcional y las excepciones son una buena manera de lidiar con eso. Sin embargo, podría usar excepciones para implementar el equivalente de una switchdeclaración, pero sería bastante tonto.
Thor84no
16

Antes de las excepciones, en C, había setjmpy longjmppodría usarse para lograr un desenrollado similar del marco de la pila.

Luego se le dio un nombre a la misma construcción: "Excepción". Y la mayoría de las respuestas se basan en el significado de este nombre para discutir sobre su uso, alegando que las excepciones están destinadas a ser utilizadas en condiciones excepcionales. Esa nunca fue la intención en el original longjmp. Solo hubo situaciones en las que necesitaba romper el flujo de control en muchos marcos de pila.

Las excepciones son un poco más generales, ya que también puedes usarlas dentro del mismo marco de pila. Esto plantea analogías con las gotoque creo que están mal. Gotos son un par fuertemente acoplado (y también lo son setjmpy longjmp). ¡Las excepciones siguen a una publicación / suscripción débilmente acoplada que es mucho más limpia! Por lo tanto, usarlos dentro del mismo marco de pila no es lo mismo que usar gotos.

La tercera fuente de confusión se relaciona con si son excepciones marcadas o no. Por supuesto, las excepciones no verificadas parecen particularmente horribles de usar para el flujo de control y quizás muchas otras cosas.

Sin embargo, las excepciones marcadas son excelentes para controlar el flujo, una vez que superas todos los problemas de Victoria y vives un poco.

Mi uso favorito es una secuencia de throw new Success()un largo fragmento de código que intenta una cosa tras otra hasta que encuentra lo que está buscando. Cada cosa, cada pieza de lógica, puede tener un anidamiento arbitrario, por lo que breakestán fuera como cualquier tipo de prueba de condición. El if-elsepatrón es frágil. Si edito elseo estropeo la sintaxis de alguna otra manera, entonces hay un error peludo.

Utilizando throw new Success() linealiza el flujo de código. UtilizoSuccessclasesdefinidas localmente, marcadas, por supuesto, de modo que si me olvido de atraparlo, el código no se compilará. Y no capto otro métodoSuccess.

A veces mi código busca una cosa tras otra y solo tiene éxito si todo está bien. En este caso tengo una linealización similar usando throw new Failure().

El uso de una función separada interfiere con el nivel natural de compartimentación. Entonces la returnsolución no es óptima. Prefiero tener una o dos páginas de código en un solo lugar por razones cognitivas. No creo en el código dividido ultra finamente.

Lo que hacen los JVM o los compiladores es menos relevante para mí a menos que haya un punto de acceso. No puedo creer que haya una razón fundamental para que los compiladores no detecten Excepciones lanzadas y capturadas localmente y simplemente las traten como muy eficientes.goto a nivel de código de máquina.

En cuanto a su uso a través de las funciones para el flujo de control, es decir, para casos comunes en lugar de excepcionales, no puedo ver cómo serían menos eficientes que las interrupciones múltiples, las pruebas de condición, los retornos para atravesar tres marcos de pila en lugar de simplemente restaurar El puntero de la pila.

Personalmente, no uso el patrón en los marcos de pila y puedo ver cómo requeriría sofisticación de diseño para hacerlo con elegancia. Pero usado con moderación, debería estar bien.

Por último, con respecto a los sorprendentes programadores vírgenes, no es una razón convincente. Si los presentas suavemente a la práctica, aprenderán a amarla. Recuerdo que C ++ solía sorprender y asustar a los programadores de C.

nigromante
fuente
3
Usando este patrón, la mayoría de mis funciones generales tienen dos pequeñas capturas al final: una para Éxito y otra para Fracaso, y ahí es donde la función concluye cosas como preparar la respuesta correcta del servlet o preparar valores de retorno. Tener un solo lugar para hacer el resumen es agradable. La returnalternativa -pattern requeriría dos funciones para cada función. Una externa para preparar la respuesta del servlet u otras acciones similares, y una interna para hacer el cálculo. PD: Un profesor de inglés probablemente sugiera que use "sorprendente" en lugar de "sorprendente" en el último párrafo :-)
nigromante
11

La respuesta estándar es que las excepciones no son regulares y deben usarse en casos excepcionales.

Una razón, que es importante para mí, es que cuando leo un try-catch estructura de control en un software que mantengo o depuro, trato de averiguar por qué el codificador original utilizó un manejo de excepciones en lugar de una if-elseestructura. Y espero encontrar una buena respuesta.

Recuerda que escribes código no solo para la computadora sino también para otros codificadores. Hay una semántica asociada a un controlador de excepciones que no puede descartar solo porque a la máquina no le importa.

Mouviciel
fuente
Una respuesta poco apreciada, creo. Es posible que la computadora no se ralentice mucho cuando encuentra que se traga una excepción, pero cuando estoy trabajando en el código de otra persona y me encuentro con eso, me detiene en seco mientras hago ejercicio si me he perdido algo importante que no hice No sé, o si en realidad no hay justificación para el uso de este antipatrón.
Tim Abell el
9

¿Qué tal el rendimiento? Durante la prueba de carga de una aplicación web .NET, superamos los 100 usuarios simulados por servidor web hasta que solucionamos una excepción común y ese número aumentó a 500 usuarios.

James Koch
fuente
8

Josh Bloch trata este tema ampliamente en Java efectivo. Sus sugerencias son esclarecedoras y deberían aplicarse también a .Net (a excepción de los detalles).

En particular, las excepciones deben usarse para circunstancias excepcionales. Las razones de esto están relacionadas principalmente con la usabilidad. Para que un método dado sea utilizable al máximo, sus condiciones de entrada y salida deben estar limitadas al máximo.

Por ejemplo, el segundo método es más fácil de usar que el primero:

/**
 * Adds two positive numbers.
 *
 * @param addend1 greater than zero
 * @param addend2 greater than zero
 * @throws AdditionException if addend1 or addend2 is less than or equal to zero
 */
int addPositiveNumbers(int addend1, int addend2) throws AdditionException{
  if( addend1 <= 0 ){
     throw new AdditionException("addend1 is <= 0");
  }
  else if( addend2 <= 0 ){
     throw new AdditionException("addend2 is <= 0");
  }
  return addend1 + addend2;
}

/**
 * Adds two positive numbers.
 *
 * @param addend1 greater than zero
 * @param addend2 greater than zero
 */
public int addPositiveNumbers(int addend1, int addend2) {
  if( addend1 <= 0 ){
     throw new IllegalArgumentException("addend1 is <= 0");
  }
  else if( addend2 <= 0 ){
     throw new IllegalArgumentException("addend2 is <= 0");
  }
  return addend1 + addend2;
}

En cualquier caso, debe verificar para asegurarse de que la persona que llama está utilizando su API adecuadamente. Pero en el segundo caso, lo necesita (implícitamente). Las excepciones suaves aún se generarán si el usuario no leyó el javadoc, pero:

  1. No necesitas documentarlo.
  2. No necesita probarlo (dependiendo de cuán agresiva sea su estrategia de prueba de unidad).
  3. No requiere que la persona que llama maneje tres casos de uso.

El punto a nivel del suelo es que las excepciones no deben deben usarse como códigos de retorno, en gran parte porque ha complicado no solo SU API, sino también la API de la persona que llama.

Hacer lo correcto tiene un costo, por supuesto. El costo es que todos deben comprender que necesitan leer y seguir la documentación. Esperemos que ese sea el caso de todos modos.

jasonnerothin
fuente
7

Creo que puede usar Excepciones para el control de flujo. Sin embargo, hay una otra cara de esta técnica. Crear excepciones es algo costoso, ya que tienen que crear un seguimiento de la pila. Por lo tanto, si desea utilizar Excepciones con más frecuencia que solo para señalar una situación excepcional, debe asegurarse de que la creación de trazas de la pila no influya negativamente en su rendimiento.

La mejor manera de reducir el costo de crear excepciones es anular el método fillInStackTrace () de esta manera:

public Throwable fillInStackTrace() { return this; }

Dicha excepción no tendrá trazados de pila completados.

paweloque
fuente
El stacktrace también requiere que la persona que llama "conozca" (es decir, dependa de) todos los Throwables de la pila. Esto es algo malo. Lanza excepciones apropiadas para el nivel de abstracción (ServiceExceptions en Servicios, DaoExceptions de los métodos Dao, etc.). Simplemente traduzca si es necesario.
jasonnerothin
5

Realmente no veo cómo está controlando el flujo del programa en el código que citó. Nunca verá otra excepción además de la excepción ArgumentOutOfRange. (Por lo tanto, su segunda cláusula catch nunca se verá afectada). Todo lo que está haciendo es usar un lanzamiento extremadamente costoso para imitar una declaración if.

Además, no está realizando las operaciones más siniestras en las que simplemente lanza una excepción simplemente para que sea atrapada en otro lugar para realizar el control de flujo. En realidad estás manejando un caso excepcional.

Jason Punyon
fuente
5

Estas son las mejores prácticas que describí en mi publicación de blog :

  • Lanza una excepción para indicar una situación inesperada en tu software.
  • Use valores de retorno para la validación de entrada .
  • Si sabe cómo manejar las excepciones que arroja una biblioteca, atrápelas en el nivel más bajo posible .
  • Si tiene una excepción inesperada, descarte la operación actual por completo. No finjas que sabes cómo lidiar con ellos .
Vladimir
fuente
4

Debido a que el código es difícil de leer, es posible que tenga problemas para depurarlo, introducirá nuevos errores al corregir errores después de mucho tiempo, es más costoso en términos de recursos y tiempo, y le molesta si está depurando su código y el depurador se detiene al producirse cada excepción;)

Gambrinus
fuente
4

Además de las razones indicadas, una razón para no usar excepciones para el control de flujo es que puede complicar en gran medida el proceso de depuración.

Por ejemplo, cuando estoy tratando de localizar un error en VS, normalmente activaré "romper todas las excepciones". Si está usando excepciones para el control de flujo, entonces voy a interrumpir el depurador de forma regular y tendré que seguir ignorando estas excepciones no excepcionales hasta que llegue al problema real. ¡Es probable que esto vuelva loco a alguien!

Sean
fuente
1
Ya he entregado ese problema: depuración de todas las excepciones: lo mismo, eso solo se hace porque el diseño para no usar excepciones es común. Mi pregunta fue: ¿por qué es común en primer lugar?
Peter
Entonces, ¿su respuesta es básicamente "es malo porque Visual Studio tiene esta característica ..."? He estado programando durante unos 20 años y ni siquiera me di cuenta de que había una opción de "interrupción en todas las excepciones". Aún así, "¡debido a esta característica!" Suena como una razón débil. Simplemente rastree la excepción a su fuente; esperamos que esté utilizando un idioma que lo haga fácil; de lo contrario, su problema es con las características del idioma, no con el uso general de las excepciones en sí mismas.
Loduwijk
3

Supongamos que tiene un método que hace algunos cálculos. Hay muchos parámetros de entrada que tiene que validar y luego devolver un número mayor que 0.

El uso de valores de retorno para señalar el error de validación es simple: si el método devolvió un número menor que 0, se produjo un error. ¿Cómo saber entonces cuál parámetro no se validó?

Recuerdo de mis días C que muchas funciones devolvían códigos de error como este:

-1 - x lesser then MinX
-2 - x greater then MaxX
-3 - y lesser then MinY

etc.

¿Es realmente menos legible que lanzar y atrapar una excepción?

kender
fuente
por eso inventaron las enumeraciones :) Pero los números mágicos son un tema completamente diferente ... en.wikipedia.org/wiki/…
Isak Savo
Gran ejemplo Estaba a punto de escribir lo mismo. @IsakSavo: las enumeraciones no son útiles en esta situación si se espera que el método devuelva algún valor u objeto de significado. Por ejemplo, getAccountBalance () debería devolver un objeto Money, no un objeto AccountBalanceResultEnum. Muchos programas C tienen un patrón similar en el que un valor centinela (0 o nulo) representa un error, y luego debe llamar a otra función para obtener un código de error separado para determinar por qué ocurrió el error. (La API MySQL C es así.)
Mark E. Haase
3

Puede usar la garra de un martillo para girar un tornillo, al igual que puede usar excepciones para controlar el flujo. Eso no significa que sea el uso previsto de la función. La ifdeclaración expresa condiciones, cuyo uso previsto es controlar el flujo.

Si está utilizando una función de manera no intencional mientras elige no utilizar la función diseñada para ese propósito, habrá un costo asociado. En este caso, la claridad y el rendimiento no tienen valor agregado real. ¿Qué te hace usar excepciones usando la ifdeclaración ampliamente aceptada ?

Dicho de otra manera: solo porque puedas no significa que debas hacerlo .

Bryan Watts
fuente
1
¿Estás diciendo que no hay necesidad de excepción, después de que tenemos ifel uso normal o el uso de las ejecuciones no está previsto porque no está destinado (argumento circular)?
Val
1
@Val: Las excepciones son para situaciones excepcionales: si detectamos lo suficiente como para lanzar una excepción y manejarla, tenemos suficiente información para no lanzarla y aún así manejarla. Podemos ir directamente a la lógica de manejo y omitir el costoso y superfluo try / catch.
Bryan Watts
Según esa lógica, es posible que no tenga excepciones y siempre salga del sistema en lugar de lanzar una excepción. Si desea hacer algo antes de salir, haga un contenedor y llame a eso. Ejemplo de Java: public class ExitHelper{ public static void cleanExit() { cleanup(); System.exit(1); } }Entonces simplemente llame a eso en lugar de lanzar: ExitHelper.cleanExit();Si su argumento fuera sólido, este sería el enfoque preferido y no habría excepciones. Básicamente estás diciendo "La única razón para las excepciones es bloquearse de una manera diferente".
Loduwijk
@ Aaron: Si lanzo y atrapo la excepción, tengo suficiente información para evitar hacerlo. Eso no significa que todas las excepciones sean repentinamente fatales. Otro código que no controlo podría atraparlo, y eso está bien. Mi argumento, que sigue siendo sólido, es que lanzar y atrapar una excepción en el mismo contexto es superfluo. No dije ni declararía que todas las excepciones deberían salir del proceso.
Bryan Watts
@BryanWatts Reconocido. Muchos otros han dicho que solo debe usar excepciones para cualquier cosa de la que no pueda recuperarse, y por extensión siempre debe fallar en las excepciones. Por eso es difícil discutir estas cosas; no solo hay 2 opiniones, sino muchas. Todavía estoy en desacuerdo contigo, pero no demasiado. Hay momentos en que lanzar / atrapar juntos es el código más legible, fácil de mantener y de mejor apariencia; generalmente esto sucede si ya está capturando otras excepciones, por lo que ya tiene try / catch y agregar 1 o 2 capturas más es más limpio que las comprobaciones de errores separadas if.
Loduwijk
3

Como otros han mencionado en numerosas ocasiones, el principio de menos asombro prohibirá que uses excepciones excesivamente para propósitos de control de flujo solamente. Por otro lado, ninguna regla es 100% correcta, y siempre hay casos en los que una excepción es "la herramienta correcta", muy parecida a gotosí misma, por cierto, que se envía en forma de breakycontinue en lenguajes como Java, que A menudo son la forma perfecta de saltar de bucles muy anidados, que no siempre son evitables.

La siguiente publicación de blog explica un caso de uso bastante complejo pero también bastante interesante para un no local ControlFlowException :

Explica cómo dentro de jOOQ (una biblioteca de abstracción SQL para Java) , tales excepciones se usan ocasionalmente para abortar el proceso de representación SQL temprano cuando se cumple alguna condición "rara".

Ejemplos de tales condiciones son:

  • Se encuentran demasiados valores de enlace. Algunas bases de datos no admiten números arbitrarios de valores de enlace en sus instrucciones SQL (SQLite: 999, Ingres 10.1.0: 1024, Sybase ASE 15.5: 2000, SQL Server 2008: 2100). En esos casos, jOOQ aborta la fase de representación de SQL y vuelve a representar la instrucción SQL con valores de enlace en línea. Ejemplo:

    // Pseudo-code attaching a "handler" that will
    // abort query rendering once the maximum number
    // of bind values was exceeded:
    context.attachBindValueCounter();
    String sql;
    try {
    
      // In most cases, this will succeed:
      sql = query.render();
    }
    catch (ReRenderWithInlinedVariables e) {
      sql = query.renderWithInlinedBindValues();
    }

    Si extrajéramos explícitamente los valores de enlace de la consulta AST para contarlos cada vez, perderíamos valiosos ciclos de CPU para el 99.9% de las consultas que no sufren este problema.

  • Parte de la lógica está disponible solo indirectamente a través de una API que queremos ejecutar solo "parcialmente". El UpdatableRecord.store()método genera una declaración INSERTo UPDATE, dependiendo de los Recordindicadores internos de. Desde el "exterior", no sabemos qué tipo de lógica está contenida store()(por ejemplo, bloqueo optimista, manejo de escucha de eventos, etc.), por lo que no queremos repetir esa lógica cuando almacenamos varios registros en una declaración por lotes, donde nos gustaría tener store()solo generar la instrucción SQL, no ejecutarla realmente. Ejemplo:

    // Pseudo-code attaching a "handler" that will
    // prevent query execution and throw exceptions
    // instead:
    context.attachQueryCollector();
    
    // Collect the SQL for every store operation
    for (int i = 0; i < records.length; i++) {
      try {
        records[i].store();
      }
    
      // The attached handler will result in this
      // exception being thrown rather than actually
      // storing records to the database
      catch (QueryCollectorException e) {
    
        // The exception is thrown after the rendered
        // SQL statement is available
        queries.add(e.query());                
      }
    }

    Si hubiéramos externalizado la store()lógica en una API "reutilizable" que se puede personalizar para que opcionalmente no ejecute el SQL, estaríamos buscando crear una API bastante difícil de mantener y difícilmente reutilizable.

Conclusión

En esencia, nuestro uso de estos mensajes no locales gotoes similar a lo que dijo [Mason Wheeler] [5] en su respuesta:

"Acabo de encontrar una situación que no puedo manejar adecuadamente en este momento, porque no tengo suficiente contexto para manejarlo, pero la rutina que me llamó (o algo más arriba en la pila de llamadas) debería saber cómo manejarlo ".

Ambos usos de ControlFlowExceptionsfueron bastante fáciles de implementar en comparación con sus alternativas, lo que nos permitió reutilizar una amplia gama de lógica sin refactorizarla de las partes internas relevantes.

Pero la sensación de que esto sea una sorpresa para los futuros mantenedores permanece. El código se siente bastante delicado y, si bien fue la elección correcta en este caso, siempre preferimos no usar excepciones para el flujo de control local , donde es fácil evitar el uso de ramificaciones ordinarias if - else.

Lukas Eder
fuente
2

Por lo general, no hay nada malo, per se, con el manejo de una excepción en un nivel bajo. Una excepción ES un mensaje válido que proporciona muchos detalles de por qué no se puede realizar una operación. Y si puedes manejarlo, deberías hacerlo.

En general, si sabe que hay una alta probabilidad de falla que puede verificar ... debe hacer la verificación ... es decir, si (obj! = Null) obj.method ()

En su caso, no estoy lo suficientemente familiarizado con la biblioteca C # para saber si la fecha y hora tiene una manera fácil de verificar si una marca de tiempo está fuera de los límites. Si lo hace, solo llame a if (.isvalid (ts)) de lo contrario, su código está básicamente bien.

Entonces, básicamente se reduce a cualquier forma que cree un código más limpio ... si la operación para protegerse contra una excepción esperada es más compleja que simplemente manejar la excepción; entonces tienes mi permiso para manejar la excepción en lugar de crear guardias complejos en todas partes


fuente
Punto adicional: si su excepción proporciona información de captura de fallas (un captador como "Param getWhatParamMessedMeUp ()"), puede ayudar al usuario de su API a tomar una buena decisión sobre qué hacer a continuación. De lo contrario, solo está dando un nombre a un estado de error.
jasonnerothin
2

Si está utilizando controladores de excepciones para el flujo de control, está siendo demasiado general y vago. Como alguien más mencionó, usted sabe que algo sucedió si está manejando el procesamiento en el controlador, pero ¿qué es exactamente? Esencialmente, está utilizando la excepción para una instrucción else, si la está utilizando para el flujo de control.

Si no sabe qué estado podría ocurrir, puede usar un controlador de excepciones para estados inesperados, por ejemplo, cuando tiene que usar una biblioteca de terceros, o debe atrapar todo en la interfaz de usuario para mostrar un buen error mensaje y registrar la excepción.

Sin embargo, si sabe qué podría salir mal y no pone una declaración if o algo para verificarlo, entonces está siendo flojo. Permitir que el controlador de excepciones sea el complemento para todo lo que sabe que podría suceder es perezoso, y volverá a perseguirlo más tarde, porque intentará solucionar una situación en su controlador de excepciones en función de una suposición posiblemente falsa.

Si coloca la lógica en su controlador de excepciones para determinar qué sucedió exactamente, entonces sería bastante estúpido por no poner esa lógica dentro del bloque try.

Los manejadores de excepciones son el último recurso, porque cuando te quedas sin ideas / formas de evitar que algo salga mal, o las cosas están más allá de tu capacidad de control. Como, el servidor está caído y se agota el tiempo de espera y no puede evitar que se lance esa excepción.

Finalmente, hacer todas las comprobaciones por adelantado muestra lo que sabe o espera que ocurra y lo hace explícito. El código debe ser claro en su intención. ¿Qué preferirías leer?


fuente
1
No es cierto en absoluto: "Esencialmente está utilizando la excepción para una instrucción else, si la está utilizando para el flujo de control". Si la utiliza para el flujo de control, sabe exactamente qué captura y nunca utiliza una captura general, sino un específico por supuesto!
Peter
2

Es posible que le interese echar un vistazo al sistema de condición de Common Lisp, que es una especie de generalización de excepciones bien hecha. Debido a que puede desenrollar la pila o no de forma controlada, también obtiene "reinicios", que son extremadamente útiles.

Esto no tiene mucho que ver con las mejores prácticas en otros idiomas, pero muestra lo que se puede hacer con un diseño pensado (aproximadamente) en la dirección en la que está pensando.

Por supuesto, todavía hay consideraciones de rendimiento si rebota arriba y abajo de la pila como un yoyo, pero es una idea mucho más general que el enfoque de tipo "oh mierda, vamos a rescatar" que la mayoría de los sistemas de excepción de captura / lanzamiento incorporan.

Simón
fuente
2

No creo que haya nada de malo en usar Excepciones para el control de flujo. Las excepciones son algo similares a las continuaciones y en lenguajes estáticamente tipados, las excepciones son más poderosas que las continuaciones, por lo tanto, si necesita continuaciones pero su idioma no las tiene, puede usar Excepciones para implementarlas.

Bueno, en realidad, si necesita continuaciones y su idioma no las tiene, eligió el idioma equivocado y debería usar uno diferente. Pero a veces no tiene otra opción: la programación web del lado del cliente es el mejor ejemplo, simplemente no hay forma de evitar JavaScript.

Un ejemplo: Microsoft Volta es un proyecto que permite escribir aplicaciones web en .NET directo, y permite que el marco se encargue de averiguar qué bits deben ejecutarse en cada lugar. Una consecuencia de esto es que Volta necesita poder compilar CIL a JavaScript, para que pueda ejecutar código en el cliente. Sin embargo, hay un problema: .NET tiene subprocesos múltiples, JavaScript no. Entonces, Volta implementa continuaciones en JavaScript usando Excepciones de JavaScript, luego implementa .NET Threads usando esas continuaciones. De esa manera, las aplicaciones Volta que usan hilos pueden compilarse para ejecutarse en un navegador no modificado, no se necesita Silverlight.

Jörg W Mittag
fuente
2

Una razón estética:

Un intento siempre viene con una trampa, mientras que un if no tiene que venir con otro

if (PerformCheckSucceeded())
   DoSomething();

Con try / catch, se vuelve mucho más detallado.

try
{
   PerformCheckSucceeded();
   DoSomething();
}
catch
{
}

Son 6 líneas de código demasiadas.

gzak
fuente
1

Pero no siempre sabrá lo que sucede en los métodos que llama. No sabrá exactamente dónde se produjo la excepción. Sin examinar el objeto de excepción con mayor detalle ...

Malla
fuente
1

Siento que no hay nada malo con tu ejemplo. Por el contrario, sería un pecado ignorar la excepción lanzada por la función llamada.

En la JVM, lanzar una excepción no es tan costoso, solo crea la excepción con la nueva xyzException (...), porque esta última implica una caminata de la pila. Entonces, si tiene algunas excepciones creadas de antemano, puede lanzarlas muchas veces sin costo. Por supuesto, de esta manera no puede pasar datos junto con la excepción, pero creo que es algo malo de todos modos.

Ingo
fuente
Lo siento, esto es completamente incorrecto, Brann. Depende de la condición. No siempre es el caso que la condición sea trivial. Por lo tanto, una declaración if podría llevar horas, días o incluso más.
Ingo
En la JVM, eso es. No más caro que una devolución. Imagínate. Pero la pregunta es, ¿qué escribiría en la declaración if, si no es el código que ya está presente en la función llamada para distinguir un caso excepcional de uno normal, por lo tanto, la duplicación de código.
Ingo
1
Ingo: una situación excepcional es una que no esperas. es decir, uno que no haya pensado como programador. Así que mi regla es "escribir código que no arroje excepciones" :)
Brann
1
Nunca escribo manejadores de excepciones, siempre soluciono el problema (excepto cuando no puedo hacerlo porque no tengo control sobre el código de falla). Y nunca lanzo excepciones, excepto cuando el código que escribí está destinado a que alguien más lo use (por ejemplo, una biblioteca). No me muestres la contradicción?
Brann
1
Estoy de acuerdo con usted para no lanzar excepciones salvajemente. Pero ciertamente, lo que es "excepcional" es una cuestión de definición. Ese String.parseDouble arroja una excepción si no puede entregar un resultado útil, por ejemplo, está en orden. ¿Qué más debería hacer? ¿Volver NaN? ¿Qué pasa con el hardware no IEEE?
Ingo
1

Existen algunos mecanismos generales a través de los cuales un lenguaje podría permitir que un método salga sin devolver un valor y relajarse al siguiente bloque "catch":

  • Haga que el método examine el marco de la pila para determinar el sitio de la llamada y utilice los metadatos del sitio de la llamada para encontrar información sobre un trybloque dentro del método de llamada o la ubicación donde el método de llamada almacenó la dirección de su interlocutor; en la última situación, examine los metadatos de la persona que llama para determinar de la misma manera que la persona que llama inmediatamente, repitiendo hasta que encuentre un trybloque o la pila esté vacía. Este enfoque agrega muy poca sobrecarga al caso de no excepción (excluye algunas optimizaciones) pero es costoso cuando ocurre una excepción.

  • Haga que el método devuelva un indicador "oculto" que distingue un retorno normal de una excepción, y haga que la persona que llama verifique ese indicador y se bifurque a una rutina de "excepción" si está configurado. Esta rutina agrega 1-2 instrucciones al caso sin excepción, pero relativamente poca sobrecarga cuando ocurre una excepción.

  • Haga que la persona que llama coloque la información o código de manejo de excepciones en una dirección fija relativa a la dirección de retorno apilada. Por ejemplo, con el ARM, en lugar de usar la instrucción "subrutina BL", se podría usar la secuencia:

        adr lr,next_instr
        b subroutine
        b handle_exception
    next_instr:
    

Para salir normalmente, la subrutina simplemente haría bx lro pop {pc}; en caso de una salida anormal, la subrutina restaría 4 de LR antes de realizar la devolución o el usosub lr,#4,pc (dependiendo de la variación de ARM, modo de ejecución, etc.) Este enfoque funcionará muy mal si la persona que llama no está diseñada para acomodarlo.

Un lenguaje o marco que utiliza excepciones marcadas podría beneficiarse si se manejan con un mecanismo como el n. ° 2 o n. ° 3 anterior, mientras que las excepciones no marcadas se manejan con el n. ° 1. Aunque la implementación de excepciones marcadas en Java es bastante molesta, no sería un mal concepto si hubiera un medio por el cual un sitio de llamadas pudiera decir, esencialmente, "Este método se declara como arrojando XX, pero no lo espero siempre que lo haga; si lo hace, vuelva a considerarlo como una excepción "no verificada". En un marco donde las excepciones verificadas se manejan de tal manera, podrían ser un medio eficaz de control de flujo para cosas como métodos de análisis que en algunos contextos pueden tener alta probabilidad de falla, pero donde la falla debería devolver información fundamentalmente diferente al éxito. Sin embargo, no conozco ningún marco que utilice dicho patrón. En cambio, el patrón más común es utilizar el primer enfoque anterior (costo mínimo para el caso sin excepción, pero alto costo cuando se lanzan excepciones) para todas las excepciones.

Super gato
fuente