Sealed Class — Kotlin 中的一种特殊类类型,它将继承层次结构限制为一组固定的子类型。所有继承者都在同一文件中声明,编译器已知它们,这允许使用详尽的 when 块而无需强制性的 else 分支。根据 Kotlin Docs, 2026,密封类是表示有限层次结构(如状态、错误类型和 UI 事件)的关键机制。
要点
Sealed Class(密封类)— 是 Kotlin 中用 sealed 修饰符标记的类。它定义了一个有限的类型层次结构:所有可能的继承者都列在同一个文件中,编译器知道每一个继承者。这使密封类不同于普通的开放类,后者的继承者可以在任何地方声明。
密封类的主要目的是 有限集合的类型安全表示。每个继承者可以有自己的数据结构,这使得密封类比枚举更灵活。在运行时,密封类是一个普通的抽象类,编译器仅在编译阶段施加限制。
密封类在 Android 架构中尤其有用:UI 状态、网络请求结果、类似于 Intent 的导航事件以及错误层次结构 — 都是典型的应用场景。
在编译时,密封类被优化为 when 表达式的跳转表,使其比 if-else 链更高效。结合 data class,每个继承者不仅可以包含状态,还可以包含方法,从而无需样板代码即可构建自文档化的领域模型。
密封类在移动应用程序中表示 有限状态机也非常有效。每个状态 — 具有唯一参数的独立继承者,状态之间的转换通过 when 表达式控制。编译器保证所有可能的状态都已处理,从而消除 UI 状态或业务逻辑更改时的运行时错误。
Kotlin 初学者经常将密封类与 枚举 混淆,因为两者都限制值集合。然而它们之间存在根本区别:枚举是同一类型的常量集合,而密封类是不同类型层次结构。
枚举 在所有变体都是没有额外结构的常量时是最优选择。例如,星期几、订单状态或无需参数的操作类型。每个枚举值都是一个具有固定名称的单例。
密封类 在每个变体有自己的数据时需要。例如,网络错误包含响应代码,解析错误包含详细信息,授权错误包含消息。密封类的每个继承者是具有唯一字段的独立类型。
// 枚举 — 同一类型的所有变体
enum class Status { LOADING, SUCCESS, ERROR }
// 密封类 — 每个变体都有自己的数据
sealed class UiState<out T> {
object Loading : UiState<Nothing>()
data class Success<T>(val data: T) : UiState<T>()
data class Error(val message: String) : UiState<Nothing>()
}
从 Kotlin 1.5 开始,可以声明 密封接口。这将密封概念扩展到接口:密封接口也具有固定的实现集合,但支持多重继承。
密封接口 在继承者需要同时实现多个契约时很方便。例如,UI 事件可以同时是可点击的和可追踪的。使用密封类时,需要选择一个基类,而使用密封接口,继承者同时实现两者。
密封类 是一个类,因此每个继承者只能有一个父类。密封接口解决了这个问题,但不能包含状态。它们之间的选择取决于任务:需要带字段的公共逻辑 — 用密封类,需要契约的灵活性 — 用密封接口。
sealed interface ScreenEvent {
data class Refresh(val force: Boolean) : ScreenEvent
data class Navigate(val route: String) : ScreenEvent
data class ShowError(val toast: String) : ScreenEvent
}
sealed interface AnalyticsEvent {
val name: String
val params: Map<String, Any>
}
// 继承者实现两个接口
data class LoginClicked(
override val name: String = "login_click",
override val params: Map<String, Any> = emptyMap()
) : ScreenEvent, AnalyticsEvent
密封类在移动开发中的主要应用之一 — 类型安全错误层次结构。与其抛掷不同类型的异常或使用通用的 Exception,密封类将所有可能的领域错误收集到一个类型中。
创建一个 DomainError 密封类,并将所有故障类型作为继承者列出。每个继承者只包含对该错误类型有意义的数据。编译器保证在处理错误时您不会忘记任何变体。
考虑一个带授权功能的应用程序,其中可能发生不同的故障场景:密码错误、账户封锁、服务器问题。密封类将它们统一为支持全面处理的单一类型。
sealed class AuthError {
data class InvalidCredentials(
val attempts: Int
) : AuthError()
data class AccountBlocked(
val until: Long
) : AuthError()
data class NetworkFailure(
val cause: Throwable
) : AuthError()
object ServerError : AuthError()
}
fun handleError(error: AuthError): String = when (error) {
is AuthError.InvalidCredentials ->
"剩余尝试次数: ${3 - error.attempts}"
is AuthError.AccountBlocked ->
"访问被锁定至 ${Date(error.until)}"
is AuthError.NetworkFailure ->
"检查连接: ${error.cause.localizedMessage}"
AuthError.ServerError ->
"服务器暂时不可用"
}
密封类 已成为 Android 应用程序架构中的标准工具。让我们看看移动开发中密封类不可或缺的三个关键模式。
特别值得一提的是密封类在 Clean Architecture 中的应用。每一层(data、domain、presentation)使用自己的错误类型的密封类,映射器将一个密封类转换为另一个。例如,数据层的 DataError 映射为业务逻辑的 DomainError,然后再映射为表示层的 UiState。这保持了应用程序所有级别的类型安全性,并保证没有错误会未经处理。
测试 密封类需要特殊方法,因为每个继承者都是具有自己状态的独立类型。建议编写遍历所有密封类继承者的参数化测试。这保证了 when 表达式覆盖所有变体,包括层次结构扩展时添加的新变体。
对于 UI 测试,密封类作为 UiState 允许检查每个状态的显示:Loading 显示加载动画,Content 显示数据,Error 显示错误消息。由于密封类是有限的,所有状态的测试覆盖提供了对 UI 逻辑正确性的完全信心。
尽管概念简单,开发人员在设计密封类层次结构时经常犯错。让我们看看主要问题及如何避免它们。
常见问题
是的,密封类可以包含抽象方法,每个继承者必须实现它们。这在所有变体应提供统一接口但具有不同执行逻辑时很方便。
在 Java 17+ 中出现了带有 sealed 修饰符的密封类和接口。Android 目前部分支持 Java 17,但在 Kotlin 项目中,密封类从 Kotlin 1.0 起可用且没有限制。
是的,一个密封类可以是另一个的继承者。密封类层次结构保持有限:编译器知道每一层的所有继承者。这允许构建详细的错误分类。
密封类在运行时不会产生额外开销。编译器将带有密封类的 when 表达式优化为跳转表(tableswitch),比 if-else 链更快。性能与枚举相同。
密封类的每个继承者单独测试。由于密封类是有限的,可以编写遍历所有变体的参数化测试。这提供了 when 块分支的完全覆盖。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。