Kotlin 2.1.0 入门教程(二十二)密封类、密封接口

密封类和密封接口

密封类和密封接口为类层次结构提供了可控的继承机制。密封类的所有直接子类在编译时都是已知的。在定义密封类的模块和包之外,不会出现其他子类。相同的逻辑也适用于密封接口及其实现:一旦包含密封接口的模块编译完成,就无法再创建新的实现。

直接子类是指直接继承自其超类的类。间接子类是指从其超类经过不止一层继承而来的类。

当你将密封类和接口与 when 表达式结合使用时,你可以涵盖所有可能的子类的行为,并确保不会创建新的子类对代码产生负面影响。

Java 15 引入了类似的概念,密封类使用 sealed 关键字并搭配 permits 子句来定义受限的类层次结构。

声明密封类或密封接口

要声明密封类或密封接口,请使用 sealed 修饰符:

// 创建一个密封接口。
sealed interface Error

// 创建一个实现密封接口 Error 的密封类。
sealed class IOError : Error

// 定义继承自密封类 IOError 的子类。
class FileReadError(val file: File) : IOError()
class DatabaseError(val source: DataSource) : IOError()

// 创建一个实现 Error 密封接口的单例对象。
object RuntimeError : Error

这个示例可以代表一个库的 API,该 API 包含了一些错误类,以便库的使用者能够处理库可能抛出的错误。如果这类错误类的层次结构中包含在公共 API 里可见的接口或抽象类,那么其他开发者就可以在客户端代码中随意实现或扩展它们,这是无法阻止的。由于库并不知道在其外部声明的错误,因此无法像处理自身类那样一致地处理这些错误。

然而,使用密封的错误类层次结构,库的开发者可以确保自己知晓所有可能的错误类型,并且后续不会出现其他错误类型。

构造函数

密封类本身始终是抽象类,因此不能直接实例化。不过,它可以包含或继承构造函数。这些构造函数并非用于创建密封类自身的实例,而是供其子类使用。以下是一个名为 Error 的密封类及其几个子类的示例,我们将对这些子类进行实例化操作:

sealed class Error(val message: String) {
    class NetworkError : Error("Network failure")
    class DatabaseError : Error("Database cannot be reached")
    class UnknownError : Error("An unknown error has occurred")
}

fun main() {
    val errors = listOf(
        Error.NetworkError(),
        Error.DatabaseError(),
        Error.UnknownError()
    )
    
    // Network failure
    // Database cannot be reached
    // An unknown error has occurred
    errors.forEach { println(it.message) }
}

可以在密封类中使用枚举类,利用枚举常量来表示状态并提供额外的详细信息。每个枚举常量仅作为单个实例存在,而密封类的子类可能有多个实例。在下面的示例中,密封类 Error 及其几个子类使用一个枚举来表示错误的严重程度。每个子类的构造函数都会初始化错误严重程度,并且可以改变其状态:

enum class ErrorSeverity { MINOR, MAJOR, CRITICAL }

sealed class Error(val severity: ErrorSeverity) {
    class FileReadError(val file: File)
        : Error(ErrorSeverity.MAJOR)
    class DatabaseError(val source: DataSource)
        : Error(ErrorSeverity.CRITICAL)
    object RuntimeError
        : Error(ErrorSeverity.CRITICAL)
    // 这里可以添加更多的错误类型。
}

密封类的构造函数可以有两种可见性:protected(默认)或 private

sealed class IOError {
    // 密封类的构造函数默认具有 protected 可见性。
    // 它在该类及其子类内部可见。
    constructor() {}

    // private 构造函数,仅在该类内部可见。
    // 在密封类中使用 private 构造函数可以对实例化进行更严格的控制,
    // 允许在类内部进行特定的初始化过程。
    private constructor(description: String): this() {}

    // 这将引发错误。
    // 密封类中不允许使用 public 和 internal 构造函数。
    // public constructor(code: Int): this() {}
}

继承

密封类和密封接口的直接子类必须在同一个包中声明。它们可以是顶级类,也可以嵌套在任意数量的其他具名类、具名接口或具名对象内部。只要符合 Kotlin 中的常规继承规则,子类可以具有任何可见性。

密封类的子类必须具有正确的限定名称。它们不能是局部对象或匿名对象。

枚举类不能继承密封类,也不能继承其他任何类。不过,它们可以实现密封接口:

sealed interface Error

// 枚举类实现密封接口 Error。
enum class ErrorType : Error {
    FILE_ERROR, DATABASE_ERROR
}

这些限制不适用于间接子类。如果密封类的直接子类没有被标记为密封类,那么它可以在其修饰符允许的范围内以任何方式被继承:

// 密封接口 Error 的实现只能位于同一包和同一模块中。
sealed interface Error

// 密封类 IOError 继承自 Error,并且只能在同一包内被继承。
sealed class IOError() : Error

// 开放类 CustomError 继承自 Error,并且在其可见的任何地方都可以被继承。
open class CustomError() : Error

将密封类与 when 表达式结合使用

使用密封类的主要优势在与 when 表达式结合使用时得以体现。当 when 表达式与密封类一起使用时,编译器能够详尽地检查是否涵盖了所有可能的情况。在这种情况下,你无需添加 else 子句:

fun log(e: Error) = when(e) {
    is Error.FileReadError -> println("${e.file}")
    is Error.DatabaseError -> println("${e.source}")
    Error.RuntimeError -> println("Runtime error")
    // 由于涵盖了所有情况,因此不需要 else 子句。
}

when 表达式中使用密封类时,你还可以添加守卫条件,以便在单个分支中进行额外的检查。

用例场景

使用密封类来表示应用程序中的不同 UI 状态。

sealed class UIState {
    data object Loading : UIState()
    data class Success(val data: String) : UIState()
    data class Error(val exception: Exception) : UIState()
}

fun updateUI(state: UIState) {
    when (state) {
        is UIState.Loading -> showLoadingIndicator()
        is UIState.Success -> showData(state.data)
        is UIState.Error -> showError(state.exception)
    }
}

高效处理各种支付方式。

sealed class Payment {
    data class CreditCard(val number: String, val expiryDate: String) : Payment()
    data class PayPal(val email: String) : Payment()
    data object Cash : Payment()
}

fun processPayment(payment: Payment) {
    when (payment) {
        is Payment.CreditCard -> processCreditCardPayment(payment.number, payment.expiryDate)
        is Payment.PayPal -> processPayPalPayment(payment.email)
        is Payment.Cash -> processCashPayment()
    }
}

你可能感兴趣的:(Kotlin,kotlin,android)