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 catch
bloqueo, 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:
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 NULL
o 0
o -1
o 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 Scanline
podría tener que manejar una falla malloc
y luego devolver un error a Convert Scanlines
, luego Convert Scanlines
tendría que verificar ese error y pasarlo a Decompress Image
, luego Decompress Image->Parse Image
, y Parse Image->Load Image
, y Load Image
al 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 error
de 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 catch
bloques en toda su base de código. En el diagrama anterior, el único lugar que debería tener un catch
bloque es Load Image User Command
donde se informa el error. Idealmente, nada más debería tener catch
algo 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 catch
bloques (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 finally
bloque 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 catch
excepciones 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 finally
decirse 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 finally
libremente 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 catch
bloqueo (y de nuevo, si me pregunta, el código bien escrito debe tener la cantidad mínima de catch
bloques, y todos los catch
bloques 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 finally
es casi ideal para la reversión de efectos secundarios, pero no del todo. Necesitamos introducir una boolean
variable 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
, rollback
y catch
(aunque todavía podría querer agregar finally
, por ejemplo, trabajar con recursos C que no se limpian solos). Sin embargo, finally
con una boolean
variable 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/finally
có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 catch
a los lugares donde tiene más sentido.