Skip to content

密封类和接口

密封(Sealed) 类和接口提供对类层次结构的受控继承。密封类的所有直接子类在编译期是已知的。在密封类定义所在的模块和包之外,不能出现其他子类。同样的逻辑也适用于密封接口及其实现:一旦包含密封接口的模块被编译,就不能创建新的实现。

直接子类是直接从其超类继承的类。

间接子类是从其超类下多于一层的类继承的类。

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

密封类最适用于以下场景:

  • 期望限制类继承: 你有一个预定义、有限的子类集合,它们扩展自某个类,并且所有这些子类都在编译期已知。
  • 需要类型安全设计: 在你的项目中,安全性和模式匹配至关重要。特别是对于状态管理或处理复杂条件逻辑。例如,请查看将密封类与 when 表达式结合使用
  • 处理封闭 API: 你希望为库提供健壮且可维护的公共 API,以确保第三方客户端按预期使用这些 API。

有关更详细的实际应用,请参见用例场景

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

声明密封类或接口

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

kotlin
// Create a sealed interface
sealed interface Error

// Create a sealed class that implements sealed interface Error
sealed class IOError(): Error

// Define subclasses that extend sealed class 'IOError'
class FileReadError(val file: File): IOError()
class DatabaseError(val source: DataSource): IOError()

// Create a singleton object implementing the 'Error' sealed interface 
object RuntimeError : Error

这个例子可以表示一个库的 API,其中包含错误类,允许库用户处理库可能抛出的错误。如果此类错误类的层次结构包含在公共 API 中可见的接口或抽象类,那么没有任何东西能阻止其他开发者在客户端代码中实现或扩展它们。由于库不知道外部声明的错误,它无法与自己的类保持一致地处理它们。然而,通过密封的错误类层次结构,库作者可以确信他们了解所有可能的错误类型,并且以后不会出现其他错误类型。

该示例的层次结构如下所示:

密封类和接口的层次结构图示

构造函数

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

kotlin
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())
    errors.forEach { println(it.message) }
}
// Network failure 
// Database cannot be reached 
// An unknown error has occurred

你可以在密封类中使用 enum 类,以使用枚举常量来表示状态并提供额外细节。每个枚举常量只存在一个单个实例,而密封类的子类可以有多个实例。 在此示例中,sealed class Error 及其几个子类,采用 enum 来表示错误严重性。 每个子类构造函数都初始化 severity 并可以改变其状态:

kotlin
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)
    // Additional error types can be added here
}

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

kotlin
sealed class IOError {
    // 密封类构造函数默认具有 protected 可见性。它在此类及其子类中可见
    constructor() { /*...*/ }

    // 私有构造函数,仅在此类中可见。
    // 在密封类中使用私有构造函数可以更严格地控制实例化,从而在类内实现特定的初始化过程。
    private constructor(description: String): this() { /*...*/ }

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

继承

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

密封类的子类必须具有合格名称。它们不能是局部或匿名对象。

enum 类不能扩展密封类或任何其他类。但是,它们可以实现密封接口:

kotlin
sealed interface Error

// enum class extending the sealed interface Error
enum class ErrorType : Error {
    FILE_ERROR, DATABASE_ERROR
}

这些限制不适用于间接子类。如果密封类的直接子类未标记为密封,则可以按照其修饰符允许的任何方式进行扩展:

kotlin
// Sealed interface 'Error' has implementations only in the same package and module
sealed interface Error

// Sealed class 'IOError' extends 'Error' and is extendable only within the same package
sealed class IOError(): Error

// Open class 'CustomError' extends 'Error' and can be extended anywhere it's visible
open class CustomError(): Error

多平台项目中的继承

多平台项目中还有一项继承限制:密封类的直接子类必须驻留在同一个源代码集中。这适用于没有 expect 与 actual 修饰符的密封类。

如果一个密封类在公共源代码集中声明为 expect,并在平台源代码集中具有 actual 实现,那么 expectactual 版本都可以在其源代码集中拥有子类。此外,如果你使用层次结构,则可以在 expectactual 声明之间的任何源代码集中创建子类。

了解更多关于多平台项目层次结构的信息

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

使用密封类的主要好处在于将其用于 when 表达式时。 when 表达式与密封类结合使用时,允许 Kotlin 编译器穷尽地检测是否覆盖了所有可能的情况。在这种情况下,你无需添加 else 子句:

kotlin
// Sealed class and its subclasses
sealed class Error {
    class FileReadError(val file: String): Error()
    class DatabaseError(val source: String): Error()
    object RuntimeError : Error()
}

// Function to log errors
fun log(e: Error) = when(e) {
    is Error.FileReadError -> println("Error while reading file ${e.file}")
    is Error.DatabaseError -> println("Error while reading from database ${e.source}")
    Error.RuntimeError -> println("Runtime error")
    // No `else` clause is required because all the cases are covered
}

// List all errors
fun main() {
    val errors = listOf(
        Error.FileReadError("example.txt"),
        Error.DatabaseError("usersDatabase"),
        Error.RuntimeError
    )

    errors.forEach { log(it) }
}

为了减少 when 表达式中的重复,可以尝试上下文敏感解析(目前处于预览状态)。 此特性允许你在匹配密封类成员时省略类型名称,如果预期类型已知的话。

欲了解更多信息,请参见上下文敏感解析预览或相关的 KEEP 提案

将密封类与 when 表达式结合使用时,你还可以添加守卫条件以在单个分支中包含额外的检测。 有关更多信息,请参阅when 表达式中的守卫条件

在多平台项目中,如果你有一个密封类并在公共代码中将其 when 表达式作为预期声明,你仍然需要一个 else 分支。 这是因为 actual 平台实现的子类可能会扩展在公共代码中未知的密封类。

用例场景

让我们探讨一些密封类和接口特别有用的实际场景。

UI 应用程序中的状态管理

你可以使用密封类来表示应用程序中不同的 UI 状态。这种方法可以结构化且安全地处理 UI 更改。此示例演示了如何管理各种 UI 状态:

kotlin
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) 
    }
}

支付方法处理

在实际业务应用程序中,高效处理各种支付方法是常见需求。 你可以使用密封类和 when 表达式来实现此类业务逻辑。 通过将不同的支付方法表示为密封类的子类,它为处理事务建立了一个清晰且易于管理的结构:

kotlin
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() 
    }
}

Payment 是一个密封类,表示电子商务系统中的不同支付方法:CreditCardPayPalCash。每个子类可以有其特定的属性,例如 CreditCardnumberexpiryDate,以及 PayPalemail

processPayment() 函数演示了如何处理不同的支付方法。这种方法确保所有可能的支付类型都得到考虑,并且系统对于将来添加新的支付方法保持灵活性。

API 请求-响应处理

你可以使用密封类和密封接口来实现一个用户身份验证系统,该系统处理 API 请求和响应。 用户身份验证系统具有登录和登出功能。 ApiRequest 密封接口定义了特定的请求类型:用于登录的 LoginRequest 和用于登出操作的 LogoutRequest。 密封类 ApiResponse 封装了不同的响应场景:包含用户数据的 UserSuccess,表示用户不存在的 UserNotFound,以及表示任何失败的 ErrorhandleRequest 函数使用 when 表达式以类型安全的方式处理这些请求,而 getUserById 则模拟用户检索:

kotlin
// 导入必要的模块
import io.ktor.server.application.*
import io.ktor.server.resources.*

import kotlinx.serialization.*

// 定义用于 API 请求的密封接口,使用 Ktor 资源
@Resource("api")
sealed interface ApiRequest

@Serializable
@Resource("login")
data class LoginRequest(val username: String, val password: String) : ApiRequest

@Serializable
@Resource("logout")
object LogoutRequest : ApiRequest

// 定义具有详细响应类型的 ApiResponse 密封类
sealed class ApiResponse {
    data class UserSuccess(val user: UserData) : ApiResponse()
    data object UserNotFound : ApiResponse()
    data class Error(val message: String) : ApiResponse()
}

// 用于成功响应的用户数据类
data class UserData(val userId: String, val name: String, val email: String)

// 用于验证用户凭据的函数(仅用于演示)
fun isValidUser(username: String, password: String): Boolean {
    // 某些验证逻辑(这只是一个占位符)
    return username == "validUser" && password == "validPass"
}

// 处理具有详细响应的 API 请求的函数
fun handleRequest(request: ApiRequest): ApiResponse {
    return when (request) {
        is LoginRequest -> {
            if (isValidUser(request.username, request.password)) {
                ApiResponse.UserSuccess(UserData("userId", "userName", "userEmail"))
            } else {
                ApiResponse.Error("Invalid username or password")
            }
        }
        is LogoutRequest -> {
            // 假设此示例中的登出操作总是成功
            ApiResponse.UserSuccess(UserData("userId", "userName", "userEmail")) // 仅用于演示
        }
    }
}

// 模拟 getUserById 调用
fun getUserById(userId: String): ApiResponse {
    return if (userId == "validUserId") {
        ApiResponse.UserSuccess(UserData("validUserId", "John Doe", "[email protected]"))
    } else {
        ApiResponse.UserNotFound
    }
    // 错误处理也会导致 Error 响应。
}

// 演示用法的 main 函数
fun main() {
    val loginResponse = handleRequest(LoginRequest("user", "pass"))
    println(loginResponse)

    val logoutResponse = handleRequest(LogoutRequest)
    println(logoutResponse)

    val userResponse = getUserById("validUserId")
    println(userResponse)

    val userNotFoundResponse = getUserById("invalidId")
    println(userNotFoundResponse)
}