unreachable - try catch swift 4 example
¿Por qué ''throws'' no son seguros en Swift? (2)
El mayor malentendido para mí en Swift es la palabra clave throws
. Considere la siguiente pieza de código:
func myUsefulFunction() throws
Realmente no podemos entender qué tipo de error arrojará. Lo único que sabemos es que podría arrojar algún error. La única forma de entender cuál es el error es consultar la documentación o verificar el error en tiempo de ejecución.
¿Pero no es esto contra la naturaleza de Swift? Swift tiene poderosos genéricos y un sistema de tipos para hacer que el código sea expresivo, sin embargo, se siente como si los throws
son exactamente opuestos porque no se puede obtener nada acerca del error al observar la firma de la función.
¿Por qué es así? ¿O me he perdido algo importante y confundido el concepto?
Fui uno de los primeros en proponer errores tipeados en Swift. Así es como el equipo de Swift me convenció de que estaba equivocado.
Los errores fuertemente tipados son frágiles en formas que pueden conducir a una evolución API pobre. Si la API promete arrojar solo uno de 3 errores precisos, cuando aparece una cuarta condición de error en una versión posterior, tengo una opción: la entierro de alguna manera en la 3 existente, o fuerzo a cada persona que llama a reescribir su código de manejo de errores para lidiar con eso. Como no estaba en el 3 original, probablemente no es una condición muy común, y esto ejerce una fuerte presión sobre las API para que no expandan su lista de errores, especialmente una vez que un framework tiene un uso extensivo durante un largo tiempo (piense: Foundation )
Por supuesto, con enumeraciones abiertas, podemos evitar eso, pero una enumeración abierta no logra ninguno de los objetivos de un error fuertemente tipado. Básicamente es un error sin tipo nuevamente porque aún necesita un "valor predeterminado".
Aún puede decir "al menos sé de dónde viene el error con una enumeración abierta", pero esto tiende a empeorar las cosas. Digamos que tengo un sistema de registro e intenta escribir y obtiene un error de IO. ¿Qué debería devolver? Swift no tiene tipos de datos algebraicos (no puedo decir () -> IOError | LoggingError
), así que probablemente tenga que ajustar IOError
en LoggingError.IO(IOError)
(lo que fuerza a cada capa a LoggingError.IO(IOError)
a envolver explícitamente; no tengo rethrows
muy a menudo). Incluso si tuviera ADT, ¿realmente quieres IOError | MemoryError | LoggingError | UnexpectedError | ...
IOError | MemoryError | LoggingError | UnexpectedError | ...
IOError | MemoryError | LoggingError | UnexpectedError | ...
? Una vez que tienes unas cuantas capas, termino con una capa sobre otra de envoltura de alguna "causa raíz" subyacente que tiene que ser desenvuelta dolorosamente para que puedas manejarla.
¿Y cómo vas a lidiar con eso? En la abrumadora mayoría de los casos, ¿cómo se ven los bloques de captura?
} catch {
logError(error)
return
}
Es extremadamente poco común que los programas de Cocoa (es decir, "aplicaciones") investiguen en profundidad la causa raíz exacta del error y realicen diferentes operaciones en función de cada caso concreto. Puede haber uno o dos casos que se recuperen, y el resto son cosas de las que no podrías hacer nada. (Este es un problema común en Java con excepción comprobada que no es solo Exception
, no es como si nadie hubiera pasado por este camino antes. Me gustan los argumentos de Yegor Bugayenko para las excepciones comprobadas en Java que básicamente argumentan que su práctica preferida de Java es exactamente la Solución Swift.)
Esto no quiere decir que no haya casos en los que los errores fuertemente tipados sean extremadamente útiles. Pero hay dos respuestas a esto: primero, usted es libre de implementar los errores fuertemente tipados por su cuenta con una enumeración y obtener una buena aplicación del compilador. No es perfecto (aún necesita una captura predeterminada fuera de la declaración de cambio, pero no dentro ), pero es bastante bueno si sigue algunas convenciones por su cuenta.
En segundo lugar, si este caso de uso resulta ser importante (y podría serlo), no es difícil agregar errores fuertemente tipados más adelante para esos casos sin romper los casos comunes que requieren un manejo de errores bastante genérico. Simplemente agregarían sintaxis:
func something() throws MyError { }
Y las personas que llaman tendrían que tratar eso como un tipo fuerte.
Por último, para que los errores fuertemente tipados sean de mucha utilidad, Foundation tendría que lanzarlos ya que es el mayor productor de errores en el sistema. (¿Con qué frecuencia creas realmente un NSError
desde cero en comparación con uno generado por Foundation?) Eso sería una revisión masiva de Foundation y muy difícil de mantener compatible con el código existente y ObjC. Por lo tanto, los errores mecanografiados deberían ser absolutamente fantásticos para resolver problemas de cacao muy comunes que valdría la pena considerar como el comportamiento predeterminado. No podría ser solo un poco más agradable (y mucho menos tener los problemas descritos anteriormente).
Así que nada de esto quiere decir que los errores sin tipo sean la solución 100% perfecta para el manejo de errores en todos los casos. Pero estos argumentos me convencieron de que era la forma correcta de ir a Swift hoy.
La elección es una decisión de diseño deliberada.
No querían la situación en la que no necesitas declarar lanzamiento de excepción como en Objective-C, C ++ y C # porque eso hace que las personas que llaman tengan que suponer que todas las funciones arrojen excepciones e incluir un texto repetitivo para manejar excepciones que podrían no suceder, o solo ignora la posibilidad de excepciones. Ninguno de estos es ideal y el segundo hace excepciones inutilizables, excepto en el caso en que desee finalizar el programa, ya que no puede garantizar que cada función en la pila de llamadas haya desasignado los recursos correctamente cuando se desenrolla la pila.
El otro extremo es la idea que usted ha defendido y que cada tipo de excepción emitida puede declararse. Desafortunadamente, las personas parecen objetar la consecuencia de esto, que es que tienes un gran número de bloques de captura para que puedas manejar cada tipo de excepción. Entonces, por ejemplo, en Java, lanzarán Exception
reduciendo la situación al mismo nivel que tenemos en Swift o peor, usan excepciones no verificadas para que puedas ignorar el problema por completo. La biblioteca GSON es un ejemplo de este último enfoque.
Elegimos usar excepciones sin marcar para indicar una falla de análisis. Esto se realiza principalmente porque, por lo general, el cliente no puede recuperarse de una entrada incorrecta y, por lo tanto, forzarlos a capturar una excepción comprobada resulta en un código descuidado en el bloque catch ().
https://github.com/google/gson/blob/master/GsonDesignDocument.md
Esa es una decisión atrozmente mala. "Hola, no se puede confiar en que hagas tu propio manejo de errores, por lo que tu aplicación debería bloquearse".
Personalmente, creo que Swift tiene el equilibrio correcto. Tienes que manejar los errores, pero no tienes que escribir montones de declaraciones catch para hacerlo. Si iban más allá, las personas encontrarían formas de subvertir el mecanismo.
El razonamiento completo de la decisión de diseño está en https://github.com/apple/swift/blob/master/docs/ErrorHandlingRationale.rst
EDITAR
Parece que algunas personas tienen problemas con algunas de las cosas que dije. Entonces aquí hay una explicación.
Hay dos amplias categorías de razones por las cuales un programa puede arrojar una excepción.
- condiciones inesperadas en el entorno externo al programa, como un error IO en un archivo o datos mal formados. Estos son errores que la aplicación generalmente puede manejar, por ejemplo, al informar el error al usuario y permitirle elegir un curso de acción diferente.
- Errores en la programación, como puntero nulo o errores vinculados a la matriz. La forma correcta de corregir esto es que el programador haga un cambio de código.
El segundo tipo de error no debe, en general, ser capturado, ya que indican una suposición falsa sobre el entorno que podría significar que los datos del programa están corruptos. Ahí no hay forma de continuar de manera segura, así que debes abortar.
El primer tipo de error generalmente se puede recuperar, pero para recuperarlo con seguridad, cada marco de pila debe desenrollarse correctamente, lo que significa que la función correspondiente a cada marco de pila debe ser consciente de que las funciones a las que llama pueden arrojar una excepción y dar pasos para asegurar que todo se limpia de forma consistente si se lanza una excepción, con, por ejemplo, un bloque finally o equivalente. Si el compilador no brinda soporte para avisarle al programador que ha olvidado planear excepciones, el programador no siempre planificará excepciones y escribirá código que filtra recursos o deja datos en un estado incoherente.
La razón por la cual la actitud gson es tan terrible es porque dicen que no puedes recuperarte de un error de análisis (en realidad, peor, te están diciendo que no tienes las habilidades para recuperarte de un error de análisis). Es una afirmación ridícula, las personas intentan analizar archivos JSON no válidos todo el tiempo. ¿Es bueno que mi programa falle si alguien selecciona un archivo XML por error? No, no es Debería informar el problema y pedirles que seleccionen un archivo diferente.
Y, por cierto, lo más importante fue solo un ejemplo de por qué el uso de excepciones sin marcar para errores de los que puede recuperarse es malo. Si quiero recuperarme de alguien que selecciona un archivo XML, necesito detectar excepciones de Java en tiempo de ejecución, pero ¿cuáles? Bueno, podría buscar en los documentos de Gson para averiguarlo, suponiendo que sean correctos y estén actualizados. Si se hubieran ido con las excepciones marcadas, la API me diría qué excepciones esperar y el compilador me diría si no las controlo.