Kotlin 1.5 新特性:密封接口有啥用?

Kotlin 1.5 新特性:密封接口有啥用?
2021年06月18日 09:09 CSDN

作者|AndroidPub责编|张红月

出品 | CSDN(ID:CSDNnews)

Kotlin 1.5 引入了密封接口(Sealed Interface),这与密封类(Sealed Class)有什么区别呢?聊密封接口之前先回顾一下密封类的进化史。

密封类的进化史

密封类可以约束子类的类型,相当于强化版的枚举,相对于枚举更加灵活:

Enum Class:每个枚举都是枚举类的实例,可以直接使用

Sealed Class:密封类约束的子类只是一个类型,你可以为不同子类定义方法和属性,并对齐动态实例化

Kotlin1.0

早期 Kotlin 1.0 中的密封类,子类型必须是密封类的内部类:

//编程语言

sealedclassProgrammingLang{

object Assembly : ProgrammingLang()

classJava(ver: String) : ProgrammingLang()

classJavaScript(ver: String) : ProgrammingLang()

}

这可以防止在在不编译密封类的前提下为其创建新的派生类。任何派生类的添加都必须重新编译密封类本身,外部调用方能时刻同步所有的子类类型,确保 when 语句的合法:

//获取指定语言的排名

val ranking = when (val item: ProgrammingLang = getProgramLang()) {

Assembly -> TODO()

is Java -> TODO()

is JavaScript -> TODO()

}

另一个潜在的好处是子类必须连同父类名字一起出现,例如 ProgrammingLang.Java,这有助于明确其namespace。

Kotlin1.1

Kotlin 1.1 取消了子类必须在密封类内部定义的约束,密封类的子类可以声明在文件的 Top-Level。但是为了保证编译的同步,仍然需要在同一文件内。

sealedclassProgrammingLang

object Assembly : ProgrammingLang()

classJava(ver: String) : ProgrammingLang()

classJavaScript(ver: String) : ProgrammingLang()

Kotlin1.5

到了Kotlin 1.5,约束进一步放宽,允许子类定义在不同的文件中,只要保证子类和父类在同一个 Gradle module 且是同一个包名下即可。在一个 module 可以保证整个所有文件同时参与编译,仍然可以保证编译的同步。

// Lang.kt

sealedclassProgrammingLang

// Compiled.kt

classJava(ver: String) : ProgrammingLang()

classCPP(ver: String) : ProgrammingLang()

// Interpreted.kt

classJavaScript(ver: String) : ProgrammingLang()

classLua(ver: String) : ProgrammingLang()

// LowLevel.kt

object Assembly : ProgrammingLang()

放宽约束后,有利于子类按文件归类,同时,较长的子类拆分为单独文件也便于阅读。

如果违反了同Module、同包名的限制,编译会报错:

e: Inheritance of sealed classes or interfaces from different module is prohibited

e: Inheritor of sealed class or interface must be in package where base class is declared

密封接口SealedInterface

Kotlin 1.5 除了进一步放宽了对密封类的使用限制,还引入了密封接口。

通常引入接口最主要的目的无非就是对外隐藏实现,但是1.5的密封类已经可以通过分割文件隐藏子类了,密封接口存在的意义是什么?

在以下几个场景中密封接口可以弥补密封类的不足:

1. "final" 的 interface

有时,我们虽然对外暴露了interface,但是并不希望外界去实现它。比如kotlinx.coroutines 的 Job

publicinterfaceJob : CoroutineContext.Element {

...

publicfunstart(): Boolean

...

publicfuncancel(): Unit

...

}

Job 作为一个接口,外界可以对它任意实现,但显然这不是 kotlinx.coroutines 希望出现的。因为未来随着协程功能的迭代,Job 中的共有属性和方法或许会出现变化和增减,如果外部有其派生类很容易出现二进制兼容问题。

如果把 Job 定义为一个密封接口,就可以很好地避免上述问题。

可以大胆猜测,未来某版本的协程中 Job 会以密封接口的形式出现。我们在自己的 library 中也可以考虑使用密封接口避免暴露的接口被随意实现。

2. “可嵌套”的枚举

枚举和密封类功能上很相近,除了文章开头介绍的一些区别外,还有一个容易被忽略的点就是枚举类无法继承其他类。

枚举类的本质都是 Enum 的子类:

enumclassJvmLang {

Java, Kotlin, Scala

}

反编译 class 后会发现,JvmLang 继承自 Enum。

publicfinalclassJvmLangextendsEnum{

privateJvmLang(String s,int i){

super(s,i);

}

publicstaticfinal JvmLang Java;

publicstaticfinal JvmLang Kotlin;

publicstaticfinal JvmLang Scala;

...

static{

Java = new Action("Java",);

Kotlin = new Action("Kotlin",1);

Scala = new Action("Scala",2);

}

}

由于单继承的限制,枚举类无法继承 Enum 以外的其他 Class:

e: Enum class cannot inherit from classes

但有时候,、我们又需要枚举能实现嵌套以处理更复杂的分类逻辑。此时密封接口就成了唯一选择

sealedinterfaceLanguage

enumclassHighLevelLang : Language {

Java, Kotlin, CPP

}

enumclassMachineLang : Language {

ARM, X86

}

object AssemblyLang : Language

如上,我们通过密封接口实际上定义了一组“可嵌套”的枚举。

之后就可以通过多级 when 语句进行分类处理了:

when(lang) {

isMachine ->

when(lang) {

MachineLang.ARM-> TODO()

MachineLang.X86-> TODO()

}

isHighLevel ->

when(lang) {

HighLevelLang.CPP-> TODO()

HighLevelLang.Java-> TODO()

HighLevelLang.Kotlin-> TODO()

}

else-> TODO()

}  

3. 多继承的密封类

前两个密封接口的使用场景和密封类没有太多关系, 但其实密封接口也可以扩大密封类的使用场景:

image.png

比如上图中对编程语言的分类,就很难用单继承的密封类进行描述。

比如,当我们像下面这样定义密封类时

sealedclassJvmLang {

object Java : JvmLang()

object Kotlin : JvmLang()

object Groovy : JvmLang()

}

sealedclassCompiledLang {

object Java : CompiledLang()

object Kotlin : CompiledLang()

object Groovy : CompiledLang()

object Cpp : CompiledLang()

}

Java 不能同时继承自 CompiledLang 与 JvmLang ,所以无法在两个密封类中复用,需要重复定义。

此时可能有人会说,密封类是可以被继承的,可以让 JvmLang 继承 CompiledLang

sealedclassJvmLang : CompiledLang

object Java : JvmLang()

object Kotlin : JvmLang()

object Groovy : JvmLang()

object Cpp : CompiledLang()

如上,Java 同时是 CompiledLang 和 JvmLang 的子类,且没有违反单继承结构。

但这只是因为 Java 的语言特性还不够“复杂”罢了。

Groovy 除了是一个编译性语言,同时具有解释性语言的特性,可以同时归类为CompiledLang 和 InterpretedLang, 此时单继承结构很难维系,需要解除接口实现多继承:

sealedinterfaceCompiledLang

sealedinterfaceInterpretedLang

sealedinterfaceFunctionalLang

sealedinterfaceJvmLang : CompiledLang

object Java : JvmLang

object Kotlin : JvmLang, FunctionalLang

object Groovy : JvmLang, FunctionalLang, InterpretedLang

object JavaScript: InterpretedLang

object Cpp : CompiledLang, FunctionalLang

//编程语言的市场份额

funshareOfCompiledLang(lang: CompiledLang) = when(lang) {

Java -> TODO()

Kotlin -> TODO()

Groovy -> TODO()

Cpp -> TODO()

}

funshareOfInterpretedLang(lang: InterpretedLang) = when(lang) {

JavaScript -> TODO()

Groovy -> TODO()

}

无论处理 InterpretedLang 还是 CompiledLang, Groovy只需要定义一次。

当然,为了更清晰的显示每种 Lang 的所有属性,可以将 interface 之间的继承关系下放:

sealedinterfaceCompiledLang

sealedinterfaceInterpretedLang

sealedinterfaceFunctionalLang

sealedinterfaceJvmLang

object Java : JvmLang, CompiledLang

object Kotlin : JvmLang, CompiledLang, FunctionalLang

object Groovy : JvmLang, CompiledLang, FunctionalLang, InterpretedLang

object JavaScript: InterpretedLang

object Cpp : CompiledLang, FunctionalLang

与Java的兼容性

JDK15 开始,Java 也引入了密封类和密封接口,所以 JDK15 以上,Kotlin 和 Java 之间的密封类和密封接口可以比较好的映射和互操作。

即使在 JDK15 以下,由于密封类在字节码中的构造函数加了 prevate 修饰,可以防止 Java 代码的继承。

//kotlin

sealedclassProgrammingLang

//java

classJavaextendsProgrammingLang

当试图在 Java 侧继承密封类 ProgrammingLang 时,编译器报错如下:

e: There is no default constructor available in 'ProgrammingLang' Java class cannot be a part of Kotlin sealed hierarchy

但是对于密封接口,JDK15 以下,Java 代码可以随意实现,这个需要特别注意

还好 JetBrains 宣布在IDE层面会给与警告,如果使用 IntelliJ IDEA 系列的 IDE,当 Java侧实现密封接口时同样会给出编译报错:

e: Java class cannot be a part of Kotlin sealed hierarchy

不管怎样,还是建议尽量少在 Java 中访问带有 Kotlin 语法特性的相关代码。

总结

财经自媒体联盟更多自媒体作者

新浪首页 语音播报 相关新闻 返回顶部