Kotlin例外及其功能

我们公司已经在生产中使用Kotlin两年多了。 就个人而言,大约一年前,我遇到过这种语言。 有很多话题要讨论,但是今天我们将讨论错误处理,包括功能性样式。 我将告诉您如何在Kotlin中进行操作。

图片

(有关该主题的会议的照片在Taganrog公司之一的办公室举行。Maxilekt工作组(Java)负责人Alexey Shafranov发言)

原则上如何处理错误?


我发现了几种方法:

  • 您可以使用一些返回值作为错误事实的指针;
  • 您可以将指标参数用于相同的目的,
  • 输入一个全局变量
  • 处理异常
  • 添加合同(DbC)

让我们详细介绍每个选项。

返回值


如果发生错误,则返回某个“魔术”值。 如果您曾经使用过脚本语言,那么您一定已经看过类似的结构。

范例1:

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

范例2:

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

指标参数


使用传递给函数的特定参数。 通过参数返回值后,您可以查看函数内部是否存在错误。

一个例子:

 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 

全局变量


全局变量的工作方式大致相同。

一个例子:

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

例外情况


我们都习惯于例外。 它们几乎在任何地方都可以使用。

一个例子:

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

合约(DbC)


坦白说,我从未见过这种方法。 经过长时间的搜寻,我发现Kotlin 1.3具有一个实际上允许使用合同的库。 即 您可以在传递给函数的变量上设置条件,在返回值,调用次数,从何处调用等条件上进行设置。 并且,如果满足所有条件,则认为该功能可以正常工作。

一个例子:

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

老实说,这个库的语法很糟糕。 也许这就是为什么我还没有看到这样的事情。

Java中的异常


让我们继续学习Java以及它从一开始就如何工作。

图片

设计语言时,提出了两种类型的异常:

  • 检查-检查;
  • 未选中-未选中。

什么是检查异常? 从理论上讲,它们是必需的,以便人们必须检查错误。 即 如果某个已检查的异常是可能的,则必须稍后对其进行检查。 从理论上讲,这种方法应该导致没有未处理的错误,并提高了代码质量。 但实际上并非如此。 我认为每个人至少一生中都有一个空洞的渔获物。

为什么这会不好?

这是直接来自Kotlin文档的经典示例-StringBuilder中实现的JDK的接口:

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

我确信您已经遇到了很多用try-catch包装的代码,在这里catch是一个空块,因为这种情况根本就不会发生。 在许多情况下,对已检查异常的处理是通过以下方式实现的:它们只是抛出RuntimeException并将其捕获到上面的某个地方(或不捕获它……)。

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

科特林有什么可能


在例外方面,Kotlin编译器的不同之处在于:

1.不区分已检查和未检查的异常。 仅检查所有异常,您可以自己决定是否捕获并处理它们。

2. try可用作表达式-您可以运行try块并从中返回最后一行,或者从catch块返回最后一行。

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

3.引用某些对象时,也可以使用类似的构造,该构造可以为空:

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

Java兼容性


Kotlin代码可以在Java中使用,反之亦然。 如何处理异常?

  • Kotlin中Java的已检查异常不能被检查或声明(因为Kotlin中没有已检查异常)。
  • 不需要在Java中检查来自Kotlin的可能检查异常(例如,最初来自Java的异常)。
  • 如果需要检查,可以使用方法中的@Throws批注来检查异常(您必须指定此方法可以抛出哪些异常)。 上面的注释仅用于Java兼容性。 但是实际上,许多人使用它来声明这种方法原则上可以引发某种异常。

尝试捕获块的替代方法


尝试捕获块具有明显的缺点。 当它出现时,部分业务逻辑将在catch中传输,并且这可以通过上述许多方法之一发生。 当业务逻辑分布在整个块或整个呼叫链中时,很难理解应用程序的工作方式。 并且可读性块本身不会添加代码。

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

有哪些选择?

一个选项为我们提供了一种处理异常的实用方法。 类似的实现如下所示:

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

我们有机会使用Try monad。 本质上,这是一个存储一些价值的容器。 flatMap是使用此容器的一种方法,该容器与当前值一起可以使用一个函数,并再次返回一个monad。

在这种情况下,该呼叫将包装在Try monad中(我们返回Try)。 它可以在一个我们需要的地方处理。 如果输出有一个值,我们将对其执行以下操作;如果抛出异常,则将在链的最末端进行处理。

功能异常处理


在哪里可以尝试?

首先,有许多Try和Either类的社区实现。 您可以接受它们甚至自己编写实现。 在其中一个“战斗”项目中,我们使用了自制的Try实施-我们管理了一个班级,并且做得很好。
其次,还有Arrow库,该库原则上为Kotlin增加了很多功能。 自然地,有Try and Either。

好吧,此外,Result类出现在Kotlin 1.3中,我将在后面详细讨论。

尝试以Arrow库为例


Arrow库为我们提供了一个Try类。 实际上,它可以处于两种状态:成功或失败:

  • 成功取款成功将保留我们的价值,
  • 故障存储在执行代码块期间发生的异常。

调用如下。 自然,它包装在常规try-catch中,但这将在我们代码内的某处发生。

 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) } } } 

相同的类应实现flatMap方法,该方法允许您传递函数并返回我们的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) } 

这是为了什么 当我们有多个错误时,为了不处理每个结果的错误。 例如,我们从不同的服务中获得了一些价值,并希望将它们结合起来。 实际上,我们可能有两种情况:要么我们成功接收并合并了它们,要么有些东西掉了。 因此,我们可以执行以下操作:

 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) 

如果两个调用都成功并且我们得到了值,则执行函数。 如果它们不成功,则失败将返回异常。

这是如果东西掉下来的样子:

 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! 

我们使用了相同的函数,但是输出是RuntimeException的Failure。

另外,Arrow库允许您使用实际上是语法糖(特别是绑定)的构造。 可以通过串行flatMap重写所有相同的内容,但是绑定使您可以读取它。

 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! 

鉴于其中一项结果下降了,我们在输出中得到了一个错误。

类似的monad可用于异步调用。 例如,这是两个异步运行的函数。 我们以相同的方式合并结果,而无需单独检查其状态:

 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 } } } 

这是一个更“战斗”的例子。 我们有一个对服务器的请求,我们对其进行处理,从中获取主体,然后尝试将其映射到我们已经从中返回数据的类。

 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 } } 

尝试捕获将使此块的可读性大大降低。 在这种情况下,我们在输出处获得response.data,我们可以根据结果进行处理。

Kotlin 1.3的结果


Kotlin 1.3引入了Result类。 实际上,它与Try类似,但有很多限制。 它最初旨在用于各种异步操作。

 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”) } ) 

如果没有记错的话,这个课程目前是实验性的。 语言开发人员可以更改其签名,行为或将其完全删除,因此目前禁止将其用作方法或变量的返回值。 但是,它可以用作局部(私有)变量。 即 实际上,可以从示例中尝试使用它。

结论


我为自己得出的结论:

  • Kotlin中的功能错误处理既简单又方便;
  • 没有人会费心地通过古典风格的尝试捕捉来处理它们(具有生命权;具有生命权;具有便利性);
  • 没有检查异常并不意味着不能处理错误;
  • 生产中未捕获的异常会导致可悲的后果。

文章作者:Maxilect工作组(Java)负责人Alexey Shafranov

PS:我们在Runet的多个站点上发表文章。 订阅我们在VKFBTelegram频道上的页面,以查找有关我们所有出版物和其他Maxilect新闻的信息。

Source: https://habr.com/ru/post/zh-CN447380/


All Articles