¿Por qué diseñar un lenguaje moderno sin un mecanismo de manejo de excepciones?

43

Muchos lenguajes modernos ofrecen una gran variedad de funciones de manejo de excepciones , pero el lenguaje de programación Swift de Apple es no proporciona un mecanismo de manejo de excepciones .

Empapado de excepciones como yo, tengo problemas para comprender lo que esto significa. Swift tiene afirmaciones y, por supuesto, devuelve valores; pero me cuesta imaginarme cómo mi forma de pensar basada en excepciones se asigna a un mundo sin excepciones (y, en realidad, por qué un mundo así es deseable ). ¿Hay cosas que no pueda hacer en un lenguaje como Swift que pueda hacer con excepciones? ¿Gano algo al perder excepciones?

¿Cómo puedo expresar mejor, por ejemplo, algo así 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?

    
pregunta orome 03.10.2014 - 20:47

8 respuestas

32

En la programación incorporada, tradicionalmente no se permitían las excepciones, ya que la sobrecarga de desenrollado 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 como plataformas en tiempo real, ahora son lo suficientemente poderosos donde las antiguas limitaciones de los sistemas integrados ya no se aplican. Solo lo menciono por el bien de la minuciosidad.

Las excepciones a menudo se admiten en los lenguajes de programación funcionales, pero rara vez se usan para que no sean así. Una razón es la evaluación perezosa, que se realiza ocasionalmente incluso en idiomas que no son perezosos de forma predeterminada. Tener una función que se ejecute con una pila diferente a la del lugar donde se puso en cola para ejecutarse hace que sea difícil determinar dónde colocar el 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 intentaron que el manejo de errores siguiera patrones de estilo más funcional. He visto ejemplos de código con los bloques success y failure que se parecen mucho a los futuros.

Aquí hay un ejemplo que utiliza Future de este tutorial de Scala :

val f: Future[List[String]] = future {
  session.getRecentPosts
}
f onFailure {
  case t => println("An error has occured: " + t.getMessage)
}
f onSuccess {
  case posts => for (post <- posts) println(post)
}

Puedes ver que tiene aproximadamente la misma estructura que tu ejemplo usando excepciones. El bloque future es como un try . El bloque onFailure es como un controlador de excepciones. En Scala, como en la mayoría de los lenguajes funcionales, Future se implementa completamente usando el propio lenguaje. No requiere ninguna sintaxis especial como lo hacen las excepciones. Eso significa que puedes definir tus propias construcciones similares. Tal vez agregue un bloque timeout , 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 hacia arriba en la pila.

Las opciones resuelven el problema de manejo de errores de una manera ligeramente diferente, lo que funciona mejor para algunos casos de uso. No estás atascado con un solo método.

Esas son el tipo de cosas que "ganas al perder excepciones".

    
respondido por el Karl Bielefeldt 03.10.2014 - 23:00
17

Las 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:

cleanMug();
brewCoffee();
pourCoffee();
drinkCoffee();

No puede saber de un vistazo si alguno de estos procedimientos puede generar una excepción. Tienes que leer la documentación de cada uno de estos procedimientos para resolverlo. (Algunos idiomas hacen que esto sea un poco más fácil al aumentar la firma de tipo con esta información). El código anterior se compilará sin importar si se produce alguno de los procedimientos, lo que hace que sea muy fácil olvidarse de manejar una excepción.

Además, incluso si la intención es propagar la excepción de vuelta a la persona que llama, a menudo se necesita agregar código adicional para evitar que las cosas queden en un estado inconsistente (por ejemplo, si su cafetera se rompe, aún necesita limpiar el ensuciar y devolver 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 idiomas que evitan las 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 si fueran excepciones.

En particular, al incorporar el manejo de errores en el sistema de tipos en lugar de implementarlo como una función separada, se pueden usar "excepciones" para otras cosas que ni siquiera están relacionadas con los errores. (Es bien sabido que el manejo de excepciones es en realidad una propiedad de las mónadas).

    
respondido por el Rufflewind 04.10.2014 - 06:06
9

Aquí hay algunas respuestas geniales, pero creo que una razón importante no se ha enfatizado lo suficiente: Cuando se producen excepciones, los objetos pueden quedar en estados no válidos. Si puede "capturar" una excepción, entonces su código de controlador de excepciones podrá acceder y trabajar con esos objetos no válidos. Eso irá terriblemente mal a menos que el código para esos objetos esté escrito perfectamente, lo cual es muy, muy difícil de hacer.

Por ejemplo, imagina implementando Vector. Si alguien crea una instancia de su Vector con un conjunto de objetos, pero se produce una excepción durante la inicialización (quizás, por ejemplo, al copiar sus objetos en la memoria recién asignada), es muy difícil codificar correctamente la implementación del Vector de tal manera que no la memoria se ha filtrado. 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.

Así que algunas lenguas modernas han decidido que no vale la pena. La oxidación, por ejemplo, tiene excepciones, pero no pueden ser "detectadas", por lo que no hay forma de que el código interactúe con objetos en un estado no válido.

    
respondido por el Charlie Flowers 12.02.2015 - 21:31
6

Algo que inicialmente me sorprendió 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 (hilo de pensar, pero no siempre un hilo de sistema operativo independiente) muere; Si inicia una tarea usted mismo, puede preguntar si salió normalmente o si fue fail!() ed.

Como tal, no es idiomático a fail muy a menudo. Los pocos casos en los que ocurre 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 (en su lugar, la mayoría de los compiladores) o cuando se invoca una devolución de llamada. en la entrada del usuario.

En cambio, el idioma común es utilizar la plantilla Result para explícitamente pasar errores que deben ser manejados. Esto se hace mucho más fácil mediante la macro try! , que se puede envolver alrededor de cualquier expresión que produce un resultado y produce el brazo exitoso si lo hay, o si no regresa antes de la función.

use std::io::IoResult;
use std::io::File;

fn load_file(name: &Path) -> IoResult<String>
{
    let mut file = try!(File::open(name));
    let s = try!(file.read_to_string());
    return Ok(s);
}

fn main()
{
    print!("{}", load_file(&Path::new("/tmp/hello")).unwrap());
}
    
respondido por el o11c 04.10.2014 - 06:53
5

En mi opinión, las excepciones son una herramienta esencial para detectar errores de código en el 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 pila, pueda averiguar qué sucedió en un registro.

Las excepciones son principalmente una herramienta de desarrollo y una forma de obtener informes de errores razonables de producción en casos inesperados.

Aparte de la separación de inquietudes (la ruta feliz con solo los errores esperados y la caída hasta llegar a algún controlador genérico para errores inesperados) es algo bueno, que hace que su código sea más legible y mantenible, de hecho, es imposible preparar su código. para todos los posibles casos inesperados, incluso hinchándolo con el código de manejo de errores para completar la ilegibilidad.

Ese es realmente 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 verificadas en Java no funcionaron: la decisión se toma al momento de desarrollar una API, cuando no está claro en absoluto qué se espera o no se espera.

Ejemplo simple: la API de un mapa hash puede tener dos métodos:

Value get(Key)

y

Option<Value> getOption(key)

el primero lanza 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 tener en cuenta que debe haber un valor para una clave determinada, por lo que si no hay uno, es un error que este código no puede corregir porque es un error básico. La suposición ha fallado. En este caso, en realidad es el comportamiento deseado el que se salga de la ruta del código y caiga hacia algún controlador genérico en caso de que la llamada falle.

El código nunca debe tratar de lidiar con suposiciones básicas fallidas.

Excepto al verificarlos y lanzar excepciones legibles, por supuesto.

Lanzar excepciones no es malo, pero atraparlas puede ser. No intentes arreglar errores inesperados. Detecte excepciones en algunos lugares donde desea continuar con algún ciclo u operación, regístrelas, quizás 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 le sea fácil expresar su intención, es decir, declarar si espera un caso determinado, como la 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 que las personas rechazan 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 inquietudes de los nuevos idiomas son malas experiencias.

Eso, y algunos malentendidos acerca de para qué sirven realmente.

Simularlos haciendo TODO a través de un enlace monádico hace que tu código sea menos legible y mantenible, y terminas sin un seguimiento de pila, lo que hace que este enfoque sea peor.

El manejo de errores de estilo funcional es excelente para los casos de error esperados.

Deje que el manejo de excepciones se encargue automáticamente de todo lo demás, para eso está :)

    
respondido por el yeoman 31.05.2016 - 09:38
3

Swift usa los mismos principios aquí que Objective-C, más en consecuencia. En Objective-C, las excepciones indican errores de programación. No se manejan, excepto por las herramientas de informes de errores. "Manejo de excepciones" se realiza mediante la fijación del código. (Hay algunas excepciones, por ejemplo, en las comunicaciones entre procesos. Pero eso es bastante raro y muchas personas nunca lo encuentran. Y Objective-C en realidad tiene try / catch / finally / throw, pero rara vez las usa). Swift acaba de eliminar la posibilidad de atrapar excepciones.

Swift tiene una característica que parece el 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 un BOOL (SÍ para el éxito) o una referencia de objeto (nulo para el fracaso, no nulo para el éxito) y tendría un parámetro "puntero a NSError *" que se utilizaría para almacenar una referencia NSError. Swift convierte automáticamente las llamadas a un método de este tipo en algo que se parece al manejo de excepciones.

En general, las funciones Swift pueden devolver alternativas fácilmente, como un resultado si una función funcionó bien y un error si falló; 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 un código exitoso en ese idioma es más fácil si el idioma no tiene excepciones.

    
respondido por el gnasher729 29.08.2016 - 18:43
2
 int result;
 if((result = operation_that_can_throw_ioerror()) == IOError)
 {
  handle_the_exception_somehow();
 }
 else
 {
   # we don't want to catch the IOError if it's raised
   result = another_operation_that_can_throw_ioerror();
 }
 result |= something_we_always_need_to_do();
 return result;

En C, terminarías con algo como lo anterior.

  

¿Hay cosas que no pueda hacer en Swift que pueda hacer con excepciones?

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 sea independiente de su código de ruta feliz, pero eso es todo.

    
respondido por el stonemetal 03.10.2014 - 21:27
1

Además de la respuesta de Charlie:

Estos ejemplos de manejo de excepciones declaradas que se ven en muchos manuales y libros, parecen 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 al tratar con una aplicación grande.

Por ejemplo, cuando tiene que lidiar con IO, usando algo de criptografía, puede tener 20 tipos de excepciones que se pueden descartar de los 50 métodos. Imagina la cantidad de código de manejo de excepciones que necesitarás. 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 nunca necesita escribir tanta gestión de excepciones, por lo que solo usa algunas soluciones para ignorar las excepciones declaradas. En mi práctica, solo el 5% de las excepciones declaradas deben manejarse en código para tener una aplicación confiable.

    
respondido por el konmik 28.08.2016 - 14:51