1. 引言
Kotlin 作为现代 JVM 语言,在类型系统设计上提供了许多实用特性。其中,数据类(Data Class)和密封类(Sealed Class)是日常开发中使用频率极高的两个特性。数据类帮助我们以极简的代码表达"纯数据"模型,而密封类则为受限的类层次结构提供了优雅的建模方式。本文将通过丰富的代码实例,深入讲解这两类特性的原理、用法与最佳实践。
2. 数据类(Data Class)
2.1 什么是数据类
数据类是 Kotlin 中专门用于承载数据的类。使用 data 关键字修饰的类,编译器会自动生成 equals()、hashCode()、toString()、copy() 以及 componentN() 等常用方法,极大减少了样板代码。
data class User(
val id: Long,
val name: String,
val email: String
)
fun main() {
val user = User(1L, "Alice", "alice@example.com")
println(user) // User(id=1, name=Alice, email=alice@example.com)
}
2.2 自动生成的方法
编译器为数据类自动生成的方法包括:
- equals() / hashCode():基于主构造函数中声明的所有属性进行相等性比较。
- toString():输出格式为
类名(属性1=值1, 属性2=值2)。 - copy():基于现有实例创建副本,可选择性修改部分属性。
- componentN():支持解构声明。
data class Point(val x: Int, val y: Int)
fun main() {
val p1 = Point(3, 4)
val p2 = p1.copy(y = 10) // 只修改 y
println(p2) // Point(x=3, y=10)
// 解构声明
val (x, y) = p1
println("x=$x, y=$y") // x=3, y=4
}
2.3 数据类的约束条件
使用数据类时需要注意以下约束:
- 主构造函数必须至少有一个参数。
- 主构造函数的所有参数必须标记为
val或var。 - 数据类不能是抽象类、开放类、密封类或内部类。
// 正确示例
data class Product(val sku: String, val price: Double)
// 错误示例:主构造函数没有参数
// data class Empty
// 错误示例:参数未标记 val/var
// data class Bad(val a: Int, b: String)
2.4 类体中的属性
在数据类类体中声明的属性不会参与 equals()、hashCode() 和 toString() 的生成。
data class Person(val name: String) {
var age: Int = 0 // 不参与 equals/hashCode/toString
}
fun main() {
val p1 = Person("Bob")
val p2 = Person("Bob")
p1.age = 30
p2.age = 40
println(p1 == p2) // true,因为 age 不参与比较
println(p1) // Person(name=Bob)
}
3. 密封类(Sealed Class)
3.1 什么是密封类
密封类用于表达受限的类层次结构,即一个值只能是有限几种类型之一。使用 sealed 关键字修饰的类,其所有直接子类必须声明在同一个文件(Kotlin 1.5 之前)或同一个包及编译单元(Kotlin 1.5+)中。
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val message: String) : Result<Nothing>()
object Loading : Result<Nothing>()
}
fun <T> handle(result: Result<T>) {
when (result) {
is Result.Success -> println("成功: ${result.data}")
is Result.Error -> println("失败: ${result.message}")
Result.Loading -> println("加载中...")
}
}
3.2 密封类与 when 表达式
密封类最大的优势在于与 when 表达式配合时,编译器可以穷尽所有分支,无需 else 分支。当新增子类时,编译器会提示所有 when 表达式需要补充新分支,提升代码安全性。
sealed class UiState {
object Idle : UiState()
object Loading : UiState()
data class Success(val data: List<String>) : UiState()
data class Error(val throwable: Throwable) : UiState()
}
fun render(state: UiState) {
// 无需 else 分支,编译器保证穷尽
when (state) {
UiState.Idle -> println("空闲状态")
UiState.Loading -> println("加载中")
is UiState.Success -> println("数据: ${state.data}")
is UiState.Error -> println("错误: ${state.throwable.message}")
}
}
3.3 密封类的子类类型
密封类的子类可以是数据类、普通类或单例对象,这为建模提供了极大的灵活性。
sealed class Shape {
data class Circle(val radius: Double) : Shape()
data class Rectangle(val width: Double, val height: Double) : Shape()
object Unknown : Shape()
}
fun area(shape: Shape): Double = when (shape) {
is Shape.Circle -> Math.PI * shape.radius * shape.radius
is Shape.Rectangle -> shape.width * shape.height
Shape.Unknown -> 0.0
}
4. 数据类与密封类的组合实战
4.1 网络请求状态管理
在实际项目中,数据类和密封类经常组合使用,用于管理网络请求的多种状态。
sealed class NetworkResult<out T> {
data class Success<T>(val data: T, val timestamp: Long) : NetworkResult<T>()
data class Failure(val code: Int, val message: String) : NetworkResult<Nothing>()
object Loading : NetworkResult<Nothing>()
}
data class Article(val id: Long, val title: String, val content: String)
fun fetchArticle(id: Long): NetworkResult<Article> {
// 模拟网络请求
return if (id > 0) {
NetworkResult.Success(Article(id, "Kotlin 数据类", "正文内容"), System.currentTimeMillis())
} else {
NetworkResult.Failure(400, "无效的 ID")
}
}
fun display(result: NetworkResult<Article>) {
when (result) {
is NetworkResult.Success -> println("文章: ${result.data.title}")
is NetworkResult.Failure -> println("错误码: ${result.code}")
NetworkResult.Loading -> println("加载中...")
}
}
4.2 表达式求值器
下面通过一个简单的表达式求值器,展示数据类与密封类在递归数据结构中的应用。
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()
}
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)
}
fun main() {
// 表达式: (1 + 2) * 3
val expr = Expr.Mul(Expr.Add(Expr.Num(1), Expr.Num(2)), Expr.Num(3))
println(eval(expr)) // 9
}
5. 最佳实践与注意事项
5.1 数据类使用建议
- 数据类适合承载不可变数据,建议属性使用
val声明。 - 避免在数据类中定义大量业务逻辑,保持类的纯粹性。
- 使用
copy()方法进行不可变更新,而不是直接修改属性。
data class Config(val host: String, val port: Int, val debug: Boolean)
fun main() {
val default = Config("localhost", 8080, false)
val debugConfig = default.copy(debug = true)
println(debugConfig) // Config(host=localhost, port=8080, debug=true)
}
5.2 密封类使用建议
- 当类型层次结构固定且有限时,优先使用密封类而非抽象类。
- 利用编译器穷尽检查,避免遗漏分支。
- 密封类常与
when表达式、状态机模式配合使用。
5.3 常见陷阱
数据类的 equals() 和 hashCode() 只基于主构造函数属性,如果类体中包含参与业务相等的属性,需要手动重写这两个方法。
data class Account(val id: String) {
var balance: Double = 0.0
// 手动重写以包含 balance
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Account) return false
return id == other.id && balance == other.balance
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + balance.hashCode()
return result
}
}
6. 总结
数据类和密封类是 Kotlin 类型系统中两个极具表现力的特性。数据类通过自动生成样板方法,让开发者专注于数据本身;密封类则通过受限的类层次结构,配合 when 表达式实现类型安全的穷尽分支处理。将两者结合使用,可以构建出既简洁又健壮的领域模型。建议读者在实际项目中多加练习,逐步体会这两个特性的设计哲学。

690

被折叠的 条评论
为什么被折叠?



