我们公司已经在生产中使用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的多个站点上发表文章。 订阅我们在
VK ,
FB或
Telegram频道上的页面,以查找有关我们所有出版物和其他Maxilect新闻的信息。