该篇内容为原博客博文,原上传于2023年1月18日。
Sealed Class和Sealed Interface是 kotlin 引入的全新特性。在初学 kotlin 时,我就一直没有掌握其用法,甚至到现在也不能在该用的时候立刻想到。
本篇文章主要总结Sealed Class的基本概念和常见用法。
什么是 Sealed
jetbrains 对 Sealed Class/Interface 的介绍如下:
Sealed classes and interfaces represent restricted class hierarchies that provide more control over inheritance. All direct subclasses of a sealed class are known at compile time. No other subclasses may appear outside a module within which the sealed class is defined. For example, third-party clients can’t extend your sealed class in their code. Thus, each instance of a sealed class has a type from a limited set that is known when this class is compiled.
The same works for sealed interfaces and their implementations: once a module with a sealed interface is compiled, no new implementations can appear.
In some sense, sealed classes are similar to enum classes: the set of values for an enum type is also restricted, but each enum constant exists only as a single instance, whereas a subclass of a sealed class can have multiple instances, each with its own state.
从以上介绍可以大致总结出 Sealed Class/Interface 有如下特性:
表示受限的类层次结构,并可以对继承提供更多的控制
密封类的子类在编译期即被确定为一个有限的集合内,不可扩展
密封类的子类只能位于定义了该密封类的包中
密封类的子类可以有多个实例
介绍中还提到,sealed class 和 enum class 在某种意义上是类似的,实际上,sealed class 诞生的重要原因之一正是为了克服 enum class 在某些场合下的局限性,也即上述特性的第一点和第四点。我们知道,枚举类有两个特性,在某些场合下是优点,但在另外一些场合下却可能成为缺点:
每个枚举类型只能有一个实例(称为枚举常量)
各枚举常量只能使用相同类型的属性
1 | enum class Drink(val id: Int) { |
在如上我们创建的枚举类中,各子类(Milk,Coffee,Water)无论有多少对象,都只能有一个实例(即单例的枚举常量),且其属性均只能为枚举类中定义的 Int 类型。
而密封类则取消了以上限制,允许密封类的子类具有多个实例,且各子类可以定义自己的属性。
1 | sealed class Drink { |
如上,密封类允许各子类具有不同类型的属性,只需要在子类的主构造函数中声明即可。需要注意的是,密封类在继承上的写法与抽象类区别较大,反而更加类似于一般的抽象类。实际上,密封类正是抽象的,不能直接生成实例,且可以具有抽象成员。因此,密封类可以像上述写法一样定义继承自密封类的子类,也可以用object直接定义子类对象,也可以再用sealed class,即密封类继承密封类,实现更加细粒度的类层次结构。
各子类允许有多个不同状态的实例:
1 | val milk1 = Drink.Milk(1) |
上述特性的第二点意义在于,密封类对运行时的扩展是封闭的。程序在编译时,即可通过继承关系确认密封类的全部子类,由此产生了密封类一个非常实用的用处,在写 when 语句时不需要添加 else 分支,因为全部可能的分支在编译时即可确定,没有其他情况:
1 | fun test(drink: Drink) = when (drink) { |
上述特性的第三点意义在于,限制了密封类的作用空间,并且随着 kotlin 版本的更迭越来越宽松:从只能在 sealed class 内,到 1.1 支持同一文件内,到 1.5 支持同一包下。
怎么用 Sealed
kotlin 初学者往往会把sealed class当做enum class用,但enum class具有很明显的适用场景,这么做是不合适的。比如,你仅需要一系列相同类型的单例,且不需要任何额外描述,也不需要特殊的函数,那么用枚举就足够了,比如在我的《设计模式之状态模式》中,各状态有统一的处理方法,用枚举来表示。
在 Android 开发中, sealed class有如下一些使用场景:
列表有不同类型的子项(文字、图片),用密封类表示列表的 item
封装网络请求中成功(含有任意类型的请求数据)和失败(含有失败信息,如异常)返回的数据
使用
object达到与枚举类似的效果(虽然在 google 的官方示例都出现了这种用法,但还是不推荐。为什么不直接用枚举呢?)其他一切不满足枚举的应用场景,但需要与枚举类似效果,可以考虑用密封类
关于 Sealed Interface
以上都在讨论sealed class,而sealed interface作为 kotlin1.5 中登场的新特性,也值得说道说道。
密封接口进一步补足了密封类和枚举类的一些不足之处,如枚举类编译后继承自Enum,由于单继承不能再继承其他类,此时可以用密封接口,对枚举类做进一步划分。当然直接用嵌套密封类也未尝不可。