Kotlin Sealed 是什么?为什么 Google 都在用
前言
在之前的文章 「 Google 推荐在项目中使用 Sealed 和 RemoteMediator :https://juejin.im/post/6854573220457086990 」中介绍了如何使用 Sealed Classes 在 Flow 基础上对网络请求成功和失败进行处理,而这篇文章是对 Sealed Classes 更加深入的解析,结合函数式编程功能很强大,掌握并且灵活运用它,需要大量的实践。
通过这篇文章你将学习到以下内容:
Sealed Classes 原理分析? 枚举和抽象类都有那些局限性? 为什么枚举可以作为单例?枚举作为单例有那些优点? 分别在什么情况下使用枚举和 Sealed Classes? Sealed Classes 究竟是什么? 为什么 Sealed Classes 用于表示受限制的类层次结构? 为什么说 Sealed Classes 是枚举类的扩展? Sealed Classes 的子类可以表示不同状态的实例,那么在项目中如何使用? 禁止在 Sealed Classes 所定义的文件外使用, Kotlin 是如何做到的呢?
枚举和抽象类的局限性
在分析 Sealed Classes 之前,我们先来分析一下枚举和抽象类都有那些局限性,注意:这些局限性是相对于 Sealed Classes 而言的,但是相对于它们自身而言是优点,而 Sealed Classes 出现也正是为了解决这些问题。先来看一下枚举的局限性:
限制枚举每个类型只允许有一个实例 限制所有枚举常量使用相同的类型的值
限制枚举每个类型只允许有一个实例
enum class Color(val value: Int) {
Red(1)
}
fun main(args: Array<String>) {
val red1 = Color.Red
val red2 = Color.Red
println("${red1 == red2}") // true
}
最后输出结果
red1 == red2 : true
正如你看到的,我们定义了一个单元素的枚举类型,无论 Color.Red 有多少个对象,最终他们的实例都是一个,每个枚举常量仅作为一个实例存在,而一个密封类的子类可以有多个包含状态的实例,这既是枚举的局限性也是枚举的优点。
枚举常量作为一个实例存在的优点: 枚举不仅能防止多次实例化,而且还可以防止反序列化,还能避免多线程同步问题,所以它也被列为实现单例方法之一。简单汇总一下。
是否只有一个实例 | 是否反序列化 | 是否是线程安全 | 是否是懒加载 |
---|---|---|---|
是 | 是 | 是 | 否 |
《Effective Java》 一书的作者 Josh Bloch 建议我们使用枚举作为单例,虽然使用枚举实现单例的方法还没有广泛采用,但是单元素的枚举类型已经成为实现 Singleton 的最佳方法。
我们来看一下如何用枚举实现一个单例(与 Java 的实现方式相同),这里不会深究其原理,因为这不是本文的重点内容,小伙伴们可以从掘金搜索,有很多分析这方面原理的文章。
interface ISingleton {
fun doSomething()
}
enum class Singleton : ISingleton {
INSTANCE {
override fun doSomething() {
// to do
}
};
fun getInstance(): Singleton = Singleton.INSTANCE
}
但是在实际项目中使用枚举作为单例的很少,我看了很多开源项目,将枚举作为单例的场景少之有少,很大部分原因是因为使用枚举的时候非常不方便。
我这有个建议如果涉及反序列化创建对象的时候,建议使用枚举,因为 Java 规定,枚举的序列化和反序列化是有特殊定制的,因此禁用编译器使用 writeObject
、readObject
、readObjectNoData
、 writeReplace
、readResolve
等方法。
限制所有枚举常量使用相同的类型的值
限制所有枚举常量使用相同的类型的值,也就是说每个枚举常量类型的值是相同的,我们还是用刚才的例子做个演示。
enum class Color(val value: Int) {
Red(1),
Green(2),
Blue(3);
}
正如你所见,我们在枚举 Color 中定义了三个常量 Red 、Green 、Blue,但是它们只能使用 Int 类型的值,不能使用其他类型的值,如果使用其它类型的值会怎么样?如下所示:
编译器会告诉你只接受 Int 类型的值,无法更改它的类型,也就是说你无法为枚举类型,添加额外的信息。
抽象类的局限性
对于一个抽象类我们可以用一些子类去继承它,但是子类不是固定的,它可以随意扩展,同时也失去枚举常量的受限性。
Sealed Classes 包含了抽象类和枚举的优势:抽象类表示的灵活性和枚举常量的受限性
到这里可能会有一个疑问,如果 Sealed Classes 没有枚举和抽象类的局限性,那么它能在实际项目中给我们带来哪些好处呢?在了解它能带来哪些好处之前,我们先来看看官方对 Sealed Classes 的解释。
Sealed Classes 是什么?
我们先来看一下官方对 Sealed Classes 的解释
我们将上面这段话,简单的总结一下:
Sealed Classes 用于表示受限制的类层次结构 从某种意义上说,Sealed Classes 是枚举类的扩展 枚举的不同之处在于,每个枚举常量仅作为单个实例存在,而 Sealed Classes 的子类可以表示不同状态的实例
那上面这三段话分别是什么意思呢?接下来我们围绕这三个方面来分析。
Sealed Classes 用于表示受限制的类层次结构
Sealed Classes 用于表示受限制的类层次结构,其实这句话可以拆成两句话来理解。
Sealed Classes 用于表示层级关系: 子类可以是任意的类, 数据类、Kotlin 对象、普通的类,甚至也可以是另一个 Sealed Sealed Classes 受限制: 必须在同一文件中,或者在 Sealed Classes 类的内部中使用,在 Kotlin 1.1 之前,规则更加严格,子类只能在 Sealed Classes 类的内部中使用
Sealed Classes 的用法也非常的简单,我们来看一下如何使用 Sealed Classes。
sealed class Color {
class Red(val value: Int) : Color()
class Green(val value: Int) : Color()
class Blue(val name: String) : Color()
}
fun isInstance(color: Color) {
when (color) {
is Color.Red -> TODO()
is Color.Green -> TODO()
is Color.Blue -> TODO()
}
}
在这里推荐大家一个快捷键 Mac/Win/Linux:Alt + Enter
可以补全 when 语句下的所有分支,效果如下所示:
更多 AndroidStudio 快捷键,可以看之前的两篇文章
为数不多的人知道的 AndroidStudio 快捷键(一)
https://juejin.im/post/6844904020511981581为数不多的人知道的 AndroidStudio 快捷键(二)
https://juejin.im/post/6844904023082926087
Sealed Classes 是枚举类的扩展
从某种意义上说,Sealed Classes 是枚举类的扩展,其实 Sealed Classes 和枚举很像,我们先来看一个例子。
正如你所看到的,在 Sealed Classes 内部中,使用 object 声明时,我们可以重用它们,不需要每次创建一个新实例,当这样使用时候,它看起来和枚举非常相似。
注意:实际上很少有人会这么使用,而且也不建议这么用,因为在这种情况枚举比 Sealed Classes 更适合
在什么情况下使用枚举
如果你不需要多次实例化,也不需要不提供特殊行为,或者也不需要添加额外的信息,仅作为单个实例存在,这个时候使用枚举更加合适。
我们来看一下 Paging3 中是如何使用枚举的,一起来看一下 androidx.paging.LoadType
这个类的源码。
enum class LoadType {
REFRESH,
PREPEND,
APPEND
}
枚举常量 | 作用 |
---|---|
refresh | 在初始化刷新的使用 |
append | 在加载更多的时候使用 |
prepend | 在当前列表头部添加数据的时候使用 |
它们不需要多次实例化,也不需要添加任何额外的信息,仅仅表示某种状态,而且它在很多地方都会用到比如 RemoteMediator、PagingSource 等等,想了解更多关于 Paging3 原理和实战案例可以看之前写的几篇文章。
Jetpack 成员 Paging3 数据库实践以及源码分析(一)
https://juejin.im/post/5ee998e8e51d4573d65df02bJetpack 成员 Paging3 网络实践及原理分析(二)
https://juejin.im/post/5eeefbf4e51d45742c53ddceJetpack 成员 Paging3 使用 RemoteMediator 实现加载网络分页数据并更新到数据库中(三)
https://juejin.im/post/6854573220457086990神奇宝贝(PokemonGo) 眼前一亮的 Jetpack + MVVM 极简实战
https://juejin.im/post/6850037271253483534
4.Sealed Classes 的子类可以表示不同状态的实例
与枚举的不同之处在于,每个枚举常量仅作为单个实例存在,而 Sealed Classes 的子类可以表示不同状态的实例,我们来看个例子可能更容易理解这句话。
这里我们延用之前在 「 Google 推荐在项目中使用 Sealed 和 RemoteMediator :https://juejin.im/post/6854573220457086990 」这篇文章中用到的例子,在请求网络的时候需要对成功或者失败进行处理,我们来看一下用 Sealed Classes 如何进行封装。
sealed class PokemonResult<out T> {
data class Success<out T>(val value: T) : PokemonResult<T>()
data class Failure(val throwable: Throwable?) : PokemonResult<Nothing>()
}
这里只贴出来部分代码,核心实现可以查看项目 PokemonGo : https://github.com/hi-dhl/PokemonGo
代码路径 :PokemonGo/app/.../com/hi/dhl/pokemon/data/remote/PokemonResult.kt
一起来看一下如何使用
when (result) {
is PokemonResult.Failure -> {
// 进行失败提示
}
is PokemonResult.Success -> {
// 进行成功处理
}
}
我们在来看另外一个例子,在一个列表中可能会有不同类型的数据,比如图片、文本等等,那么用 Sealed Classes 如何表示。
sealed class ListItem {
class Text(val title: String, val content: String) : ListItem()
class Image(val url: String) : ListItem()
}
这是两个比较常见的例子,当然 Sealed Classes 强大不止于此,还有更多场景,等着一起来挖掘。
分享的一个比较有趣的例子,对 View 进行的一系列操作可以封装在 Sealed Classes 中,我们来看一下会有什么样的效果。
sealed class UiOp {
object Show: UiOp()
object Hide: UiOp()
class TranslateX(val px: Float): UiOp()
class TranslateY(val px: Float): UiOp()
}
fun execute(view: View, op: UiOp) = when (op) {
UiOp.Show -> view.visibility = View.VISIBLE
UiOp.Hide -> view.visibility = View.GONE
is UiOp.TranslateX -> view.translationX = op.px
is UiOp.TranslateY -> view.translationY = op.px
}
在 Sealed Classes 类中,我们定义了一系列 View 的操作 Show
、 Hide
、 TranslateX
、 TranslateY
,现在我们创建一个类,将这些对视图的操作整合在一起。
class Ui(val uiOps: List = emptyList()) {
operator fun plus(uiOp: UiOp) = Ui(uiOps + uiOp)
}
在 Ui 这个类中声明了一个 List 存储了所有的操作,并重写了 plus 操作符,关于 plus 操作符可以看之前的文章 「 为数不多的人知道的 Kotlin 技巧以及 原理解析 :https://juejin.im/post/6844904184974835720 」 通过 plus 操作符将这些对视图的操作拼接在一起,这样不仅可以提高代码的可读性,而且使用起来也非常的方便,都定义好之后,我们来看一下如何使用这个类。
val ui = Ui() +
UiOp.Show +
UiOp.TranslateX(20f) +
UiOp.TranslateY(40f) +
UiOp.Hide
run(view, ui)
定义了一系列操作之后,然后通过 run 方法来执行这些操作,来看一下 run 方法的实现。
fun run(view: View, ui: Ui) {
ui.uiOps.forEach { execute(view, it) }
}
代码很简单,这里就不多做解释了,在 kotlin 中函数可以作为参数传递,可以将 run 方法传递给另一个函数或者一个类,并且这些操作完全可互换的,将它们结合在一起功能将非常强大。
Sealed Classes 强大不止于此,还有很多很多非常实用的场景,现在我对 Sealed Classes 的理解也非常有限,还不够灵活的使用它,我相信在更多项目,更多的场景,会看到更多实用的一些技巧。
Sealed Classes 原理
在这里我们还是使用上文中用到的例子,来分析 Sealed Classes 原理。
sealed class Color {
object Red : Color()
object Green : Color()
object Blue : Color()
}
一起来分析一下反编译后的 Java 代码都做了什么。PS:Tools → Kotlin → Show Kotlin Bytecode
... // 省略部分代码
@Metadata(
mv = {1, 1, 13},
bv = {1, 0, 3},
k = 1,
d1 = {"\u0000\u001a\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0004\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0000\b6\u0018\u00002\u00020\u0001:\u0003\u0003\u0004\u0005B\u0007\b\u0002¢\u0006\u0002\u0010\u0002\u0082\u0001\u0003\u0006\u0007\b¨\u0006\t"},
d2 = {"Lcom/hidhl/leetcode/test/saledvsemun/Color;", "", "()V", "Blue", "Green", "Red", "Lcom/hidhl/leetcode/test/saledvsemun/Color$Red;", "Lcom/hidhl/leetcode/test/saledvsemun/Color$Green;", "Lcom/hidhl/leetcode/test/saledvsemun/Color$Blue;", "Java-kotlin"}
)
public abstract class Color {
private Color() {
}
public Color(DefaultConstructorMarker $constructor_marker) {
this();
}
}
... // 省略部分代码
@Metadata
这个注解会出现在 Kotlin 编译器生成的任何类文件中,可以通过反射的方式获取 @Metadata
信息。参数名称都非常短,可以帮助减少 class 文件的大小。
@Metadata
存储了 Kotlin 主要的语法信息例如扩展函数、typealias 等等,这些信息都是由 kotlinc 编译器,并以注解的形式存放在 Java 的字节码中的,如果元数据被丢弃掉,运行在 JVM 上会抛出异常,那么如何才能确定它们之间的对应关系呢,其实就是通过 @Metadata
这个注解提供的信息。
正因为元数据不能被丢掉,R8 带了新的优化,将元数据信息记录在 R8 的内部数据结构中,当 R8 完成对第三库或者应用程序的优化和收缩时,它会为所有 Kotlin 类合成新的正确的 Kotlin 元数据,其目的就是为了减少应用程序的大小,目前我也在研究中,日后会分享。
而在本例中 @Metadata
保存了一个子类的列表,编译器在使用的时候会用到这些信息。正如你看到的 Sealed class 被编译成了 abstract class,它本身是不能被实例化,只能用它的子类实例化对象。
抽象类 Color 默认的构造方法被私有化了,所以在 Kotlin 1.1 之前,子类必须嵌套在 Sealed Classes 类中,后来放宽了要求,禁止在 Sealed Classes 所定义的文件外使用, Kotlin 是如何做到的呢?如果我们在 Sealed Classes 所定义的文件外使用会怎么样?
正如你所看到,会导致编译错误,那么为什么 Sealed Classes 可以在同文件内使用呢?来看一下反编译后的代码。
// sealed
sealed class Color
// 同文件中使用 sealed class
class Red : Color()
// 以下是反编译代码
... // 省略部分代码
public final class Red extends Color {
public Red() {
super((DefaultConstructorMarker)null);
}
}
... // 省略部分代码
public abstract class Color {
private Color() {
}
// $FF: synthetic method
public Color(DefaultConstructorMarker $constructor_marker) {
this();
}
}
... // 省略部分代码
可以看到 Red class 被编译成了 final class, Sealed class 被编译成了 abstract class,同时编译器生成了一个 公有 的构造方法,其他的类无法直接调用,只有 Kotlin 编译器可以使用,Red class 被编译成 final class,在其构造方法内调用了 Color class 公有 的构造方法,而这些都是 Kotlin 编译器帮我们做的。
构造函数私有化,限制了子类必须嵌套在 Sealed Classes 类中 编译器生成了一个 公有 的构造方法,在子类的构造方法中调用了父类 公有 的构造方法,而这些都是 Kotlin 编译器帮我们做的
总结
枚举的局限性
限制枚举每个类型只允许有一个实例 限制所有枚举常量使用相同的类型的值
抽象类的局限性
对于一个抽象类我们可以用一些子类去继承它,但是子类不是固定的,它可以随意扩展,同时也失去枚举常量受限性。
枚举作为单例的优点
是否只有一个实例 | 是否反序列化 | 是否是线程安全 | 是否是懒加载 |
---|---|---|---|
是 | 是 | 是 | 否 |
Sealed Classes 是什么?
Sealed 是一个 abstract 类,它本身是不能被实例化,只能用它的子类实例化对象。Sealed 的构造方法私有化,禁止在 Sealed 所定义的文件外使用。
Sealed Classes 用于表示受限制的类层次结构 从某种意义上说,Sealed Classes 是枚举类的扩展 枚举的不同之处在于,每个枚举常量仅作为单个实例存在,而 Sealed Classes 的子类可以表示不同状态的实例。
在什么情况下使用枚举或者 Sealed?
如果涉及反序列化创建对象的时候,建议使用枚举,因为 Java 规定,枚举的序列化和反序列化是有特殊定制的,因此禁用编译器使用 writeObject
、readObject
、readObjectNoData
、writeReplace
、readResolve
等方法。如果你不需要多次实例化,也不需要不提供特殊行为,或者也不需要添加额外的信息,仅作为单个实例存在,这个时候使用枚举更加合适。 其他情况下使用 Sealed Classes,在一定程度上可以使用 Sealed Classes 代替枚举
自动补全 when 语句下的所有分支
推荐给大家一个快捷键 Mac/Win/Linux:Alt + Enter
可以补全 when 语句下的所有分支,效果如下所示:
参考文献
https://antonioleiva.com/sealed-classes-kotlin https://kotlinlang.org/docs/reference/sealed-classes.html https://mp.weixin.qq.com/s/yrWJ-ApIMTs979NW1v42UQ https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-metadata
最后推荐我一直在更新维护的项目和网站:
「为互联网人而设计,国内国外名站导航」涵括新闻、体育、生活、娱乐、设计、产品、运营、前端开发、Android 开发等等网址
https://site.51git.cn最新的 AndroidX Jetpack 相关组件的实战项目 以及 原理分析的文章
https://github.com/hi-dhl/AndroidX-Jetpack-PracticeLeetCode 算法题解涵盖:数组、栈、队列、字符串、链表、树,查找算法、搜索算法、位运算、排序等等,每道题目都会用 Java 和 kotlin 去实现
https://github.com/hi-dhl/Leetcode-Solutions-with-Java-And-Kotlin最新 Android 10 源码分析系列文章
https://github.com/hi-dhl/Leetcode-Solutions-with-Java-And-Kotlin一系列精选国外的技术文章,每篇文章都会有译者思考部分,对原文的更加深入的分析
https://github.com/hi-dhl/Technical-Article-Translation
长按二维码关注我