¿Por qué las excepciones deben usarse de manera conservadora?


80

A menudo veo / escucho a personas decir que las excepciones solo deben usarse raras veces, pero nunca explicar por qué. Si bien eso puede ser cierto, la lógica es normalmente simplista: "se llama una excepción por una razón" que, para mí, parece ser el tipo de explicación que nunca debería ser aceptada por un programador / ingeniero respetable.

Existe una variedad de problemas que se pueden resolver con una excepción. ¿Por qué no es prudente utilizarlos para controlar el flujo? ¿Cuál es la filosofía detrás de ser excepcionalmente conservador con la forma en que se usan? ¿Semántica? ¿Actuación? ¿Complejidad? ¿Estética? ¿Convención?

He visto algunos análisis sobre el rendimiento antes, pero a un nivel que sería relevante para algunos sistemas e irrelevante para otros.

Una vez más, no estoy necesariamente en desacuerdo con que deban guardarse para circunstancias especiales, pero me pregunto cuál es la razón fundamental del consenso (si tal cosa existe).



32
No es un duplicado. El ejemplo vinculado trata sobre si el manejo de excepciones es útil en absoluto. Se trata de la distinción entre cuándo utilizar excepciones y cuándo utilizar otro mecanismo de notificación de errores.
Adrian McCarthy


1
No hay una justificación de consenso. Diferentes personas tienen opiniones diferentes sobre la "idoneidad" de lanzar excepciones, y estas opiniones generalmente están influenciadas por el lenguaje en el que se desarrollan. Etiquetó esta pregunta con C ++, pero sospecho que si la etiqueta con Java obtendrá opiniones diferentes.
Charles Salvia

5
No, no debería ser una wiki. Las respuestas aquí requieren experiencia y deben ser recompensadas con reputación.
bobobobo

Respuestas:


92

El principal punto de fricción es la semántica. Muchos desarrolladores abusan de las excepciones y las aprovechan en cada oportunidad. La idea es utilizar la excepción para situaciones algo excepcionales. Por ejemplo, la entrada incorrecta del usuario no cuenta como una excepción porque espera que esto suceda y está listo para eso. Pero si intentó crear un archivo y no había suficiente espacio en el disco, entonces sí, esta es una excepción definitiva.

Otro problema es que las excepciones a menudo se lanzan y se tragan. Los desarrolladores utilizan esta técnica para simplemente "silenciar" el programa y dejar que se ejecute el mayor tiempo posible hasta que colapse por completo. Esto está muy mal. Si no procesa las excepciones, si no reacciona apropiadamente liberando algunos recursos, si no registra la ocurrencia de la excepción o al menos no notifica al usuario, entonces no está usando la excepción para lo que significan.

Respondiendo directamente a tu pregunta. Las excepciones rara vez deben usarse porque las situaciones excepcionales son raras y las excepciones son costosas.

Rara, porque no espera que su programa se bloquee cada vez que presione un botón o cada entrada de usuario con formato incorrecto. Por ejemplo, es posible que la base de datos de repente no sea accesible, puede que no haya suficiente espacio en el disco, algún servicio de terceros del que depende esté fuera de línea, todo esto puede suceder, pero muy raramente, estos serían casos excepcionales claros.

Caro, porque lanzar una excepción interrumpirá el flujo normal del programa. El tiempo de ejecución desenrollará la pila hasta que encuentre un manejador de excepciones apropiado que pueda manejar la excepción. También recopilará la información de la llamada a lo largo del camino para pasarla al objeto de excepción que recibirá el controlador. Todo tiene costos.

Esto no quiere decir que no pueda haber excepción al uso de excepciones (sonrisa). A veces, puede simplificar la estructura del código si lanza una excepción en lugar de reenviar códigos de retorno a través de muchas capas. Como regla simple, si espera que algún método sea llamado con frecuencia y descubre alguna situación "excepcional" la mitad del tiempo, entonces es mejor encontrar otra solución. Sin embargo, si espera un flujo de operación normal la mayor parte del tiempo, mientras que esta situación "excepcional" solo puede surgir en algunas circunstancias raras, entonces está bien lanzar una excepción.

@Comments: La excepción definitivamente se puede usar en algunas situaciones menos excepcionales si eso pudiera hacer que su código sea más simple y fácil. Esta opción está abierta, pero diría que es bastante rara en la práctica.

¿Por qué no es prudente utilizarlos para controlar el flujo?

Porque las excepciones interrumpen el "flujo de control" normal. Genera una excepción y la ejecución normal del programa se abandona, dejando potencialmente los objetos en un estado inconsistente y algunos recursos abiertos sin completar. Claro, C # tiene la declaración using que se asegurará de que el objeto se elimine incluso si se lanza una excepción desde el cuerpo using. Pero abstraigamos por el momento del lenguaje. Suponga que el marco no eliminará los objetos por usted. Lo haces manualmente. Tienes algún sistema sobre cómo solicitar y liberar recursos y memoria. Tiene un acuerdo en todo el sistema sobre quién es responsable de liberar objetos y recursos en qué situaciones. Tiene reglas sobre cómo tratar con bibliotecas externas. Funciona muy bien si el programa sigue el flujo de operación normal. Pero de repente, en medio de la ejecución, lanza una excepción. La mitad de los recursos quedan sin cultivar. La mitad aún no se ha solicitado. Si la operación estaba destinada a ser transaccional, ahora está rota. Sus reglas para manejar recursos no funcionarán porque las partes del código responsables de liberar recursos simplemente no se ejecutarán. Si alguien más quisiera usar esos recursos, puede encontrarlos en un estado inconsistente y colapsar también porque no pueden predecir esta situación en particular.

Digamos, usted quería un método M () llamar al método N () para hacer un trabajo y arreglar algún recurso y luego devolverlo a M () que lo usará y luego lo desechará. Multa. Ahora algo sale mal en N () y arroja una excepción que no esperaba en M (), por lo que la excepción burbujea en la parte superior hasta que tal vez quede atrapada en algún método C () que no tendrá idea de lo que estaba sucediendo en el fondo. en N () y si liberar algunos recursos y cómo hacerlo.

Con las excepciones de lanzamiento, crea una manera de llevar su programa a muchos estados intermedios nuevos e impredecibles que son difíciles de pronosticar, comprender y manejar. Es algo similar a usar GOTO. Es muy difícil diseñar un programa que pueda saltar aleatoriamente su ejecución de una ubicación a otra. También será difícil mantenerlo y depurarlo. Cuando el programa crece en complejidad, simplemente perderá una descripción general de qué, cuándo y dónde está sucediendo menos para solucionarlo.


5
Si compara una función que genera una excepción con una función que devuelve un código de error, el desenrollado de la pila sería el mismo a menos que me falte algo.
Catskul

9
@Catskul: no necesariamente. Un retorno de función devuelve el control directamente al llamador inmediato. Una excepción lanzada devuelve el control al primer controlador de captura para ese tipo de excepción (o una clase base del mismo o "...") en la totalidad de la pila de llamadas actual.
Jon-Hanson

5
También debe tenerse en cuenta que los depuradores a menudo fallan de forma predeterminada en las excepciones. El depurador fallará mucho si usa excepciones para condiciones que no son inusuales.
Qwertie

5
@jon: Eso solo sucedería si no se detectara y necesitara escapar de más ámbitos para alcanzar el manejo de errores. En ese mismo caso, usando valores de retorno (y también necesitando pasar a través de múltiples ámbitos) se produciría la misma cantidad de desenrollado de la pila.
Catskul

3
-1 lo siento. No tiene sentido hablar de para qué están "destinadas" las instalaciones excepcionales. Son solo un mecanismo, uno que puede usarse para manejar situaciones excepcionales como quedarse sin memoria, etc., pero eso no significa que no deban usarse también para otros fines. Lo que quiero ver es una explicación de por qué no deberían usarse para otros fines. Aunque tu respuesta habla un poco sobre eso, está mezclada con mucha charla sobre para qué están "destinadas" las excepciones.
j_random_hacker

61

Si bien "lanzar excepciones en circunstancias excepcionales" es la respuesta simplista, en realidad puede definir cuáles son esas circunstancias: cuando se satisfacen las condiciones previas, pero no se pueden satisfacer las condiciones posteriores . Esto le permite escribir condiciones posteriores más estrictas, estrictas y útiles sin sacrificar el manejo de errores; de lo contrario, sin excepciones, debe cambiar la condición posterior para permitir todos los posibles estados de error.

  • Las condiciones previas deben ser verdaderas antes de llamar a una función.
  • La poscondición es lo que garantiza la función después de que regresa .
  • La seguridad de las excepciones establece cómo las excepciones afectan la consistencia interna de una función o estructura de datos y, a menudo, se ocupan del comportamiento que se transmite desde el exterior (por ejemplo, functor, ctor de un parámetro de plantilla, etc.).

Constructores

Hay muy poco que pueda decir sobre cada constructor para cada clase que posiblemente podría estar escrito en C ++, pero hay algunas cosas. El principal de ellos es que los objetos construidos (es decir, para los cuales el constructor logró regresar) serán destruidos. No puede modificar esta condición posterior porque el lenguaje asume que es verdadera y llamará a los destructores automáticamente. (Técnicamente, puede aceptar la posibilidad de un comportamiento indefinido para el que el lenguaje no ofrece garantías sobre nada, pero que probablemente esté mejor cubierto en otro lugar).

La única alternativa a lanzar una excepción cuando un constructor no puede tener éxito es modificar la definición básica de la clase (la "clase invariante") para permitir estados válidos "nulos" o zombies y así permitir que el constructor "tenga éxito" construyendo un zombi .

Ejemplo de zombie

Un ejemplo de esta modificación zombie es std :: ifstream , y siempre debe verificar su estado antes de poder usarlo. Debido a que std :: string , por ejemplo, no lo hace, siempre se le garantiza que puede usarlo inmediatamente después de la construcción. Imagínese si tuviera que escribir código como este ejemplo, y si olvidara verificar el estado zombie, obtendría silenciosamente resultados incorrectos o corrompería otras partes de su programa:

string s = "abc";
if (s.memory_allocation_succeeded()) {
  do_something_with(s); // etc.
}

Incluso nombrar ese método es un buen ejemplo de cómo debe modificar el invariante de la clase y la interfaz para una situación que la cadena no puede predecir ni manejarse por sí misma.

Ejemplo de validación de entrada

Abordemos un ejemplo común: validar la entrada del usuario. El hecho de que queramos permitir la entrada fallida no significa que la función de análisis deba incluir eso en su condición posterior. Sin embargo, significa que nuestro controlador debe verificar si el analizador falla.

// boost::lexical_cast<int>() is the parsing function here
void show_square() {
  using namespace std;
  assert(cin); // precondition for show_square()
  cout << "Enter a number: ";
  string line;
  if (!getline(cin, line)) { // EOF on cin
    // error handling omitted, that EOF will not be reached is considered
    // part of the precondition for this function for the sake of example
    //
    // note: the below Python version throws an EOFError from raw_input
    //  in this case, and handling this situation is the only difference
    //  between the two
  }
  int n;
  try {
    n = boost::lexical_cast<int>(line);
    // lexical_cast returns an int
    // if line == "abc", it obviously cannot meet that postcondition
  }
  catch (boost::bad_lexical_cast&) {
    cout << "I can't do that, Dave.\n";
    return;
  }
  cout << n * n << '\n';
}

Desafortunadamente, esto muestra dos ejemplos de cómo el alcance de C ++ requiere que rompa RAII / SBRM. Un ejemplo en Python que no tiene ese problema y muestra algo que me gustaría que tuviera C ++ - try-else:

# int() is the parsing "function" here
def show_square():
  line = raw_input("Enter a number: ") # same precondition as above
  # however, here raw_input will throw an exception instead of us
  # using assert
  try:
    n = int(line)
  except ValueError:
    print "I can't do that, Dave."
  else:
    print n * n

Condiciones previas

Las condiciones previas no tienen que verificarse estrictamente; violar una siempre indica una falla lógica y son responsabilidad de la persona que llama, pero si las verifica, entonces lanzar una excepción es apropiado. (En algunos casos, es más apropiado devolver basura o bloquear el programa; aunque esas acciones pueden ser terriblemente incorrectas en otros contextos. La mejor manera de manejar el comportamiento indefinido es otro tema).

En particular, contraste las ramas std :: logic_error y std :: runtime_error de la jerarquía de excepciones stdlib. El primero se usa a menudo para violaciones de condiciones previas, mientras que el segundo es más adecuado para violaciones posteriores a condiciones.


5
Respuesta decente, pero básicamente solo está estableciendo una regla y no proporcionando una justificación. ¿Es tu racional entonces: Convención y estilo?
Catskul

6
+1. Así es como se diseñaron las excepciones para su uso, y usarlas de esta manera realmente mejora la legibilidad y reutilización del código (en mi humilde opinión).
Daniel Pryden

15
Catskul: la primera es la definición de circunstancias excepcionales, no la justificación ni la convención. Si no puede garantizar las condiciones posteriores, no puede regresar . Sin excepciones, debe hacer que la condición posterior sea extremadamente amplia para incluir todos los estados de error, hasta el punto en que sea prácticamente inútil. Parece que no respondí tu pregunta literal de "por qué deberían ser raros" porque no los veo de esa manera. Son una herramienta que me permite ajustar las condiciones posteriores de manera útil y al mismo tiempo permitir que se produzcan errores (... en circunstancias excepcionales :).

9
+1. La condición previa / posterior es la MEJOR explicación que he escuchado.
KitsuneYMG

6
Mucha gente dice "pero eso se reduce a la convención / semántica". Sí es cierto. Pero la convención y la semántica son importantes porque tienen profundas implicaciones para la complejidad, la usabilidad y el mantenimiento. Después de todo, ¿no es la decisión de definir formalmente las condiciones previas y posteriores también una cuestión de convención y semántica? Y, sin embargo, su uso puede hacer que su código sea mucho más fácil de usar y mantener.
Darryl

40

  1. Llamadas costosas del kernel (u otras invocaciones de API del sistema) para administrar las interfaces de señal del kernel (sistema)
  2. Difícil de analizar
    Muchos de los problemas de lagotodeclaración se aplican a las excepciones. Saltan sobre cantidades potencialmente grandes de código a menudo en múltiples rutinas y archivos fuente. Esto no siempre es evidente al leer el código fuente intermedio. (Está en Java).
  3. No siempre anticipado por el código intermedio
    El código que se salta puede o no haber sido escrito con la posibilidad de una salida de excepción en mente. Si originalmente se escribió así, es posible que no se haya mantenido con eso en mente. Piense: fugas de memoria, fugas de descriptor de archivos, fugas de socket, ¿quién sabe?
  4. Complicaciones de mantenimiento
    Es más difícil mantener un código que salte el procesamiento de excepciones.

24
+1, buenas razones en general. Pero tenga en cuenta que el costo de desenrollar la pila y llamar a los destructores (al menos) se paga mediante cualquier mecanismo de manejo de errores funcionalmente equivalente, por ejemplo, probando y devolviendo códigos de error como en C.
j_random_hacker

Voy a marcar esto como la respuesta aunque estoy de acuerdo con j_random. El desenrollamiento de la pila y la des / asignación de recursos serían los mismos en todos los mecanismos funcionalmente equivalentes. Si está de acuerdo, cuando vea esto, simplemente elimínelos de la lista para que la respuesta sea un poco mejor.
Catskul

14
Julien: el comentario de j_random señala que no hay ahorros comparativos: int f() { char* s = malloc(...); if (some_func() == error) { free(s); return error; } ... }tienes que pagar para deshacer la pila, ya sea que lo hagas manualmente o mediante excepciones. No se puede comparar el uso de excepciones a ningún control de errores en absoluto .

14
1. Caro: la optimización prematura es la raíz de todos los males. 2. Difícil de analizar: ¿en comparación con las capas anidadas de código de retorno de error? Respetuosamente no estoy de acuerdo. 3. No siempre anticipado por el código intermedio: en comparación con las capas anidadas que no siempre tratan y traducen los errores de manera razonable. Los novatos fallan en ambos. 4. Complicaciones de mantenimiento: ¿cómo? ¿Son más fáciles de mantener las dependencias mediadas por códigos de error? ... pero esta parece ser una de esas áreas donde los desarrolladores de cualquiera de las escuelas tienen dificultades para aceptar los argumentos de los demás. Como todo, el diseño de manejo de errores es una compensación.
Pontus Gagge

1
Estoy de acuerdo en que se trata de un asunto complejo y que es difícil responder con precisión con generalidades. Pero tenga en cuenta que la pregunta original simplemente preguntaba por qué se dio el consejo anti-excepción, no para una explicación de las mejores prácticas generales.
DigitalRoss

22

Lanzar una excepción es, hasta cierto punto, similar a una instrucción goto. Haga eso para controlar el flujo y terminará con un código espagueti incomprensible. Peor aún, en algunos casos ni siquiera sabe a dónde va exactamente el salto (es decir, si no está detectando la excepción en el contexto dado). Esto viola descaradamente el principio de "mínima sorpresa" que mejora la capacidad de mantenimiento.


5
¿Cómo es posible utilizar una excepción para cualquier propósito que no sea el control de flujo? Incluso en una situación excepcional, siguen siendo un mecanismo de control de flujo, con consecuencias para la claridad de su código (asumido aquí: incomprensible). Supongo que podrías usar excepciones pero prohibir catch, aunque en ese caso casi podrías llamarte a std::terminate()ti mismo. En general, este argumento me parece que dice "nunca use excepciones", en lugar de "solo use excepciones raramente".
Steve Jessop

5
La declaración de retorno dentro de la función lo saca de la función. Excepciones lo lleva quién sabe cuántas capas; no hay una forma sencilla de averiguarlo.

2
Steve: Entiendo tu punto, pero eso no es lo que quise decir. Las excepciones están bien para situaciones inesperadas. Algunas personas abusan de ellos como "devoluciones anticipadas", tal vez incluso como una especie de declaración de cambio.
Erich Kitzmueller

2
Creo que esta pregunta asume que "inesperado" no es lo suficientemente explicativo. Estoy de acuerdo hasta cierto punto. Si alguna condición impide que una función se complete como se desea, entonces su programa maneja esa situación correctamente o no. Si lo maneja, entonces es "esperado", y el código debe ser comprensible. Si no lo maneja correctamente, está en problemas. A menos que permita que la excepción termine su programa, algún nivel de su código debe "esperarlo". Y, sin embargo, en la práctica, como digo en mi respuesta, es una buena regla a pesar de ser teóricamente insatisfactoria.
Steve Jessop

2
Además, por supuesto, los envoltorios pueden convertir una función de lanzamiento en una de retorno de error, o viceversa. Entonces, si la persona que llama no está de acuerdo con su idea de "inesperado", no necesariamente rompe la comprensión de su código. Puede sacrificar algo de rendimiento en el que provocan muchas condiciones que usted cree que son "inesperadas", pero que creen que son normales y recuperables y, por lo tanto, se detectan y se convierten en un error o lo que sea.
Steve Jessop

16

Las excepciones hacen que sea más difícil razonar sobre el estado de su programa. En C ++, por ejemplo, debe pensar más para asegurarse de que sus funciones sean muy seguras para las excepciones, de lo que tendría que hacer si no fuera necesario.

La razón es que, sin excepciones, una llamada de función puede regresar o puede terminar el programa primero. Con excepciones, una llamada de función puede regresar, o puede terminar el programa, o puede saltar a un bloque de captura en algún lugar. Por lo tanto, ya no puede seguir el flujo de control con solo mirar el código frente a usted. Necesita saber si las funciones llamadas pueden lanzar. Es posible que necesite saber qué se puede lanzar y dónde se atrapa, dependiendo de si le importa a dónde va el control o solo le importa que salga del alcance actual.

Por esta razón, la gente dice "no use excepciones a menos que la situación sea realmente excepcional". Cuando se analiza, "realmente excepcional" significa que "ha ocurrido alguna situación en la que los beneficios de manejarlo con un valor de retorno de error se ven compensados ​​por los costos". Así que sí, esto es algo así como una declaración vacía, aunque una vez que tienes algunos instintos para "realmente excepcional", se convierte en una buena regla general. Cuando la gente habla de control de flujo, quiere decir que la capacidad de razonar localmente (sin hacer referencia a los bloques de captura) es un beneficio de los valores de retorno.

Java tiene una definición más amplia de "realmente excepcional" que C ++. Es más probable que los programadores de C ++ quieran ver el valor de retorno de una función que los programadores de Java, por lo que en Java "realmente excepcional" podría significar "No puedo devolver un objeto no nulo como resultado de esta función". En C ++, es más probable que signifique "Dudo mucho que mi interlocutor pueda continuar". Entonces, una secuencia de Java arroja si no puede leer un archivo, mientras que una secuencia de C ++ (por defecto) devuelve un valor que indica un error. En todos los casos, sin embargo, es una cuestión de qué código está dispuesto a forzar a escribir a la persona que llama. Por lo tanto, es una cuestión de estilo de codificación: debe llegar a un consenso sobre cómo debería verse su código y cuánto código de "verificación de errores" desea escribir en comparación con la cantidad de "seguridad de excepción"

El amplio consenso en todos los idiomas parece ser que esto se hace mejor en términos de cuán recuperable es probable que sea el error (ya que los errores irrecuperables no dan como resultado ningún código con excepciones, pero aún así es necesario verificar y devolver el suyo propio) error en el código que utiliza devoluciones de error). Así que la gente viene a esperar "esta llamada de función que se produce una excepción" en el sentido de " yo no puedo continuar", no sólo " seno puede continuar ". Eso no es inherente a las excepciones, es solo una costumbre, pero como cualquier buena práctica de programación, es una costumbre defendida por personas inteligentes que lo han probado de otra manera y no han disfrutado de los resultados. Yo también he tenido malas Experiencias que arrojan demasiadas excepciones. Así que, personalmente, pienso en términos de "realmente excepcional", a menos que algo en la situación haga que una excepción sea particularmente atractiva.

Por cierto, aparte de razonar sobre el estado de su código, también hay implicaciones de rendimiento. Las excepciones suelen ser económicas ahora, en idiomas en los que tiene derecho a preocuparse por el rendimiento. Pueden ser más rápidos que varios niveles de "oh, el resultado es un error, entonces será mejor que salga yo mismo con un error". En los viejos tiempos, existía el temor real de que lanzar una excepción, detectarla y continuar con lo siguiente, haría que lo que está haciendo sea tan lento que resulte inútil. Entonces, en ese caso, "realmente excepcional" significa "la situación es tan mala que el desempeño horrible ya no importa". Ese ya no es el caso (aunque todavía se nota una excepción en un circuito cerrado) y con suerte indica por qué la definición de "realmente excepcional" debe ser flexible.


2
"En C ++, por ejemplo, debe pensar más para asegurarse de que sus funciones sean muy seguras para las excepciones, de lo que tendría que hacer si no fuera necesario". - Dado que las construcciones básicas de C ++ (como new) y la biblioteca estándar arrojan excepciones, no veo cómo no usar excepciones en su código lo libra de escribir código seguro para excepciones independientemente.
Pavel Minaev

1
Tendrías que preguntarle a Google. Pero sí, claro, si su función no es arrojar, entonces las personas que llaman tendrán que hacer algo de trabajo, y agregar condiciones adicionales que causen excepciones no lo convierte en "más excepciones". Pero la falta de memoria es un caso especial de todos modos. Es común tener programas que no lo manejan y simplemente se apagan. Podría tener un estándar de codificación que diga, "no use excepciones, no dé garantías de excepción, si hay nuevos lanzamientos, terminaremos y usaremos compiladores que no desenrollen la pila". No es un concepto alto, pero volaría.
Steve Jessop

Una vez que usa un compilador que no desenrolla la pila (o deshabilita las excepciones en él), técnicamente hablando, ya no es C ++ :)
Pavel Minaev

No desenrolla la pila en una excepción no detectada, quiero decir.
Steve Jessop

¿Cómo sabe que no está capturado a menos que desenrolle la pila para encontrar un bloque de captura?
jmucchiello

11

Realmente no hay consenso. Todo el asunto es algo subjetivo, porque la "conveniencia" de lanzar una excepción a menudo es sugerida por las prácticas existentes dentro de la biblioteca estándar del lenguaje mismo. La biblioteca estándar de C ++ arroja excepciones con mucha menos frecuencia que, por ejemplo, la biblioteca estándar de Java, que casi siempre prefiere las excepciones, incluso para los errores esperados, como una entrada de usuario no válida (por ejemplo Scanner.nextInt). Esto, creo, influye significativamente en las opiniones de los desarrolladores sobre cuándo es apropiado lanzar una excepción.

Como programador de C ++, personalmente prefiero reservar excepciones para circunstancias muy "excepcionales", por ejemplo, falta de memoria, falta de espacio en el disco, ocurrió el apocalipsis, etc. Pero no insisto en que esta sea la forma absolutamente correcta de hacerlo. cosas.


2
Creo que hay una especie de consenso, pero tal vez el consenso se base más en una convención que en un razonamiento sólido. Puede haber razones sólidas para usar excepciones solo en circunstancias excepcionales, pero la mayoría de los desarrolladores no son realmente conscientes de las razones.
Qwertie

4
De acuerdo, a menudo se oye hablar de "las excepciones son para circunstancias excepcionales", pero nadie se molesta en definir adecuadamente lo que es una "situación excepcional"; es en gran medida solo personalizado y definitivamente específico del idioma. Diablos, en Python, los iteradores usan una excepción para señalar el final de la secuencia, ¡y se considera perfectamente normal!
Pavel Minaev

2
+1. No hay reglas estrictas y rápidas, solo convenciones, pero es útil adherirse a las convenciones de otros que usan su lenguaje, ya que facilita que los programadores comprendan el código de los demás.
j_random_hacker

1
"el apocalipsis sucedió" - no importa las excepciones, si algo justifica UB, eso es ;-)
Steve Jessop

3
Catskul: Casi toda la programación es convencional. Técnicamente, ni siquiera necesitamos excepciones, o realmente muchas. Si no implica NP-completo, big-O / theta / little-o o Universal Turing Machines, probablemente sea una convención. :-)
Ken

7

No creo que las excepciones deban usarse raramente. Pero.

No todos los equipos y proyectos están listos para usar excepciones. El uso de excepciones requiere una alta calificación de los programadores, técnicas especiales y falta de un gran código heredado que no sea seguro para excepciones. Si tiene una base de código antigua enorme, entonces casi siempre no es seguro para excepciones. Estoy seguro de que no querrás reescribirlo.

Si va a utilizar las excepciones de forma extensiva, entonces:

  • Esté preparado para enseñarle a su gente qué es la seguridad de excepción
  • no debe utilizar la gestión de memoria sin formato
  • utilizar RAII ampliamente

Por otro lado, el uso de excepciones en proyectos nuevos con un equipo sólido puede hacer que el código sea más limpio, más fácil de mantener e incluso más rápido:

  • no te perderás ni ignorarás errores
  • no tiene que escribir las verificaciones de los códigos de retorno, sin saber realmente qué hacer con el código incorrecto en el nivel bajo
  • cuando se ve obligado a escribir código seguro para excepciones, se vuelve más estructurado

1
Me gustó especialmente tu mención del hecho de que "no todos los equipos ... están listos para usar excepciones". Las excepciones son definitivamente algo que parece fácil de hacer, pero es extremadamente difícil de hacer bien , y eso es parte de lo que las hace peligrosas.
j_random_hacker

1
+1 para "cuando se ve obligado a escribir código seguro para excepciones, se vuelve más estructurado". El código basado en excepciones se ve obligado a tener más estructura y, de hecho, me resulta mucho más fácil razonar sobre objetos e invariantes cuando es imposible ignorarlos. De hecho, creo que la seguridad de excepciones sólidas se trata de escribir código casi reversible, lo que hace que sea realmente fácil evitar un estado indeterminado.
Tom

7

EDITAR 20/11/2009 :

Estaba leyendo este artículo de MSDN sobre cómo mejorar el rendimiento del código administrado y esta parte me recordó esta pregunta:

El costo de rendimiento de lanzar una excepción es significativo. Aunque el manejo estructurado de excepciones es la forma recomendada de manejar las condiciones de error, asegúrese de usar las excepciones solo en circunstancias excepcionales cuando ocurren condiciones de error. No utilice excepciones para el flujo de control regular.

Por supuesto, esto es solo para .NET, y también está dirigido específicamente a aquellos que desarrollan aplicaciones de alto rendimiento (como yo); así que obviamente no es una verdad universal. Aún así, somos muchos los desarrolladores de .NET, así que sentí que valía la pena señalarlo.

EDITAR :

Bien, en primer lugar, aclaremos una cosa: no tengo ninguna intención de pelear con nadie por la cuestión del rendimiento. En general, de hecho, me inclino a estar de acuerdo con aquellos que creen que la optimización prematura es un pecado. Sin embargo, permítanme señalar dos puntos:

  1. El cartel pide una justificación objetiva detrás de la sabiduría convencional de que las excepciones deben usarse con moderación. Podemos discutir la legibilidad y el diseño adecuado todo lo que queramos; pero estos son asuntos subjetivos con personas dispuestas a discutir de ambos lados. Creo que el cartel lo sabe. El hecho es que usar excepciones para controlar el flujo del programa es a menudo una forma ineficiente de hacer las cosas. No, no siempre , pero a menudo . Es por eso que es un consejo razonable utilizar las excepciones con moderación, al igual que es un buen consejo comer carne roja o beber vino con moderación.

  2. Existe una diferencia entre optimizar sin una buena razón y escribir código eficiente. El corolario de esto es que hay una diferencia entre escribir algo que es robusto, si no optimizado, y algo que es simplemente ineficiente. A veces pienso que cuando las personas discuten sobre cosas como el manejo de excepciones, en realidad están hablando entre ellos, porque están discutiendo cosas fundamentalmente diferentes.

Para ilustrar mi punto, considere los siguientes ejemplos de código C #.

Ejemplo 1: detección de entrada de usuario no válida

Este es un ejemplo de lo que yo llamaría abuso de excepción .

int value = -1;
string input = GetInput();
bool inputChecksOut = false;

while (!inputChecksOut) {
    try {
        value = int.Parse(input);
        inputChecksOut = true;

    } catch (FormatException) {
        input = GetInput();
    }
}

Este código es, para mí, ridículo. Por supuesto que funciona . Nadie está discutiendo con eso. Pero debería ser algo como:

int value = -1;
string input = GetInput();

while (!int.TryParse(input, out value)) {
    input = GetInput();
}

Ejemplo 2: Comprobación de la existencia de un archivo

Creo que este escenario es muy común. Ciertamente, parece mucho más "aceptable" para mucha gente, ya que se trata de E / S de archivos:

string text = null;
string path = GetInput();
bool inputChecksOut = false;

while (!inputChecksOut) {
    try {
        using (FileStream fs = new FileStream(path, FileMode.Open)) {
            using (StreamReader sr = new StreamReader(fs)) {
                text = sr.ReadToEnd();
            }
        }

        inputChecksOut = true;

    } catch (FileNotFoundException) {
        path = GetInput();
    }
}

Esto parece bastante razonable, ¿verdad? Estamos intentando abrir un archivo; si no está allí, detectamos esa excepción e intentamos abrir un archivo diferente ... ¿Qué hay de malo en eso?

Nada en realidad. Pero considere esta alternativa, que no arroja ninguna excepción:

string text = null;
string path = GetInput();

while (!File.Exists(path)) path = GetInput();

using (FileStream fs = new FileStream(path, FileMode.Open)) {
    using (StreamReader sr = new StreamReader(fs)) {
        text = sr.ReadToEnd();
    }
}

Por supuesto, si el desempeño de estos dos enfoques fuera realmente el mismo, esto realmente sería una cuestión puramente doctrinal. Entonces, echemos un vistazo. Para el primer ejemplo de código, hice una lista de 10000 cadenas aleatorias, ninguna de las cuales representaba un número entero adecuado, y luego agregué una cadena entera válida al final. Usando los dos enfoques anteriores, estos fueron mis resultados:

Usando try/ catchblock: 25.455 segundos
Usando int.TryParse: 1.637 milisegundos

Para el segundo ejemplo, hice básicamente lo mismo: hice una lista de 10000 cadenas aleatorias, ninguna de las cuales era una ruta válida, luego agregué una ruta válida al final. Estos fueron los resultados:

Usando try/ catchblock: 29,989 segundos
Usando File.Exists: 22,820 milisegundos

Mucha gente respondería a esto diciendo: "Sí, bueno, lanzar y atrapar 10,000 excepciones es extremadamente poco realista; esto exagera los resultados". Claro que lo hace. La diferencia entre lanzar una excepción y manejar una entrada incorrecta por su cuenta no será notoria para el usuario. El hecho es que el uso de excepciones es, en estos dos casos, de 1,000 a más de 10,000 veces más lento que los enfoques alternativos que son igualmente legibles, si no más.

Por eso incluí el ejemplo del GetNine()método a continuación. No es que sea intolerablemente lento o inaceptablemente lento; es que es más lento de lo que debería ser ... sin una buena razón .

Nuevamente, estos son solo dos ejemplos. Por supuesto , habrá ocasiones en las que el impacto en el rendimiento del uso de excepciones no sea tan grave (Pavel tiene razón; después de todo, depende de la implementación). Todo lo que digo es: seamos realistas, muchachos. En casos como el anterior, lanzar y atrapar una excepción es análogo a GetNine(); es solo una forma ineficaz de hacer algo que fácilmente podría hacerse mejor .


Estás pidiendo una justificación como si esta fuera una de esas situaciones en las que todos se subieron al carro sin saber por qué. Pero, de hecho, la respuesta es obvia y creo que ya la sabe. El manejo de excepciones tiene un rendimiento horrendo.

De acuerdo, tal vez esté bien para su escenario comercial particular, pero en términos relativos , lanzar / capturar una excepción introduce mucha más sobrecarga de la necesaria en muchos, muchos casos. Lo sabes, yo lo sé: la mayoría de las veces , si estás usando excepciones para controlar el flujo del programa, solo estás escribiendo código lento.

También podría preguntar: ¿por qué este código es malo?

private int GetNine() {
    for (int i = 0; i < 10; i++) {
        if (i == 9) return i;
    }
}

Apuesto a que si perfilara esta función, encontraría que funciona bastante rápido para su aplicación comercial típica. Eso no cambia el hecho de que es una forma horriblemente ineficiente de lograr algo que podría hacerse mucho mejor.

Eso es lo que la gente quiere decir cuando habla de "abuso" de excepción.


2
"El manejo de excepciones tiene un rendimiento terrible". - es un detalle de implementación y no es válido para todos los lenguajes, e incluso para C ++ específicamente, para todas las implementaciones.
Pavel Minaev

"Es un detalle de implementación" No recuerdo con qué frecuencia escuché este argumento, y apesta. Recuerde: todo lo relacionado con la informática se trata de una suma de detalles de implementación. Y también: No es prudente usar un destornillador como reemplazo de un martillo; eso es lo que hace la gente, que habla mucho sobre "solo detalles de implementación".
Juergen

2
Y, sin embargo, Python felizmente usa excepciones para el control de flujo genérico, porque el impacto de rendimiento para su implementación no es tan malo. Entonces, si su martillo parece un destornillador, bueno, culpe al martillo ...
Pavel Minaev

1
@j_random_hacker: Tienes razón, GetNinehace bien su trabajo. Mi punto es que no hay razón para usarlo. No es como si fuera la forma "rápida y fácil" de hacer algo, mientras que el enfoque más "correcto" requeriría mucho más esfuerzo. En mi experiencia, a menudo sucede que el enfoque "correcto" en realidad requeriría menos esfuerzo, o casi lo mismo. De todos modos, para mí el rendimiento es la única razón objetiva por la que se desaconseja el uso de excepciones a la izquierda y a la derecha. En cuanto a si la pérdida de rendimiento está "muy sobreestimada", eso es de hecho subjetivo.
Dan Tao

1
@j_random_hacker: Su comentario es muy interesante y realmente lleva a casa el punto de Pavel sobre el impacto en el rendimiento dependiendo de la implementación. Una cosa de la que dudaría mucho es que alguien pueda encontrar un escenario en el que el uso de excepciones realmente supere a la alternativa (donde existe una alternativa, al menos, como en mis ejemplos).
Dan Tao

6

Todas las reglas generales sobre las excepciones se reducen a términos subjetivos. No debe esperar obtener definiciones estrictas y rápidas de cuándo usarlos y cuándo no. "Solo en circunstancias excepcionales". Buena definición circular: las excepciones son por circunstancias excepcionales.

Cuándo usar excepciones cae en el mismo grupo que "¿cómo sé si este código es una clase o dos?" Es en parte una cuestión de estilo, en parte una preferencia. Las excepciones son una herramienta. Se pueden usar y abusar de ellos, y encontrar la línea entre los dos es parte del arte y la habilidad de la programación.

Hay muchas opiniones y compensaciones por hacer. Encuentra algo que te hable y síguelo.


6

No es que las excepciones deban usarse raramente. Es solo que solo deben arrojarse en circunstancias excepcionales. Por ejemplo, si un usuario ingresa una contraseña incorrecta, eso no es excepcional.

La razón es simple: las excepciones salen de una función abruptamente y se propagan por la pila hasta un catchbloque. Este proceso es muy costoso desde el punto de vista computacional: C ++ construye su sistema de excepciones para tener poca sobrecarga en las llamadas a funciones "normales", por lo que cuando se genera una excepción, tiene que trabajar mucho para encontrar dónde ir. Además, dado que cada línea de código podría generar una excepción. Si tenemos alguna función fque genera excepciones con frecuencia, ahora debemos tener cuidado de usar nuestros bloques try/ catchen cada llamada de f. Ese es un acoplamiento interfaz / implementación bastante malo.


8
Básicamente, ha repetido la lógica de "se llaman excepciones por una razón". Estoy buscando gente para convertir esto en algo más sustancial.
Catskul

4
¿Qué significa "circunstancias excepcionales"? En la práctica, mi programa tiene que manejar las IOExceptions reales con más frecuencia que las contraseñas de usuario incorrectas. ¿ Eso significa que cree que debería hacer de BadUserPassword una excepción, o que los chicos de stdlib no deberían haber hecho de IOException una excepción? "Muy caro computacionalmente" no puede ser la razón real, porque nunca he visto un programa (y ciertamente no el mío) para el cual manejar una contraseña incorrecta, a través de cualquier mecanismo de control, fue un cuello de botella en el rendimiento.
Ken

5

Mencioné este problema en un artículo sobre excepciones de C ++ .

La parte relevante:

Casi siempre, usar excepciones para afectar el flujo "normal" es una mala idea. Como ya comentamos en la sección 3.1, las excepciones generan rutas de código invisibles. Se puede decir que estas rutas de código son aceptables si se ejecutan solo en los escenarios de manejo de errores. Sin embargo, si usamos excepciones para cualquier otro propósito, nuestra ejecución de código "normal" se divide en una parte visible e invisible y hace que el código sea muy difícil de leer, comprender y extender.


1
Verá, llegué a C ++ desde C, y creo que los "escenarios de manejo de errores" son normales. Puede que no se ejecuten con frecuencia, pero paso más tiempo pensando en ellos que en las rutas de código sin errores. Entonces, nuevamente, generalmente estoy de acuerdo con el sentimiento, pero no nos acerca a la definición de "normal", y cómo podemos deducir de la definición de "normal" que usar excepciones es una mala opción.
Steve Jessop

También llegué a C ++ desde C, pero incluso en CI hice una distinción entre el manejo de errores de fin de flujo "normal". Si llama a ie fopen (), hay una rama que se ocupa del caso de éxito y que es "normal", y otra que se ocupa de las posibles causas de falla, y es el manejo de errores.
Nemanja Trifunovic

2
Correcto, pero no me pongo misteriosamente inteligente al razonar sobre el código de error. Entonces, si las "rutas de código invisibles" son muy difíciles de leer, comprender y extender para el código "normal", entonces no veo por qué aplicar la palabra "error" las hace "posiblemente aceptables". Las situaciones de error ocurren todo el tiempo, en mi experiencia, y esto las hace "normales". Si puede averiguar las excepciones en situaciones de error, puede averiguarlas en situaciones sin errores. Si no puede, no puede, y todo lo que ha hecho es hacer que su código de error sea impenetrable pero ignorar ese hecho porque son "solo errores".
Steve Jessop

1
Ejemplo: tiene una función en la que es necesario hacer algo, pero va a probar una gran cantidad de enfoques diferentes y no sabe cuál tendrá éxito, y cada uno de ellos podría probar algunos subenfoques adicionales, y así sucesivamente, hasta que algo funcione. Entonces yo diría que el fracaso es "normal" y el éxito es "excepcional", aunque claramente no es un error. Lanzar una excepción en caso de éxito no es más difícil de entender que lanzar una excepción en un error horrible en la mayoría de los programas: te aseguras de que solo un fragmento de código necesita saber qué hacer a continuación y saltas directamente allí.
Steve Jessop

1
@onebyone: Su segundo comentario en realidad hace un buen punto que no he visto en otro lugar. Creo que la respuesta a eso es que (como dijiste efectivamente en tu propia respuesta) el uso de excepciones para condiciones de error se ha absorbido en las "prácticas estándar" de muchos idiomas, lo que lo convierte en una guía útil para cumplir incluso si las razones originales para hacer eso estaba equivocado.
j_random_hacker

5

Mi enfoque para el manejo de errores es que hay tres tipos fundamentales de errores:

  • Una situación extraña que se puede manejar en el sitio del error. Esto podría ocurrir si un usuario ingresa una entrada no válida en una línea de comandos. El comportamiento correcto es simplemente quejarse al usuario y hacer un bucle en este caso. Otra situación podría ser una división por cero. Estas situaciones no son realmente situaciones de error y generalmente son causadas por una entrada defectuosa.
  • Una situación como la anterior, pero que no se puede manejar en el sitio del error. Por ejemplo, si tiene una función que toma un nombre de archivo y analiza el archivo con ese nombre, es posible que no pueda abrir el archivo. En este caso, no puede lidiar con el error. Aquí es cuando brillan las excepciones. En lugar de utilizar el enfoque C (devolver un valor no válido como indicador y establecer una variable de error global para indicar el problema), el código puede generar una excepción. El código de llamada podrá entonces lidiar con la excepción, por ejemplo, para solicitar al usuario otro nombre de archivo.
  • Una situación que no debería suceder. Esto es cuando se viola un invariante de clase, o una función recibe un parámetro no válido o similar. Esto indica una falla lógica dentro del código. Dependiendo del nivel de falla, una excepción puede ser apropiada, o puede ser preferible forzar la terminación inmediata (como lo asserthace). Generalmente, estas situaciones indican que algo se ha roto en algún lugar del código y, de hecho, no puede confiar en que nada más sea correcto; puede haber una corrupción desenfrenada de la memoria. Tu barco se está hundiendo, bájate.

Parafraseando, las excepciones son para cuando tienes un problema con el que puedes lidiar, pero no puedes resolverlo en el lugar donde lo notas. Los problemas con los que no puede lidiar deberían simplemente matar el programa; los problemas que puede resolver de inmediato simplemente deben resolverse.


2
Estás respondiendo la pregunta incorrecta. No queremos saber por qué deberíamos (o no deberíamos) considerar el uso de excepciones para manejar escenarios de error; queremos saber por qué deberíamos (o no deberíamos) usarlas para escenarios sin manejo de errores.
j_random_hacker

5

Leí algunas de las respuestas aquí. Todavía me sorprende de qué se trata toda esta confusión. Estoy totalmente en desacuerdo con todas estas excepciones == código spagetty. Con confusión, quiero decir que hay gente que no aprecia el manejo de excepciones de C ++. No estoy seguro de cómo aprendí sobre el manejo de excepciones de C ++, pero entendí las implicaciones en minutos. Esto fue alrededor de 1996 y estaba usando el compilador de Borland C ++ para OS / 2. Nunca tuve problemas para decidir cuándo usar las excepciones. Por lo general, envuelvo acciones de deshacer y deshacer falibles en clases de C ++. Estas acciones de deshacer y deshacer incluyen:

  • crear / destruir un identificador de sistema (para archivos, mapas de memoria, identificadores de GUI de WIN32, sockets, etc.)
  • manipuladores de armado / desarmado
  • asignar / desasignar memoria
  • reclamar / liberar un mutex
  • incrementar / decrementar un recuento de referencia
  • mostrar / ocultar una ventana

Que hay envoltorios funcionales. Para envolver las llamadas del sistema (que no entran en la categoría anterior) en C ++. Por ejemplo, leer / escribir desde / a un archivo. Si algo falla, se lanzará una excepción, que contiene información completa sobre el error.

Luego están las excepciones de captura / relanzamiento para agregar más información a una falla.

El manejo general de excepciones de C ++ conduce a un código más limpio. La cantidad de código se reduce drásticamente. Finalmente, se puede usar un constructor para asignar recursos falibles y aún mantener un entorno libre de corrupción después de tal falla.

Uno puede encadenar tales clases en clases complejas. Una vez que se ejecuta un constructor de algún miembro / objeto base, se puede confiar en que todos los demás constructores del mismo objeto (ejecutados antes) se ejecutaron con éxito.


3

Las excepciones son un método de control de flujo muy inusual en comparación con las construcciones tradicionales (bucles, ifs, funciones, etc.) Las construcciones de flujo de control normales (bucles, ifs, llamadas a funciones, etc.) pueden manejar todas las situaciones normales. Si se encuentra buscando una excepción para una ocurrencia de rutina, entonces quizás deba considerar cómo está estructurado su código.

Pero hay ciertos tipos de errores que no se pueden manejar fácilmente con las construcciones normales. Las fallas catastróficas (como las fallas en la asignación de recursos) se pueden detectar en un nivel bajo, pero probablemente no se puedan manejar allí, por lo que una simple declaración if es inadecuada. Estos tipos de fallas generalmente deben manejarse a un nivel mucho más alto (por ejemplo, guardar el archivo, registrar el error, salir). Intentar informar un error como este a través de métodos tradicionales (como valores devueltos) es tedioso y propenso a errores. Además, inyecta gastos generales en capas de API de nivel medio para manejar esta falla extraña e inusual. La sobrecarga distrae al cliente de estas API y les obliga a preocuparse por problemas que están fuera de su control. Las excepciones proporcionan una forma de realizar un tratamiento no local para errores grandes que '

Si un cliente llama ParseIntcon una cadena y la cadena no contiene un número entero, entonces la persona que llama inmediatamente probablemente se preocupa por el error y sabe qué hacer al respecto. Entonces, diseñaría ParseInt para devolver un código de falla para algo así.

Por otro lado, si ParseIntfalla porque no pudo asignar un búfer porque la memoria está terriblemente fragmentada, la persona que llama no sabrá qué hacer al respecto. Tendría que hacer burbujear este error inusual hacia arriba y hasta alguna capa que se ocupe de estas fallas fundamentales. Eso grava a todos los que están en el medio (porque tienen que adaptarse al mecanismo de paso de errores en sus propias API). Una excepción permite omitir esas capas (sin dejar de garantizar que se lleve a cabo la limpieza necesaria).

Cuando escribe código de bajo nivel, puede ser difícil decidir cuándo usar métodos tradicionales y cuándo lanzar excepciones. El código de bajo nivel tiene que tomar la decisión (tirar o no). Pero es el código de nivel superior el que realmente sabe lo que se espera y lo que es excepcional.


1
Y, sin embargo, en Java, parseInten realidad no lanzar una excepción. Por lo tanto, diría que las opiniones sobre la conveniencia de lanzar excepciones dependen en gran medida de las prácticas preexistentes dentro de las bibliotecas estándar del lenguaje de desarrollo elegido.
Charles Salvia

El tiempo de ejecución de Java tiene que realizar un seguimiento de la información de todos modos, por lo que facilita la generación de excepciones ... el código c ++ tiende a no tener esa información en las manos, por lo que tiene una penalización de rendimiento para generarla. Al no tenerlo a mano, tiende a ser más rápido / más pequeño / más amigable con el caché, etc. Además, el código Java tiene controles dinámicos en cosas como matrices, etc., una característica que no es inherente a C ++, por lo que Java es una clase adicional de excepciones que el desarrollador es ya se encarga de muchas de estas cosas con bloques try / catch, así que ¿por qué no usar exceptons para TODO?
Ape-inago

1
@Charles: la pregunta está etiquetada con C ++, así que respondí desde esa perspectiva. Nunca escuché a un programador de Java (o C #) decir que las excepciones son solo para condiciones excepcionales. Los programadores de C ++ dicen esto con regularidad, y la pregunta era por qué.
Adrian McCarthy

1
En realidad, las excepciones de C # dicen eso también, y la razón es que las excepciones son muy caras de incluir en .NET (más que, por ejemplo, en Java).
Pavel Minaev

1
@Adrian: cierto, la pregunta está etiquetada con C ++, aunque la filosofía de manejo de excepciones es algo así como un diálogo entre idiomas, ya que los idiomas ciertamente se influyen entre sí. De todos modos, Boost.lexical_cast lanza una excepción. Pero, ¿una cadena no válida es realmente tan "excepcional"? Probablemente no lo sea, pero los desarrolladores de Boost pensaron que valía la pena usar excepciones para tener esa sintaxis de transmisión genial. Esto solo demuestra cuán subjetivo es todo el asunto.
Charles Salvia

3

Hay varias razones en C ++.

Primero, con frecuencia es difícil ver de dónde provienen las excepciones (ya que pueden lanzarse desde casi cualquier cosa) y, por lo tanto, el bloque de captura es algo así como una declaración COME FROM. Es peor que un IR A, ya que en un IR A sabes de dónde vienes (la declaración, no una llamada de función aleatoria) y hacia dónde vas (la etiqueta). Son básicamente una versión potencialmente segura para los recursos de setjmp () y longjmp () de C, y nadie quiere usarlos.

En segundo lugar, C ++ no tiene la recolección de basura incorporada, por lo que las clases de C ++ que poseen recursos se deshacen de ellos en sus destructores. Por lo tanto, en el manejo de excepciones de C ++, el sistema tiene que ejecutar todos los destructores dentro del alcance. En lenguajes con GC y sin constructores reales, como Java, lanzar excepciones es mucho menos oneroso.

En tercer lugar, la comunidad de C ++, incluidos Bjarne Stroustrup y el Comité de Estándares y varios redactores de compiladores, ha asumido que las excepciones deberían ser excepcionales. En general, no vale la pena ir en contra de la cultura del idioma. Las implementaciones se basan en el supuesto de que las excepciones serán raras. Los mejores libros tratan las excepciones como excepcionales. Un buen código fuente usa pocas excepciones. Los buenos desarrolladores de C ++ tratan las excepciones como excepcionales. Para ir en contra de eso, querrás una buena razón, y todas las razones que veo están del lado de mantenerlas excepcionales.


1
"C ++ no tiene una recolección de basura incorporada, por lo que las clases de C ++ que poseen recursos se deshacen de ellos en sus destructores. Por lo tanto, en el manejo de excepciones de C ++, el sistema tiene que ejecutar todos los destructores dentro del alcance. En lenguajes con GC y sin constructores reales , como Java, lanzar excepciones es mucho menos oneroso ". - Los destructores deben ejecutarse cuando se sale del ámbito normalmente, y los finallybloques de Java no son diferentes de los destructores de C ++ en términos de sobrecarga de implementación.
Pavel Minaev

Me gusta esta respuesta pero, como Pavel, no creo que el segundo punto sea legítimo. Cuando el alcance finaliza por cualquier motivo, incluidos otros tipos de manejo de errores o simplemente continuar con el programa, los destructores se llamarían de todos modos.
Catskul

+1 por mencionar la cultura del idioma como la razón para seguir la corriente. Esa respuesta es insatisfactoria para algunos, pero es una razón genuina (y creo que es la razón más precisa).
j_random_hacker

@Pavel: ignore mi comentario incorrecto anterior (ahora eliminado) que dice que finallyno se garantiza que los bloques de Java se ejecuten, por supuesto que sí. Me estaba confundiendo con el finalize()método Java , que no se garantiza que se ejecute.
j_random_hacker

Mirando hacia atrás en esta respuesta, el problema con los destructores es que todos deben llamarse en ese momento, en lugar de espaciarse potencialmente con cada retorno de función. Esa todavía no es una muy buena razón, pero creo que tiene un poco de validez.
David Thornley

2

Este es un mal ejemplo de uso de excepciones como flujo de control:

int getTotalIncome(int incomeType) {
   int totalIncome= 0;
   try {
      totalIncome= calculateIncomeAsTypeA();
   } catch (IncorrectIncomeTypeException& e) {
      totalIncome= calculateIncomeAsTypeB();
   }

   return totalIncome;
}

Lo cual es muy malo, pero deberías estar escribiendo:

int getTotalIncome(int incomeType) {
   int totalIncome= 0;
   if (incomeType == A) {
      totalIncome= calculateIncomeAsTypeA();
   } else if (incomeType == B) {
      totalIncome= calculateIncomeAsTypeB();
   }
   return totalIncome;
}

Este segundo ejemplo obviamente necesita algo de refactorización (como usar la estrategia del patrón de diseño), pero ilustra bien que las excepciones no están pensadas para controlar el flujo.

Las excepciones también tienen algunas penalizaciones de rendimiento asociadas, pero los problemas de rendimiento deben seguir la regla: "la optimización prematura es la raíz de todos los males"


Parece un caso en el que más polimorfismo sería bueno, en lugar de verificar incomeType.
Sarah Vessels

@Sarah sí, sé que sería bueno. Está aquí solo con fines ilustrativos.
Edison Gustavo Muenz

3
¿Porque es malo? ¿Por qué debería escribirlo de la segunda forma? El autor de la pregunta quiere saber las razones de las reglas, no las reglas. -1.
j_random_hacker

2
  1. Mantenibilidad: como lo mencionaron las personas anteriormente, arrojar excepciones en un abrir y cerrar de ojos es similar a usar gotos.
  2. Interoperabilidad: no puede conectar bibliotecas C ++ con módulos C / Python (al menos no fácilmente) si está utilizando excepciones.
  3. Degradación del rendimiento: RTTI se utiliza para encontrar realmente el tipo de excepción que impone una sobrecarga adicional. Por lo tanto, las excepciones no son adecuadas para manejar casos de uso que ocurren comúnmente (el usuario ingresó int en lugar de string, etc.).

2
1. Pero en algunos idiomas, las excepciones se lanzan en un abrir y cerrar de ojos. 3. A veces, el rendimiento no importa. (¿80% del tiempo?)
UncleBens

2
2. Los programas C ++ casi siempre usan excepciones, porque gran parte de la biblioteca estándar las usa. Las clases de contenedores lanzan. La clase de cuerda lanza. Las clases de corriente arrojan.
David Thornley

¿Qué operaciones de contenedor y cadena arrojan, a excepción de at()? Dicho esto, cualquiera newpuede lanzar, así que ...
Pavel Minaev

RTTI no es estrictamente necesario para Try / Throw / Catch hasta donde yo entiendo. La degradación del rendimiento siempre se menciona, y lo creo, pero nadie menciona una escala o enlaces a referencias.
Catskul

UncleBens: (1) es para mantenimiento, no para rendimiento. El uso excesivo de try / catch / throw reduce la legibilidad del código. Regla de oro (y podría ser criticado por esto, pero es solo mi opinión), cuanto menos los puntos de entrada y salida, más fácil es leer el código. David Thornley: No cuando se necesita interoperabilidad. -fno-exceptions flag en gcc deshabilita las excepciones cuando está compilando una biblioteca que se llama desde el código C.
Sridhar Iyer

2

Yo diría que las excepciones son un mecanismo para sacarlo del contexto actual (fuera del marco de pila actual en el sentido más simple, pero es más que eso) de una manera segura. Es lo más parecido que tiene la programación estructurada a un goto. Para usar las excepciones en la forma en que fueron diseñadas, debe tener una situación en la que no pueda continuar con lo que está haciendo ahora y no pueda manejarlo en el punto en el que se encuentra ahora. Entonces, por ejemplo, cuando la contraseña del usuario es incorrecta, puede continuar devolviendo false. Pero si el subsistema de la interfaz de usuario informa que ni siquiera puede avisar al usuario, simplemente devolver "error de inicio de sesión" sería incorrecto. El nivel actual de código simplemente no sabe qué hacer. Por lo tanto, utiliza un mecanismo de excepción para delegar la responsabilidad a alguien superior que pueda saber qué hacer.


2

Una razón muy práctica es que, al depurar un programa, a menudo selecciono Excepciones a la primera oportunidad (Depurar -> Excepciones) para depurar una aplicación. Si ocurren muchas excepciones, es muy difícil encontrar dónde algo salió "mal".

Además, conduce a algunos anti-patrones como el infame "tiro de captura" y ofusca los problemas reales. Para obtener más información al respecto, consulte una publicación de blog que hice sobre el tema.


2

Prefiero utilizar las excepciones lo menos posible. Las excepciones obligan al desarrollador a manejar alguna condición que puede ser o no un error real. La definición de si la excepción en cuestión es un problema fatal o un problema que debe manejarse de inmediato.

El argumento contrario a eso es que solo requiere que las personas perezosas escriban más para dispararse en sus pies.

La política de codificación de Google dice que nunca use excepciones , especialmente en C ++. Su aplicación no está preparada para manejar excepciones o lo está. Si no es así, entonces la excepción probablemente lo propagará hasta que su aplicación muera.

Nunca es divertido descubrir que alguna biblioteca ha usado arroja excepciones y no estaba preparado para manejarlas.


1

Caso legítimo para lanzar una excepción:

  • Intenta abrir un archivo, no está allí, se lanza una FileNotFoundException;

Caso ilegítimo:

  • Desea hacer algo solo si un archivo no existe, intenta abrir el archivo y luego agrega algo de código al bloque de captura.

Utilizo excepciones cuando quiero interrumpir el flujo de la aplicación hasta cierto punto . Este punto es donde está el truco (...) para esa excepción. Por ejemplo, es muy común que tengamos que procesar una gran cantidad de proyectos, y cada proyecto debe procesarse independientemente de los demás. Entonces, el ciclo que procesa los proyectos tiene un bloque try ... catch, y si se lanza alguna excepción durante el procesamiento del proyecto, todo se revierte para ese proyecto, se registra el error y se procesa el siguiente proyecto. La vida continua.

Creo que debería usar excepciones para cosas como un archivo que no existe, una expresión que no es válida y cosas similares. No debe usar excepciones para pruebas de rango / pruebas de tipo de datos / existencia de archivos / cualquier otra cosa si hay una alternativa fácil / barata. No debe usar excepciones para pruebas de rango / pruebas de tipo de datos / existencia de archivos / cualquier otra cosa si hay una alternativa fácil / barata porque este tipo de lógica hace que el código sea difícil de entender:

RecordIterator<MyObject> ri = createRecordIterator();
try {
   MyObject myobject = ri.next();
} catch(NoSuchElement exception) {
   // Object doesn't exist, will create it
}

Esto sería mejor:

RecordIterator<MyObject> ri = createRecordIterator();
if (ri.hasNext()) {
   // It exists! 
   MyObject myobject = ri.next();
} else {
   // Object doesn't exist, will create it
}

COMENTARIO AÑADIDO A LA RESPUESTA:

Tal vez mi ejemplo no fue muy bueno: ri.next () no debería generar una excepción en el segundo ejemplo, y si lo hace, hay algo realmente excepcional y alguna otra acción debería tomarse en otro lugar. Cuando el ejemplo 1 se usa mucho, los desarrolladores detectarán una excepción genérica en lugar de la específica y asumirán que la excepción se debe al error que esperan, pero puede deberse a otra cosa. Al final, esto lleva a que se ignoren las excepciones reales, ya que las excepciones se convierten en parte del flujo de la aplicación y no en una excepción.

Los comentarios sobre esto pueden agregar más que mi propia respuesta.


1
Por qué no? ¿Por qué no utilizar excepciones para el segundo caso que menciona? El autor de la pregunta quiere saber las razones de las reglas, no las reglas.
j_random_hacker

Gracias por la elaboración, pero en mi humilde opinión, sus 2 fragmentos de código tienen una complejidad casi idéntica; ambos usan una lógica de control altamente localizada. Donde la complejidad de las excepciones supera claramente a la de ella / entonces / si no es cuando tiene un montón de declaraciones dentro del bloque try, cualquiera de las cuales podría arrojar, ¿está de acuerdo?
j_random_hacker

Tal vez mi ejemplo no fue muy bueno: ri.next () no debería generar una excepción en el segundo ejemplo, y si lo hace, hay algo realmente excepcional y alguna otra acción debería tomarse en otro lugar. Cuando el ejemplo 1 se usa mucho, los desarrolladores detectarán una excepción genérica en lugar de la específica y asumirán que la excepción se debe al error que esperan, pero puede deberse a otra cosa. Al final, esto lleva a que se ignoren las excepciones reales, ya que las excepciones se convirtieron en parte del flujo de la aplicación y no en una excepción.
Ravi Wallau

Entonces estás diciendo: Con el tiempo, otras declaraciones pueden acumularse dentro del trybloque y luego ya no estás seguro de que el catchbloque realmente esté capturando lo que creías que estaba atrapando, ¿es así? Si bien es más difícil hacer un mal uso del enfoque if / then / else de la misma manera porque solo puede probar 1 cosa a la vez en lugar de un conjunto de cosas a la vez, por lo que el enfoque excepcional puede conducir a un código más frágil. Si es así, discuta esto en su respuesta y felizmente haré +1, ya que creo que la fragilidad del código es una razón genuina.
j_random_hacker

0

Básicamente, las excepciones son una forma de control de flujo no estructurada y difícil de entender. Esto es necesario cuando se trata de condiciones de error que no forman parte del flujo normal del programa, para evitar que la lógica de manejo de errores desordene demasiado el control de flujo normal de su código.

En mi humilde opinión, las excepciones deben usarse cuando desee proporcionar un valor predeterminado sano en caso de que la persona que llama se olvide de escribir el código de manejo de errores, o si el error podría manejarse mejor en la pila de llamadas que la persona que llama inmediatamente. Lo mejor por defecto es salir del programa con un mensaje de error de diagnóstico razonable. La loca alternativa es que el programa cojea en un estado erróneo y se bloquea o produce silenciosamente una mala salida en algún momento posterior, más difícil de diagnosticar. Si el "error" es una parte normal del flujo del programa suficiente para que la persona que llama no pueda olvidar razonablemente comprobarlo, no se deben utilizar excepciones.


0

Creo que "usarlo raramente" no es la oración correcta. Preferiría "lanzar solo en situaciones excepcionales".

Muchos han explicado por qué las excepciones no deben usarse en situaciones normales. Las excepciones tienen derecho a la gestión de errores y únicamente a la gestión de errores.

Me centraré en otro punto:

Otra cosa es la cuestión del rendimiento. Los compiladores se esforzaron mucho para conseguirlos rápidamente. No estoy seguro de cuál es el estado exacto ahora, pero cuando usa excepciones para controlar el flujo, tendrá otro problema: ¡su programa se volverá lento!

La razón es que las excepciones no son solo sentencias goto muy poderosas, sino que también tienen que desenrollar la pila para todos los marcos que dejan. Por lo tanto, implícitamente también los objetos en la pila tienen que ser deconstruidos y así sucesivamente. Entonces, sin ser consciente de ello, un solo lanzamiento de una excepción realmente involucrará a un montón de mecánicas. El procesador tendrá que hacer mucho.

Así que terminarás quemando elegantemente tu procesador sin saberlo.

Entonces: use excepciones solo en casos excepcionales - Significado: ¡Cuando ocurrieron errores reales!


1
"La razón es que las excepciones no son sólo sentencias goto muy poderosas, sino que también tienen que desenrollar la pila para todos los marcos que dejan. Por lo tanto, implícitamente también los objetos de la pila tienen que ser deconstruidos y así sucesivamente". - si está abajo varios marcos de pila en la pila de llamadas, entonces todos esos marcos de pila (y objetos en ellos) tendrán que ser destruidos eventualmente de todos modos, no importa si eso sucede porque regresa normalmente o porque lanza un excepción. Al final, sin llamar std::terminate(), todavía tienes que destruir.
Pavel Minaev

Por supuesto que tienes razón. Pero aún así recuerde: cada vez que use excepciones, el sistema debe proporcionar la infraestructura para hacer todo esto mágicamente. Solo quería dejar un poco claro, que no es solo un goto. También al desenrollarse, el sistema tiene que encontrar el lugar de captura adecuado, lo que costará tiempo extra, código y el uso de RTTI. Así que es mucho más que un salto; la mayoría de la gente simplemente no sabe comprender esto.
Juergen

Siempre que se ciña a C ++ compatible con los estándares, RTTI es inevitable. De lo contrario, por supuesto, hay gastos generales. Es solo que a menudo lo veo significativamente exagerado.
Pavel Minaev

1
-1. "Las excepciones tienen su derecho para el manejo de errores y simplemente para el manejo de errores", ¿quién dice? ¿Por qué? El rendimiento no puede ser la única razón. (A) La mayor parte del código del mundo no se encuentra en el bucle más interno de un motor de renderizado de juegos o una función de multiplicación de matrices, por lo que el uso de excepciones no tendrá una diferencia de rendimiento perceptible. (B) Como señaló uno por uno en un comentario en alguna parte, todo el desenrollado de la pila debería tener lugar de todos modos, incluso si el antiguo estilo C comprueba los valores de retorno de error y devuelve el error si es necesario. se utilizó un enfoque de manipulación.
j_random_hacker

Digo yo. En este hilo hay muchas razones citadas. Solo léelos. Quería describir solo uno. Si no es el que necesitabas, no me culpes.
Juergen

0

El propósito de las excepciones es hacer que el software sea tolerante a fallas. Sin embargo, tener que proporcionar una respuesta a cada excepción lanzada por una función conduce a la supresión. Las excepciones son solo una estructura formal que obliga a los programadores a reconocer que ciertas cosas pueden salir mal con una rutina y que el programador del cliente debe estar consciente de estas condiciones y atenderlas según sea necesario.

Para ser honesto, las excepciones son un lujo agregado a los lenguajes de programación para proporcionar a los desarrolladores algún requisito formal que transfiera la responsabilidad de manejar los casos de error del desarrollador inmediato a algún desarrollador futuro.

Creo que un buen lenguaje de programación no admite excepciones como las conocemos en C ++ y Java. Debe optar por lenguajes de programación que puedan proporcionar un flujo alternativo para todo tipo de valores de retorno de funciones. El programador debería ser responsable de anticipar todas las formas de salidas de una rutina y manejarlas en un archivo de código separado si pudiera hacer lo que quisiera.


0

Uso excepciones si:

  • Ocurrió un error que no se puede recuperar de forma local Y
  • si el error no se recupera, el programa debería terminar.

Si el error se puede recuperar de (el usuario ingresó "manzana" en lugar de un número), recupere (solicite la entrada nuevamente, cambie al valor predeterminado, etc.).

Si el error no se puede recuperar de forma local pero la aplicación puede continuar (el usuario intentó abrir un archivo pero el archivo no existe), entonces un código de error es apropiado.

Si el error no se puede recuperar de forma local y la aplicación no puede continuar sin recuperarse (no tiene memoria / espacio en disco / etc.), entonces una excepción es el camino correcto a seguir.


1
-1. Lea atentamente la pregunta. La mayoría de los programadores piensan que las excepciones son apropiadas para ciertos tipos de manejo de errores o que nunca son apropiadas; ni siquiera consideran la posibilidad de usarlas para otras formas más exóticas de control de flujo. La pregunta es: ¿por qué?
j_random_hacker

También debes leerlo con atención. Respondí "¿Cuál es la filosofía detrás de ser excepcionalmente conservador con la forma en que se usan?" con mi filosofía detrás de ser conservador con la forma en que se usan.
Proyecto de ley

En mi humilde opinión, no ha explicado por qué es necesario el conservadurismo. ¿Por qué solo son "apropiados" a veces? ¿Por qué no todo el tiempo? (Por cierto, creo que su enfoque sugerido está bien, es más o menos lo que yo mismo hago, simplemente no creo que
entienda

El OP hizo siete preguntas distintas. Elegí responder solo una. Lamento que crea que vale la pena votar en contra.
Factura

0

¿Quién dijo que deberían usarse de forma conservadora? Nunca use excepciones para el control de flujo y eso es todo. ¿Y a quién le importa el costo de la excepción cuando ya se lanzó?


0

Mis dos centavos:

Me gusta usar excepciones, porque me permite programar como si no hubiera errores. Entonces mi código sigue siendo legible, no disperso con todo tipo de manejo de errores. Por supuesto, el manejo de errores (manejo de excepciones) se mueve al final (el bloque de captura) o se considera responsabilidad del nivel de llamada.

Un gran ejemplo para mí es el manejo de archivos o el manejo de bases de datos. Suponga que todo está bien y cierre su archivo al final o si ocurre alguna excepción. O deshaga su transacción cuando ocurrió una excepción.

El problema con las excepciones es que rápidamente se vuelve muy detallado. Si bien estaba destinado a permitir que su código permaneciera muy legible y solo se enfocara en el flujo normal de las cosas, pero si se usa de manera consistente, casi todas las llamadas a funciones deben estar envueltas en un bloque try / catch, y comienza a frustrar el propósito.

Para un ParseInt como se mencionó anteriormente, me gusta la idea de excepciones. Solo devuelve el valor. Si el parámetro no se puede analizar, lanza una excepción. Hace que su código sea más limpio por un lado. En el nivel de la llamada, debes hacer algo como

try 
{
   b = ParseInt(some_read_string);
} 
catch (ParseIntException &e)
{
   // use some default value instead
   b = 0;
}

El código está limpio. Cuando obtengo ParseInt como este esparcido por todas partes, hago funciones de contenedor que manejan las excepciones y me devuelven los valores predeterminados. P.ej

int ParseIntWithDefault(String stringToConvert, int default_value=0)
{
   int result = default_value;
   try
   {
     result = ParseInt(stringToConvert);
   }
   catch (ParseIntException &e) {}

   return result;
}

Entonces, para concluir: lo que me perdí durante la discusión fue el hecho de que las excepciones me permiten hacer mi código más fácil / más legible porque puedo ignorar más las condiciones de error. Problemas:

  • las excepciones aún deben manejarse en alguna parte. Problema adicional: c ++ no tiene la sintaxis que le permita especificar qué excepciones puede generar una función (como lo hace Java). Por lo tanto, el nivel de llamada no sabe qué excepciones deben manejarse.
  • a veces, el código puede volverse muy detallado, si cada función necesita estar envuelta en un bloque try / catch. Pero a veces esto todavía tiene sentido.

Eso hace que a veces sea difícil encontrar un buen equilibrio.


-1

Lo siento, pero la respuesta es "se llaman excepciones por una razón". Esa explicación es una "regla general". No puede proporcionar un conjunto completo de circunstancias bajo las cuales las excepciones deben o no deben usarse porque lo que es una excepción fatal (definición en inglés) para un dominio de problemas es un procedimiento operativo normal para un dominio de problemas diferente. Las reglas generales no están diseñadas para seguirse ciegamente. En cambio, están diseñados para guiar su investigación de una solución. "Se llaman excepciones por una razón" le indica que debe determinar con anticipación cuál es un error normal que la persona que llama puede manejar y cuál es una circunstancia inusual que la persona que llama no puede manejar sin una codificación especial (bloques de captura).

Casi todas las reglas de programación son en realidad una guía que dice "No hagas esto a menos que tengas una buena razón": "Nunca uses goto", "Evita las variables globales", "Las expresiones regulares incrementan previamente el número de problemas en uno. ", etc. Las excepciones no son una excepción ...


... y al que pregunta le gustaría saber por qué es una regla general, en lugar de escuchar (una vez más) que es una regla general. -1.
j_random_hacker

Lo reconocí en mi respuesta. No hay un por qué explícito. Las reglas generales son vagas por definición. Si hubiera un por qué explícito, sería una regla y no una regla de oro. Cada explicación en todas las demás respuestas anteriores contiene advertencias, por lo que tampoco explican por qué.
jmucchiello

Puede que no haya un "por qué" definitivo, pero hay "por qué" parciales que otros mencionan, por ejemplo, "porque eso es lo que todos los demás están haciendo" (en mi humilde opinión, una razón triste pero real) o "rendimiento" (en mi humilde opinión, esta razón suele ser exagerada , pero es una razón, no obstante). Lo mismo ocurre con las otras reglas empíricas como evitar goto (por lo general, complica el análisis de flujo de control más que un bucle equivalente) y evitar las variables globales (potencialmente introducen mucho acoplamiento, dificultando los cambios de código posteriores, y generalmente lo mismo los objetivos se pueden lograr con menos acoplamiento de otras formas).
j_random_hacker

Y todos esos por qué tienen largas listas de advertencias, que fue mi respuesta. No hay un por qué real más allá de una amplia experiencia en programación. Hay algunas reglas generales que van más allá del por qué. La misma regla general puede hacer que los "expertos" no estén de acuerdo sobre por qué. Usted mismo se toma la "actuación" con un grano de sal. Ese sería mi primer lugar en la lista. El análisis de control de flujo ni siquiera se registra conmigo porque (como "no goto") encuentro que los problemas de control de flujo son exagerados. También señalaré que mi respuesta intenta explicar CÓMO usa la respuesta "trillada".
jmucchiello

Estoy de acuerdo con usted en cuanto a que todas las reglas generales tienen una larga lista de advertencias. En lo que no estoy de acuerdo es en que creo que vale la pena intentar identificar las razones originales de la regla, así como las advertencias específicas. (Me refiero a un mundo ideal, donde tenemos tiempo infinito para reflexionar sobre estas cosas y sin fechas límite, por supuesto;)) Creo que eso es lo que pedía el OP.
j_random_hacker
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.