sealed 意为密封的,可修饰类 class 和接口 interface,用来表示受限的继承结构。
Sealed Class
介绍
sealed class
,密封类,密封类是一种特殊的抽象类,用于限制可以继承它的子类。
密封类具备最重要的一个特点:
- 其子类可以出现在定义 sealed class 的不同文件中,但不允许出现在与之不同的
module
中,且需要保证package
一致。
这样既可以避免 sealed class 文件过于庞大,又可以确保第三方库无法扩展你定义的 sealed class,达到限制类的扩展目的。事实上在早期版本中,只允许在 sealed class 内部或定义的同文件内扩展子类,这些限制在 Kotlin 1.5
中被逐步放开。
sealed class 还具有如下特点或限制:
- sealed class 是抽象类,可以拥有抽象方法,无法直接实例化。
- sealed class 的构造函数只能拥有两种可见性:默认情况下是
protected
,还可以指定成 private,但不允许指定成 public。 - sealed class 子类可扩展局部以及匿名类以外的任意类型子类,包括普通 class、
data class
、object
、sealed class 等,子类信息在编译期可知。 - sealed class 的实例,可配合
when
表达式进行判断,当所有类型覆盖后可以省略else
分支。 - 当 sealed class 子类没有指定构造方法或定义任意属性的时候,建议定义成单例
object
,因为即便实例化成多个实例,互相之间没有状态的区别。在 Kotlin 1.9 版本新增 data object 用于取代 object 的用法,编译器会提示“'sealed' sub-object can be converted to 'data object' ”,toString() 不会打印 HashCode 等无用信息让输出更有意义。
使用
可以在 sealed class 内部定义子类:
sealed class Language {
//定义在内部
data object English : Language()
data class French(val str: String) : Language()
class German(str: String) : Language()
}
还可以在外部定义子类:
sealed class Language {
//定义在内部
data object English : Language()
data class French(val str: String) : Language()
class German(str: String) : Language()
}
//定义在同一文件中
data object Chinese : Language()
data class Japanese(val str: String) : Language()
此外还可以定义在同包名不同一文件下
//定义在同包名不同一文件下
data class Korean(val str: String) : Language()
对于不同类型的扩展子类,when
表达式的判断亦不同:
- 判断 sealed class 内部子类类型自然需要指定父类前缀
- 判断 sealed class 外部子类类型自然无需指定前缀
- object class 的话可以直接进行实例判断,也可以用
is
关键字判断类型匹配 - 普通 class 类型的话则必须使用 is 关键字判断类型匹配
fun demo(language: Language) {
when (language) {
Language.English -> {}
is Language.French -> {}
is Language.German -> {}
Chinese -> {}
is Japanese -> {}
is Korean -> {}
}
}
我们知道Kotlin代码最终会编译成Java字节码的,让我们来看一下,上述的Kotlin代码反编译之后是怎么样的:
public abstract class Language {
public /* synthetic */ Language(DefaultConstructorMarker defaultConstructorMarker) {
this();
}
private Language() {
}
// subclass:data object
public static final class English extends Language {
public static final English INSTANCE = new English();
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof English)) {
return false;
}
English english = (English) obj;
return true;
}
public int hashCode() {
return -765073291;
}
public String toString() {
return "English";
}
private English() {
super(null);
}
}
// subclass:data class
public static final class French extends Language {
private final String str;
public static /* synthetic */ French copy$default(French french, String str2, int i, Object obj) {
if ((i & 1) != 0) {
str2 = french.str;
}
return french.copy(str2);
}
public final String component1() {
return this.str;
}
public final French copy(String str2) {
Intrinsics.checkNotNullParameter(str2, "str");
return new French(str2);
}
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
return (obj instanceof French) && Intrinsics.areEqual(this.str, ((French) obj).str);
}
public int hashCode() {
return this.str.hashCode();
}
public String toString() {
return "French(str=" + this.str + ')';
}
public French(String str2) {
super(null);
Intrinsics.checkNotNullParameter(str2, "str");
this.str = str2;
}
public final String getStr() {
return this.str;
}
}
// subclass:class
public static final class German extends Language {
public German(String str) {
super(null);
Intrinsics.checkNotNullParameter(str, "str");
}
}
}
可以看到 sealed class 本身被编译为 abstract
class,其内部子类按类型有所不同:
- data object 类型在 class 内部集成了静态的
INSTANCE
实例,同时还有equals
、toString
以及hashCode
函数。 - data class 类型在 class 内部集成了属性的
get
、equals
、copy
、toString
以及hashCode
函数。 - class 类型仍是普通的 class。
而外部子类和同包名不同一文件下的子类则自然是定义在 Language 抽象类外部,内容也是跟上面一样的。
Sealed Interface
sealed interface
,密封接口,和 sealed class 有几乎一样的特点。此外,还有额外的优势,可以帮助密封类、枚举类等类实现多继承和扩展性,比如搭配枚举,以处理更复杂的分类逻辑。
举例:Flappy Bird 游戏的过程中会产生很多 Action 来触发数据的计算以推动 UI 刷新以及游戏的进程,Action 可以用 enum class 来管理。
其中有些 Action 是关联的,有些则没有关联、不是同一层级。但是 enum class 默认扩展自 Enum 类,无法再嵌套 enum。
这将导致层级混乱、阅读性不佳,甚至有的时候功能相近的时候还得特意取个不同的名称。
enum class Action {
Tick,
// GameAction
Start, Exit, Restart,
// BirdAction
Up, Down, HitGround, HitPipe, CrossedPipe,
// PipeAction
Move, Reset,
// RoadAction
// 防止和 Pipe 的 Action 重名导致编译出错,
// 将功能差不多的 Road 移动和重置 Action 定义加上了前缀
RoadMove, RoadReset
}
fun dispatch(action: Action) {
when (action) {
Action.Tick -> {}
Action.Start -> {}
Action.Exit -> {}
Action.Restart -> {}
Action.Up -> {}
Action.Down -> {}
Action.HitGround -> {}
Action.HitPipe -> {}
Action.CrossedPipe -> {}
Action.Move -> {}
Action.Reset -> {}
Action.RoadMove -> {}
Action.RoadReset -> {}
}
}
借助 sealed interface 我们可以给抽出 interface,并将 enum 进行层级拆分。更加清晰、亦不用担心重名。
sealed interface Action
enum class GameAction : Action {
Start, Exit, Restart
}
enum class BirdAction : Action {
Up, Down, HitGround, HitPipe, CrossedPipe
}
enum class PipeAction : Action {
Move, Reset
}
enum class RoadAction : Action {
Move, Reset
}
object Tick: Action
使用的时候就可以对抽成的 Action 进行嵌套判断:
fun dispatch(action: Action) {
when (action) {
Tick -> {}
is GameAction -> {
when (action) {
GameAction.Start -> {}
GameAction.Exit -> {}
GameAction.Restart -> {}
}
}
is BirdAction -> {
when (action) {
BirdAction.Up -> {}
BirdAction.Down -> {}
else -> {}
}
}
is PipeAction -> {
when (action) {
PipeAction.Move -> {}
PipeAction.Reset -> {}
}
}
is RoadAction -> {
when (action) {
RoadAction.Move -> {}
RoadAction.Reset -> {}
}
}
}
}
sealed 与 enum 的区别
-
enum
:enum 只是一个值(常量),每个 enum 常量只能以单例的形式存在。 -
sealed
:sealed可以是一个值(定义成 data object 不携带数据),还可以是一个有状态的值(定义成 data class 携带数据)。sealed class 子类可以拥有多个实例,不受限制,每个均可以拥有自己的状态。 - enum class 不能扩展自 sealed class 以及其他任何 Class,但可以实现 sealed interface,正如上面 Action 的举例。
网友评论