在开发中经常需要在代码中声明一些有限集合,如:网络请求可能为失败或成功;用户账号是高级用户或者普通用户。
可以使用枚举来实现这类模型,但枚举自身存在很多限制。枚举类型每个值只允许有一个实例,同时枚举也无法为每个类型添加额外的信息。也可以使用一个抽象类然后让一些类继承它,这样就可以随意扩展,但这会失去枚举所带来的有限集合的优势。而sealed class则同时包含前面两者的优势——抽象类表示的灵活性和枚举里集合的受限性。
密封类的基本使用
和抽象类类似,密封类可用于表示层级关系。子类可以是任意的类:数据类(data class)、kotlin对象(object)、普通的类,也可以是另一个密封类。但不同于抽象类的是,必须把层级声明在同一个文件中,或者嵌套在类的内部:
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>()
}
尝试在密封类所定义的文件外继承类则会导致编译错误。
when表达式
在when语句中,常常需要处理所有可能的类型:
fun handleResult(result: Result<*>){
when(result){
is Result.Error->{}
is Result.Success->{}
}
}
如果要为Result类添加一个新的类型InProgress:
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>()
object InProgress:Result<Nothing>()
}
如果想防止泄漏对新类型的处理,并不一定需要依赖我们自己去记忆或者使用IDE的搜索功能确认新添加的类型。使用when语句处理密封类时,如果没有覆盖所有情况,可以让编译器给我们一个错误提示。和if语言一样,when语句在作为表达式使用时,会通过编译器报错来强制要求覆盖所有选项:
fun handleResult(result: Result<*>){
//编译错误
val action = when(result){
is Result.Error->{}
is Result.Success->{}
}
}
工作原理
为什么密封类会拥有这些特效?反编译后的Java代码如下:
sealed class Result
data class Success(val data: Any) : Result()
data class Error(val exception: Exception) : Result()
@Metadata(
...
d2 = {"Lio/testapp/Result;", "T", "", "()V", "Error", "Success", "Lio/testapp/Result$Success;", "Lio/testapp/Result$Error;" ...}
)
public abstract class Result {
private Result() {
}
// $FF: synthetic method
public Result(DefaultConstructorMarker $constructor_marker) {
this();
}
}
密封类的元数据中保存类一个子类的列表,编译器可以在需要的地方使用这些信息。
Result是一个抽象类,并且包含两个构造方法:
- 一个私有的构造方法
- 一个合成构造方法,只有Kotlin编译器可以使用
这意味着其他的类无法直接调用密封类的构造方法。查看Success类反编译后的代码,可以看到他调用类Result的合成构造方法:
public final class Success extends Result {
@NotNull
private final Object data
public Success(@NotNull Object data) {
Intrinsics.checkParameterIsNotNull(data, "data");
super((DefaultConstructorMarker)null);
this.data = data;
}