Effective Kotlin 翻译系列 - 第一章 - 条目 7 - 返回有可能缺失时优先使用 null 或 Failure

640 阅读3分钟

📢📢📢 最近我们团队在翻译《Effective Kotlin: Best practices》,关注我们,我们会定时更新,欢迎指正,欢迎收藏 😁 翻译:ippan 校对:liming

条目 7:返回有可能缺失时优先使用 nullFailure

TLDR:

  1. 仅当有特殊情况才抛出异常,否则用 null 或 sealed class 封装返回结果
  2. 错误返回,当需要返回额外信息时,优先使用 sealed class
  3. 提供 get / getOrNull / getOrDefault 之类的变体让调用者更方便

有时候,函数不能如预期地返回结果:

  • 试图从服务器获取数据,但网络出错错误
  • 视图从列表中获取首个匹配数据,但实际无匹配结果
  • 视图从一段文本解析一个对象,但文本格式是错的

以上情况主要有两种处理策略:

  • 返回 null 或一个标记错误的密封类(一般命名为Failure
  • 抛出异常

两种方式有个很重要的区别。异常不应作为标准的传递信息方式,所有异常都意味着存在不正确的特殊场景,异常应只在有异常场景才使用(Effective Java),主要理由如下:

  • 异常的传递方式缺乏可读性更差且更容易被忽略
  • Kotlin 没有受检异常(checked exception),不强制要求使用者处理异常。通常没有相关的文档说明,当我们使用 API 时是看不到内部抛出的异常。
  • try catch exception 性能上会比 return null 后做值判断要差。因为异常是为特殊场景而设计的,因此 JVM 开发者没有动力去优化这一点
  • try catch 会阻止编译器优化代码

nullFailure 都能完美地呈现异常,它们更明确和高效,且能用惯常写法处理。所以,当错误符合预期时应优先考虑返回 nullFailure,而当错误不符合预期时才抛出异常。这里有几个例子:

inline fun <reified T> String.readObjectOrNull(): T? {
    // ...
    if (incorrectSign) {
        return null
    }
    // ...
    return result
}
inline fun <reified T> String.readObjectOrNull(): Result<T> {
    // ...
    if (incorrectSign) {
        return Failure(JsonParsingException())
    }
    // ...
    return Surccess(result)
}
seal class Result<out T>
class Success<out T>(val result: T) : Result<T>()
class Failure(val throwable: Throwable) : Result<Nothing>()
class JsonParsingException : Exception()

用这种方式传递错误更容易处理,也不容易被忽略,当我们选择返回 null 时,调用方可以使用空安全的调用或者 Elvis 表达式来处理:

val age = userText.readObjectOrNull<Person>()?.age ?: -1

当我们返回 Result 类型时,调用方可以使用 when 表达式来处理:

val personResult = userText.readObject<Preson>()
val age = when(personResult) {
    is Success -> personResult.value.age
    is Failure -> -1
}

使用这样的错误处理方式不仅仅比 try-catch 的方式高效,对于使用者也更加友好和明确,如果一个异常被遗漏了,当它发生时,应用程序将会被停止运行。而 nullFailure 类型都需要被调用方显式地处理,它并不会中断应用程序的执行。

当出错时,如果需要传递信息,nullFailure 类型应考虑后者。因为后者可以传递额外的数据。

当我们设计接口时,通常应考虑有两种函数变体,一种是预期会有异常发生,另一种是预期不会有异常发生。如 List 同时提供了 getgetOrNull。前者当参数越界时会抛出 IndexOutOfBoundsException 而后者则返回 null。 还可以考虑提供一个 getOrDefault 的变体,在有些场景下十分有用,因为调用方不需要被强制处理可空的结果,如果调用方有任何疑问,也可以用很方便地替换成 getOrNull 加 Elvis 表达式的用法。