【Kotlin】Result详解

2,891 阅读9分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第4天,点击查看活动详情

前言

对于一些需要关心结果的操作来说,成功时需要得到数据,失败时需要得到失败信息。作为曾经的Java选手,一般采用如下方式解决:

  • 只需要关心是否成功,使用布尔值。
  • 需要关心失败信息,使用整型code或者字符串。
  • 需要关心成功的数据,判断数据是否为空(失败返回空数据)。
  • 既需要关心成功的数据,也需要关心失败的信息,使用一个类去包装。

接触了Kotlin之后,发现了Result这个API,基本上可以满足上述情景。关键是即拿即用以及统一项目规范。

贴一段Result的官方注释:

A discriminated union that encapsulates a successful outcome with a value of type [T] or a failure with an arbitrary [Throwable] exception.

翻译一下就是:封装了一个类型为T的成功结果或者一个类型为Throwable的错误结果的可区分联合体。

理解一下,也就是一个具体的Result只能是成功和失败的其中一种,成功结果包含一个泛型的数据;失败结果则包含一个异常。

构造方法

首先来看一下Result的构造方法:

@JvmInline
public value class Result<out T> internal constructor(
    internal val value: Any?
) : Serializable

逐个看,首先是value class

value class即是曾经的内联类(inline class)。不了解内联类的朋友们可以将其理解为装箱类。它必须在主要构造方法里面传入一个属性(有且仅可以有一个,以后不知道会不会改这个规则),而value class则是该属性的装箱类。可以以int和Integer之间的关系去类比。

那为什么Result声明成value class呢?因为value class有一个特性,在Runtime中,实际使用的是拆箱之后的值,也就是说它可以做到编写代码的时候是装箱的,但实际运行的时候却是拆箱的,因此节省了拆装箱的性能消耗。而Result的重点其实只是它所持有的数据而不是Result这个类本身,所以刚好声明为value class。

(PS:使用value class需要在类声明前加上@JvmInline这个注解)

接下来是。T我们很熟悉,表达泛型,不在编码时就定死具体类型。out则是协变的关键字(还有一个孪生兄弟in,逆变的关键字)。关于协变与逆变的相关知识,我也不敢说我就搞懂了,所以也不在这里展开了,反正加上了out则表示该泛型只能作为生产者,只能作为返回值类型而不能作为入参类型。也就是说Result只能被读取而不能被修改,这也是非常符合Result的设计思想的。

internal constructor表明Result的构造方法是同module可见。换言之,我们使用者是不可以直接调用该构造方法进行创建的。那不能初始化怎么使用呢?不用着急,下面会介绍的。

上面说了,value class构造方法只能接收一个参数,那这个value自然就是被装箱的东西了。同样的internal修饰符表明module可见,val声明只读变量,Any?则是这个变量的类型。可能有的朋友会有疑惑了,不是定义了T泛型吗?那值的类型不应该直接用T就行了吗?这是因为Result需要兼容处理失败情况,往下看就会明白了。

Result实现了Serializable,说明它是可以序列化的。这个Serializable实际上就是java.io下的Serializable。也就是说,如果被装箱的类没有同样实现Serializable同时也不是基本类型的话,Result的序列化就会抛出 NotSerializableException 的异常。

产生结果

说完了构造方法,现在来说说如何产生这么一个Result。既然构造方法是module可见的,那肯定会有其他方式让我们去创建Result。答案就在Result的伴生对象(如果大家不熟悉伴生对象,就将它理解为属于这个类的一个静态对象吧)里。

...class Result...{
    ...
    companion object{
        @InlineOnly
        @JvmName("success")
        public inline fun <T> success(value: T): Result<T> =
            Result(value)
            
        @InlineOnly
        @JvmName("failure")
        public inline fun <T> failure(exception: Throwable): Result<T> =
            Result(createFailure(exception))
    }
    ...
}

一并说一下这两个方法的注解:

  • @InlineOnly——该注解表明该方法只能被内联调用。由于Java调用Kotlin的内联方法时并不会进行实际的内联,并且开发者无法保证非内联调用该方法的正确性,所以该注解标记的对应的java方法会被private标记。
  • @JvmName——该注解的作用是指定生成java类或方法时的名字。

接着说一下inline关键字。这个关键字标记该方法为内联方法,也就是当生成字节码的时候会在调用该方法的地方直接复制该方法的方法体,而不是进行实际的方法调用。这样可以减少方法调用堆栈。

success方法没什么好说的,用传入的值构造一个Result并返回。

重点说一下failure。首先看入参,是个Throwable,说明产生一个失败结果需要一个Throwable。再看返回值,和success的返回值一致,是Result,这也是为了保证外部接收这个Result时候的一致性。最后看看方法体,createFailure方法如下:

internal fun createFailure(exception: Throwable): Any =
    Result.Failure(exception)

返回了一个Failure的类,那Failure又是什么呢?Failure定义在Result的内部:

internal class Failure(
        @JvmField
        val exception: Throwable
    ) : Serializable {
        override fun equals(other: Any?): Boolean = other is Failure && exception == other.exception
        override fun hashCode(): Int = exception.hashCode()
        override fun toString(): String = "Failure($exception)"
    }

同样是internal,没什么好说的。从代码上可以看出,Failure就是一个普普通通的类,包含了一个Throwable。这就是为什么Result的value要定义成Any?而不是泛型T。因为当返回失败的时候,Failure需要作为value去构建Result,但Failure只是一个普通的类。

剩下的没啥好说了,同样的Serializable,以及复写了equals,hashCode,toString方法。

至此,总结一下产生结果的两种方式:

  • 传入类型T的数据产生成功结果
  • 传入Throwable类型产生失败结果

Result成员属性以及方法

  • isSuccess(布尔值)

    //只要value不是Failure即为成功
    public val isSuccess: Boolean get() = value !is Failure
    
  • isFailure(布尔值)

    //只要value是Failure即为失败
    public val isFailure: Boolean get() = value is Failure
    

    (PS:并不推荐直接使用这两个属性判断,有更好的替代方案:扩展方法onSuccess和onFailure)

  • getOrNull

    //如果成功则返回value,失败则返回null,不关心失败的exception
    public inline fun getOrNull(): T? =
            when {
                isFailure -> null
                else -> value as T
            }
    
  • exceptionOrNull

    //如果成功返回null,失败则返回Throwable,不关心成功的数据
    public fun exceptionOrNull(): Throwable? =
            when (value) {
                is Failure -> value.exception
                else -> null
            }
    

好了!至此,Result的成员属性和方法已经全部介绍完毕!本文结束?

肯定不是啦!虽然说就靠这么几个方法就足够完成工作了,但是Kotlin还是提供了更多便捷的扩展方法,让我们玩转Result!

Result扩展方法

  • runCatching

    runCatching为我们省去了显式的try-catch,当catch发生时,自动返回失败结果的Result,有两个重载方法:

    //最简单的用法,block的返回值是什么类型,就返回什么类型的Result
    public inline fun <R> runCatching(block: () -> R): Result<R> {
        return try {
            Result.success(block())
        } catch (e: Throwable) {
            Result.failure(e)
        }
    }
    
    //这个方法等于给T类型新增了一个扩展方法,以T为Receiver调用block。
    public inline fun <T, R> T.runCatching(block: T.() -> R): Result<R> {
        return try {
            Result.success(block())
        } catch (e: Throwable) {
            Result.failure(e)
        }
    }
    
  • getOrThrow

    //与getOrNull类似,该方法如果是失败则会将失败的Throwable抛出
    public inline fun <T> Result<T>.getOrThrow(): T {
        throwOnFailure()
        return value as T
    }
    
  • getOrElse

    //与getOrThrow类似,该方法失败不会直接抛出,而且执行传入的onFailure并返回运行结果
    //所以onFailure的返回值必须是T的子类
    public inline fun <R, T : R> Result<T>.getOrElse(onFailure: (exception: Throwable) -> R): R {
        contract {
            callsInPlace(onFailure, InvocationKind.AT_MOST_ONCE)
        }
        return when (val exception = exceptionOrNull()) {
            null -> value as T
            else -> onFailure(exception)
        }
    }
    

    在这个方法里面可以看到contract的用法,这段代码的意思是,约定好onFailure这个代码块最多只能调用1次。contract的身影在后续的方法里也会看到,作用也是类似的。

  • getOrDefault

    //与getOrElse类似,不传方法,直接传一个默认值,类似的默认值的类型也需要是T的子类
    public inline fun <R, T : R> Result<T>.getOrDefault(defaultValue: R): R {
        if (isFailure) return defaultValue
        return value as T
    }
    
  • fold

    //分别接受onSuccess和onFailure两个方法,并在成功和失败的时候分别调用
    //需要保证两个方法的返回值都为相同类型
    public inline fun <R, T> Result<T>.fold(
        onSuccess: (value: T) -> R,
        onFailure: (exception: Throwable) -> R
    ): R {
        contract {
            callsInPlace(onSuccess, InvocationKind.AT_MOST_ONCE)
            callsInPlace(onFailure, InvocationKind.AT_MOST_ONCE)
        }
        return when (val exception = exceptionOrNull()) {
            null -> onSuccess(value as T)
            else -> onFailure(exception)
        }
    }
    
  • map

    //将T类型的数据映射成R类型
    public inline fun <R, T> Result<T>.map(transform: (value: T) -> R): Result<R> {
        contract {
            callsInPlace(transform, InvocationKind.AT_MOST_ONCE)
        }
        return when {
            isSuccess -> Result.success(transform(value as T))
            else -> Result(value)
        }
    }
    
  • mapCatching

    //将map和runCatching结合
    public inline fun <R, T> Result<T>.mapCatching(transform: (value: T) -> R): Result<R> {
        return when {
            isSuccess -> runCatching { transform(value as T) }
            else -> Result(value)
        }
    }
    
  • recover

    //该方法可以在失败的时候,根据调用者的意愿“恢复”到成功的结果
    public inline fun <R, T : R> Result<T>.recover(transform: (exception: Throwable) -> R): Result<R> {
        contract {
            callsInPlace(transform, InvocationKind.AT_MOST_ONCE)
        }
        return when (val exception = exceptionOrNull()) {
            null -> this
            else -> Result.success(transform(exception))
        }
    }
    
  • recoverCatching

    //recover + runCatching
    public inline fun <R, T : R> Result<T>.recoverCatching(transform: (exception: Throwable) -> R): Result<R> {
        return when (val exception = exceptionOrNull()) {
            null -> this
            else -> runCatching { transform(exception) }
        }
    }
    
  • onFailure

    //该方法可以当成回调使用,失败时执行action
    public inline fun <T> Result<T>.onFailure(action: (exception: Throwable) -> Unit): Result<T> {
        contract {
            callsInPlace(action, InvocationKind.AT_MOST_ONCE)
        }
        exceptionOrNull()?.let { action(it) }
        return this
    }
    
  • onSuccess

    //与OnFailure类似,成功时执行action
    public inline fun <T> Result<T>.onSuccess(action: (value: T) -> Unit): Result<T> {
        contract {
            callsInPlace(action, InvocationKind.AT_MOST_ONCE)
        }
        if (isSuccess) action(value as T)
        return this
    }
    

总结

Result虽然不是多高级的东西,只是一个对结果的封装。但在阅读源码的时候真真切切地感受到设计的精妙。协变、contract、伴生对象控制产生、扩展方法……

Result还有一个很重要的意义,能够规范项目代码中各种方法调用的结果返回,不再出现滥用字符串、滥用整型值、各种魔法值的情况,让代码更统一更易于管理。