Skip to content

密封类与接口

密封类与接口为您的类层次结构提供了受控继承。密封类的所有直接子类在编译时都是已知的。在该密封类定义的模块和软件包之外,不能出现其他子类。同样的逻辑也适用于密封接口及其实现:一旦编译了包含密封接口的模块,就无法创建新的实现。

直接子类是指直接继承自其超类的类。

间接子类是指继承自其超类一级以下的类。

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

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

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

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

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

声明密封类或接口

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

kotlin
// 创建一个密封接口
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 中可见的接口或抽象类,那么没有什么能阻止其他开发者在客户端代码中实现或扩展它们。由于库不知道在其外部声明的错误,因此无法将其与其自身的类进行一致处理。然而,通过密封的错误类层次结构,库作者可以确信他们了解所有可能的错误类型,并且以后不会出现其他错误类型。

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

密封类与接口的层次结构插图

构造函数

密封类本身始终是一个抽象类,因此不能直接实例化。但是,它可以包含或继承构造函数。这些构造函数不是为了创建密封类本身的实例,而是为了其子类。考虑以下示例,其中包含一个名为 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)
    // 此处可以添加其他错误类型
}

密封类的构造函数可以具有两种可见性之一: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

// 扩展密封接口 Error 的枚举类
enum class ErrorType : Error {
    FILE_ERROR, DATABASE_ERROR
}

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

kotlin
// 密封接口 'Error' 仅在同一个软件包和模块中有实现
sealed interface Error

// 密封类 'IOError' 扩展了 'Error',且仅在同一个软件包内可扩展
sealed class IOError(): Error

// 开放类 'CustomError' 扩展了 'Error',且可以在任何可见的地方被扩展
open class CustomError(): Error

多平台项目中的继承

多平台项目中还有一个继承限制:密封类的直接子类必须位于同一个源集中。这适用于没有 expectactual 修饰符的密封类。

如果一个密封类在通用源集中被声明为 expect,并在平台源集中有 actual 实现,那么 expectactual 版本都可以在各自的源集中拥有子类。此外,如果您使用分层结构,可以在 expectactual 声明之间的任何源集中创建子类。

详细了解多平台项目的分层结构

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

使用密封类的主要好处在于您在 when 表达式中使用它们时。 与密封类配合使用的 when 表达式允许 Kotlin 编译器穷举检查是否覆盖了所有可能的情况。在这种情况下,您不需要添加 else 子句:

kotlin
// 密封类及其子类
sealed class Error {
    class FileReadError(val file: String): Error()
    class DatabaseError(val source: String): Error()
    object RuntimeError : Error()
}

// 用于记录错误的函数
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")
    // 不需要 `else` 子句,因为所有情况都已覆盖
}

// 列出所有错误
fun main() {
    val errors = listOf(
        Error.FileReadError("example.txt"),
        Error.DatabaseError("usersDatabase"),
        Error.RuntimeError
    )

    errors.forEach { log(it) }
}

为了减少 when 表达式中的重复,请尝试上下文相关解析(目前处于预览阶段)。 此功能允许您在已知预期类型时,在匹配密封类成员时省略类型名称。

有关更多信息,请参阅上下文相关解析预览或相关的 KEEP 提案

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

在多平台项目中,如果您的通用代码中有一个密封类作为 expect 声明并在 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.*

// 使用 Ktor 资源定义 API 请求的密封接口
@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)
}