密封类与代数数据类型
密封类与when穷举
概述
密封类(Sealed Class)是 Kotlin 中一种特殊的类层次结构,它限制子类只能在同一个文件(或同一个模块,Kotlin 1.5+)中定义。这个限制带来了一个重要的好处:编译器知道所有可能的子类,因此在使用 when 表达式时可以检查是否穷举了所有分支,如果遗漏了某个分支,编译器会报错。
密封类与代数数据类型(Algebraic Data Type,ADT)密切相关。代数数据类型是函数式编程中的概念,通过组合已有类型来定义新类型。密封类就是 Kotlin 对代数数据类型的实现,让你用类型系统精确地描述业务领域。
基础概念
- sealed class:用
sealed关键字标记的类,子类数量有限且已知 - sealed interface:Kotlin 1.5 引入,与密封类类似但是接口
- when 穷举:对密封类使用 when 时,必须覆盖所有子类,否则编译错误
- 代数数据类型:通过”和类型”(Sum Type,即密封类)和”积类型”(Product Type,即数据类)组合数据
- 和类型(Sum Type):一个值只能是多种类型中的一种,如 Result 要么是 Success 要么是 Failure
快速上手
// 定义密封接口,表示操作结果
sealed interface Result<out T> {
data class Success<T>(val value: T) : Result<T>
data class Failure(val error: String) : Result<Nothing>
}
// 使用 when 处理所有分支
fun handle(result: Result<Int>) = when (result) {
is Result.Success -> println("成功: ${result.value}")
is Result.Failure -> println("失败: ${result.error}")
// 编译器确保穷举所有分支,不需要 else
}
详细用法
密封类与数据类组合
// 用密封类建模支付方式
sealed class PaymentMethod {
// 每个子类携带不同的数据
data class CreditCard(
val number: String,
val expiryMonth: Int,
val expiryYear: Int
) : PaymentMethod()
data class WeChatPay(
val openId: String
) : PaymentMethod()
data class BankTransfer(
val accountNumber: String,
val bankName: String
) : PaymentMethod()
// 子类可以不带数据(object)
object Cash : PaymentMethod()
}
// 处理不同支付方式
fun processPayment(method: PaymentMethod): String = when (method) {
is PaymentMethod.CreditCard -> "信用卡支付: 尾号${method.number.takeLast(4)}"
is PaymentMethod.WeChatPay -> "微信支付: ${method.openId}"
is PaymentMethod.BankTransfer -> "银行转账: ${method.bankName}"
PaymentMethod.Cash -> "现金支付"
}
fun main() {
val methods = listOf(
PaymentMethod.CreditCard("1234567890123456", 12, 2025),
PaymentMethod.WeChatPay("user_123"),
PaymentMethod.BankTransfer("6222000000", "工商银行"),
PaymentMethod.Cash
)
methods.forEach { println(processPayment(it)) }
}
密封接口
Kotlin 1.5 引入了密封接口,允许更灵活的层次结构:
// 密封接口可以跨文件定义实现(同一模块内)
sealed interface Shape {
val area: Double
}
sealed interface ColoredShape : Shape {
val color: String
}
data class Circle(val radius: Double, override val color: String) : ColoredShape {
override val area: Double = Math.PI * radius * radius
}
data class Rectangle(
val width: Double,
val height: Double,
override val color: String
) : ColoredShape {
override val area: Double = width * height
}
data class Triangle(
val base: Double,
val height: Double,
override val color: String
) : ColoredShape {
override val area: Double = 0.5 * base * height
}
fun describeShape(shape: Shape): String = when (shape) {
is Circle -> "圆形,半径${shape.radius},面积${shape.area}"
is Rectangle -> "矩形,${shape.width}x${shape.height},面积${shape.area}"
is Triangle -> "三角形,底${shape.base}高${shape.height},面积${shape.area}"
}
嵌套密封类
sealed class UiState {
// 加载中状态
object Loading : UiState()
// 成功状态,携带数据
data class Success(val data: List<String>) : UiState()
// 错误状态,可以进一步分类
sealed class Error(val message: String) : UiState() {
// 网络错误
data class NetworkError(override val message: String) : Error(message)
// 服务器错误
data class ServerError(val code: Int, override val message: String) : Error(message)
// 未知错误
data class UnknownError(override val message: String) : Error(message)
}
}
fun render(state: UiState): String = when (state) {
is UiState.Loading -> "加载中..."
is UiState.Success -> "数据: ${state.data}"
is UiState.Error.NetworkError -> "网络错误: ${state.message}"
is UiState.Error.ServerError -> "服务器错误(${state.code}): ${state.message}"
is UiState.Error.UnknownError -> "未知错误: ${state.message}"
}
密封类与泛型
// 用密封类实现类型安全的异步结果
sealed class AsyncResult<out T> {
// 未开始
object Idle : AsyncResult<Nothing>()
// 加载中
object Loading : AsyncResult<Nothing>()
// 成功
data class Success<T>(val value: T) : AsyncResult<T>()
// 失败
data class Error(val exception: Throwable) : AsyncResult<Nothing>()
}
// 使用
fun <T> AsyncResult<T>.getOrElse(default: T): T = when (this) {
is AsyncResult.Success -> value
else -> default
}
fun <T> AsyncResult<T>.onSuccess(action: (T) -> Unit): AsyncResult<T> {
if (this is AsyncResult.Success) action(value)
return this
}
fun <T> AsyncResult<T>.onError(action: (Throwable) -> Unit): AsyncResult<T> {
if (this is AsyncResult.Error) action(exception)
return this
}
常见场景
网络请求状态管理
sealed class Resource<out T> {
data class Success<T>(val data: T) : Resource<T>()
data class Error(val message: String, val code: Int? = null) : Resource<Nothing>()
object Loading : Resource<Nothing>()
}
class UserViewModel : ViewModel() {
private val _userState = MutableStateFlow<Resource<User>>(Resource.Loading)
val userState: StateFlow<Resource<User>> = _userState
fun loadUser(id: String) {
viewModelScope.launch {
_userState.value = Resource.Loading
try {
val user = repository.getUser(id)
_userState.value = Resource.Success(user)
} catch (e: Exception) {
_userState.value = Resource.Error(e.message ?: "未知错误")
}
}
}
}
// 在 UI 中处理
@Composable
fun UserScreen(state: Resource<User>) {
when (state) {
is Resource.Loading -> CircularProgressIndicator()
is Resource.Success -> Text("用户: ${state.data.name}")
is Resource.Error -> Text("错误: ${state.message}")
}
}
权限系统建模
sealed class Permission {
sealed class Read : Permission() {
object OwnData : Read()
object AllData : Read()
}
sealed class Write : Permission() {
object OwnData : Write()
object AllData : Write()
}
object Admin : Permission()
}
fun checkAccess(userRole: String, permission: Permission): Boolean =
when (permission) {
Permission.Read.OwnData -> true // 所有用户可读自己的数据
Permission.Read.AllData -> userRole == "admin" || userRole == "manager"
Permission.Write.OwnData -> userRole != "guest"
Permission.Write.AllData -> userRole == "admin"
Permission.Admin -> userRole == "admin"
}
表达式求值器
// 用密封类建模数学表达式(经典的代数数据类型用法)
sealed class Expr {
data class Num(val value: Int) : Expr()
data class Add(val left: Expr, val right: Expr) : Expr()
data class Mul(val left: Expr, val right: Expr) : Expr()
data class Neg(val expr: Expr) : Expr()
}
// 递归求值
fun eval(expr: Expr): Int = when (expr) {
is Expr.Num -> expr.value
is Expr.Add -> eval(expr.left) + eval(expr.right)
is Expr.Mul -> eval(expr.left) * eval(expr.right)
is Expr.Neg -> -eval(expr.expr)
}
fun main() {
// 表达式: (2 + 3) * -4
val expr = Expr.Mul(
Expr.Add(Expr.Num(2), Expr.Num(3)),
Expr.Neg(Expr.Num(4))
)
println(eval(expr)) // -20
}
注意事项
- 子类必须在同一模块:Kotlin 1.5 之前要求同一文件,1.5 之后放宽到同一模块
- when 必须穷举:如果作为表达式使用,必须覆盖所有子类;如果作为语句使用,建议也覆盖所有子类(编译器会警告)
- 密封类是抽象的:不能直接实例化密封类本身,只能实例化它的子类
- 构造器限制:密封类的构造器默认是 protected 的,不能声明为 public
- 添加新子类会破坏现有代码:如果添加了新的子类,所有 when 表达式都需要更新,这既是优点(不会遗漏)也是缺点(需要修改的地方多)
进阶用法
与序列化配合
import kotlinx.serialization.*
import kotlinx.serialization.json.*
@Serializable
sealed class Message {
abstract val id: String
@Serializable
@SerialName("text")
data class Text(override val id: String, val content: String) : Message()
@Serializable
@SerialName("image")
data class Image(override val id: String, val url: String, val width: Int, val height: Int) : Message()
@Serializable
@SerialName("system")
data class System(override val id: String, val action: String) : Message()
}
fun main() {
val messages: List<Message> = listOf(
Message.Text("1", "你好"),
Message.Image("2", "photo.jpg", 800, 600),
Message.System("3", "user_joined")
)
val json = Json { prettyPrint = true }
val jsonString = json.encodeToString(messages)
// 序列化时会自动添加 type 鉴别器字段
println(jsonString)
// 反序列化时自动还原为正确的子类
val decoded = json.decodeFromString<List<Message>>(jsonString)
}
递归密封类
sealed class JsonValue {
data class JsonString(val value: String) : JsonValue()
data class JsonNumber(val value: Double) : JsonValue()
data class JsonBoolean(val value: Boolean) : JsonValue()
object JsonNull : JsonValue()
data class JsonArray(val elements: List<JsonValue>) : JsonValue()
data class JsonObject(val entries: Map<String, JsonValue>) : JsonValue()
}
// 递归打印 JSON
fun prettyPrint(value: JsonValue, indent: Int = 0): String = when (value) {
is JsonValue.JsonString -> "\"${value.value}\""
is JsonValue.JsonNumber -> value.value.toString()
is JsonValue.JsonBoolean -> value.value.toString()
JsonValue.JsonNull -> "null"
is JsonValue.JsonArray -> {
val items = value.elements.joinToString(", ") { prettyPrint(it, indent + 2) }
"[$items]"
}
is JsonValue.JsonObject -> {
val entries = value.entries.entries.joinToString(", ") { (k, v) ->
"\"$k\": ${prettyPrint(v, indent + 2)}"
}
"{$entries}"
}
}