Muchos lenguajes modernos ofrecen características de manejo de excepciones , pero el lenguaje de programación Swift de Apple no proporciona un mecanismo de manejo de excepciones .
Cargada de excepciones como estoy, tengo problemas para entender lo que esto significa. Swift tiene afirmaciones y, por supuesto, devuelve valores; pero tengo problemas para imaginar cómo mi forma de pensar basada en excepciones se asigna a un mundo sin excepciones (y, por lo demás, por qué ese mundo es deseable ). ¿Hay cosas que no puedo hacer en un lenguaje como Swift que podría hacer con excepciones? ¿Gano algo al perder excepciones?
¿Cómo, por ejemplo, podría expresar mejor algo como
try:
operation_that_can_throw_ioerror()
except IOError:
handle_the_exception_somehow()
else:
# we don't want to catch the IOError if it's raised
another_operation_that_can_throw_ioerror()
finally:
something_we_always_need_to_do()
en un idioma (Swift, por ejemplo) que carece de manejo de excepciones?
panic
cuál no es exactamente lo mismo. Además de lo que se dice allí, una excepción no es mucho más que una forma sofisticada (pero cómoda) de realizar unaGOTO
, aunque nadie lo llama así, por razones obvias.Respuestas:
En la programación incrustada, tradicionalmente no se permitían excepciones, porque la sobrecarga del desbobinado de la pila que tenía que hacer se consideraba una variabilidad inaceptable al intentar mantener el rendimiento en tiempo real. Si bien los teléfonos inteligentes podrían considerarse técnicamente plataformas en tiempo real, ahora son lo suficientemente potentes donde las antiguas limitaciones de los sistemas integrados ya no se aplican. Solo lo menciono en aras de la minuciosidad.
Las excepciones a menudo se admiten en lenguajes de programación funcionales, pero se usan tan raramente que es posible que no lo sean. Una razón es la evaluación diferida, que se realiza ocasionalmente incluso en idiomas que no son diferidos de manera predeterminada. Tener una función que se ejecuta con una pila diferente de la que estaba en la cola para ejecutar hace que sea difícil determinar dónde colocar su controlador de excepciones.
La otra razón es que las funciones de primera clase permiten construcciones como opciones y futuros que le brindan los beneficios sintácticos de las excepciones con más flexibilidad. En otras palabras, el resto del lenguaje es lo suficientemente expresivo como para que las excepciones no le compren nada.
No estoy familiarizado con Swift, pero lo poco que he leído sobre su manejo de errores sugiere que pretendían que el manejo de errores siguiera patrones de estilo más funcional. He visto ejemplos de código con
success
yfailure
bloques que se parecen mucho a futuros.Aquí hay un ejemplo usando una
Future
de este tutorial Scala :Puede ver que tiene aproximadamente la misma estructura que su ejemplo usando excepciones. El
future
bloque es como atry
. ElonFailure
bloque es como un controlador de excepciones. En Scala, como en la mayoría de los lenguajes funcionales,Future
se implementa completamente usando el lenguaje mismo. No requiere ninguna sintaxis especial como las excepciones. Eso significa que puedes definir tus propias construcciones similares. Tal vez agregue untimeout
bloque, por ejemplo, o la funcionalidad de registro.Además, puede pasar el futuro, devolverlo desde la función, almacenarlo en una estructura de datos o lo que sea. Es un valor de primera clase. No está limitado como las excepciones que deben propagarse directamente hacia la pila.
Las opciones resuelven el problema de manejo de errores de una manera ligeramente diferente, que funciona mejor para algunos casos de uso. No estás atrapado con el único método.
Esas son las cosas que "ganas al perder excepciones".
fuente
Future
es esencialmente una forma de examinar el valor devuelto por una función, sin detenerse a esperar. Al igual que Swift, se basa en el valor de retorno, pero a diferencia de Swift, la respuesta al valor de retorno puede ocurrir en un momento posterior (un poco como excepciones). ¿Derecho?Future
correcto, pero creo que podrías estar describiendo mal a Swift. Vea la primera parte de esta respuesta stackoverflow , por ejemplo.Either
sería un mejor ejemplo en mi humilde opiniónLas excepciones pueden hacer que el código sea más difícil de razonar. Si bien no son tan poderosos como los gotos, pueden causar muchos de los mismos problemas debido a su naturaleza no local. Por ejemplo, supongamos que tiene un código imperativo como este:
No puede decir de un vistazo si alguno de estos procedimientos puede generar una excepción. Tiene que leer la documentación de cada uno de estos procedimientos para resolverlo. (Algunos idiomas facilitan esto un poco al aumentar la firma de tipo con esta información.) El código anterior se compilará bien independientemente de si alguno de los procedimientos arroja, por lo que es muy fácil olvidar manejar una excepción.
Además, incluso si la intención es propagar la excepción a la persona que llama, a menudo es necesario agregar un código adicional para evitar que las cosas se queden en un estado inconsistente (por ejemplo, si su cafetera se rompe, aún necesita limpiar el desorden y regresar ¡La taza!). Por lo tanto, en muchos casos, el código que usa excepciones se vería tan complejo como el código que no lo hizo debido a la limpieza adicional requerida.
Las excepciones se pueden emular con un sistema de tipos suficientemente potente. Muchos de los lenguajes que evitan excepciones usan valores de retorno para obtener el mismo comportamiento. Es similar a cómo se hace en C, pero los sistemas de tipo moderno generalmente lo hacen más elegante y también más difícil de olvidar para manejar la condición de error. También pueden proporcionar azúcar sintáctica para hacer las cosas menos engorrosas, a veces casi tan limpias como lo serían con excepciones.
En particular, al incorporar el manejo de errores en el sistema de tipos en lugar de implementarlo como una característica separada, se pueden usar "excepciones" para otras cosas que ni siquiera están relacionadas con errores. (Es bien sabido que el manejo de excepciones es en realidad una propiedad de las mónadas).
fuente
goto
:goto
está restringido a una sola función, que es un rango bastante pequeño, siempre que la función sea realmente pequeña, la excepción actúa más como un cruce degoto
ycome from
(ver en.wikipedia.org/wiki/INTERCAL ;-)). Puede conectar casi cualquier dos piezas de código, posiblemente saltando sobre el código algunas terceras funciones. Lo único que no puede hacer, quegoto
puede, es regresar.Aquí hay algunas respuestas excelentes, pero creo que una razón importante no se ha enfatizado lo suficiente: cuando se producen excepciones, los objetos se pueden dejar en estados no válidos. Si puede "atrapar" una excepción, entonces su código de controlador de excepciones podrá acceder y trabajar con esos objetos no válidos. Eso va a ir terriblemente mal a menos que el código para esos objetos se haya escrito perfectamente, lo cual es muy, muy difícil de hacer.
Por ejemplo, imagine implementar Vector. Si alguien crea una instancia de su Vector con un conjunto de objetos, pero ocurre una excepción durante la inicialización (quizás, digamos, mientras copia sus objetos en la memoria recién asignada), es muy difícil codificar correctamente la implementación de Vector de tal manera que no Se pierde la memoria. Este breve artículo de Stroustroup cubre el ejemplo de Vector .
Y eso es simplemente la punta del iceberg. ¿Qué pasaría si, por ejemplo, hubiera copiado algunos de los elementos, pero no todos? Para implementar un contenedor como Vector correctamente, casi tiene que hacer que cada acción que realice sea reversible, por lo que toda la operación es atómica (como una transacción de base de datos). Esto es complicado y la mayoría de las aplicaciones se equivocan. E incluso cuando se hace correctamente, complica enormemente el proceso de implementación del contenedor.
Entonces, algunos idiomas modernos han decidido que no vale la pena. El óxido, por ejemplo, tiene excepciones, pero no se pueden "atrapar", por lo que no hay forma de que el código interactúe con objetos en un estado no válido.
fuente
Una cosa que me sorprendió inicialmente del lenguaje Rust es que no admite excepciones de captura. Puede lanzar excepciones, pero solo el tiempo de ejecución puede detectarlas cuando una tarea (piense en un hilo, pero no siempre un hilo separado del sistema operativo) muere; Si comienza una tarea usted mismo, puede preguntar si salió normalmente o si se
fail!()
editó.Como tal, no es idiomático
fail
muy a menudo. Los pocos casos en los que sucede son, por ejemplo, en el arnés de prueba (que no sabe cómo es el código de usuario), como el nivel superior de un compilador (la mayoría de los compiladores se bifurcan) o cuando se invoca una devolución de llamada en la entrada del usuario.En cambio, el idioma común es usar la
Result
plantilla para pasar explícitamente los errores que se deben manejar. Esto es mucho más fácil gracias a latry!
macro , que se puede envolver alrededor de cualquier expresión que produzca un Resultado y produzca el brazo exitoso si hay uno, o de lo contrario regresa temprano de la función.fuente
assert
, pero nocatch
?En mi opinión, las excepciones son una herramienta esencial para detectar errores de código en tiempo de ejecución. Tanto en pruebas como en producción. Haga que sus mensajes sean lo suficientemente detallados para que, en combinación con un seguimiento de la pila, pueda descubrir qué sucedió en un registro.
Las excepciones son principalmente una herramienta de desarrollo y una forma de obtener informes de error razonables de la producción en casos inesperados.
Aparte de la separación de las preocupaciones (el camino feliz con solo los errores esperados frente a la falla hasta llegar a un controlador genérico para errores inesperados) es algo bueno, hacer que su código sea más legible y mantenible, de hecho es imposible preparar su código para todo lo posible casos inesperados, incluso al hincharlo con código de manejo de errores para completar la ilegibilidad.
Ese es en realidad el significado de "inesperado".
Por cierto, lo que se espera y lo que no es una decisión que solo se puede tomar en el sitio de la llamada. Es por eso que las excepciones comprobadas en Java no funcionaron: la decisión se toma al momento de desarrollar una API, cuando no está del todo claro qué se espera o qué inesperado.
Ejemplo simple: la API de un mapa hash puede tener dos métodos:
y
el primero arroja una excepción si no se encuentra, el último le da un valor opcional. En algunos casos, este último tiene más sentido, pero en otros, su código debe esperar que haya un valor para una clave determinada, por lo que si no hay uno, es un error que este código no puede solucionar porque La suposición ha fallado. En este caso, en realidad es el comportamiento deseado salir de la ruta del código y pasar a un controlador genérico en caso de que falle la llamada.
El código nunca debe tratar con supuestos básicos fallidos.
Excepto al marcarlos y lanzar excepciones bien legibles, por supuesto.
Lanzar excepciones no es malo, pero atraparlas puede serlo. No intentes arreglar errores inesperados. Capture excepciones en algunos lugares donde desea continuar algún ciclo u operación, regístrelos, tal vez informe un error desconocido, y eso es todo.
Los bloques de captura en todo el lugar son una muy mala idea.
Diseñe sus API de manera que sea fácil expresar su intención, es decir, declarar si espera un caso determinado, como clave no encontrada o no. Los usuarios de su API pueden elegir la llamada de lanzamiento solo para casos realmente inesperados.
Supongo que la razón por la cual las personas resienten las excepciones y van demasiado lejos al omitir esta herramienta crucial para la automatización del manejo de errores y una mejor separación de las preocupaciones de los nuevos idiomas son malas experiencias.
Eso y algunos malentendidos sobre para qué son realmente buenos.
Simularlos haciendo TODO a través del enlace monádico hace que su código sea menos legible y fácil de mantener, y termina sin un seguimiento de la pila, lo que empeora este enfoque.
El manejo de errores de estilo funcional es ideal para casos de error esperados.
Deje que el manejo de excepciones se encargue automáticamente de todo lo demás, para eso está :)
fuente
Swift usa los mismos principios aquí que Objective-C, solo que más consecuentemente. En Objective-C, las excepciones indican errores de programación. No se manejan excepto por las herramientas de informe de fallas. El "manejo de excepciones" se realiza reparando el código. (Hay algunas excepciones de ejem. Por ejemplo, en las comunicaciones entre procesos. Pero eso es bastante raro y muchas personas nunca se topan con él. Y Objective-C en realidad tiene try / catch / finally / throw, pero rara vez los usas). Swift acaba de eliminar la posibilidad de detectar excepciones.
Swift tiene una característica que se parece al manejo de excepciones, pero solo es un manejo forzado de errores. Históricamente, Objective-C tenía un patrón de manejo de errores bastante generalizado: un método devolvería BOOL (YES para el éxito) o una referencia de objeto (nil para falla, no nil para éxito), y tenía un parámetro "puntero a NSError *" que se usaría para almacenar una referencia NSError. Swift convierte automáticamente las llamadas a dicho método en algo que parece un manejo de excepciones.
En general, las funciones Swift pueden devolver fácilmente alternativas, como un resultado si una función funcionó bien y un error si falla; eso hace que el manejo de errores sea mucho más fácil. Pero la respuesta a la pregunta original: los diseñadores de Swift obviamente sintieron que crear un lenguaje seguro y escribir código exitoso en dicho idioma es más fácil si el idioma no tiene excepciones.
fuente
En C terminarías con algo como lo anterior.
No, no hay nada Simplemente terminas manejando códigos de resultados en lugar de excepciones.
Las excepciones le permiten reorganizar su código para que el manejo de errores esté separado de su código de ruta feliz, pero eso es todo.
fuente
...throw_ioerror()
devolver errores en lugar de arrojar excepciones?Result<T, E>
enumeración, que puede ser anOk<T>
o anErr<E>
,T
siendo el tipo que deseaba si existiera yE
un tipo que representa un error. La coincidencia de patrones y algunos métodos particulares simplifican el manejo de éxitos y fracasos. En resumen, no asuma que la falta de excepciones significa automáticamente códigos de error.Además de la respuesta de Charlie:
Estos ejemplos de manejo de excepciones declarados que se ven en muchos manuales y libros, se ven muy inteligentes solo en ejemplos muy pequeños.
Incluso si deja de lado el argumento sobre el estado de objeto no válido, siempre provocan un gran dolor cuando se trata de una aplicación grande.
Por ejemplo, cuando tiene que lidiar con IO, usando alguna criptografía, puede tener 20 tipos de excepciones que pueden ser eliminadas de 50 métodos. Imagine la cantidad de código de manejo de excepciones que necesitará. El manejo de excepciones tomará varias veces más código que el código mismo.
En realidad, sabe cuándo no puede aparecer una excepción y simplemente no necesita escribir tanto manejo de excepciones, por lo que solo usa algunas soluciones para ignorar las excepciones declaradas. En mi práctica, solo alrededor del 5% de las excepciones declaradas deben manejarse en código para tener una aplicación confiable.
fuente