sealed
关键字在C#
中用来修饰类。sealed
这个词“人如其名”,显而易见的表明:它使得被它修饰的类或者方法被封闭,不允许被继承(sealed class),或者被重写(sealed method)。Kotlin中也有sealed
关键字,但它的含义与C#中的含义有巨大的不同。
在很多语言中都有sealed
关键字。比如在C#
中
1sealed class A {
2 public void foo() {
3 }
4}
5
6class B: A {
7}
上面这个C#
类被用sealed
修饰,因此如果我们试图声明一个新类class B
继承自class A
就会编译失败。当然在C#
中也可以将sealed
应用到方法上,从而禁止任何子类重写该方法。详情参见C# reference。
事实上,java
也有类似功能的关键字final
,同样可以实现禁止一个类被继承。Kotlin
是一款JVM,也有sealed
关键词。但该关键词的含义却与C#
中的含义相差巨大,这里做一些简要介绍。
enum class
Kotlin
中有很多不同的类,其中一种类叫enum class
。与其他语言中的枚举类型是一个意思,但是是以类的形式出现,也多了一些类的特性。
1enum class Fruit {
2 Apple, Peach, Unknown
3}
4
5fun main() {
6 val fruit = Fruit.Apple
7 print("$fruit")
8}
通常用到enum
我们都希望语言最好能支持:
- 给
enum
的字面值赋予数字值 - 给
enum
增加一些好用的方法
Kotlin
作为现代化的语言自然会给予支持:
1enum class Fruit(val num: Int, val unitWeight: Float) {
2 Apple(1, 1.0f) {
3 override fun eat() = Peach
4 },
5
6 Peach(2, 2.0f) {
7 override fun eat() = Unknown
8 },
9
10 Unknown(3, 3.0f) {
11 override fun eat() = Apple
12 };
13
14 abstract fun eat(): Fruit
15}
16
17fun main() {
18 val fruit = Fruit.Apple
19 println("$fruit")
20 println("${fruit.eat()}")
21 println("${fruit.num}")
22 println("${fruit.unitWeight}")
23}
以上示例很好的描述了Kotlin
中enum
的各种灵活语法。enum
的实例Apple
,Peach
,Unknown
都只能有一个。如果你希望多几个Apple
,enum class
是不支持的。比如我可能需要num
是2代表两个苹果;我甚至需要Peach
的num
也是2,表示两个桃子,然而这些都不是enum class
能做,甚至该做的。这时候就轮到sealed class
出场了。
sealed class
在Kotlin
语言中,sealed class
依旧可以有自己的子类。但是要求子类必须和sealed class
在同一个kt文件内。和C#
中的sealed class
做个对比:
1.Kotlin
中的sealed class
是个抽象类,不能实例化。C#
中的sealed class
恰好相反,必须是一个可以实例化的类。
2.Kotlin
中的sealed class
可以被继承,但仅限在同一个文件内的子类继承。C#
中的sealed class
无论在哪个文件里也不能被继承。
这是我不喜欢Kotlin
的原因之一。sealed
到底是sealed
啥? 既不是包级别的sealed
,也不是类级别的sealed
, 搞出一个不伦不类的 文件sealed
。按官方说法 这是一个enum class
的增强,即一个支持内部状态的enum
类型。enum
真的需要增强吗?特别是 用增加一个新语法的方式 来增强?有性价比吗?Ugly不?
暂且接受这一切。既然sealed class
是enum class
的一种增强,那我们就先用sealed class
实现一下同样的Fruit继承体系。
1sealed class Fruit(val num: Int, val unitWeight: Float) {
2 object Apple: Fruit(1, 1.0f) {
3 override fun eat(): Fruit {
4 return Fruit.Peach
5 }
6 }
7
8 object Peach: Fruit(2, 2.0f) {
9 override fun eat(): Fruit {
10 return Fruit.Unknown
11 }
12 }
13
14 object Unknown: Fruit(3, 3.0f) {
15 override fun eat(): Fruit {
16 return Fruit.Apple
17 }
18 }
19
20 abstract fun eat(): Fruit
21}
22
23fun main() {
24 val fruit = Fruit.Apple
25 println("$fruit")
26 println("${fruit.eat()}")
27 println("${fruit.num}")
28 println("${fruit.unitWeight}")
29 println("${Fruit.Apple.eat() is Fruit.Peach}")
30}
fruit
和fruit.eat()
输出的都是object
,故实际打印结果类似xyz.dev66.Fruit$Apple@3a03464。sealed class
这么用就失去它的意义了。它与enum class
最大的不同就是它可以实例化多个“水果”:
1sealed class Fruit(val num: Int, val unitWeight: Float) {
2 class Apple(num: Int, unitWeight: Float): Fruit(num, unitWeight) {
3 override fun eat(): Fruit {
4 return Fruit.Peach(2, 2.0f)
5 }
6 }
7
8 class Peach(num: Int, unitWeight: Float): Fruit(num, unitWeight) {
9 override fun eat(): Fruit {
10 return Fruit.Unknown(3, 3.0f)
11 }
12 }
13
14 class Unknown(num: Int, unitWeight: Float): Fruit(num, unitWeight) {
15 override fun eat(): Fruit {
16 return Fruit.Apple(1, 1.0f)
17 }
18 }
19
20 abstract fun eat(): Fruit
21}
22
23fun main() {
24 val fruit = Fruit.Apple(2, 1.0f)
25 println("$fruit")
26 println("${fruit.eat()}")
27 println("${fruit.num}")
28 println("${fruit.unitWeight}")
29 println("${Fruit.Apple(1, 1.0f) == Fruit.Unknown(3, 3.0f).eat()}")
30}
将object
改为class
,就能初始化多个苹果和桃子了。特别最后一个println
提醒我们尽管Fruit.Unknown(3, 3.0f).eat()
返回了Fruit.Apple(1, 1.0f)
,但是它和==
左侧的苹果实例并不是一个。
sealed
限制了只有Fruit
所在文件的维护者可以增加水果的类型。任何其他没有该文件修改权限的开发者,无法创建更多的水果。但是其他开发者可以在其他文件中创建苹果的子类,或者桃子的子类。所以到底sealed
的意义在哪里?
官方提供了一个sealed class
使用的方法,与when
结合,避免使用else
,从而使代码更健壮。我理解因为不使用else
,当增加了新的sealed class
子类之后,编译器会警告开发者补全when
的分支。
1fun printlnFruit(fruit: Fruit) {
2 when (fruit) {
3 is Fruit.Apple -> println("apple ${fruit.num}")
4 is Fruit.Peach -> println("peach ${fruit.num}")
5 is Fruit.Unknown -> println("unknown ${fruit.num}")
6 }
7}
用一句话解释sealed class
,一个支持多实例(有内部状态)的enum class
。