什么是 sealed class
先来看官网的定义:
- 密封类用来表示受限的类继承结构:当一个值为有限几种的类型、而不能有任何其他类型时。在某种意义上,他们是枚举类的扩展:枚举类型的值集合也是受限的,但每个枚举常量只存在一个实例,而密封类的一个子类可以有可包含状态的多个实例
密封类和枚举同作为一个值的列举功能,但是密封类的功能更加强大。体现在:
- 密封类的每个子类可以有多个对象,枚举常量就是枚举类型的一个对象
- 密封类子类可以持有其他引用数据,枚举常量只能持有枚举类构造方法传入的值
- 密封类可以自定义方法,枚举常量只能重写枚举类的方法
sealed class 的声明
在 Kotlin 1.1 之前,子类必须嵌套在密封类声明的内部。从 1.1 开始,变更为直接子类可以嵌套在密封类内部,也可以在密封类外,但是必须在密封类的 $\color{red} {Red} $ 同一个文件下面,间接子类则可以放在任意一个地方。
先来看个简单的加载状态的密封类的声明
// 声明一个密封类 LoadState
sealed class LoadState {
abstract val stateMessage: String
}
/**
* 直接继承密封类 LoadState,可以嵌套在 LoadState 内部,也可以放在同文件下
* 由于没有引用其他数据,这里建议直接用单例,可以复用
*/
object LoadStart: LoadState() {
override val stateMessage: String
get() = "Start load"
}
class LoadSuccess<out T>(val result: T): LoadState() {
override val stateMessage: String
get() = "load success"
}
class LoadFail(private val throwable: Throwable): LoadState() {
override val stateMessage: String
get() = "load failed with exception : ${Log.getStackTraceString(throwable)}"
}
如果用枚举去声明这个加载状态
enum class LoadState(val stateMessage: String) {
LoadStart("Start load"),
LoadSuccess("load success"),
LoadFail("load failed")
}
想要在枚举常量中的 LoadSuccess 和 LoadFail 中单独加入加载结果 result 和 Throwable 信息是不能够的。除非是在枚举类当中再加入 result 和 Throwable 变量,而 LoadSuccess 不需要 Throwable,LoadFail 不需要
result, LoadStart 更是都不需要,这就造成变量的冗余和资源的浪费。
enum class LoadState(val stateMessage: String) {
LoadStart("Start load") {
// LoadStart 不需要这两个字段
override var result: Any
get() = TODO("Not yet implemented")
set(value) {}
override var throwable: Throwable
get() = TODO("Not yet implemented")
set(value) {}
},
LoadSuccess("load success") {
override var result: Any
get() = TODO("Not yet implemented")
set(value) {}
// LoadSuccess 不需要这两个字段
override var throwable: Throwable
get() = TODO("Not yet implemented")
set(value) {}
},
LoadFail("load failed") {
// LoadFail 不需要这两个字段
override var result: Any
get() = TODO("Not yet implemented")
set(value) {}
override var throwable: Throwable
get() = TODO("Not yet implemented")
set(value) {}
};
abstract var result: Any
abstract var throwable: Throwable
}
when 表达式中的使用
当使用 when 表达式时,对于密封类需要覆盖所有的子类,如果没有,IDE 会提醒覆盖密封类的所有子类或者加入 else 语句,以达到逻辑的完整链路,这样在开发时就保证了程序的稳定性
fun testLoadState(loadState: LoadState) {
val result = when (loadState) {
LoadStart -> showLoadStartToast(loadState.stateMessage)
is LoadSuccess<*> -> showResult(loadState.result)
is LoadFail -> showLoadFailDialog(loadState.stateMessage)
}
}
这一点普通类是无法做到的,因为编译器不知道这个普通类到底有几个子类,一定要在最后加上一条 else 分支。假设 LoadState 是个普通抽象类
abstract class LoadState {
abstract val stateMessage: String
}
fun testLoadState(loadState: LoadState) {
val result = when (loadState) {
LoadStart -> showLoadStartToast(loadState.stateMessage)
is LoadSuccess<*> -> showResult(loadState.result)
is LoadFail -> showLoadFailDialog(loadState.stateMessage)
else -> {} // 不加入这条 else 分支,编译器会提示错误
}
}
总结
- 如果当只需要一个常量集以区分不同类型,可以直接使用 enum class
- 如果当需要一个 event,message 或者 state 类型,需要携带其他数据时,推荐使用 sealed class
参考文档
https://www.kotlincn.net/docs/reference/sealed-classes.html
https://blog.kotlin-academy.com/enum-vs-sealed-class-which-one-to-choose-dc92ce7a4df5