¿Por qué usar try ... finalmente sin una cláusula catch?

79

La forma clásica de programar es con try ... catch. ¿Cuándo es apropiado usar trysin catch?

En Python, lo siguiente parece legal y puede tener sentido:

try:
  #do work
finally:
  #do something unconditional

Sin embargo, el código no hizo catchnada. Del mismo modo, se podría pensar en Java que sería de la siguiente manera:

try {
    //for example try to get a database connection
}
finally {
  //closeConnection(connection)
}

Se ve bien y de repente no tengo que preocuparme por los tipos de excepción, etc. Si es una buena práctica, ¿cuándo es una buena práctica? Alternativamente, ¿cuáles son las razones por las cuales esto no es una buena práctica o no es legal? (No compilé la fuente. Lo pregunto, ya que podría ser un error de sintaxis para Java. Verifiqué que Python seguramente compila).

Un problema relacionado con el que me he encontrado es este: sigo escribiendo la función / método, al final del cual debe devolver algo. Sin embargo, puede estar en un lugar que no debe ser alcanzado y debe ser un punto de retorno. Entonces, incluso si manejo las excepciones anteriores, todavía estoy regresando NULLo una cadena vacía en algún punto del código que no se debe alcanzar, a menudo al final del método / función. Siempre he logrado reestructurar el código para que no tenga que hacerlo return NULL, ya que parece que no parece una buena práctica.

Niklas Rosencrantz
fuente
44
En Java, ¿por qué no poner la declaración de retorno al final del bloque try?
Kevin Cline
2
Me entristece que try..finally y try..catch utilicen la palabra clave try, aparte de que ambos comienzan con try, son dos construcciones totalmente diferentes.
Pieter B
3
try/catchno es "la forma clásica de programar". Es la forma clásica de programar en C ++ , porque C ++ carece de una construcción adecuada de prueba / finalmente, lo que significa que debe implementar cambios de estado reversibles garantizados utilizando hacks feos que involucran RAII. Pero los lenguajes OO decentes no tienen ese problema, ya que proporcionan prueba / finalmente. Se usa para un propósito muy diferente al de try / catch.
Mason Wheeler
3
Lo veo mucho con recursos de conexión externa. Desea la excepción, pero debe asegurarse de no dejar una conexión abierta, etc. Si la detecta, la volverá a lanzar a la siguiente capa de todos modos en algunos casos.
Aparejo
44
@MasonWheeler "Hacks feos" , por favor, ¿explica qué tiene de malo que un objeto maneje su propia limpieza?
Baldrickk

Respuestas:

146

Depende de si puede lidiar con las excepciones que se pueden generar en este momento o no.

Si puede manejar las excepciones localmente, debería hacerlo, y es mejor manejar el error lo más cerca posible de donde se genera.

Si no puede manejarlos localmente, simplemente tener un try / finallybloque es perfectamente razonable, suponiendo que haya algún código que deba ejecutar independientemente de si el método tuvo éxito o no. Por ejemplo ( del comentario de Neil ), abrir un flujo y luego pasarlo a un método interno para cargarlo es un excelente ejemplo de cuándo lo necesitaría try { } finally { }, utilizando la cláusula final para garantizar que el flujo se cierre independientemente del éxito o fallo de la lectura.

Sin embargo, aún necesitará un controlador de excepciones en algún lugar de su código, a menos que, por supuesto, desee que su aplicación se bloquee por completo. Depende de la arquitectura de su aplicación exactamente dónde está ese controlador.

ChrisF
fuente
8
"y es mejor manejar el error lo más cerca posible de donde se genera". eh, depende Si puedes recuperarte y aún completar la misión (por así decirlo), sí, por supuesto. Si no puede, es mejor dejar que la excepción llegue hasta arriba, donde (probablemente) se requerirá la intervención del usuario para lidiar con lo que sucedió.
Será
13
@will - es por eso que usé la frase "como sea posible".
ChrisF
8
Buena respuesta, pero agregaría un ejemplo: abrir una secuencia y pasar esa secuencia a un método interno para que se cargue es un excelente ejemplo de cuándo lo necesitaría try { } finally { }, aprovechando la cláusula final para garantizar que la secuencia finalmente se cierre independientemente del éxito / fracaso.
Neil
porque a veces todo el camino arriba es lo más cerca que uno puede hacerlo
Newtopian
"el solo hecho de intentar / finalmente bloquear es perfectamente razonable" estaba buscando exactamente esta respuesta. gracias @ChrisF
neal aise
36

El finallybloque se utiliza para el código que siempre debe ejecutarse, independientemente de si se produjo una condición de error (excepción) o no.

El código en el finallybloque se ejecuta después de tryque se completa el bloque y, si se produce una excepción detectada, después de que se catchcompleta el bloque correspondiente . Siempre se ejecuta, incluso si se produjo una excepción no detectada en el bloque tryo catch.

El finallybloque se usa generalmente para cerrar archivos, conexiones de red, etc. que se abrieron en el trybloque. La razón es que el archivo o la conexión de red deben cerrarse, ya sea que la operación con ese archivo o conexión de red haya tenido éxito o haya fallado.

Se debe tener cuidado en el finallybloque para asegurarse de que no arroje una excepción. Por ejemplo, asegúrese doblemente de verificar todas las variables null, etc.

Yfeldblum
fuente
8
+1: Es idiomático para "debe limpiarse". La mayoría de los usos de try-finallyse pueden reemplazar con una withdeclaración.
S.Lott
12
Varios idiomas tienen mejoras específicas de lenguaje extremadamente útiles para la try/finallyconstrucción. C # has using, Python has with, etc.
yfeldblum
55
@yfeldblum: hay una diferencia sutil entre usingy try-finally, ya que Disposeel usingbloque no llamará al método si ocurre una excepción en el IDisposableconstructor del objeto. try-finallyle permite ejecutar código incluso si el constructor del objeto arroja una excepción.
Scott Whitlock
55
@ScottWhitlock: ¿Eso es algo bueno ? ¿Qué estás tratando de hacer, llamar a un método en un objeto no construido? Eso es un billón de tipos de malos.
DeadMG
1
@DeadMG: Ver también stackoverflow.com/q/14830534/18192
Brian
17

Un ejemplo donde intentar ... finalmente sin una cláusula catch es apropiado (y aún más, idiomático ) en Java es el uso de Lock en el paquete de bloqueos de utilidades concurrentes.

  • Así es como se explica y justifica en la documentación de la API (la fuente en negrita entre comillas es mía):

    ... La ausencia de bloqueo estructurado por bloques elimina la liberación automática de bloqueos que ocurre con los métodos y las declaraciones sincronizadas. En la mayoría de los casos, se debe usar el siguiente idioma :

     Lock l = ...;
     l.lock();
     try {
         // access the resource protected by this lock
     } finally {
         l.unlock();
     }

    Cuando el bloqueo y el desbloqueo se producen en diferentes ámbitos, se debe tener cuidado para garantizar que todo el código que se ejecuta mientras se mantiene el bloqueo esté protegido por try-finally o try-catch para garantizar que el bloqueo se libere cuando sea necesario .

mosquito
fuente
¿Puedo l.lock()probarlo? try{ l.lock(); }finally{l.unlock();}
RMachnik
1
técnicamente, puedes. No lo puse allí porque semánticamente, tiene menos sentido. tryen este fragmento se pretende envolver el acceso a los recursos, por qué contaminarlo con algo que no está relacionado con eso
mosquito
44
Puede, pero si l.lock()falla, el finallybloque aún se ejecutará si l.lock()está dentro del trybloque. Si lo hace como sugiere el mosquito, el finallybloqueo solo se ejecutará cuando sepamos que se adquirió el bloqueo.
Wtrmute
12

En un nivel básico catchy finallyresolver dos problemas relacionados pero diferentes:

  • catch se usa para manejar un problema que fue informado por el código que llamó
  • finally se usa para limpiar datos / recursos que el código actual creó / modificó, sin importar si ocurrió un problema o no

Entonces, ambos están relacionados de alguna manera con problemas (excepciones), pero eso es casi todo lo que tienen en común.

Una diferencia importante es que el finallybloque debe estar en el mismo método donde se crearon los recursos (para evitar pérdidas de recursos) y no se puede colocar en un nivel diferente en la pila de llamadas.

Sin catchembargo, el asunto es diferente: el lugar correcto depende de dónde puede manejar la excepción. No sirve de nada detectar una excepción en un lugar donde no se puede hacer nada al respecto, por lo tanto, a veces es mejor simplemente dejarla pasar.

Joachim Sauer
fuente
2
Nitpick: "... el bloque finalmente debe estar en el mismo método donde se crearon los recursos ..." . Ciertamente es una buena idea hacerlo de esa manera, porque es más fácil ver que no hay pérdida de recursos. Sin embargo, no es una condición previa necesaria; es decir, no tienes que hacerlo de esa manera. Puede liberar el recurso en finallyuna declaración de prueba adjunta (estática o dinámica) ... y aún así estar 100% a prueba de fugas.
Stephen C
6

@yfeldblum tiene la respuesta correcta: try-finally sin una declaración catch generalmente debe reemplazarse con un lenguaje apropiado.

En C ++, está utilizando RAII y constructores / destructores; en Python es una withdeclaración; y en C #, es una usingdeclaración.

Casi siempre son más elegantes porque el código de inicialización y finalización se encuentra en un lugar (el objeto abstraído) en lugar de en dos lugares.

Neil G
fuente
2
Y en Java son bloques ARM
MarkJ
2
Supongamos que tiene un objeto mal diseñado (por ejemplo, uno que no implementa adecuadamente IDisposable en C #) que no siempre es una opción viable.
Mootinator
1
@mootinator: ¿no puedes heredar del objeto mal diseñado y arreglarlo?
Neil G
2
O encapsulación? Bah. Quiero decir que sí, por supuesto.
Mootinator
4

En muchos idiomas, una finallydeclaración también se ejecuta después de la declaración de devolución. Esto significa que puede hacer algo como:

try {
  // Do processing
  return result;
} finally {
  // Release resources
}

Que libera los recursos independientemente de cómo se terminó el método con una excepción o una declaración de devolución regular.

Si esto es bueno o malo está en debate, pero try {} finally {}no siempre se limita al manejo de excepciones.

Kamiel Wanrooij
fuente
0

Podría invocar la ira de Pythonistas (no sé, ya que no uso mucho Python) o programadores de otros idiomas con esta respuesta, pero en mi opinión, la mayoría de las funciones no deberían tener un catchbloqueo, idealmente hablando. Para mostrar por qué, permítanme comparar esto con la propagación manual del código de error del tipo que tenía que hacer cuando trabajaba con Turbo C a fines de los 80 y principios de los 90.

Entonces, digamos que tenemos una función para cargar una imagen o algo así en respuesta a un usuario que selecciona un archivo de imagen para cargar, y esto está escrito en C y ensamblado:

ingrese la descripción de la imagen aquí

Omití algunas funciones de bajo nivel, pero podemos ver que he identificado diferentes categorías de funciones, codificadas por colores, en función de las responsabilidades que tienen con respecto al manejo de errores.

Punto de falla y recuperación

Ahora nunca fue difícil escribir las categorías de funciones que llamo el "posible punto de fallas" (las que throw, es decir) y las funciones de "recuperación e informe de errores" (las que catch, es decir).

Esas funciones fueron siempre trivial para escribir correctamente antes de que el manejo de excepciones ya estaba disponible una función que se puede ejecutar en un fallo externo, como no asignar memoria, simplemente puede devolver una NULLo 0o -1o establecer un código de error global o algo a este efecto. Y la recuperación / informe de errores siempre fue fácil, ya que una vez que bajó por la pila de llamadas hasta un punto en el que tenía sentido recuperarse e informar fallas, simplemente toma el código y / o mensaje de error y se lo informa al usuario. Y, naturalmente, una función en la hoja de esta jerarquía que nunca puede fallar, sin importar cómo se cambie en el futuro ( Convert Pixel) es muy simple de escribir correctamente (al menos con respecto al manejo de errores).

Propagación de error

Sin embargo, las funciones tediosas propensas a errores humanos fueron los propagadores de errores , los que no se encontraron directamente con fallas, sino que se llamaron funciones que podrían fallar en algún lugar más profundo de la jerarquía. En ese punto, Allocate Scanlinepodría tener que manejar una falla mallocy luego devolver un error a Convert Scanlines, luego Convert Scanlinestendría que verificar ese error y pasarlo a Decompress Image, luego Decompress Image->Parse Image, y Parse Image->Load Image, y Load Imageal comando de final de usuario donde finalmente se informa el error .

Aquí es donde muchos humanos cometen errores, ya que solo se necesita un propagador de errores para no verificar y transmitir el error para que toda la jerarquía de funciones se derrumbe cuando se trata de manejar adecuadamente el error.

Además, si las funciones devuelven códigos de error, prácticamente perdemos la capacidad, por ejemplo, en el 90% de nuestra base de código, de devolver valores de interés en caso de éxito, ya que muchas funciones tendrían que reservar su valor de retorno para devolver un código de error en fracaso .

Reducción del error humano: códigos de error globales

Entonces, ¿cómo podemos reducir la posibilidad de error humano? Aquí incluso podría invocar la ira de algunos programadores de C, pero una mejora inmediata en mi opinión es usar códigos de error globales , como OpenGL con glGetError. Esto al menos libera las funciones para devolver valores significativos de interés en el éxito. Hay formas de hacer que este hilo sea seguro y eficiente donde el código de error se localiza en un hilo.

También hay algunos casos en los que una función puede encontrarse con un error, pero es relativamente inofensivo que continúe un poco más antes de que regrese prematuramente como resultado de descubrir un error anterior. Esto permite que tal cosa suceda sin tener que verificar si hay errores en el 90% de las llamadas a funciones realizadas en cada función, por lo que aún puede permitir el manejo adecuado de errores sin ser tan meticuloso.

Reducción de errores humanos: manejo de excepciones

Sin embargo, la solución anterior todavía requiere tantas funciones para tratar el aspecto del flujo de control de la propagación de errores manual, incluso si pudiera haber reducido el número de líneas if error happened, return errorde código de tipo manual . No lo eliminaría por completo, ya que a menudo necesitaría al menos un lugar para verificar un error y regresar para casi todas las funciones de propagación de errores. Entonces esto es cuando el manejo de excepciones entra en escena para salvar el día (más o menos).

Pero el valor del manejo de excepciones aquí es liberar la necesidad de lidiar con el aspecto del flujo de control de la propagación de errores manual. Eso significa que su valor está vinculado a la capacidad de evitar tener que escribir una gran cantidad de catchbloques en toda su base de código. En el diagrama anterior, el único lugar que debería tener un catchbloque es Load Image User Commanddonde se informa el error. Idealmente, nada más debería tener catchalgo porque, de lo contrario, comienza a ser tan tedioso y propenso a errores como el manejo de códigos de error.

Entonces, si me preguntas, si tienes una base de código que realmente se beneficia del manejo de excepciones de una manera elegante, debería tener el número mínimo de catchbloques (por mínimo no me refiero a cero, sino más como uno por cada efecto único) operación del usuario final que podría fallar, y posiblemente incluso menos si todas las operaciones del usuario de gama alta se invocan a través de un sistema de comando central).

Limpieza de recursos

Sin embargo, el manejo de excepciones solo resuelve la necesidad de evitar tratar manualmente los aspectos del flujo de control de la propagación de errores en rutas excepcionales separadas de los flujos normales de ejecución. A menudo, una función que sirve como un propagador de errores, incluso si lo hace automáticamente ahora con EH, podría adquirir algunos recursos que necesita destruir. Por ejemplo, dicha función podría abrir un archivo temporal que necesita cerrar antes de regresar de la función sin importar qué, o bloquear un mutex que necesita desbloquear sin importar qué.

Para esto, podría invocar la ira de muchos programadores de todo tipo de lenguajes, pero creo que el enfoque de C ++ es ideal. El lenguaje introduce destructores que se invocan de manera determinista en el instante en que un objeto sale del alcance. Debido a esto, el código C ++ que, por ejemplo, bloquea un mutex a través de un objeto mutex con un destructor no necesita desbloquearlo manualmente, ya que se desbloqueará automáticamente una vez que el objeto salga del alcance sin importar lo que suceda (incluso si se produce una excepción encontrado). Por lo tanto, realmente no es necesario que un código C ++ bien escrito tenga que lidiar con la limpieza de recursos locales.

En los idiomas que carecen de destructores, es posible que necesiten usar un finallybloque para limpiar manualmente los recursos locales. Dicho esto, aún supera tener que ensuciar su código con propagación de errores manual siempre que no tenga que hacer catchexcepciones en todo el lugar.

Revertir los efectos secundarios externos

Este es el problema conceptual más difícil de resolver. Si alguna función, ya sea un propagador de error o un punto de falla, causa efectos secundarios externos, entonces debe revertir o "deshacer" esos efectos secundarios para devolver el sistema a un estado como si la operación nunca ocurriera, en lugar de un " medio válido "estado en el que la operación hasta la mitad tuvo éxito. No conozco ningún lenguaje que facilite mucho este problema conceptual, excepto los que simplemente reducen la necesidad de que la mayoría de las funciones causen efectos secundarios externos en primer lugar, como los lenguajes funcionales que giran en torno a la inmutabilidad y las estructuras de datos persistentes.

Podría finallydecirse que esta es una de las soluciones más elegantes para el problema en los lenguajes que giran en torno a la mutabilidad y los efectos secundarios, porque a menudo este tipo de lógica es muy específica para una función en particular y no se corresponde tan bien con el concepto de "limpieza de recursos". ". Y recomiendo usarlo finallylibremente en estos casos para asegurarse de que su función revierta los efectos secundarios en los idiomas que lo admiten, independientemente de si necesita o no un catchbloqueo (y de nuevo, si me pregunta, el código bien escrito debe tener la cantidad mínima de catchbloques, y todos los catchbloques deben estar en lugares donde tenga más sentido como en el diagrama de arriba Load Image User Command).

Lenguaje soñado

Sin embargo, IMO finallyes casi ideal para la reversión de efectos secundarios, pero no del todo. Necesitamos introducir una booleanvariable para revertir efectivamente los efectos secundarios en el caso de una salida prematura (de una excepción lanzada o de otra manera), así:

bool finished = false;
try
{
    // Cause external side effects.
    ...

    // Indicate that all the external side effects were
    // made successfully.
    finished = true; 
}
finally
{
    // If the function prematurely exited before finishing
    // causing all of its side effects, whether as a result of
    // an early 'return' statement or an exception, undo the
    // side effects.
    if (!finished)
    {
        // Undo side effects.
        ...
    }
}

Si alguna vez pudiera diseñar un lenguaje, mi forma ideal de resolver este problema sería la siguiente: automatizar el código anterior:

transaction
{
    // Cause external side effects.
    ...
}
rollback
{
    // This block is only executed if the above 'transaction'
    // block didn't reach its end, either as a result of a premature
    // 'return' or an exception.

    // Undo side effects.
    ...
}

... con destructores para automatizar la limpieza de los recursos locales, haciendo que solo lo necesitemos transaction, rollbacky catch(aunque todavía podría querer agregar finally, por ejemplo, trabajar con recursos C que no se limpian solos). Sin embargo, finallycon una booleanvariable es lo más parecido a hacer esto directo que he encontrado hasta ahora sin el lenguaje de mis sueños. La segunda solución más sencilla que he encontrado para esto son los protectores de alcance en lenguajes como C ++ y D, pero siempre encontré los protectores de alcance un poco incómodos conceptualmente, ya que desdibuja la idea de "limpieza de recursos" y "inversión de efectos secundarios". En mi opinión, esas son ideas muy distintas que deben abordarse de manera diferente.

Mi pequeño sueño de un lenguaje también giraría en gran medida en torno a la inmutabilidad y las estructuras de datos persistentes para que sea mucho más fácil, aunque no obligatorio, escribir funciones eficientes que no tengan que copiar en profundidad estructuras de datos masivas en su totalidad a pesar de que la función causa sin efectos secundarios.

Conclusión

De todos modos, con mis divagaciones a un lado, creo que su try/finallycódigo para cerrar el zócalo está bien y es excelente teniendo en cuenta que Python no tiene el equivalente C ++ de destructores, y personalmente creo que debería usarlo libremente para lugares que necesitan revertir los efectos secundarios y minimice la cantidad de lugares donde tiene que ir catcha los lugares donde tiene más sentido.


fuente
-66

Se recomienda encarecidamente detectar errores / excepciones y manejarlos de manera ordenada, incluso si no es obligatorio.

La razón por la que digo esto es porque creo que cada desarrollador debe conocer y abordar el comportamiento de su aplicación; de lo contrario, no ha completado su trabajo de manera adecuada. No existe una situación en la que un bloque try-finally sustituya al bloque try-catch-finally.

Le daré un ejemplo simple: suponga que ha escrito el código para cargar archivos en el servidor sin detectar excepciones. Ahora, si por alguna razón falla la carga, el cliente nunca sabrá qué salió mal. Pero, si ha detectado la excepción, puede mostrar un mensaje de error claro que explique qué salió mal y cómo puede remediarlo el usuario.

Regla de oro: siempre atrapa la excepción, porque adivinar lleva tiempo

Pankaj Upadhyay
fuente
22
-1: en Java, puede ser necesaria una cláusula final para liberar recursos (por ejemplo, cerrar un archivo o liberar una conexión de base de datos). Eso es independiente de la capacidad de manejar una excepción.
Kevin Cline
@kevincline, Él no está preguntando si usarlo finalmente o no ... Todo lo que está preguntando es si se requiere o no atrapar una excepción ... Él sabe qué intentar, atrapar y finalmente lo hace ..... Finalmente es el parte más esencial, todos lo sabemos y por qué se usa ...
Pankaj Upadhyay
63
@ Pankaj: su respuesta sugiere que catchsiempre debe haber una cláusula siempre que haya un try. Los contribuyentes más experimentados, incluido yo, creen que este es un mal consejo. Tu razonamiento es defectuoso. El método que contiene tryno es el único lugar posible donde se puede detectar la excepción. A menudo es más simple y mejor permitir capturas e informar excepciones desde el nivel más alto, en lugar de duplicar cláusulas de captura en todo el código.
Kevin Cline
@kevincline, creo que la percepción de la pregunta por parte de otros es un poco diferente. La pregunta era precisamente si deberíamos detectar una excepción o no ... Y respondí al respecto. El manejo de la excepción se puede hacer de varias maneras y no solo intentar-finalmente. Pero, esa no era la preocupación de OP. Si plantear una excepción es lo suficientemente bueno para ti y para otros chicos, entonces debo decir la mejor de las suertes.
Pankaj Upadhyay
Con excepciones, desea que se interrumpa la ejecución normal de las declaraciones (y sin verificar manualmente el éxito en cada paso). Este es especialmente el caso con llamadas a métodos profundamente anidados: un método de 4 capas dentro de alguna biblioteca no puede simplemente "tragar" una excepción; necesita ser expulsado a través de todas las capas. Por supuesto, cada capa podría envolver la excepción y agregar información adicional; Esto a menudo no se hace por varias razones, tiempo adicional para desarrollar y verbosidad innecesaria son los dos más grandes.
Daniel B