Manejo de excepciones
En computación y programación de computadoras, el manejo de excepciones es el proceso de responder a la ocurrencia de excepciones (condiciones anómalas o excepcionales que requieren un procesamiento especial) durante la ejecución de un programa.. En general, una excepción interrumpe el flujo normal de ejecución y ejecuta un controlador de excepciones registrado previamente; los detalles de cómo se hace esto dependen de si se trata de una excepción de hardware o de software y de cómo se implementa la excepción de software. El manejo de excepciones, si se proporciona, es facilitado por construcciones de lenguaje de programación especializadas, mecanismos de hardware como interrupciones o instalaciones de comunicación entre procesos (IPC) del sistema operativo (SO) como señales. Algunas excepciones, especialmente las de hardware, pueden manejarse con tanta gracia que la ejecución puede reanudarse donde se interrumpió.
Definición
La definición de una excepción se basa en la observación de que cada procedimiento tiene una condición previa, un conjunto de circunstancias por las cuales terminará "normalmente". Un mecanismo de manejo de excepciones permite que el procedimiento provoque una excepción si se viola esta condición previa, por ejemplo, si se ha llamado al procedimiento en un conjunto anormal de argumentos. El mecanismo de manejo de excepciones luego maneja la excepción. La condición previa, y la definición de excepción, es subjetiva. El conjunto de "normal" las circunstancias son definidas completamente por el programador, p. el programador puede considerar que la división por cero no está definida, por lo tanto, una excepción, o idear algún comportamiento como devolver cero o un "ZERO DIVIDE" valor (eludiendo la necesidad de excepciones). Las excepciones comunes incluyen un argumento no válido (p. ej., el valor está fuera del dominio de una función), un recurso no disponible (como un archivo faltante, un error de disco duro o errores de falta de memoria) o que la rutina ha detectado un error normal. condición que requiere un manejo especial, por ejemplo, atención, fin de archivo.
El manejo de excepciones resuelve el problema de los semipredicados, ya que el mecanismo distingue los valores devueltos normales de los erróneos. En lenguajes sin manejo de excepciones incorporado como C, las rutinas necesitarían señalar el error de alguna otra manera, como el código de retorno común y el patrón errno. Desde un punto de vista amplio, los errores pueden considerarse un subconjunto adecuado de excepciones, y los mecanismos de error explícitos, como errno, pueden considerarse formas (detalladas) de manejo de excepciones. El término "excepción" se prefiere a "error" porque no implica que algo esté mal: una condición vista como un error por un procedimiento o programador puede no ser vista de esa manera por otro. Incluso el término "excepción" puede ser engañoso porque su connotación típica de "atípico" indica que ha ocurrido algo poco frecuente o inusual, cuando en realidad generar la excepción puede ser una situación normal y habitual en el programa. Por ejemplo, suponga que una función de búsqueda para una matriz asociativa genera una excepción si la clave no tiene ningún valor asociado. Dependiendo del contexto, esta "clave ausente" la excepción puede ocurrir mucho más a menudo que una búsqueda exitosa.
Una influencia importante en el alcance y el uso de las excepciones es la presión social, es decir, "ejemplos de uso, que normalmente se encuentran en bibliotecas principales y ejemplos de código en libros técnicos, artículos de revistas y foros de discusión en línea, y en un estándares de código de la organización".
Historia
El primer manejo de excepciones de hardware se encontró en UNIVAC I de 1951. El desbordamiento aritmético ejecutó dos instrucciones en la dirección 0, lo que podría transferir el control o arreglar el resultado.
El manejo de excepciones de software se desarrolló en las décadas de 1960 y 1970. LISP 1.5 (1958-1961) permitió que la pseudofunción ERROR
generara excepciones, de manera similar a los errores generados por el intérprete o el compilador. Las excepciones fueron detectadas por la palabra clave ERRORSET
, que devolvía NIL
en caso de error, en lugar de terminar el programa o ingresar al depurador.
PL/I introdujo su propia forma de manejo de excepciones alrededor de 1964, lo que permitió que las interrupciones se manejaran con unidades ON.
MacLisp observó que ERRSET
y ERR
se usaban no solo para generar errores, sino también para el flujo de control no local y, por lo tanto, agregó dos nuevas palabras clave, CATCH
y THROW
(junio de 1972). El comportamiento de limpieza ahora generalmente se llama "finally" se introdujo en NIL (Nueva Implementación de LISP) a mediados o finales de la década de 1970 como UNWIND-PROTECT
. Esto fue luego adoptado por Common Lisp. Contemporáneo de esto fue dynamic-wind
en Scheme, que manejaba las excepciones en los cierres. Los primeros artículos sobre el manejo estructurado de excepciones fueron Goodenough (1975a) y Goodenough (1975b). Posteriormente, el manejo de excepciones fue ampliamente adoptado por muchos lenguajes de programación desde la década de 1980 en adelante.
Excepciones de hardware
No existe un consenso claro sobre el significado exacto de una excepción con respecto al hardware. Desde el punto de vista de la implementación, se maneja de manera idéntica a una interrupción: el procesador detiene la ejecución del programa actual, busca el controlador de interrupción en la tabla de vectores de interrupción para esa excepción o condición de interrupción, guarda el estado y cambia el control.
Excepciones de punto flotante IEEE 754
El manejo de excepciones en el estándar de punto flotante IEEE 754 se refiere en general a condiciones excepcionales y define una excepción como "un evento que ocurre cuando una operación en algunos operandos particulares no tiene un resultado adecuado para cada aplicación razonable. Esa operación podría señalar una o más excepciones al invocar el manejo predeterminado o, si se solicita explícitamente, un manejo alternativo definido por el idioma."
De forma predeterminada, una excepción IEEE 754 es reanudable y se maneja sustituyendo un valor predefinido por diferentes excepciones, p. infinito para una excepción de división por cero, y proporciona indicadores de estado para verificar más tarde si se produjo la excepción (consulte el lenguaje de programación C99 para ver un ejemplo típico de manejo de excepciones IEEE 754). Un estilo de manejo de excepciones habilitado por el uso de indicadores de estado involucra: primero computar una expresión usando una implementación rápida y directa; verificar si falló probando indicadores de estado; y luego, si es necesario, llamando a una implementación más lenta y numéricamente más robusta.
El estándar IEEE 754 utiliza el término "trapping" para referirse a la llamada de una rutina de manejo de excepciones proporcionada por el usuario en condiciones excepcionales, y es una característica opcional del estándar. El estándar recomienda varios escenarios de uso para esto, incluida la implementación de la sustitución previa no predeterminada de un valor seguida de la reanudación, para manejar de manera concisa las singularidades removibles.
El comportamiento predeterminado de manejo de excepciones IEEE 754 de reanudación después de la sustitución previa de un valor predeterminado evita los riesgos inherentes al cambio del flujo de control del programa en excepciones numéricas. Por ejemplo, el lanzamiento de la nave espacial Cluster de 1996 terminó en una explosión catastrófica debido en parte a la política de manejo de excepciones de Ada de cancelar el cálculo en caso de error aritmético. William Kahan afirma que el comportamiento de manejo de excepciones predeterminado de IEEE 754 habría evitado esto.
Soporte de excepciones en lenguajes de programación
El manejo de excepciones de software y el soporte proporcionado por las herramientas de software difiere un poco de lo que se entiende por manejo de excepciones en hardware, pero involucra conceptos similares. En los mecanismos del lenguaje de programación para el manejo de excepciones, el término excepción se usa típicamente en un sentido específico para denotar una estructura de datos que almacena información sobre una condición excepcional. Un mecanismo para transferir el control, o generar una excepción, se conoce como lanzamiento. Se dice que la excepción es lanzada. La ejecución se transfiere a un "captura".
Los lenguajes de programación difieren sustancialmente en su noción de lo que es una excepción. Las lenguas contemporáneas se pueden dividir aproximadamente en dos grupos:
- Idiomas donde las excepciones están diseñadas para ser utilizadas como estructuras de control de flujo: Ada, Modula-3, ML, OCaml, PL/I, Python y Ruby caen en esta categoría. Por ejemplo, los iteradores de Python lanzan excepciones de StopIteration para indicar que no hay más elementos producidos por el iterador.
- Idiomas donde las excepciones sólo se utilizan para manejar situaciones anormales, impredecibles y erróneas: C++, Java, C#, Common Lisp, Eiffel y Modula-2.
PL/Usé excepciones de ámbito dinámico. El manejo de excepciones PL/I incluyó eventos que no son errores, por ejemplo, atención, fin de archivo, modificación de variables enumeradas.
Sintaxis
Muchos lenguajes informáticos tienen soporte sintáctico incorporado para excepciones y manejo de excepciones. Esto incluye ActionScript, Ada, BlitzMax, C++, C#, Clojure, COBOL, D, ECMAScript, Eiffel, Java, ML, Object Pascal (por ejemplo, Delphi, Free Pascal y similares), PowerBuilder, Objective-C, OCaml, PHP (a partir de la versión 5), PL/I, PL/SQL, Prolog, Python, REALbasic, Ruby, Scala, Seed7, Smalltalk, Tcl, Visual Prolog y la mayoría de los lenguajes.NET.
Excluyendo diferencias sintácticas menores, solo hay un par de estilos de manejo de excepciones en uso. En el estilo más popular, una excepción se inicia con una declaración especial (throw
o raise
) con un objeto de excepción (por ejemplo, con Java u Object Pascal) o un valor de un tipo enumerado extensible especial (por ejemplo, con Ada o SML). El alcance de los controladores de excepciones comienza con una cláusula de marcador (try
o el iniciador de bloque del idioma, como begin
) y finaliza al comienzo de la primera cláusula del controlador (atrapar
, excepto
, rescatar
). Pueden seguir varias cláusulas de controlador, y cada una puede especificar qué tipos de excepción maneja y qué nombre usa para el objeto de excepción. Como variación menor, algunos lenguajes usan una cláusula de controlador único, que trata internamente con la clase de la excepción.
También es común una cláusula relacionada (finally
o ensure
) que se ejecuta ya sea que ocurra una excepción o no, generalmente para liberar los recursos adquiridos dentro del cuerpo de la excepción. bloque de manipulación. En particular, C ++ no proporciona esta construcción, y en su lugar recomienda la técnica de adquisición de recursos es inicialización (RAII) que libera recursos utilizando destructores. Según un artículo de 2008 de Westley Weimer y George Necula, la sintaxis de los bloques try
...finally
en Java es un factor que contribuye a los defectos del software. Cuando un método necesita manejar la adquisición y liberación de 3 a 5 recursos, los programadores aparentemente no están dispuestos a anidar suficientes bloques debido a problemas de legibilidad, incluso cuando esta sería una solución correcta. Es posible usar un solo bloque try
...finally
incluso cuando se trata de múltiples recursos, pero eso requiere un uso correcto de los valores centinela, que es otra fuente común de errores para este tipo de problema.
Python y Ruby también permiten una cláusula (else
) que se utiliza en caso de que no se produzca ninguna excepción antes de que se alcance el final del alcance del controlador.
En su totalidad, el código de manejo de excepciones podría verse así (en pseudocódigo similar a Java):
Prueba {} línea = consola.readLine(); si ()línea.longitud() == 0) {} tiro nuevo EmptyLineException()"¡La línea leída de la consola estaba vacía!"); } consola.printLine()"¡Hola %s!" % línea);}captura ()EmptyLineException e) {} consola.printLine()"¡Hola!");}captura ()Excepción e) {} consola.printLine()"Error: " + e.Mensaje());}más {} consola.printLine()"El programa funcionó con éxito.");}finalmente {} consola.printLine()"El programa está terminando".);}
C no tiene manejo de excepciones try-catch, pero usa códigos de retorno para verificar errores. Las funciones de biblioteca estándar setjmp y longjmp se pueden usar para implementar el manejo de intentos y capturas a través de macros.
Perl 5 usa die
para throw
y eval {} if ($@) {}
para try-catch. Tiene módulos CPAN que ofrecen semántica de prueba y captura.
Semántica de finalización y reanudación
Cuando se lanza una excepción, el programa busca en la pila de llamadas de funciones hasta que encuentra un controlador de excepciones. Algunos idiomas exigen que se desenrolle la pila a medida que avanza esta búsqueda. Es decir, si la función f contiene un controlador H para la excepción E, llama a la función g, que a su vez llama a la función h, y una excepción E span> ocurre en h, entonces las funciones h y g pueden terminar y H en f manejarán E. Se dice que esto es semántica de terminación. Alternativamente, los mecanismos de manejo de excepciones pueden no desenrollar la pila al ingresar a un manejador de excepciones, dando al manejador de excepciones la opción de reiniciar el cálculo, reanudar o desenrollar. Esto permite que el programa continúe el cálculo exactamente en el mismo lugar donde ocurrió el error (por ejemplo, cuando un archivo que faltaba anteriormente está disponible) o implementar notificaciones, registros, consultas y variables fluidas además del mecanismo de manejo de excepciones (como se hizo en Smalltalk). Permitir que el cálculo se reanude donde lo dejó se denomina semántica de reanudación.
Existen argumentos teóricos y de diseño a favor de cualquiera de las dos decisiones. Las discusiones sobre la estandarización de C++ entre 1989 y 1991 dieron como resultado una decisión definitiva de utilizar la semántica de terminación en C++. Bjarne Stroustrup cita una presentación de Jim Mitchell como dato clave:
Jim había utilizado el manejo de excepción en media docena de idiomas durante un período de 20 años y fue un defensor temprano de la reanudación de la semántica como uno de los principales diseñadores e implementadores del sistema de Cedro/Mesa de Xerox. Su mensaje era
- “La determinación se prefiere sobre la reanudación; esto no es una cuestión de opinión sino una cuestión de años de experiencia. El consumo es seductor, pero no válido. ”
Respaldó esta declaración con experiencia de varios sistemas operativos. El ejemplo clave fue Cedar/Mesa: Fue escrito por personas que quisieron y utilizaron la reanudación, pero después de diez años de uso, sólo quedaba un uso de la reanudación en el sistema de medio millón de líneas – y eso era una investigación contextual. Debido a que la reanudación no era realmente necesaria para tal investigación contextual, la retiraron y encontraron un aumento significativo de la velocidad en esa parte del sistema. En cada uno de los casos en que se había utilizado la reanudación se había convertido, a lo largo de diez años, en un problema y un diseño más apropiado lo había reemplazado. Básicamente, todo uso de la reanudación había representado una incapacidad para mantener niveles separados de abstracción.
Los lenguajes de manejo de excepciones con reanudación incluyen Common Lisp con su Condition System, PL/I, Dylan, R y Smalltalk. Sin embargo, la mayoría de los lenguajes de programación más nuevos siguen C++ y usan semántica de terminación.
Implementación del manejo de excepciones
La implementación del manejo de excepciones en los lenguajes de programación generalmente implica una buena cantidad de soporte tanto de un generador de código como del sistema de tiempo de ejecución que acompaña a un compilador. (Fue la adición del manejo de excepciones a C++ lo que puso fin a la vida útil del compilador original de C++, Cfront). Dos esquemas son los más comunes. El primero, registro dinámico, genera código que actualiza continuamente las estructuras sobre el estado del programa en términos de manejo de excepciones. Por lo general, esto agrega un nuevo elemento al diseño del marco de pila que sabe qué controladores están disponibles para la función o el método asociado con ese marco; si se lanza una excepción, un puntero en el diseño dirige el tiempo de ejecución al código del controlador apropiado. Este enfoque es compacto en términos de espacio, pero agrega una sobrecarga de ejecución en la entrada y salida de tramas. Se usaba comúnmente en muchas implementaciones de Ada, por ejemplo, donde la generación compleja y la compatibilidad con el tiempo de ejecución ya eran necesarias para muchas otras características del lenguaje. El Manejo estructurado de excepciones (SEH) de 32 bits de Microsoft utiliza este enfoque con una pila de excepciones separada. El registro dinámico, al ser bastante sencillo de definir, es susceptible de prueba de corrección.
El segundo esquema, y el implementado en muchos compiladores C++ de calidad de producción y Microsoft SEH de 64 bits, es un < span class="vanchor-text">enfoque basado en tablas. Esto crea tablas estáticas en tiempo de compilación y tiempo de enlace que relacionan los rangos del contador del programa con el estado del programa con respecto al manejo de excepciones. Luego, si se lanza una excepción, el sistema de tiempo de ejecución busca la ubicación de la instrucción actual en las tablas y determina qué controladores están en juego y qué se debe hacer. Este enfoque minimiza la sobrecarga ejecutiva en el caso de que no se produzca una excepción. Esto sucede a costa de algo de espacio, pero este espacio se puede asignar a secciones de datos de propósito especial de solo lectura que no se cargan ni se reubican hasta que se lanza una excepción. La ubicación (en la memoria) del código para manejar una excepción no necesita ubicarse dentro (o incluso cerca) de la región de la memoria donde se almacena el resto del código de la función. Entonces, si se lanza una excepción, puede ocurrir un impacto en el rendimiento, más o menos comparable a una llamada de función, si el código de manejo de excepciones necesario debe cargarse/almacenarse en caché. Sin embargo, este esquema tiene un costo de rendimiento mínimo si no se lanza ninguna excepción. Dado que se supone que las excepciones en C++ son eventos excepcionales (es decir, poco comunes/raros), la frase "excepciones de costo cero" a veces se usa para describir el manejo de excepciones en C++. Al igual que la identificación de tipo en tiempo de ejecución (RTTI), es posible que las excepciones no se adhieran al principio de sobrecarga cero de C++, ya que implementar el control de excepciones en tiempo de ejecución requiere una cantidad de memoria distinta de cero para la tabla de búsqueda. Por esta razón, el manejo de excepciones (y RTTI) se puede deshabilitar en muchos compiladores de C++, lo que puede ser útil para sistemas con memoria muy limitada (como los sistemas integrados). Este segundo enfoque también es superior en términos de lograr la seguridad de subprocesos.
También se han propuesto otros esquemas de definición e implementación. Para los lenguajes que admiten la metaprogramación, se han avanzado enfoques que no implican ningún tipo de sobrecarga (más allá del soporte ya presente para la reflexión).
Manejo de excepciones basado en diseño por contrato
Una visión diferente de las excepciones se basa en los principios del diseño por contrato y se apoya en particular en el lenguaje Eiffel. La idea es proporcionar una base más rigurosa para el manejo de excepciones definiendo con precisión qué es "normal" y "anormal" comportamiento. En concreto, el enfoque se basa en dos conceptos:
- Fallo: la incapacidad de una operación para cumplir su contrato. Por ejemplo, una adición puede producir un flujo aritmético (no cumple su contrato de computar una buena aproximación a la suma matemática); o una rutina puede no cumplir su condición post.
- Excepción: un evento anormal que ocurre durante la ejecución de una rutina (esa rutina es la "receptor"de la excepción) durante su ejecución. Tal evento anormal resulta de la fracaso de una operación llamada por la rutina.
El "principio de manejo seguro de excepciones" como lo introdujo Bertrand Meyer en Object-Oriented Software Construction, sostiene que solo hay dos formas significativas en que una rutina puede reaccionar cuando ocurre una excepción:
- Failure, o "organizado pánico": La rutina fija el estado del objeto restableciendo el invariante (esta es la parte "organizada"), y luego falla (panics), desencadenando una excepción en su llamada (para que el evento anormal no sea ignorado).
- Retry: La rutina vuelve a intentar el algoritmo, generalmente después de cambiar algunos valores para que el próximo intento tenga una mejor oportunidad de tener éxito.
En particular, no se permite simplemente ignorar una excepción; un bloque debe volver a intentarse y completarse con éxito, o propagar la excepción a su llamador.
Aquí hay un ejemplo expresado en sintaxis Eiffel. Asume que una rutina send_fast
es normalmente la mejor manera de enviar un mensaje, pero puede fallar y desencadenar una excepción; si es así, el algoritmo usa a continuación send_slow
, que fallará con menos frecuencia. Si send_slow< /span>
falla, la rutina send
debería fallar en su conjunto, lo que provocaría que la persona que llama obtuviera una excepción.
Enviar ()m: MESSAGE) es -- Enviar m a través de un enlace rápido, si es posible, de lo contrario a través de un enlace lento.local tried_fast, Lo intenté.: BOOLEANdo si tried_fast entonces Lo intenté. := Cierto. send_slow ()m) más tried_fast := Cierto. send_fast ()m) finalrescate si no Lo intenté. entonces retry finalfinal
Las variables locales booleanas se inicializan en False al principio. Si send_fast< /span>
falla, el cuerpo (do span>
cláusula) se ejecutará de nuevo, provocando la ejecución de send_slow
. Si esta ejecución de send_slow
falla, el rescate< La cláusula /span>
se ejecutará hasta el final sin reintentar
(sin else
en la cláusula final if
), haciendo que la ejecución de la rutina falle en su conjunto.
Este enfoque tiene el mérito de definir claramente lo que es "normal" y "anormal" Los casos son: un caso anormal, causante de una excepción, es aquel en el que la rutina es incapaz de cumplir su contrato. Define una clara distribución de roles: el hacer< la cláusula span class="w">
(cuerpo normal) está a cargo de lograr, o intentar lograr, el contrato de la rutina; el rescate< La cláusula /span>
está a cargo de restablecer el contexto y reiniciar el proceso, si esto tiene la posibilidad de tener éxito, pero no de realizar ningún cálculo real.
Aunque las excepciones en Eiffel tienen una filosofía bastante clara, Kiniry (2006) critica su implementación porque "las excepciones que son parte de la definición del lenguaje están representadas por valores INTEGER, las excepciones definidas por el desarrollador por valores STRING. [...] Además, debido a que son valores básicos y no objetos, no tienen una semántica inherente más allá de la que se expresa en una rutina de ayuda que necesariamente no puede ser infalible debido a la sobrecarga de representación en efecto (por ejemplo, uno no puede diferenciar dos enteros del mismo valor)."
Excepciones no detectadas
Las aplicaciones contemporáneas enfrentan muchos desafíos de diseño cuando se consideran estrategias de manejo de excepciones. Particularmente en las aplicaciones modernas de nivel empresarial, las excepciones a menudo deben cruzar los límites del proceso y los límites de la máquina. Parte del diseño de una estrategia sólida de manejo de excepciones es reconocer cuándo un proceso ha fallado hasta el punto en que no puede ser manejado económicamente por la parte de software del proceso.
Si se lanza una excepción y no se detecta (desde el punto de vista operativo, se lanza una excepción cuando no se especifica un controlador aplicable), el tiempo de ejecución gestiona la excepción no detectada; la rutina que hace esto se llama controlador de excepciones no detectadas intervalo>. El comportamiento predeterminado más común es finalizar el programa e imprimir un mensaje de error en la consola, que generalmente incluye información de depuración, como una representación de cadena de la excepción y el seguimiento de la pila. Esto a menudo se evita al tener un controlador de nivel superior (nivel de aplicación) (por ejemplo, en un bucle de eventos) que detecta las excepciones antes de que lleguen al tiempo de ejecución.
Tenga en cuenta que aunque una excepción no detectada puede provocar que el programa finalice de forma anormal (el programa puede no ser correcto si no se detecta una excepción, en particular al no revertir transacciones parcialmente completadas o al no liberar recursos), el proceso finaliza normalmente (suponiendo que el tiempo de ejecución funcione correctamente), ya que el tiempo de ejecución (que controla la ejecución del programa) puede garantizar el cierre ordenado del proceso.
En un programa de subprocesos múltiples, una excepción no detectada en un subproceso puede resultar en la finalización de ese subproceso, no de todo el proceso (las excepciones no detectadas en el controlador de nivel de subproceso son capturadas por el controlador de nivel superior). Esto es especialmente importante para los servidores, donde, por ejemplo, un servlet (que se ejecuta en su propio subproceso) se puede terminar sin que el servidor en general se vea afectado.
Este controlador predeterminado de excepciones no detectadas se puede anular, ya sea globalmente o por subproceso, por ejemplo, para proporcionar registros alternativos o informes de usuarios finales sobre excepciones no detectadas, o para reiniciar subprocesos que terminan debido a una excepción no detectada. Por ejemplo, en Java esto se hace para un solo hilo a través de Thread.setUncaughtExceptionHandler
y globalmente a través de Thread.setDefaultUncaughtExceptionHandler
; en Python esto se hace modificando sys.excepthook
.
Excepciones comprobadas
Java introdujo la noción de excepciones comprobadas, que son clases especiales de excepciones. Las excepciones marcadas que un método puede generar deben ser parte de la firma del método. Por ejemplo, si un método puede arrojar una IOException< /code>, debe declarar este hecho explícitamente en la firma de su método. Si no lo hace, se genera un error en tiempo de compilación. Según Hanspeter Mössenböck, las excepciones comprobadas son menos convenientes pero más sólidas. Las excepciones comprobadas pueden, en tiempo de compilación, reducir la incidencia de excepciones no controladas que aparecen en tiempo de ejecución en una aplicación determinada.
Kiniry escribe que "Como sabe cualquier programador de Java, el volumen del código try catch
en una aplicación típica de Java es a veces mayor que el código comparable necesario para la comprobación explícita de parámetros formales y valores devueltos en otros lenguajes. que no tienen excepciones comprobadas. De hecho, el consenso general entre los programadores de Java en las trincheras es que lidiar con excepciones verificadas es una tarea casi tan desagradable como escribir documentación. Por lo tanto, muchos programadores informan que "resienten" las excepciones comprobadas.". Martin Fowler ha escrito "...en general, creo que las excepciones son buenas, pero las excepciones comprobadas por Java son más problemáticas de lo que valen." A partir de 2006, ningún lenguaje de programación importante ha seguido a Java para agregar excepciones verificadas. Por ejemplo, C# no requiere ni permite la declaración de ninguna especificación de excepción, con lo siguiente publicado por Eric Gunnerson:
"La evaluación de programas pequeños conduce a la conclusión de que exigir especificaciones de excepción podría mejorar la productividad de los desarrolladores y mejorar la calidad del código, pero la experiencia con grandes proyectos de software sugiere un resultado diferente – disminución de la productividad y poco o ningún aumento de la calidad del código".
Anders Hejlsberg describe dos preocupaciones con excepciones comprobadas:
- Versión: Se puede declarar un método para lanzar excepciones X y Y. En una versión posterior del código, uno no puede tirar la excepción Z del método, porque haría que el nuevo código incompatible con los usos anteriores. Las excepciones verificadas requieren que los calladores del método añadan Z a su cláusula de lanzamientos o manejen la excepción. Alternately, Z may be misrepresented as an X or a Y.
- Escalabilidad: En un diseño jerárquico, cada sistema puede tener varios subsistemas. Cada subsistema puede lanzar varias excepciones. Cada sistema padre debe hacer frente a las excepciones de todos los subsistemas debajo de él, lo que da lugar a un número exponencial de excepciones a tratar. Las excepciones comprobadas exigen que todas estas excepciones sean tratadas explícitamente.
Para evitar esto, Hejlsberg dice que los programadores recurren a eludir la función mediante el uso de un lanza Excepción
declaración. Otra elusión es usar un intentar { ... } atrapar (Excepción e) {}
controlador. Esto se conoce como manejo de excepciones catch-all o manejo de excepciones de Pokémon después del eslogan del programa "Gotta Catch ‘Em All!". Los tutoriales de Java desaconsejan el manejo de excepciones comodín, ya que puede detectar excepciones "para las que el controlador no fue diseñado". Otra elusión desaconsejada es hacer que todas las excepciones sean subclases RuntimeException
. Una solución recomendada es usar un controlador general o una cláusula throws pero con una superclase específica de todas las excepciones potencialmente lanzadas en lugar de la superclase general Excepción
. Otra solución recomendada es definir y declarar tipos de excepción que sean adecuados para el nivel de abstracción del método llamado y asignar excepciones de nivel inferior a estos tipos mediante el encadenamiento de excepciones.
Mecanismos similares
Las raíces de las excepciones comprobadas se remontan a la noción de especificación de excepciones del lenguaje de programación CLU. Una función podría generar solo las excepciones enumeradas en su tipo, pero cualquier excepción de fuga de las funciones llamadas se convertiría automáticamente en la única excepción de tiempo de ejecución, failure
, en lugar de generar un error en tiempo de compilación. Más tarde, Modula-3 tuvo una característica similar. Estas características no incluyen la verificación del tiempo de compilación que es central en el concepto de excepciones verificadas.
Las primeras versiones del lenguaje de programación C++ incluían un mecanismo opcional similar a las excepciones verificadas, llamado especificaciones de excepción. De forma predeterminada, cualquier función podría generar cualquier excepción, pero esto podría estar limitado por un throw
cláusula añadida a la firma de la función, que especifica qué excepciones puede lanzar la función. Las especificaciones de excepción no se aplicaron en tiempo de compilación. Las violaciones dieron como resultado la función global std::inesperado
llamado. Se podría dar una especificación de excepción vacía, lo que indica que la función no generará ninguna excepción. Esto no se convirtió en el valor predeterminado cuando se agregó el manejo de excepciones al lenguaje porque habría requerido demasiada modificación del código existente, habría impedido la interacción con el código escrito en otros lenguajes y habría tentado a los programadores a escribir demasiados controladores en el entorno local. nivel. Sin embargo, el uso explícito de especificaciones de excepción vacías podría permitir que los compiladores de C++ realicen optimizaciones significativas de diseño de código y pila que se excluyen cuando el manejo de excepciones puede tener lugar en una función. Algunos analistas consideraron que el uso adecuado de las especificaciones de excepción en C++ era difícil de lograr. Este uso de especificaciones de excepción se incluyó en C++98 y C++03, quedó en desuso en el estándar de lenguaje C++ de 2012 (C++11) y se eliminó del lenguaje en C++17. Una función que no lanzará ninguna excepción ahora se puede indicar con noexcept
palabra clave.
Existe un analizador de excepciones no detectadas para el lenguaje de programación OCaml. La herramienta informa el conjunto de excepciones generadas como una firma de tipo extendida. Pero, a diferencia de las excepciones verificadas, la herramienta no requiere anotaciones sintácticas y es externa (es decir, es posible compilar y ejecutar un programa sin haber verificado las excepciones).
Comprobación dinámica de excepciones
El objetivo de las rutinas de manejo de excepciones es garantizar que el código pueda manejar las condiciones de error. Para establecer que las rutinas de manejo de excepciones son lo suficientemente sólidas, es necesario presentar el código con un amplio espectro de entradas no válidas o inesperadas, como las que se pueden crear a través de la inyección de fallas de software y las pruebas de mutación (que a veces también se denomina fuzz). pruebas). Uno de los tipos de software más difíciles para escribir rutinas de manejo de excepciones es el software de protocolo, ya que se debe preparar una implementación de protocolo robusta para recibir entradas que no cumplan con las especificaciones relevantes.
Para garantizar que se pueda realizar un análisis de regresión significativo a lo largo de un proceso de ciclo de vida de desarrollo de software, cualquier prueba de manejo de excepciones debe estar altamente automatizada y los casos de prueba deben generarse de manera científica y repetible. Existen varios sistemas disponibles comercialmente que realizan dichas pruebas.
En entornos de motor de tiempo de ejecución como Java o.NET, existen herramientas que se conectan al motor de tiempo de ejecución y cada vez que ocurre una excepción de interés, registran la información de depuración que existía en la memoria en el momento en que se lanzó la excepción (llame valores de pila y montón). Estas herramientas se denominan manejo automatizado de excepciones o herramientas de interceptación de errores y proporcionan información sobre la 'causa raíz' información para excepciones.
Excepciones asíncronas
Excepciones asincrónicas son eventos generados por un hilo independiente o un proceso externo, como presionar Ctrl-C para interrumpir un programa, recibir una señal o enviar un mensaje disruptivo como "stop& #34; o "suspender" de otro hilo de ejecución. Mientras que las excepciones síncronas ocurren en una instrucción throw
específica, las excepciones asíncronas se pueden generar en cualquier momento. De ello se deduce que el compilador no puede optimizar el manejo de excepciones asincrónicas, ya que no puede probar la ausencia de excepciones asincrónicas. También son difíciles de programar correctamente, ya que las excepciones asincrónicas deben bloquearse durante las operaciones de limpieza para evitar fugas de recursos.
Los lenguajes de programación normalmente evitan o restringen el manejo de excepciones asincrónicas, por ejemplo, C++ prohíbe generar excepciones de los manejadores de señales, y Java ha desaprobado el uso de su excepción ThreadDeath que se usaba para permitir que un subproceso detuviera otro. Otra característica es un mecanismo semiasincrónico que genera una excepción asincrónica solo durante ciertas operaciones del programa. Por ejemplo, Java's Thread.interrupción()< /code> solo afecta al subproceso cuando el subproceso llama a una operación que arroja
Excepción interrumpida
. El POSIX similar pthread_cancel
La API tiene condiciones de carrera que hacen que sea imposible usarla de manera segura.
Sistemas de acondicionamiento
Common Lisp, Dylan y Smalltalk tienen un sistema de condiciones (consulte Sistema de condiciones de Common Lisp) que abarca los sistemas de manejo de excepciones antes mencionados. En esos lenguajes o entornos, la llegada de una condición (una "generalización de un error" según Kent Pitman) implica una llamada de función, y solo al final del controlador de excepciones se puede tomar la decisión de deshacer la pila.
Las condiciones son una generalización de las excepciones. Cuando surge una condición, se busca y selecciona un controlador de condición apropiado, en orden de pila, para manejar la condición. Las condiciones que no representan errores pueden quedar completamente sin manejar de manera segura; su único propósito puede ser propagar sugerencias o advertencias hacia el usuario.
Excepciones continuables
Esto está relacionado con el llamado modelo de reanudación de manejo de excepciones, en el que se dice que algunas excepciones son continuables: se permite volver a la expresión que señaló una excepción, después de haber tomado medidas correctivas en el controlador. El sistema de condiciones se generaliza así: dentro del controlador de una condición no grave (también conocido como excepción continua), es posible saltar a puntos de reinicio predefinidos (también conocido como reinicios) que se encuentran entre la expresión de señalización y el controlador de condiciones. Los reinicios son funciones cerradas sobre algún entorno léxico, lo que permite al programador reparar este entorno antes de salir completamente del controlador de condiciones o desenredar la pila, incluso parcialmente.
Un ejemplo es la condición ENDPAGE en PL/I; la unidad ON podría escribir líneas de avance de página y líneas de encabezado para la página siguiente, luego fallar para reanudar la ejecución del código interrumpido.
Reinicia el mecanismo separado de la política
El manejo de condiciones además proporciona una separación entre el mecanismo y la política. Los reinicios proporcionan varios mecanismos posibles para recuperarse de un error, pero no seleccionan qué mecanismo es apropiado en una situación determinada. Esa es la provincia del manejador de condiciones, que (ya que está ubicado en un código de nivel superior) tiene acceso a una vista más amplia.
Un ejemplo: supongamos que hay una función de biblioteca cuyo propósito es analizar una única entrada de archivo syslog. ¿Qué debería hacer esta función si la entrada tiene un formato incorrecto? No hay una respuesta correcta, porque la misma biblioteca podría implementarse en programas para muchos propósitos diferentes. En un navegador interactivo de archivos de registro, lo correcto podría ser devolver la entrada sin analizar, para que el usuario pueda verla, pero en un programa automatizado de resumen de registros, lo correcto podría ser proporcionar valores nulos para el campos ilegibles, pero abortar con un error, si demasiadas entradas han sido mal formadas.
Es decir, la pregunta solo puede responderse en términos de los objetivos más amplios del programa, que no son conocidos por la función de biblioteca de propósito general. No obstante, salir con un mensaje de error rara vez es la respuesta correcta. Entonces, en lugar de simplemente salir con un error, la función puede establecer reinicios ofreciendo varias formas de continuar, por ejemplo, para omitir la entrada del registro, proporcionar valores predeterminados o nulos para los campos ilegibles, preguntar al usuario para los valores faltantes, o para desenredar la pila y cancelar el procesamiento con un mensaje de error. Los reinicios ofrecidos constituyen los mecanismos disponibles para recuperarse de un error; la selección de reinicio por parte del controlador de condiciones proporciona la política.
Crítica
El manejo de excepciones a menudo no se maneja correctamente en el software, especialmente cuando hay varias fuentes de excepciones; El análisis del flujo de datos de 5 millones de líneas de código Java encontró más de 1300 defectos en el manejo de excepciones. Citando múltiples estudios previos realizados por otros (1999-2004) y sus propios resultados, Weimer y Necula escribieron que un problema significativo con las excepciones es que "crean rutas de flujo de control ocultas que son difíciles de razonar para los programadores". "Aunque try-catch-finally es conceptualmente simple, tiene la descripción de ejecución más complicada en la especificación del lenguaje [Gosling et al. 1996] y requiere cuatro niveles de "si" anidados en su descripción oficial en inglés. En resumen, contiene una gran cantidad de casos extremos que los programadores suelen pasar por alto."
Las excepciones, como el flujo no estructurado, aumentan el riesgo de fugas de recursos (como escapar de una sección bloqueada por un mutex, o una que mantiene abierto un archivo temporalmente) o un estado inconsistente. Existen varias técnicas para la gestión de recursos en presencia de excepciones, la mayoría de las cuales combinan el patrón de disposición con algún tipo de protección de desenredado (como una cláusula finally
), que libera automáticamente el recurso cuando el control sale de una sección de código.
Tony Hoare en 1980 describió que el lenguaje de programación Ada tiene "... una plétora de funciones y convenciones de notación, muchas de ellas innecesarias y algunas, como el manejo de excepciones, incluso peligrosas. [...] No permita que este lenguaje en su estado actual se use en aplicaciones donde la confiabilidad es crítica [...]. El próximo cohete que se extravíe como resultado de un error en el lenguaje de programación puede no ser un cohete espacial de exploración en un viaje inofensivo a Venus: puede ser una ojiva nuclear que explota sobre una de nuestras propias ciudades."
Los desarrolladores de Go creen que el lenguaje try-catch-finally ofusca el flujo de control e introdujeron el panic
/recuperar mecanismo
. recuperar()
difiere de catch
ya que solo se puede llamar desde dentro de un defer
bloque de código en una función, por lo que el controlador solo puede limpiar y cambiar los valores de retorno de la función, y no puede devolver el control a un punto arbitrario dentro de la función. El aplazar< span class="w">
el propio bloque funciona de manera similar a un finalmente
.
Manejo de excepciones en jerarquías de interfaz de usuario
Frameworks front-end web, como React y Vue, han introducido mecanismos de manejo de errores en los que los errores se propagan hacia arriba en la jerarquía de componentes de la interfaz de usuario, de manera análoga a cómo los errores se propagan hacia arriba en la pila de llamadas al ejecutar el código. Aquí, el mecanismo de límite de error sirve como análogo al mecanismo típico de prueba y captura. Por lo tanto, un componente puede garantizar que los errores de sus componentes secundarios se detecten y manejen, y que no se propaguen a los componentes principales.
Por ejemplo, en Vue, un componente detectaría errores al implementar errorCaptured
Vue.componente()"Padre" ', {} plantilla: "Seguido" ', errorCaptured: ()err, vm, info) = alerta()' Se produjo un error ');})Vue.componente()Niño ', {} plantilla: "Seguido" ♪♪♪ '})
Cuando se usa así en el marcado:
.padre■ .niño>niño■c)padre■
El error producido por el componente secundario es capturado y manejado por el componente principal.
Contenido relacionado
IEEE754-1985
LiveScript (lenguaje de programación)
Lista de programadores