Kotlin / Java错误处理:如何正确执行?


来源


错误处理在任何开发中都起着至关重要的作用。 程序中几乎所有内容都会出错:用户将输入错误的数据,或者它们可能通过http输入,或者在编写序列化/反序列化时出错,并且在处理过程中程序因错误而崩溃。 是的,它可能会浪费磁盘空间。


扰流板

¯_(ツ)_ /¯,没有单一的方法,在每种特定情况下,您都必须选择最合适的选项,但是对于如何做得更好,有一些建议。


前言


不幸的是(还是这样的生活?),这个清单还在继续。 开发人员经常需要考虑以下事实:错误可能在某处发生,并且有两种情况:


  • 当在调用我们提供的函数并尝试进行处理时发生预期的错误时;
  • 当我们无法预见的操作过程中发生意外错误时。

而且,如果预期的错误至少已本地化,那么其余错误几乎可以在任何地方发生。 如果我们不处理任何重要的事情,那么我们可能会因错误而崩溃(尽管此行为还不够,并且您至少需要在错误日志中添加一条消息)。 但是,如果现在付款正在处理中,您就不能拒绝,但是至少您需要返回有关操作失败的回复?


在介绍处理错误的方法之前,请先介绍一些有关Exception(异常)的信息:


例外情况



来源


异常的层次结构已得到很好的描述,您可以找到很多有关异常的信息,因此在此处绘制异常是没有意义的。 有时仍引起激烈讨论的是已checkedunchecked错误。 而且尽管大多数人都接受unchecked例外作为首选(在Kotlin中根本没有checked例外),但并非所有人都对此表示赞同。


checked异常确实有一个很好的意图,使它们成为一种方便的错误处理机制,但是现实进行了调整,尽管将可以从此函数抛出的所有异常引入签名中的想法是可以理解和逻辑的。


让我们来看一个例子。 假设我们有一个method函数可以抛出一个经过检查的PanicException 。 这样的功能如下所示:


 public void method() throws PanicException { } 

从她的描述中很明显,她可以抛出一个异常,并且只能有一个异常。 看起来很舒服吗? 尽管我们有一个小程序,仅此而已。 但是,如果程序稍大,并且有更多这样的功能,则会出现一些问题。


根据规范,检查异常要求在功能签名中列出所有可能的检查异常(或它们的共同祖先)。 因此,如果我们有一连串的调用a-> b > c并且嵌套最多的函数抛出某种异常,那么应该为该链中的每个人都放下该异常。 并且,如果有几个例外,则签名中最顶层的函数应具有所有例外的描述。


因此,随着程序变得更加复杂,这种方法导致这样一个事实,即顶层函数的异常逐渐向普通祖先崩溃,并最终下降为Exception 。 这种形式的内容类似于unchecked异常,并且抵消了检查的异常的所有优点。


鉴于该程序作为一种活生物体正在不断变化和发展,因此几乎不可能事先预见该程序中可能会出现什么例外情况。 结果是,当我们添加一个带有新异常的新功能时,我们必须遍历整个使用过程,并更改所有功能的签名。 同意,这不是最令人愉快的任务(即使考虑到现代IDE为我们做到这一点)。


但是最后一个,也可能是最大的检查异常是从Java 8“驱使” lambda。签名中没有检查异常__(ツ)_ /(因为可以在lambda中调用任何函数,而任何签名),因此任何来自lambda的具有已检查异常的函数调用都将其包装为未检查的异常转发中:


 Stream.of(1,2,3).forEach(item -> { try { functionWithCheckedException(); } catch (Exception e) { throw new RuntimeException("rethrow", e); } }); 

幸运的是,在JVM规范中根本没有经过检查的异常,因此在Kotlin中,您不能在同一个lambda中包装任何内容,而只是调用所需的函数。


虽然有时...

尽管有时这会导致意想不到的后果,例如, Spring Framework@Transactional的错误操作,该错误仅“期待”异常情况。 但这更多是框架的功能,也许在不久的将来的github问题中 ,Spring的这种行为将会改变。


异常本身是特殊对象。 除了可以通过方法“抛出”这些事实外,它们还在创建时收集stacktrace。 然后,此功能有助于分析问题和查找错误,但是如果应用程序逻辑与抛出的异常紧密联系在一起,则还可能导致某些性能问题。 如该文章所示,在这种情况下,禁用stacktrace程序集可以显着提高其性能,但是只有在确实需要它的特殊情况下,才应诉诸于此!


错误处理


处理“意外”错误的主要方法是找到一个可以拦截它们的地方。 在JVM语言中,这可以是http方法的流创建点或过滤器/入口点,您可以在其中放置try-catch来处理unchecked错误。 如果使用任何框架,则很可能已经具有创建常见错误处理程序的能力,例如在Spring框架中,可以使用带有@ExceptionHandler批注的方法。


您可以通过抛出相同的unckecked处理的异常来“引发”我们不想在特定位置处理的这些中央处理点的异常(例如,当我们不知道在特定位置做什么以及如何处理错误时)。 但是这种方法并不总是合适的,因为有时可能需要处理错误,并且您需要检查是否正确处理了函数调用的所有位置。 考虑执行此操作的方法。


  1. 仍然使用异常和相同的try-catch:


      int a = 10; int b = 20; int sum; try { sum = calculateSum(a,b); } catch (Exception e) { sum = -1; } 

    主要缺点是,我们可以“忘记”在调用位置将其包装在try-catch中,而跳过尝试就地处理它的尝试,因此,异常将被抛出到错误处理的常见点。 在这里,我们可以转到checked异常(对于Java),但是然后我们将获得上面提到的所有缺点。 如果并非总是需要就地进行错误处理,但是在极少数情况下需要使用此方法,则使用此方法很方便。


  2. 使用密封类作为调用的结果(Kotlin)。
    在Kotlin中,您可以限制类继承人的数量,使其在编译阶段可计算-这使编译器可以验证代码中是否解析了所有可能的选项。 在Java中,您可以创建一个公共接口和多个后代,但是会丢失编译级检查。


     sealed class Result data class SuccessResult(val value: Int): Result() data class ExceptionResult(val exception: Exception): Result() val a = 10 val b = 20 val sum = when (val result = calculateSum(a,b)) { is SuccessResult -> result.value is ExceptionResult -> { result.exception.printStackTrace() -1 } } 

    当您需要显式检查结果值(或显式忽略)时,这里得到类似于golang错误方法的信息。 该方法非常实用,在每种情况下都需要抛出很多参数时特别方便。 可以使用各种方法扩展Result类,使其更容易获得上面抛出异常的结果(如果有的话)(即,我们不需要在调用位置处理错误)。 主要缺点将仅是创建中间多余的对象(以及稍微冗长的条目),但也可以使用inline类将其删除(如果一个参数足以满足我们的需要)。 作为一个特定示例,还有Kotlin的Result类。 没错,它仅供内部使用,因为 将来,其实现可能会稍有变化,但是如果要使用它,可以添加编译标志-Xallow-result-return-type


  3. 作为权利要求2的一种可能的类型,使用Either的功能编程中的类型,它可以是结果,也可以是错误。 类型本身可以是sealed类或inline类。 以下是使用arrow库中的实现的示例:


     val a = 10 val b = 20 val value = when(val result = calculateSum(a,b)) { is Either.Left -> { result.a.printStackTrace() -1 } is Either.Right -> result.b } 

    Either最适合喜欢功能性方法并且喜欢建立呼叫链的人员。


  4. 使用Kotlin中的Optionnullable类型:


     fun testFun() { val a = 10 val b = 20 val sum = calculateSum(a,b) ?: throw RuntimeException("some exception") } fun calculateSum(a: Int, b: Int): Int? 

    如果错误原因不是很重要,并且仅是一个原因,则此方法适用。 空答案被认为是错误,并被抛出更高的值。 最短的记录,没有创建其他对象,但是这种方法不能总是被应用。


  5. 与第4项类似,仅将硬编码值用作错误标记:


     fun testFun() { val a = 10 val b = 20 val sum = calculateSum(a,b) if (sum == -1) { throw RuntimeException(“error”) } } fun calculateSum(a: Int, b: Int): Int 

    这可能是C (甚至Algol)提供的最古老的错误处理方法。 没有开销,只有一个不完全清楚的代码(以及对结果选择的限制),但是与第4段不同,如果需要多个可能的异常,则可以制作各种错误代码。



结论


可以根据情况将所有方法组合在一起,其中没有一种方法适合所有情况。


因此,例如,您可以使用sealed类实现golang处理错误的方法,如果不是很方便,则转到unchecked错误。


或者,在大多数地方, nullablenullable类型作为无法计算值或无法从某处获取值的标记(例如,作为表明在数据库中找不到值的指示符)。


并且,如果您具有arrow或其他类似库的功能齐全的代码,则最有可能最好使用Either


对于http服务器,最容易将所有错误提高到中心点,并且仅在某些地方将nullable方法与sealed类结合使用。


我会很高兴在评论中看到您正在使用此功能,或者还有其他便捷的错误处理方法?


并感谢阅读到底的每个人!

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


All Articles