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。