11、数据类与密封类:data class的自动生成方法、copy()、解构声明、sealed class的使用场景
说实话,Kotlin 里我最常用的两个特性,就是数据类和密封类。一个帮我省掉了大量模板代码,另一个帮我写出更安全的逻辑分支。今天咱们就好好聊聊它们。
一、data class:你只管写属性,剩下的交给编译器
先说说数据类。它的核心价值就一句话:自动生成那些你懒得写、但又必须写的方法。
你想想看,在 Java 里定义一个纯数据载体类,你得写多少东西?getter、setter、equals()、hashCode()、toString()…… 每次写都像在重复造轮子。Kotlin 的 data class 直接把这些全包了。
data class User(
val id: Long,
val name: String,
val email: String
)
就这么一行,编译器自动生成了:
equals()/hashCode()—— 基于所有主构造属性toString()—— 格式是User(id=1, name=Alice, email=...)componentN()—— 用于解构声明copy()—— 基于所有属性的浅拷贝
注意:只有主构造函数里声明的属性才会参与这些自动生成。类体里的属性不会被考虑。
二、copy():不可变对象的“修改”利器
我个人习惯把 data class 的属性都声明为 val,也就是不可变的。这样对象一旦创建就不会变,线程安全、逻辑清晰。但问题来了——我想改其中一个字段怎么办?
这时候 copy() 就派上用场了。它返回一个新对象,只修改你指定的字段,其他保持不变。
val alice = User(1, "Alice", "alice@example.com")
val bob = alice.copy(name = "Bob", email = "bob@example.com")
println(alice) // User(id=1, name=Alice, email=alice@example.com)
println(bob) // User(id=1, name=Bob, email=bob@example.com)
我在项目中遇到过这样一个场景:用户修改个人资料时,后端只返回了部分字段。我用 copy() 把旧对象和新字段合并,几行代码就搞定了,不用手写合并逻辑。
小技巧:copy() 是浅拷贝。如果属性是可变对象(比如 ArrayList),拷贝后两个对象会共享同一个引用。这点要小心。
三、解构声明:把对象拆开用
data class 自动生成了 component1()、component2() 等方法,这就是解构声明的基础。
val user = User(1, "Alice", "alice@example.com")
val (id, name, email) = user
println("ID: $id, Name: $name, Email: $email")
解构的顺序就是主构造参数的声明顺序。你还可以用下划线 _ 跳过不需要的字段:
val (_, name, _) = user // 只取 name
我记得有一次写 RecyclerView 的 Adapter,在 onBindViewHolder 里直接解构 data class,代码瞬间清爽了很多。你想想看,以前在 Java 里要写 user.getId()、user.getName()…… 现在一行搞定。
避坑指南:我曾经在解构时搞错了字段顺序,导致数据错乱。后来我养成了一个习惯——解构时尽量用命名清晰的变量,或者干脆用 _ 跳过不用的字段,避免混淆。
四、sealed class:让分支逻辑更安全
密封类,说白了就是受限的继承体系。它告诉编译器:“我这个类的子类就这么多,没有别的了。”
为什么需要它?我们来看一个常见的场景:网络请求结果。
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val message: String, val code: Int) : Result<Nothing>()
object Loading : Result<Nothing>()
}
当你用 when 处理这个密封类时,编译器会强制你覆盖所有子类。少写一个分支?编译不通过。
fun handleResult(result: Result<String>) {
when (result) {
is Result.Success -> println("数据: ${result.data}")
is Result.Error -> println("错误: ${result.message}")
Result.Loading -> println("加载中...")
// 不需要 else 分支,因为所有情况都已覆盖
}
}
我个人觉得,这是密封类最大的价值——把运行时错误变成编译时错误。你想想看,如果以后新增了一个 Result.Cancelled 子类,所有 when 表达式都会报编译错误,逼着你去处理新情况。这比运行时崩溃强太多了。
五、sealed class 的实际应用场景
除了网络请求结果,密封类还有很多用武之地:
- UI 状态管理:比如页面有加载中、空数据、错误、正常显示等状态
- 事件总线:定义有限的事件类型,避免用字符串或整数做区分
- 状态机:订单状态、登录状态等有限状态集合
- 多类型列表:RecyclerView 里不同类型的 item
我在项目中遇到过这样一个需求:一个页面有 5 种不同的 UI 状态。如果用枚举 + 数据类组合,代码会变得很别扭。用密封类就自然多了——每个状态可以携带自己的数据,而且类型安全。
核心区别:枚举的每个实例是单例,不能携带数据。密封类的子类可以是 data class,可以携带任意数据。所以密封类 = 枚举的受限性 + 数据类的灵活性。
六、知识体系总览
下面这张图帮你理清今天讲的内容:
七、总结
数据类和密封类,一个帮你省代码,一个帮你保安全。两者结合,能写出既简洁又健壮的 Kotlin 代码。
数据类的核心是自动生成——你只管定义属性,编译器帮你搞定 equals、hashCode、toString、copy 和解构。密封类的核心是受限继承——编译器知道所有子类,强制你穷举处理,把运行时错误扼杀在编译期。
嗯,这两个特性我每天都在用。希望你也用得上。
最后一个小建议:如果你的类只是用来装数据,就用 data class。如果你的逻辑分支是有限的、明确的,就用 sealed class。别滥用,也别不用。
公众号:蓝海资料掘金营,微信 deep3321