kotlin系列知识卡片 - 密封类
前言
随着kotlin在Android日常开发中越来越频繁地使用,笔者觉得有必要对kotlin的一些特性做一些原理的梳理,所谓知其然知其所以然,才能更好让kotlin服务于我们的业务开发,所以会做一个kotlin系列,每个知识点简短,像个小卡片一样,便于理解和快速回顾。所有的源码为:Kotlin 标准库版本为1.9.10。
密封类
密封类(sealed class)是一种特殊的类,它的继承有更多的限制,密封类的字类数量是有限的,在编译时其所有的子类都是已知的,定义密封类的模块和包之外不得出现其他子类,编译时可以检查所有可能的字类。对比于枚举类,密封类和枚举都是类型有限个,但密封类只是类型有限,其可有多个实例,而枚举类则是常量,仅作为单个实例存在。
在不同的kotlin版本对密封类的使用有一些限制:
- Kotlin 1.0: 密封类的子类必须位于与密封类相同的文件中。不可在其他文件中被定义。
- Kotlin 1.1及更高版本:允许子类位于其他文件中,提高了代码的重用性和拓展性;子类可为嵌套类、顶层类、内部类
使用
密封接口/类及其子类
- 场景一:单例对象+自定义实例
sealed interface IDirection {
data class Custom(val dir: Int): IDirection
object Top: IDirection
object Left: IDirection
object Right: IDirection
object Bottom: IDirection
}
sealed class Type(val v: Int) {
data object Type1: Type(1)
data object Type2: Type(2)
data object Type3: Type(3)
data object Type4: Type(4)
}
数据的解析处理
private fun test(dir: IDirection) {
when (dir) {
is IDirection.Top -> { ... }
is IDirection.Left -> { ... }
is IDirection.Right -> { ... }
is IDirection.Bottom -> { ... }
is IDirection.Custom -> { ... }
}
}
多层嵌套密封类
密封类数据结构定义
sealed interface IMsg {
sealed interface Page: IMsg {
object Init: Page
data class StateChange(val state: Int): Page
}
sealed interface User: IMsg {
object Login: User
object Logout: User
}
}
多层级数据kotlin的解析处理
private fun test(msg: IMsg) {
when (msg) {
is IMsg.Page -> { dispatchPageMsg(it) }
is IMsg.User -> { dispatchUserMsg(it) }
}
}
/**
* 处理IMsg.Page类型数据
*/
private fun dispatchPageMsg(msg: IMsg.Page) {
when (msg) {
is IMsg.Page.Init -> { ... }
is IMsg.Page.StateChange -> { ... }
}
}
/**
* 处理IMsg.User类型数据
*/
private fun dispatchUserMsg(msg: IMsg.User) {
when (msg) {
is IMsg.User.Login -> { ... }
is IMsg.User.Logout -> { ... }
}
}
原理
单例类型的密封类对象
我们以【使用】部分提到的IDirection
的IDirection.Top
密封单例对象为例,反编译得到代码如下:
- 定义一个
INSTANCE
作为Top
的实例引用;- 将构造函数设置为私有构造,防止外部创建实例
- 静态代码块初始化Top对象实例,静态代码块在类加载时就自动执行,确保INSTANCE对象在类载入时就完成初始化,且仅被初始化一次
@Metadata(...)
public static final class Top implements IDirection {
@NotNull
public static final Top INSTANCE;
private Top() {
}
static {
Top var0 = new Top();
INSTANCE = var0;
}
}
可实例化的密封类
我们以【使用】部分提到的IDirection
的IDirection.Custom
密封单例对象为例,反编译得到代码如下:
从反编译后的源码中,我们可以看到代码中除了Custom类本身的构造及参数外,还默认实现了copy、toString、hashCode、equals等方法以支持kotlin的数据类的功能,因为kotlin的数据类支持解构声明,Custom中有一个dir参数,所以编译器生成了一个component1的函数方法。
public static final class Custom implements IDirection {
private final int dir;
public final int getDir() {
return this.dir;
}
public Custom(int dir) {
this.dir = dir;
}
public final int component1() {
return this.dir;
}
@NotNull
public final Custom copy(int dir) {
return new Custom(dir);
}
// $FF: synthetic method
public static Custom copy$default(Custom var0, int var1, int var2, Object var3) {
if ((var2 & 1) != 0) {
var1 = var0.dir;
}
return var0.copy(var1);
}
@NotNull
public String toString() {
return "Custom(dir=" + this.dir + ")";
}
public int hashCode() {
return Integer.hashCode(this.dir);
}
public boolean equals(@Nullable Object var1) {
if (this != var1) {
if (var1 instanceof Custom) {
Custom var2 = (Custom)var1;
if (this.dir == var2.dir) {
return true;
}
}
return false;
} else {
return true;
}
}
}
密封类的拓展受限规则如何实现?
密封接口的子类必须在与密封接口相同的模块和包中定义,所以在其他模块和包中是无法扩展密封接口的。但从反编译的代码中可以看到,密封接口编译器会将其编译为一个公共(public)的接口,从理论上是可以被外部访问和拓展的,但 Kotlin 编译器在编译密封接口时,会为其生成一个特殊的元数据(metadata),用于记录密封接口的限制信息。这个元数据会在编译时被 Kotlin 编译器检查,以确保密封接口的子类只能在与密封接口相同的模块和包中定义。
在
d1
字段中,\u0086\b
表示这是一个密封接口
@Metadata(
mv = {1, 9, 0},
k = 1,
d1 = {"\u0000&\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\b\n\u0002\b\u0006\n\u0002\u0010\u000b\n\u0000\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u000e\n\u0000\b\u0086\b\u0018\u00002\u00020\u0001B\r\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0002\u0010\u0004J\t\u0010\u0007\u001a\u00020\u0003HÆ\u0003J\u0013\u0010\b\u001a\u00020\u00002\b\b\u0002\u0010\u0002\u001a\u00020\u0003HÆ\u0001J\u0013\u0010\t\u001a\u00020\n2\b\u0010\u000b\u001a\u0004\u0018\u00010\fHÖ\u0003J\t\u0010\r\u001a\u00020\u0003HÖ\u0001J\t\u0010\u000e\u001a\u00020\u000fHÖ\u0001R\u0011\u0010\u0002\u001a\u00020\u0003¢\u0006\b\n\u0000\u001a\u0004\b\u0005\u0010\u0006¨\u0006\u0010"},
d2 = {"LIDirection$Custom;", "LIDirection;", "dir", "", "(I)V", "getDir", "()I", "component1", "copy", "equals", "", "other", "", "hashCode", "toString", "", "calendarview_debug"}
)
public static final class Custom implements IDirection {