Excepciones de Kotlin y sus características.

Nuestra empresa ha estado utilizando Kotlin en producción durante más de dos años. Personalmente, me encontré con este idioma hace aproximadamente un año. Hay muchos temas de discusión, pero hoy hablaremos sobre el manejo de errores, incluso en un estilo funcional. Te diré cómo hacer esto en Kotlin.

imagen

(Foto de la reunión sobre este tema, que tuvo lugar en la oficina de una de las compañías de Taganrog. Alexey Shafranov, el líder del grupo de trabajo (Java) en Maxilekt, habló)

¿Cómo se pueden manejar los errores en principio?


Encontré varias formas:

  • puede usar algún valor de retorno como puntero al hecho de que hay un error;
  • puedes usar el parámetro indicador para el mismo propósito,
  • ingrese una variable global
  • manejar excepciones
  • Agregar contratos (DbC) .

Consideremos con más detalle cada una de las opciones.

Valor de retorno


Se devuelve un cierto valor "mágico" si se produce un error. Si alguna vez ha usado lenguajes de script, debe haber visto construcciones similares.

Ejemplo 1:

function sqrt(x) { if(x < 0) return -1; else return √x; } 

Ejemplo 2

 function getUser(id) { result = db.getUserById(id) if (result) return result as User else return “Can't find user ” + id } 

Parámetro indicador


Se utiliza un cierto parámetro pasado a la función. Después de devolver el valor por el parámetro, puede ver si hubo un error dentro de la función.

Un ejemplo:

 function divide(x,y,out Success) { if (y == 0) Success = false else Success = true return x/y } divide(10, 11, Success) id (!Success) //handle error 

Variable global


La variable global funciona aproximadamente de la misma manera.

Un ejemplo:

 global Success = true function divide(x,y) { if (y == 0) Success = false else return x/y } divide(10, 11, Success) id (!Success) //handle error 

Excepciones


Todos estamos acostumbrados a las excepciones. Se usan en casi todas partes.

Un ejemplo:

 function divide(x,y) { if (y == 0) throw Exception() else return x/y } try{ divide(10, 0)} catch (e) {//handle exception} 

Contratos (DbC)


Francamente, nunca he visto este enfoque en vivo. Al buscar en Google por mucho tiempo, descubrí que Kotlin 1.3 tiene una biblioteca que realmente permite el uso de contratos. Es decir puede establecer la condición en las variables que se pasan a la función, la condición en el valor de retorno, el número de llamadas, desde dónde se llama, etc. Y si se cumplen todas las condiciones, se cree que la función funcionó correctamente.

Un ejemplo:

 function sqrt (x) pre-condition (x >= 0) post-condition (return >= 0) begin calculate sqrt from x end 

Honestamente, esta biblioteca tiene una sintaxis terrible. Quizás es por eso que no he visto algo así en vivo.

Excepciones en Java


Pasemos a Java y cómo funcionó todo desde el principio.

imagen

Al diseñar un lenguaje, se establecieron dos tipos de excepciones:

  • comprobado - comprobado;
  • sin marcar - sin marcar.

¿Para qué se verifican las excepciones? Teóricamente, son necesarios para que las personas deben verificar si hay errores. Es decir Si es posible una cierta excepción verificada, entonces debe verificarse más adelante. Teóricamente, este enfoque debería haber llevado a la ausencia de errores no procesados ​​y a una mejor calidad del código. Pero en la práctica esto no es así. Creo que todos al menos una vez en su vida vieron un bloque vacío.

¿Por qué puede ser esto malo?

Aquí hay un ejemplo clásico directamente de la documentación de Kotlin: una interfaz del JDK implementada en StringBuilder:

 Appendable append(CharSequence csq) throws IOException; try { log.append(message) } catch (IOException e) { //Must be safe } 

Estoy seguro de que ha encontrado una gran cantidad de código envuelto en try-catch, donde catch es un bloque vacío, ya que tal situación simplemente no debería haber sucedido, según el desarrollador. En muchos casos, el manejo de excepciones verificadas se implementa de la siguiente manera: simplemente lanzan una RuntimeException y la capturan en algún lugar arriba (o no la capturan ...).

 try { // do something } catch (IOException e) { throw new RuntimeException(e); //  - ... 

Lo que es posible en Kotlin


En términos de excepciones, el compilador de Kotlin es diferente en eso:

1. No distingue entre excepciones marcadas y no marcadas. Todas las excepciones no están marcadas, y usted decide si las captura y procesa.

2. Try se puede usar como una expresión: puede ejecutar el bloque try y devolver la última línea o devolver la última línea del bloque catch.

 val value = try {Integer.parseInt(“lol”)} catch(e: NumberFormanException) { 4 } //  

3. También puede usar una construcción similar al referirse a algún objeto, que puede ser anulable:

 val s = obj.money ?: throw IllegalArgumentException(“ , ”) 

Compatibilidad Java


El código de Kotlin se puede usar en Java y viceversa. ¿Cómo manejar las excepciones?

  • Las excepciones comprobadas de Java en Kotlin no se pueden verificar ni declarar (ya que no hay excepciones comprobadas en Kotlin).
  • Las posibles excepciones verificadas de Kotlin (por ejemplo, aquellas que vinieron originalmente de Java) no requieren ser verificadas en Java.
  • Si es necesario verificar, la excepción puede hacerse verificable usando la anotación @Throws en el método (es necesario indicar qué excepciones puede arrojar este método). La anotación anterior es solo para compatibilidad con Java. Pero en la práctica, muchas personas lo usan para declarar que dicho método, en principio, puede arrojar algún tipo de excepción.

Alternativa al bloque try-catch


El bloque try-catch tiene un inconveniente significativo. Cuando aparece, parte de la lógica de negocios se transfiere dentro de la captura, y esto puede suceder en uno de los muchos métodos anteriores. Cuando la lógica de negocios se extiende por bloques o por toda la cadena de llamadas, es más difícil entender cómo funciona la aplicación. Y los bloques de legibilidad en sí mismos no agregan código.

 try { HttpService.SendNotification(endpointUrl); MarkNotificationAsSent(); } catch (e: UnableToConnectToServerException) { MarkNotificationAsNotSent(); } 

Cuales son las alternativas?

Una opción nos ofrece un enfoque funcional para el manejo de excepciones. Una implementación similar se ve así:

 val result: Try<Result> = Try{HttpService.SendNotification(endpointUrl)} when(result) { is Success -> MarkNotificationAsSent() is Failure -> MarkNotificationAsNotSent() } 

Tenemos la oportunidad de usar la mónada Try. En esencia, este es un contenedor que almacena algún valor. flatMap es un método de trabajo con este contenedor que, junto con el valor actual, puede tomar una función y, de nuevo, devolver una mónada.

En este caso, la llamada se envuelve en la mónada Try (devolvemos Try). Se puede procesar en un solo lugar, donde lo necesitamos. Si el resultado tiene un valor, realizamos las siguientes acciones con él, si se produce una excepción, lo procesamos al final de la cadena.

Manejo de excepciones funcionales


¿Dónde puedo conseguir probar?

Primero, hay bastantes implementaciones comunitarias de las clases Try y Either. Puede tomarlos o incluso escribir una implementación usted mismo. En uno de los proyectos de "combate", utilizamos la implementación de prueba hecha a sí misma: logramos con una clase e hicimos un excelente trabajo.
En segundo lugar, está la biblioteca Arrow, que en principio agrega mucha funcionalidad a Kotlin. Naturalmente, hay Try and Either.

Bueno, además, la clase Result apareció en Kotlin 1.3, que discutiré con más detalle más adelante.

Intente usar la biblioteca Arrow como ejemplo


La biblioteca Arrow nos da una clase de prueba. De hecho, puede estar en dos estados: Éxito o Fracaso:

  • El éxito en un retiro exitoso retendrá nuestro valor,
  • La falla almacena una excepción que ocurrió durante la ejecución de un bloque de código.

La llamada es la siguiente. Naturalmente, está envuelto en un intento regular de captura, pero esto sucederá en algún lugar dentro de nuestro código.

 sealed class Try<out A> { data class Success<out A>(val value: A) : Try<A>() data class Failure(val e: Throwable) : Try<Nothing>() companion object { operator fun <A> invoke(body: () -> A): Try<A> { return try { Success(body()) } catch (e: Exception) { Failure(e) } } } 

La misma clase debe implementar el método flatMap, que le permite pasar una función y devolver nuestra prueba monad:

 inline fun <B> map(f: (A) -> B): Try<B> = flatMap { Success(f(it)) } inline fun <B> flatMap(f: (A) -> TryOf<B>): Try<B> = when (this) { is Failure -> this is Success -> f(value) } 

¿Para qué es esto? Para no procesar errores para cada uno de los resultados cuando tenemos varios de ellos. Por ejemplo, obtuvimos varios valores de diferentes servicios y queremos combinarlos. De hecho, podemos tener dos situaciones: o las recibimos y combinamos con éxito, o algo cayó. Por lo tanto, podemos hacer lo siguiente:

 val result1: Try<Int> = Try { 11 } val result2: Try<Int> = Try { 4 } val sum = result1.flatMap { one -> result2.map { two -> one + two } } println(sum) //Success(value=15) 

Si ambas llamadas fueron exitosas y obtuvimos los valores, ejecutamos la función. Si no tienen éxito, Failure regresará con una excepción.

Así es como se ve si algo cae:

 val result1: Try<Int> = Try { 11 } val result2: Try<Int> = Try { throw RuntimeException(“Oh no!”) } val sum = result1.flatMap { one -> result2.map { two -> one + two } } println(sum) //Failure(exception=java.lang.RuntimeException: Oh no! 

Usamos la misma función, pero el resultado es una falla de una excepción RuntimeException.

Además, la biblioteca Arrow le permite utilizar construcciones que de hecho son azúcar sintáctica, en particular vinculante. De todos modos, se puede reescribir a través de un flatMap en serie, pero el enlace le permite hacerlo legible.

 val result1: Try<Int> = Try { 11 } val result2: Try<Int> = Try { 4 } val result3: Try<Int> = Try { throw RuntimeException(“Oh no, again!”) } val sum = binding { val (one) = result1 val (two) = result2 val (three) = result3 one + two + three } println(sum) //Failure(exception=java.lang.RuntimeException: Oh no, again! 

Dado que uno de los resultados ha caído, tenemos un error en la salida.

Se puede usar una mónada similar para llamadas asíncronas. Por ejemplo, aquí hay dos funciones que se ejecutan de forma asincrónica. Combinamos sus resultados de la misma manera, sin verificar por separado su estado:

 fun funA(): Try<Int> { return Try { 1 } } fun funB(): Try<Int> { Thread.sleep(3000L) return Try { 2 } } val a = GlobalScope.async { funA() } val b = GlobalScope.async { funB() } val sum = runBlocking { a.await().flatMap { one -> b.await().map {two -> one + two } } } 

Y aquí hay un ejemplo más de "combate". Tenemos una solicitud al servidor, la procesamos, obtenemos el cuerpo y tratamos de asignarla a nuestra clase, de la cual ya estamos devolviendo datos.

 fun makeRequest(request: Request): Try<List<ResponseData>> = Try { httpClient.newCall(request).execute() } .map { it.body() } .flatMap { Try { ObjectMapper().readValue(it, ParsedResponse::class.java) } } .map { it.data } fun main(args : Array<String>) { val response = makeRequest(RequestBody(args)) when(response) { is Try.Success -> response.data.toString() is Try.Failure -> response.exception.message } } 

Try-catch haría que este bloque fuera mucho menos legible. Y en este caso, obtenemos response.data en la salida, que podemos procesar según el resultado.

Resultado de Kotlin 1.3


Kotlin 1.3 introdujo la clase Resultado. De hecho, es algo similar a Try, pero con varias limitaciones. Originalmente está destinado a ser utilizado para varias operaciones asincrónicas.

 val result: Result<VeryImportantData> = Result.runCatching { makeRequest() } .mapCatching { parseResponse(it) } .mapCatching { prepareData(it) } result.fold{ { data -> println(“We have $data”) }, exception -> println(“There is no any data, but it's your exception $exception”) } ) 

Si no se equivoca, esta clase es actualmente experimental. Los desarrolladores de lenguaje pueden cambiar su firma, comportamiento o eliminarlo por completo, por lo que en este momento está prohibido usarlo como un valor de retorno de los métodos o una variable. Sin embargo, se puede usar como una variable local (privada). Es decir de hecho, puede usarse como una prueba del ejemplo.

Conclusiones


Conclusiones que hice por mí mismo:

  • El manejo de errores funcionales en Kotlin es simple y conveniente;
  • nadie se molesta en procesarlos mediante try-catch en el estilo clásico (tanto eso como eso tiene derecho a la vida; tanto eso como eso son convenientes);
  • la ausencia de excepciones marcadas no significa que no se puedan manejar los errores;
  • Excepciones no detectadas en la producción conducen a tristes consecuencias.

Autor del artículo: Alexey Shafranov, líder del grupo de trabajo (Java), Maxilect

PD: publicamos nuestros artículos en varios sitios de Runet. Suscríbase a nuestras páginas en VK , FB o Telegram-channel para conocer todas nuestras publicaciones y otras noticias de Maxilect.

Source: https://habr.com/ru/post/447380/


All Articles