ITmob-Ly
发布于 2023-05-08 / 130 阅读
0

Kotlin 中的密封类与密封接口对比

Kotlin-Blog

介绍

密封类和密封接口 表示受限的类层次结构,提供对继承的更多控制。密封类和密封接口的所有直接子类在编译时都是已知的,没有其他子类可以出现在定义密封类的模块和包之外。

第三方无法在他们的代码中对密封类进行扩展,密封类每个实例的类型在编译此类时就是已知的。同样密封接口及其实现一旦编译了就不会出现新的实现。

下面是官方文档:Sealed classes and interfaces 中的介绍示例:

例如,考虑一个库的 API。它包含 Error 类,让库的用户处理可能抛出的 Error。如果此类是包括在公共 API 中可见的接口或抽象类,则没有什么可以阻止用户在客户端代码中实现或扩展它们。但是,库不知道在它外部的实现或扩展,因此它不能像对待自己的类一样对待它们。使用密封类,库作者可以确保他们知道所有可能的 Error 类型,并且以后不会出现其他 Error 类型。

要声明密封类或接口,请将 sealed 修饰符放在其名称之前:

sealed interface Error

sealed class IOError(): Error

class FileReadError(val file: File): IOError()
class DatabaseError(val source: DataSource): IOError()

object RuntimeError : Error

密封类本身是抽象的,不能直接实例化,可以有 abstract 成员。

密封类的构造函数的可见性只能是:(protected默认情况下)或private

sealed class IOError {
    constructor() { /*...*/ } // 默认是 protected
    private constructor(description: String): this() { /*...*/ } // 可以是 private
    // public constructor(code: Int): this() {} // Error: 不允许 public 和 internal
}

直接子类的位置

密封类和密封接口的直接子类必须在同一个包中定义。可以是顶级位置,也可以嵌套在任意多的其他有名称的类,有名称的接口,或有名称的对象之内。

密封类的子类可以设置为任意的可见度,但必须拥有一个适当的限定名称。不能是局部对象或匿名对象。

这些限制不适用于非直接子类。如果密封类的一个直接子类没有标记为密封,那么它可以按照其修饰符允许的方式任意扩展:

sealed interface Error // 只在同一个模块的同一个包内可以有实现类

sealed class IOError(): Error // 只可以在同一个模块的同一个包内扩展这个类
open class CustomError(): Error // 可以在这个类可见的任何地方扩展这个类

密封接口/Sealed Interface

Kotlin 1.5 引入了密封接口。如上面提到的密封类和密封接口的直接子类必须在同一个包中定义,这个特性保证了我们定义的接口可以对第三方调用者可见但不能去实现它,这在定义库供其他人调用时很有用,可以避免调用者随意实现我们的接口,未来库升级时接口发生改变,调用者不必担心兼容性问题。

密封类与 when 表达式

密封类的主要好处是体现在 [when](https://kotlinlang.org/docs/control-flow.html#when-expression) 表达式中的使用场景。

如果能够确保 when 的分支已经覆盖了所有可能情况,可以不必添加 else 分支:

fun log(e: Error) = when(e) {
    is FileReadError -> { println("Error while reading file ${e.file}") }
    is DatabaseError -> { println("Error while reading from database ${e.source}") }
    is RuntimeError ->  { println("Runtime error") }
    // 不需要 `else` 分支, 因为已经覆盖了所有的可能情况
}

enum 类不能扩展密封类 (也不能扩展任何其他类), 但它们可以实现密封接口.

某种程度上, 密封类很象 枚举类(enum) 类: 枚举类型的值也是有限的, 但每个枚举常数都只存在单个实例,而密封类的子类可以存在多个实例, 每个实例都可以包含它自己的状态数据。

在跨平台项目的共通代码中, 使用 [expect](https://kotlinlang.org/docs/multiplatform-connect-to-apis.html) 密封类的 when 表达式仍然需要 else 分支. 这是因为在共通代码中, 无法确定各平台实现中的 actual 子类.

扩展知识

上面是密封类和密封接口的介绍和用法,这里介绍一下它们过往版本的用法和限制等,对于了解 Kotlin 的 ChangeLog 和 维护旧版本代码时有所帮助。

密封类/sealed class

  1. Kotlin 1.0 中密封类的子类在密封类的内部定义:

    sealed class LoginErrors {
        data class InvalidUserName(val userName: String) : LoginErrors()
        object InvalidPasswordFormat : LoginErrors()
      }
    
  2. Kotlin 1.1 取消了子类必须在密封类内部定义的约束,可以定义在文件的顶级位置。但是为了保证编译的同步,他们仍然需要在同一文件内。

  3. Kotlin 1.5,约束更加放宽,允许子类定义在不同的文件中,只要保证子类和父类在同一个 module 且是同一个包名下即可。在一个 module 为保证编译的同步。

    这样内容较多的子类就可以拆分为单独文件进行定义

密封接口/sealed interface

  1. 当使用 Java 代码实现使用 Kotlin 定义的密封接口时 IDE 可能给出如下警告:

    Java class cannot be a part of Kotlin sealed hierarchy 
    

    这是因为 JDK 15 开始 Java 才引入了密封类和密封接口,使用 JDK 15 以下版本时 Java 代码是可以实现 Kotlin 代码定义的密封接口的,但 IDE 会给出警告。