Algunos lenguajes (como C ++ y versiones anteriores de PHP) no admiten la finally
parte de una try ... catch ... finally
construcción. ¿Es finally
necesario alguna vez? Debido a que el código siempre se ejecuta, ¿por qué no debería / no debería colocar ese código después de un try ... catch
bloque sin una finally
cláusula? ¿Por qué usar uno? (Estoy buscando una razón / motivación para usar / no usar finally
, no una razón para eliminar 'atrapar' o por qué es legal hacerlo).
exception-handling
Agi Hammerthief
fuente
fuente
Respuestas:
Además de lo que otros han dicho, también es posible que se arroje una excepción dentro de la cláusula catch. Considera esto:
En este ejemplo, la
Cleanup()
función nunca se ejecuta, porque se genera una excepción en la cláusula catch y la siguiente captura más alta en la pila de llamadas lo captará. Usar un bloque finalmente elimina este riesgo y hace que el código sea más limpio para arrancar.fuente
Como otros han mencionado, no hay garantía de que el código después de una
try
declaración se ejecute a menos que detecte todas las excepciones posibles. Dicho esto, esto:puede reescribirse 1 como:
Pero esto último requiere que capture todas las excepciones no controladas, duplique el código de limpieza y recuerde volver a lanzar. Entonces
finally
no es necesario , pero es útil .C ++ no tiene
finally
porque Bjarne Stroustrup cree que RAII es mejor , o al menos es suficiente para la mayoría de los casos:1 El código específico para capturar todas las excepciones y volver a lanzar sin perder la información de seguimiento de la pila varía según el idioma. He utilizado Java, donde se captura el seguimiento de la pila cuando se crea la excepción. En C # simplemente usarías
throw;
.fuente
handleError()
en el segundo caso, ¿no?catch (Throwable t) {}
, con el intento ... atrapar bloque alrededor del bloque inicial completo (para atraparhandleError
también loshandleErro();
que lo hará aún mejor argumento de por qué finalmente los bloques son útiles (aunque esa no era la pregunta original).finally
, que es mucho más matizado.try
está dentrocatch
de la excepción específica . En segundo lugar, es posible que no sepa si puede manejar el error con éxito hasta que haya examinado la excepción, o que la causa de la excepción también le impida manejar el error (al menos a ese nivel). Eso es bastante común al hacer E / S. La repetición está ahí porque la única forma de garantizarcleanUp
ejecuciones es atrapar todo , pero el código original permitiría que las excepciones que se originan en elcatch (SpecificException e)
bloque se propaguen hacia arriba.finally
Los bloques generalmente se usan para eliminar recursos que pueden ayudar con la legibilidad cuando se usan múltiples declaraciones de retorno:vs
fuente
finally
. (Usaría el código como en el segundo bloque ya que se desaconsejan las declaraciones de retorno múltiples donde trabajo.)Como aparentemente ya has supuesto, sí, C ++ proporciona las mismas capacidades sin ese mecanismo. Como tal, estrictamente hablando, el mecanismo
try
/finally
no es realmente necesario.Dicho esto, prescindir de él impone algunos requisitos en la forma en que se diseña el resto del lenguaje. En C ++, el mismo conjunto de acciones está incorporado en el destructor de una clase. Esto funciona principalmente (¿exclusivamente?) Porque la invocación del destructor en C ++ es determinista. Esto, a su vez, conduce a algunas reglas bastante complejas sobre la vida útil de los objetos, algunas de las cuales son decididamente no intuitivas.
La mayoría de los otros idiomas proporcionan alguna forma de recolección de basura. Si bien hay cosas sobre la recolección de basura que son controvertidas (por ejemplo, su eficiencia en relación con otros métodos de administración de memoria), una cosa generalmente no lo es: el momento exacto en que el recolector de basura "limpia" un objeto no está directamente vinculado al alcance del objeto. Esto evita su uso cuando la limpieza debe ser determinista, ya sea simplemente para un funcionamiento correcto, o cuando se trata de recursos tan valiosos que la limpieza no se retrasa arbitrariamente.
try
/finally
proporciona una forma para que dichos lenguajes aborden aquellas situaciones que requieren una limpieza determinista.Creo que aquellos que afirman que la sintaxis de C ++ para esta capacidad es "menos amigable" que la de Java, están perdiendo el punto. Peor aún, se están perdiendo un punto mucho más crucial sobre la división de responsabilidad que va mucho más allá de la sintaxis y tiene mucho más que ver con la forma en que se diseña el código.
En C ++, esta limpieza determinista ocurre en el destructor del objeto. Eso significa que el objeto puede estar (y normalmente debería estar) diseñado para limpiarse después de sí mismo. Esto va a la esencia del diseño orientado a objetos: una clase debe diseñarse para proporcionar una abstracción y hacer cumplir sus propias invariantes. En C ++, uno hace precisamente eso, y uno de los invariantes que proporciona es que cuando se destruye el objeto, los recursos controlados por ese objeto (todos ellos, no solo la memoria) se destruirán correctamente.
Java (y similares) son algo diferentes. Si bien admiten (más o menos) un tipo
finalize
que teóricamente podría proporcionar capacidades similares, el soporte es tan débil que es básicamente inutilizable (y de hecho, esencialmente nunca se usa).Como resultado, en lugar de que la clase misma pueda realizar la limpieza requerida, el cliente de la clase debe tomar medidas para hacerlo. Si hacemos una comparación suficientemente miope, a primera vista puede parecer que esta diferencia es bastante menor y Java es bastante competitivo con C ++ a este respecto. Terminamos con algo como esto. En C ++, la clase se ve así:
... y el código del cliente se ve así:
En Java intercambiamos un poco más de código donde el objeto se usa por un poco menos en la clase. Inicialmente, esto parece una compensación bastante pareja. Sin embargo, en realidad está lejos de serlo, porque en el código más típico solo definimos la clase en un lugar, pero la usamos en muchos lugares. El enfoque de C ++ significa que solo escribimos ese código para manejar la limpieza en un solo lugar. El enfoque de Java significa que tenemos que escribir ese código para manejar la limpieza muchas veces, en muchos lugares, en cada lugar donde usamos un objeto de esa clase.
En resumen, el enfoque de Java básicamente garantiza que muchas abstracciones que intentamos proporcionar son "permeables": cualquier clase que requiera una limpieza determinista obliga al cliente de la clase a conocer los detalles de qué limpiar y cómo hacerlo. , en lugar de que esos detalles estén ocultos en la clase misma.
Aunque lo he llamado "el enfoque de Java" anteriormente,
try
/finally
y mecanismos similares bajo otros nombres no están completamente restringidos a Java. Para un ejemplo destacado, la mayoría (¿todos?) De los lenguajes .NET (por ejemplo, C #) proporcionan lo mismo.Las iteraciones recientes de Java y C # también proporcionan un punto intermedio entre Java "clásico" y C ++ a este respecto. En C #, un objeto que quiere automatizar su limpieza puede implementar la
IDisposable
interfaz, que proporciona unDispose
método (al menos vagamente) similar a un destructor de C ++. Si bien esto se puede usar a través de untry
/finally
like en Java, C # automatiza la tarea un poco más con unausing
declaración que le permite definir los recursos que se crearán a medida que se ingresa un alcance y se destruyen cuando se sale del alcance. Aunque todavía está muy por debajo del nivel de automatización y certeza proporcionado por C ++, esta sigue siendo una mejora sustancial sobre Java. En particular, el diseñador de la clase puede centralizar los detalles de cómodisponer de la clase en su implementación deIDisposable
. Todo lo que queda para el programador del cliente es la menor carga de escribir unausing
declaración para garantizar que laIDisposable
interfaz se utilizará cuando debería. En Java 7 y versiones posteriores, los nombres se han cambiado para proteger al culpable, pero la idea básica es básicamente idéntica.fuente
No puedo creer que nadie más haya planteado esto (sin juego de palabras): ¡no necesita una cláusula catch !
Esto es perfectamente razonable:
No hay ninguna cláusula catch a la vista, porque este método no puede hacer nada útil con esas excepciones; se dejan propagar de nuevo la pila de llamadas a un controlador que pueda . Capturar y volver a lanzar excepciones en cada método es una mala idea, especialmente si solo estás volviendo a lanzar la misma excepción. Va completamente en contra de cómo se supone que funciona el Manejo de excepciones estructuradas (y está muy cerca de devolver un "código de error" de cada método, solo en la "forma" de una Excepción).
Lo que este método no tiene que ver, sin embargo, para limpiar después de sí mismo, de modo que el "mundo exterior" no necesita saber nada sobre el desorden que se puso en sí. La última cláusula hace exactamente eso: no importa cómo se comporten los métodos llamados, la cláusula final se ejecutará "al salir" del método (y lo mismo es cierto para cada cláusula final entre el punto en el que se lanza la Excepción y la eventual cláusula catch que lo maneja); cada uno se ejecuta mientras la pila de llamadas "se desenrolla".
fuente
¿Qué pasaría si se lanzara una excepción que no esperaba? El intento saldría en el medio y no se ejecuta ninguna cláusula catch.
El último bloque es ayudar con eso y garantizar que, sin importar la excepción, la limpieza se realice.
fuente
finally
, ya que puede evitar excepciones "inesperadas" concatch(Object)
ocatch(...)
generalidades.Algunos lenguajes ofrecen tanto constructores como destructores para sus objetos (por ejemplo, C ++, creo). Con estos idiomas puede hacer la mayoría (posiblemente todo) de lo que generalmente se hace
finally
en un destructor. Como tal, en esos idiomas, unafinally
cláusula puede ser superflua.En un lenguaje sin destructores (por ejemplo, Java) es difícil (tal vez incluso imposible) lograr una limpieza correcta sin la
finally
cláusula. NB: en Java hay unfinalise
método, pero no hay garantía de que alguna vez se llame.fuente
finalise
pero preferiría no entrar en los argumentos políticos sobre destructores / finalistas en este momento.finalise
pero con un sabor extensible y un mecanismo tipo OOP, muy expresivo y comparable alfinalise
mecanismo de otros idiomas.Intentar finalmente y tratar de atrapar son dos cosas diferentes que solo comparten la palabra clave "probar". Personalmente me hubiera gustado ver eso diferente. La razón por la que los ven juntos es porque las excepciones producen un "salto".
E intentar finalmente está diseñado para ejecutar código incluso si el flujo de programación salta. Ya sea por una excepción o por cualquier otra razón. Es una buena forma de adquirir un recurso y asegurarse de que se limpie después sin tener que preocuparse por los saltos.
fuente
try catch
pero notry finally
; el código que usa este último se convierte en código usando solo el primero, copiando el contenido delfinally
bloque en todos los puntos del código donde podría necesitar ejecutarse.Como esta pregunta no especifica C ++ como lenguaje, consideraré una combinación de C ++ y Java, ya que adoptan un enfoque diferente para la destrucción de objetos, que se sugiere como una de las alternativas.
Motivos por los que puede usar un bloque finalmente, en lugar de un código después del bloque try-catch
regresas temprano desde el bloque try: considera esto
comparado con:
regresas temprano de los bloques de captura: compara
vs:
Revocas excepciones. Comparar:
vs:
Estos ejemplos no hacen que parezca tan malo, pero a menudo tiene varios de estos casos interactuando y más de un tipo de excepción / recurso en juego.
finally
puede ayudar a evitar que su código se convierta en una pesadilla de mantenimiento enredada.Ahora en C ++ estos pueden manejarse con objetos basados en el alcance. Pero en mi opinión, hay dos desventajas para este enfoque 1. La sintaxis es menos amigable. 2. El orden de construcción al reverso del orden de destrucción puede aclarar las cosas.
En Java, no puedes conectar el método de finalización para hacer tu limpieza, ya que no sabes cuándo sucederá (bueno, pero ese es un camino lleno de divertidas condiciones de carrera: JVM tiene mucho margen para decidir cuándo destruye cosas, a menudo no es cuando lo espera, ya sea antes o después de lo que podría esperar, y eso puede cambiar a medida que el compilador de puntos calientes se activa ... suspiro ...)
fuente
Todo lo que es lógicamente "necesario" en un lenguaje de programación son las instrucciones:
Cualquier algoritmo se puede implementar utilizando solo las instrucciones anteriores, todas las demás construcciones de lenguaje están ahí para hacer que los programas sean más fáciles de escribir y más comprensibles para otros programadores.
Vea la vieja computadora para el hardware real usando un conjunto de instrucciones tan mínimo.
fuente
En realidad, la brecha más grande para mí suele estar en los idiomas que admiten
finally
pero carecen de destructores, porque puede modelar toda la lógica asociada con la "limpieza" (que separaré en dos categorías) a través de destructores a nivel central sin tener que lidiar manualmente con la limpieza lógica en cada función relevante. Cuando veo que C # o el código Java hacen cosas como desbloquear manualmente mutexes y cerrar archivos enfinally
bloques, eso se siente desactualizado y algo así como el código C cuando todo eso está automatizado en C ++ a través de destructores de manera que libera a los humanos de esa responsabilidad.Sin embargo, todavía encontraría una leve comodidad si se incluye C ++
finally
y es porque hay dos tipos de limpieza:El segundo, al menos, no se asigna tan intuitivamente a la idea de la destrucción de recursos, aunque puede hacerlo bien con los protectores de alcance que revierten automáticamente los cambios cuando se destruyen antes de ser cometidos. No
finally
podría decirse que proporciona al menos un poco (sólo un poco de pequeñísima) mecanismo más sencillo para el trabajo de los guardias de alcance.Sin embargo, un mecanismo aún más directo sería un
rollback
bloque que nunca antes había visto en ningún idioma. Es una especie de sueño mío si alguna vez diseñé un lenguaje que involucrara el manejo de excepciones. Se parecería a esto:Esa sería la forma más sencilla de modelar las reversiones de efectos secundarios, mientras que los destructores son prácticamente el mecanismo perfecto para la limpieza de recursos locales. Ahora solo guarda un par de líneas adicionales de código de la solución de protección de alcance, pero la razón por la que quiero ver un lenguaje con esto es que la reversión de efectos secundarios tiende a ser el aspecto más descuidado (pero más complicado) del manejo de excepciones en lenguajes que giran en torno a la mutabilidad. Creo que esta característica alentaría a los desarrolladores a pensar en el manejo de excepciones de la manera adecuada en términos de deshacer las transacciones siempre que las funciones causen efectos secundarios y no se completen y, como una ventaja adicional, cuando las personas ven lo difícil que puede ser hacer retrocesos correctamente, En primer lugar, podrían favorecer escribir más funciones libres de efectos secundarios.
También hay algunos casos oscuros en los que solo desea hacer cosas diversas sin importar qué al salir de una función, independientemente de cómo salió, como tal vez registrar una marca de tiempo. Podría
finally
decirse que es la solución más sencilla y perfecta para el trabajo, ya que tratar de crear una instancia de un objeto solo para usar su destructor con el único propósito de registrar una marca de tiempo simplemente se siente realmente extraño (aunque puede hacerlo bien y de manera conveniente con lambdas )fuente
Al igual que muchas otras cosas inusuales sobre el lenguaje C ++, la falta de una
try/finally
construcción es una falla de diseño, incluso si puede llamarlo así en un lenguaje que con frecuencia parece no haber realizado ningún trabajo de diseño real .RAII (el uso de invocación de destructor determinista basada en el alcance en objetos basados en pila para la limpieza) tiene dos fallas graves. La primera es que requiere el uso de objetos basados en la pila , que son una abominación que viola el Principio de sustitución de Liskov. Hay muchas buenas razones por las cuales ningún otro lenguaje OO antes o después de que C ++ los haya usado, dentro de epsilon; D no cuenta ya que se basa en gran medida en C ++ y no tiene participación de mercado de todos modos, y explicar los problemas que causan está más allá del alcance de esta respuesta.
En segundo lugar, lo que
finally
puede hacer es un superconjunto de destrucción de objetos. Gran parte de lo que se hace con RAII en C ++ se describiría en el lenguaje Delphi, que no tiene recolección de basura, con el siguiente patrón:Este es el patrón RAII hecho explícito; Si fuera a crear una rutina C ++ que contenga solo el equivalente a la primera y tercera líneas anteriores, lo que generaría el compilador terminaría pareciéndose a lo que escribí en su estructura básica. Y debido a que es el único acceso a la
try/finally
construcción que proporciona C ++, los desarrolladores de C ++ terminan con una visión bastante miope detry/finally
: cuando todo lo que tienes es un martillo, todo comienza a verse como un destructor, por así decirlo.Pero hay otras cosas que un desarrollador experimentado puede hacer con una
finally
construcción. No se trata de la destrucción determinista, incluso frente a una excepción planteada; se trata de la ejecución de código determinista , incluso ante una excepción que se genera.Aquí hay otra cosa que puede ver comúnmente en el código de Delphi: un objeto de conjunto de datos con controles de usuario vinculados a él. El conjunto de datos contiene datos de una fuente externa, y los controles reflejan el estado de los datos. Si está a punto de cargar una gran cantidad de datos en su conjunto de datos, querrá deshabilitar temporalmente el enlace de datos para que no haga cosas extrañas en su interfaz de usuario, tratando de actualizarlo una y otra vez con cada nuevo registro que ingrese , así que lo codificaría así:
Claramente, no hay ningún objeto destruido aquí, y no hay necesidad de uno. El código es simple, conciso, explícito y eficiente.
¿Cómo se haría esto en C ++? Bueno, primero tendrías que codificar una clase entera . Probablemente se llamaría
DatasetEnabler
o algo así. Toda su existencia sería como un ayudante RAII. Entonces necesitarías hacer algo como esto:Sí, esas llaves aparentemente superfluas son necesarias para administrar el alcance adecuado y garantizar que el conjunto de datos se vuelva a habilitar de inmediato y no al final del método. Entonces, lo que termina no requiere menos líneas de código (a menos que use llaves egipcias). Requiere que se cree un objeto superfluo, que tiene sobrecarga. (¿No se supone que el código C ++ es rápido?) No es explícito, sino que depende de la magia del compilador. El código que se ejecuta no se describe en ninguna parte de este método, sino que reside en una clase completamente diferente, posiblemente en un archivo completamente diferente . En resumen, de ninguna manera es una mejor solución que poder escribir el
try/finally
bloque usted mismo.Este tipo de problema es lo suficientemente común en el diseño del lenguaje que tiene un nombre: inversión de abstracción. Ocurre cuando una construcción de alto nivel se construye encima de una construcción de bajo nivel, y luego la construcción de bajo nivel no se admite directamente en el lenguaje, lo que requiere que aquellos que desean usarla la vuelvan a implementar en términos de construcción de alto nivel, a menudo con fuertes penalizaciones tanto para la legibilidad como para la eficiencia del código.
fuente