kotlin修炼指南6-Sealed到底密封了啥

843 阅读4分钟

在代码中,我们经常需要限定一些有限集合的状态值,例如:

  • 网络请求:成功——失败
  • 账户状态:VIP——穷逼VIP——普通
  • 工具栏:展开——半折叠——收缩

等等。

通常情况下,我们会使用enum class来做封装,将可见的状态值通过枚举来使用。

enum class NetworkState(val value: Int) {
    SUCCESS(0),
    ERROR(1)
}

但枚举的缺点也很明显,首先,枚举比普通代码更占内存,同时,每个枚举只能定义一个实例,不能拓展更多信息。

除此之外,还有种方式,通过抽象类来对状态进行封装,但这种方式的缺点也很明显,它打破了枚举的限制性,所以,Kotlin给出了新的解决方案——Sealed Class(密封类)。

创建状态集

下面我们以网络请求的例子来看下具体如何使用Sealed Class来进行状态的封装。

和抽象类类似,Sealed Class可用于表示层级关系。它的子类可以是任意的类:data class、普通Kotlin对象、普通的类,甚至也可以是另一个密封类,所以,我们定义一个Result Sealed Class:

sealed class Result<out T : Any> {
    data class Success<out T : Any>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()
}

当然,也不一定非要写在顶层类中:

sealed class Result<out T : Any> 
data class Success<out T : Any>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()

这样也是可以的,它们的区别在于引用的时候,是否包含顶层类来引用而已。

大部分场景下,还是建议第一种方式,可以比较清晰的展示调用的层级关系。

在这个例子中,我们定义了两个场景,分别是Success和Error,它表示我们假设的网络状态就这两种,分别在每种状态下,例如Success,都可以传入自定义的数据类型,因为它本身就是一个class,所以借助这一点,就可以自定义状态携带的场景值。在上面这个例子中,我们定义在Success中,传递data,而在Error时,传递Exception信息。

所以,使用Sealed Class的第一步,就是对场景进行封装,梳理具体的场景枚举,并定义需要传递的数据类型。

如果场景值不需要传递数据,那么可以简单的使用:object xxxx,定义一个变量即可。

使用

接下来,我们来看下如何使用Sealed Class。

fun main() {
    // 模拟封装枚举的产生
    val result = if (true) {
        Result.Success("Success")
    } else {
        Result.Error(Exception("error"))
    }

    when (result) {
        is Result.Success -> print(result.data)
        is Result.Error -> print(result.exception)
    }
}

大部分场景下,Sealed Class都会配合when一起使用,同时,如果when的参数是Sealed Class,在IDE中可以快速补全所有分支,而且不会需要你单独补充else 分支,因为Sealed Class已经是完备的了。

所以when和Sealed Class真是天作之合。

进一步简化

其实我们还可以进一步简化代码的调用,因为我们每次使用Sealed Class的时候,都需要when一下,有些时候,也会产生一些代码冗余,所以,借助拓展函数,我们进一步对代码进行简化。

inline fun Result<Any>.doSuccess(success: (Any) -> Unit) {
    if (this is Result.Success) {
        success(data)
    }
}

inline fun Result<Any>.doError(error: (Exception?) -> Unit) {
    if (this is Result.Error) {
        error(exception)
    }
}

这里我对Result进行了拓展,增加了doSuccess和doError两个拓展,同时接收两个高阶函数来接收处理行为,这样我们在调用的时候就更加简单了。

result.doSuccess { }
result.doError { }

所以when和Sealed Class和拓展函数,真是天作之合。

那么你一定好奇了,Sealed Class又是怎么实现的,其实反编译一下就一目了然了,实际上Sealed Class也是通过抽象类来实现的,编译器生成了一个只能编译器调用的构造函数,从而避免其它类进行修改,实现了Sealed Class的有限性。

封装?

Sealed Class与抽象类类似,可以对逻辑进行拓展,我们来看下面这个例子。

sealed class TTS {
    
    abstract fun speak()

    class BaiduTTS(val value: String) : TTS() {
        override fun speak() = print(value)
    }

    class TencentTTS(val value: String) : TTS() {
        override fun speak() = print(value)
    }
}

这时候如果要进行拓展,就很方便了,代码如下所示。

class XunFeiTTS(val value: String) : TTS() {
    override fun speak() = print(value)
}

所以,Sealed Class可以说是在抽象类的基础上,增加了对状态有限性的控制,拓展与抽象,比枚举更加灵活和方便了。

再例如前面网络的封装:

sealed class Result<out T : Any> {
    data class Success<out T : Any>(val data: T) : Result<T>()
    sealed class Error(val exception: Exception) : Result<Nothing>() {
        class RecoverableError(exception: Exception) : Error(exception)
        class NonRecoverableError(exception: Exception) : Error(exception)
    }

    object InProgress : Result<Nothing>()
}

通过Sealed Class可以很方便的对Error类型进行拓展,同时,增加新的状态也非常简单,更重要的是,通过IDE的自动补全功能,IDE可以自动生成各个条件分支,避免人工编码的遗漏。

向大家推荐下我的网站 xuyisheng.top/ 专注 Android-Kotlin-Flutter 欢迎大家访问