sealed keyword in Kotlin

Posted on | 1990 words | ~4 mins
Android Kotlin

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我们都希望语言最好能支持:

  1. enum的字面值赋予数字值
  2. 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}

以上示例很好的描述了Kotlinenum的各种灵活语法。enum的实例Apple,Peach,Unknown都只能有一个。如果你希望多几个Appleenum class是不支持的。比如我可能需要num是2代表两个苹果;我甚至需要Peachnum也是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 classenum 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}

fruitfruit.eat()输出的都是object,故实际打印结果类似xyz.dev66.Fruit$Apple@3a03464sealed 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