Nossa empresa utiliza o Kotlin na produção há mais de dois anos. Pessoalmente, me deparei com esse idioma há cerca de um ano. Existem muitos tópicos para discussão, mas hoje falaremos sobre tratamento de erros, inclusive em um estilo funcional. Vou lhe dizer como fazer isso no Kotlin.
(Foto da reunião sobre esse tópico, realizada no escritório de uma das empresas de Taganrog. Falou Alexey Shafranov, líder do grupo de trabalho (Java) da Maxilekt)Como você pode lidar com erros em princípio?
Eu encontrei várias maneiras:
- você pode usar algum valor de retorno como um ponteiro para o fato de que há um erro;
- você pode usar o parâmetro do indicador para a mesma finalidade,
- insira uma variável global
- lidar com exceções
- Adicionar contratos (DbC) .
Vamos nos aprofundar em mais detalhes em cada uma das opções.
Valor de retorno
Um certo valor "mágico" é retornado se ocorrer um erro. Se você já usou linguagens de script, deve ter visto construções semelhantes.
Exemplo 1:
function sqrt(x) { if(x < 0) return -1; else return √x; }
Exemplo 2:
function getUser(id) { result = db.getUserById(id) if (result) return result as User else return “Can't find user ” + id }
Parâmetro indicador
Um determinado parâmetro passado para a função é usado. Depois de retornar o valor pelo parâmetro, você pode ver se houve um erro dentro da função.
Um exemplo:
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
Variável global
A variável global funciona aproximadamente da mesma maneira.
Um exemplo:
global Success = true function divide(x,y) { if (y == 0) Success = false else return x/y } divide(10, 11, Success) id (!Success) //handle error
Exceções
Estamos todos acostumados a exceções. Eles são usados em quase todos os lugares.
Um exemplo:
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 vi essa abordagem ao vivo. Pesquisando bastante, descobri que o Kotlin 1.3 tem uma biblioteca que permite o uso de contratos. I.e. você pode definir a condição nas variáveis que são passadas para a função, a condição no valor de retorno, o número de chamadas, de onde é chamado, etc. E se todas as condições forem atendidas, acredita-se que a função funcionou corretamente.
Um exemplo:
function sqrt (x) pre-condition (x >= 0) post-condition (return >= 0) begin calculate sqrt from x end
Honestamente, esta biblioteca tem uma sintaxe terrível. Talvez seja por isso que eu não tenha visto algo ao vivo.
Exceções em Java
Vamos para o Java e como tudo funcionou desde o início.

Ao projetar uma linguagem, dois tipos de exceções foram lançados:
- verificado - verificado;
- desmarcado - desmarcado.
Para que são verificadas as exceções? Teoricamente, eles são necessários para que as pessoas procurem erros. I.e. se uma certa exceção marcada for possível, ela deverá ser verificada mais tarde. Teoricamente, essa abordagem deveria ter levado à ausência de erros não processados e à melhoria da qualidade do código. Mas, na prática, não é assim. Acho que todo mundo pelo menos uma vez na vida viu um bloco de captura vazio.
Por que isso pode ser ruim?
Aqui está um exemplo clássico diretamente da documentação do Kotlin - uma interface do JDK implementada no StringBuilder:
Appendable append(CharSequence csq) throws IOException; try { log.append(message) } catch (IOException e) { //Must be safe }
Tenho certeza de que você encontrou bastante código envolvido no try-catch, onde catch é um bloco vazio, pois essa situação simplesmente não deveria ter acontecido, de acordo com o desenvolvedor. Em muitos casos, o tratamento de exceções verificadas é implementado da seguinte maneira: eles simplesmente lançam uma RuntimeException e a capturam em algum lugar acima (ou não a capturam ...).
try { // do something } catch (IOException e) { throw new RuntimeException(e); // - ...
O que é possível no Kotlin
Em termos de exceções, o compilador Kotlin é diferente:
1. Não faz distinção entre exceções verificadas e não verificadas. Todas as exceções são desmarcadas e você decide se deve capturá-las e processá-las.
2. Try pode ser usado como uma expressão - você pode executar o bloco try e retornar a última linha dele ou retornar a última linha do bloco catch.
val value = try {Integer.parseInt(“lol”)} catch(e: NumberFormanException) { 4 } //
3. Você também pode usar uma construção semelhante ao se referir a algum objeto, que pode ser anulável:
val s = obj.money ?: throw IllegalArgumentException(“ , ”)
Compatibilidade com Java
O código Kotlin pode ser usado em Java e vice-versa. Como lidar com exceções?
- As exceções verificadas do Java no Kotlin não podem ser verificadas nem declaradas (pois não há exceções verificadas no Kotlin).
- As possíveis exceções verificadas do Kotlin (por exemplo, aquelas que vieram originalmente do Java) não precisam ser verificadas no Java.
- Se for necessário verificar, a exceção pode ser verificada usando a anotação @Throws no método (você deve especificar quais exceções esse método pode lançar). A anotação acima é apenas para compatibilidade com Java. Mas, na prática, muitas pessoas o usam para declarar que esse método, em princípio, pode gerar algum tipo de exceção.
Alternativa ao bloco try-catch
O bloco try-catch tem uma desvantagem significativa. Quando aparece, parte da lógica de negócios é transferida dentro da captura e isso pode acontecer em um dos muitos métodos acima. Quando a lógica comercial se espalha por blocos ou por toda a cadeia de chamadas, fica mais difícil entender como o aplicativo funciona. E os próprios blocos de legibilidade não adicionam código.
try { HttpService.SendNotification(endpointUrl); MarkNotificationAsSent(); } catch (e: UnableToConnectToServerException) { MarkNotificationAsNotSent(); }
Quais são as alternativas?
Uma opção nos oferece uma abordagem funcional para o tratamento de exceções. Uma implementação semelhante é assim:
val result: Try<Result> = Try{HttpService.SendNotification(endpointUrl)} when(result) { is Success -> MarkNotificationAsSent() is Failure -> MarkNotificationAsNotSent() }
Temos a oportunidade de usar a mônada Try. Em essência, este é um contêiner que armazena algum valor. O flatMap é um método de trabalhar com esse contêiner que, junto com o valor atual, pode assumir uma função e, novamente, retornar uma mônada.
Nesse caso, a chamada é encerrada na mônada Try (retornamos Try). Pode ser processado em um único local - onde precisamos. Se a saída tiver um valor, executamos as seguintes ações com ela; se uma exceção for lançada, a processaremos no final da cadeia.
Tratamento de exceções funcionais
Onde posso obter o Try?
Primeiro, existem algumas implementações comunitárias das classes Try e Either. Você pode levá-los ou até mesmo escrever uma implementação. Em um dos projetos de “combate”, usamos a implementação Try feita por nós mesmos - gerenciamos com uma classe e fizemos um excelente trabalho.
Em segundo lugar, existe a biblioteca Arrow, que em princípio adiciona muitas funcionalidades ao Kotlin. Naturalmente, existem Tente e qualquer um.
Além disso, a classe Result apareceu no Kotlin 1.3, que discutirei com mais detalhes posteriormente.
Tente usar a biblioteca Arrow como exemplo
A biblioteca Arrow nos dá uma classe Try. De fato, ele pode estar em dois estados: Sucesso ou Falha:
- O sucesso na retirada bem-sucedida manterá nosso valor,
- A falha armazena uma exceção que ocorreu durante a execução de um bloco de código.
A chamada é a seguinte. Naturalmente, ele é envolvido em um try-catch regular, mas isso acontece em algum lugar dentro do nosso 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) } } }
A mesma classe deve implementar o método flatMap, que permite que você passe uma função e retorne nossa try 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 que é isso? Para não processar erros para cada um dos resultados quando tivermos vários deles. Por exemplo, obtivemos vários valores de diferentes serviços e queremos combiná-los. De fato, podemos ter duas situações: recebemos e combinamos com sucesso ou algo caiu. Portanto, podemos fazer o seguinte:
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)
Se as duas chamadas foram bem-sucedidas e obtivemos os valores, executamos a função Se não forem bem-sucedidas, Failure retornará com uma exceção.
Aqui está o que parece se algo caiu:
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 a mesma função, mas a saída é uma falha de uma RuntimeException.
Além disso, a biblioteca Arrow permite que você use construções que são de fato açúcar sintático, em particular vinculativo. Tudo o mesmo pode ser reescrito através de um flatMap serial, mas a encadernação permite torná-lo legível.
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 um dos resultados caiu, obtemos um erro na saída.
Uma mônada semelhante pode ser usada para chamadas assíncronas. Por exemplo, aqui estão duas funções executadas de forma assíncrona. Combinamos os resultados da mesma maneira, sem verificar separadamente seu status:
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 } } }
E aqui está um exemplo mais de "combate". Temos uma solicitação para o servidor, processamos, obtemos o corpo e tentamos mapear para nossa classe, da qual já estamos retornando dados.
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 } }
O try-catch tornaria esse bloco muito menos legível. E, nesse caso, obtemos response.data na saída, que podemos processar dependendo do resultado.
Resultado do Kotlin 1.3
Kotlin 1.3 introduziu a classe Result. De fato, é algo semelhante ao Try, mas com várias limitações. Ele foi originalmente projetado para ser usado em várias operações assíncronas.
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”) } )
Se não estiver enganado, essa classe é atualmente experimental. Os desenvolvedores de idiomas podem alterar sua assinatura, comportamento ou removê-lo completamente; portanto, no momento é proibido usá-lo como valor de retorno de métodos ou variáveis. No entanto, ele pode ser usado como uma variável local (privada). I.e. de fato, ele pode ser usado como uma tentativa do exemplo.
Conclusões
Conclusões que fiz para mim:
- a manipulação de erros funcionais no Kotlin é simples e conveniente;
- ninguém se incomoda em processá-las através do try-catch no estilo clássico (isso e aquilo tem direito à vida; isso e aquilo são convenientes);
- a ausência de exceções verificadas não significa que erros não possam ser tratados;
- exceções não capturadas na produção levam a tristes conseqüências.
Autor do artigo: Alexey Shafranov, líder do grupo de trabalho (Java), Maxilect
PS Publicamos nossos artigos em vários sites do Runet. Assine nossas páginas no
canal VK ,
FB ou
Telegram para descobrir todas as nossas publicações e outras notícias do Maxilect.