Kotlin1.5新特性:密封接口有啥用?

2021年7月19日11:46:31 发表评论 176 views

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

Kotlin1.5新特性:密封接口有啥用?

密封类的进化史

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

  • Enum Class:每个枚举都是枚举类的实例,可以直接使用
  • Sealed Class:密封类约束的子类只是一个类型,你可以为不同子类定义方法和属性,并对齐动态实例化
Kotlin1.5新特性:密封接口有啥用?

Kotlin 1.0

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

//编程语言sealed class ProgrammingLang {    object Assembly : ProgrammingLang()    class Java(ver: String) : ProgrammingLang()    class JavaScript(ver: String) : ProgrammingLang()}
这可以防止在在不编译密封类的前提下为其创建新的派生类。任何派生类的添加都必须重新编译密封类本身,外部调用方能时刻同步所有的子类类型,确保 when 语句的合法:
//获取指定语言的排名val ranking = when (val item: ProgrammingLang = getProgramLang()) {    Assembly -> TODO()    is Java -> TODO()    is JavaScript -> TODO()}
另一个潜在的好处是子类必须连同父类名字一起出现,例如 ,这有助于明确其namespace。

Kotlin1.5新特性:密封接口有啥用?

Kotlin 1.1

Kotlin 1.1 取消了子类必须在密封类内部定义的约束,密封类的子类可以声明在文件的 Top-Level。但是为了保证编译的同步,仍然需要在同一文件内。
sealed class ProgrammingLang
object Assembly : ProgrammingLang()class Java(ver: String) : ProgrammingLang()class JavaScript(ver: String) : ProgrammingLang()

Kotlin1.5新特性:密封接口有啥用?

Kotlin

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

// Lang.ktsealed class ProgrammingLang
// Compiled.ktclass Java(ver: String) : ProgrammingLang()class Cpp(ver: String) : ProgrammingLang()
// Interpreted.ktclass JavaScript(ver: String) : ProgrammingLang()class Lua(ver: String) : ProgrammingLang()
// LowLevel.ktobject 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

Kotlin1.5新特性:密封接口有啥用?

密封接口 Sealed Interface

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

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

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

1. "final" 的 interface

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

public interface Job :  {    ...    public fun start(): Boolean    ...    public fun cancel(): Unit    ...}

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

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

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

2. “可嵌套”的枚举

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

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

enum class JvmLang {    Java, Kotlin, Scala}

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

public final class JvmLang extends Enum{    private JvmLang(String s,int i){        super(s,i);    }    public static final JvmLang Java;    public static final JvmLang Kotlin;    public static final JvmLang Scala;    ...    static{        Java = new Action("Java",0);        Kotlin = new Action("Kotlin",1);        Scala = new Action("Scala",2);    }}

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

e: Enum class cannot inherit from classes

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

sealed interface Languageenum class HighLevelLang : Language {    Java, Kotlin, CPP}enum class MachineLang : Language {    ARM, X86}object AssemblyLang : Language

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

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

 when (lang) {        is Machine ->            when (lang) {                MachineLang.ARM -> TODO()                MachineLang.X86 -> TODO()            }        is HighLevel ->            when (lang) {                HighLevelLang.CPP -> TODO()                HighLevelLang.Java -> TODO()                HighLevelLang.Kotlin -> TODO()            }        else -> TODO()    }  

3. 多继承的密封类

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

Kotlin1.5新特性:密封接口有啥用?

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

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

sealed class JvmLang {    object Java : JvmLang()    object Kotlin : JvmLang()    object Groovy : JvmLang()}
sealed class CompiledLang {    object Java : CompiledLang()    object Kotlin : CompiledLang()    object Groovy : CompiledLang()    object Cpp : CompiledLang()}

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

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

sealed class JvmLang : CompiledLangobject Java : JvmLang()object Kotlin : JvmLang()object Groovy : JvmLang()object Cpp : CompiledLang()

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

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

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

sealed interface CompiledLangsealed interface InterpretedLangsealed interface FunctionalLangsealed interface JvmLang : CompiledLangobject Java : JvmLangobject Kotlin : JvmLang, FunctionalLangobject Groovy : JvmLang, FunctionalLang, InterpretedLangobject JavaScript: InterpretedLangobject Cpp : CompiledLang, FunctionalLang
//编程语言的市场份额fun shareOfCompiledLang(lang: CompiledLang) = when(lang) {    Java -> TODO()    Kotlin -> TODO()    Groovy -> TODO()    Cpp -> TODO()}
fun shareOfInterpretedLang(lang: InterpretedLang) = when(lang) {    JavaScript -> TODO()    Groovy -> TODO()}

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

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

sealed interface CompiledLangsealed interface InterpretedLangsealed interface FunctionalLangsealed interface JvmLang
object Java : JvmLang, CompiledLangobject Kotlin : JvmLang, CompiledLang, FunctionalLangobject Groovy : JvmLang, CompiledLang, FunctionalLang, InterpretedLangobject JavaScript: InterpretedLangobject Cpp : CompiledLang, FunctionalLang

Kotlin1.5新特性:密封接口有啥用?

与 Java 的兼容性

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

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

//kotlinsealed class ProgrammingLang
//javaclass Java extends ProgrammingLang

当试图在 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 语法特性的相关代码。

Kotlin1.5新特性:密封接口有啥用?

总结

Kotlin 引进了密封接口,为开发者带来如下便利:

  1. 定义“final”的interface
  2. 定义“可嵌套”的枚举
  3. 使得密封类可以多继承

未来,空密封类(没有成员定义)应该尽量使用密封接口替代;此外,自定义 Library 可以使用密封接口对外提供 API 以提高安全性。

可以预见密封接口的应用场景会越来越多。

作者 | AndroidPub       责编 | 张红月
出品 | CSDN(ID:CSDNnews)

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: