3. 数据类的copy函数:不止是复制那么简单
好,咱们接着聊数据类。前面讲了数据类的自动生成方法,今天重点说说 copy() 函数。说实话,这个函数我刚开始用的时候,觉得它就是个「复制粘贴」的语法糖。后来踩过坑才明白——它背后的设计思想,其实跟不可变架构紧密相关。
3.1 copy函数的使用场景
先问个问题:你写代码时,有没有遇到过这种情况?
一个对象大部分字段不变,只想改其中一两个。比如用户信息,只是更新了邮箱,其他都不动。用传统 Java 写法,你得 new 一个新对象,把所有字段再传一遍。字段少还好,十几个字段时,那代码简直不忍直视。
我个人习惯是,只要数据类超过 3 个字段,就优先考虑用 copy()。它本质上是一个「按需修改」的工厂方法。
data class User(
val id: Long,
val name: String,
val email: String,
val phone: String,
val address: String
)
fun main() {
val original = User(1, "张三", "zhang@example.com", "13800138000", "北京市朝阳区")
// 只改邮箱,其他不变
val updated = original.copy(email = "newzhang@example.com")
println(original) // 原对象不变
println(updated) // 新对象,只有email变了
}
你看,一行代码搞定。这在函数式编程里叫「持久化数据结构」——原对象不受影响,新对象共享未修改的部分。
核心思想: copy 不是修改原对象,而是基于原对象创建一个新对象。这是不可变架构的基石。
3.2 部分属性修改:命名参数的妙用
为什么 copy() 能只改部分字段?这得益于 Kotlin 的命名参数和默认参数机制。
编译器自动生成的 copy() 函数,签名长这样:
fun copy(
id: Long = this.id,
name: String = this.name,
email: String = this.email,
phone: String = this.phone,
address: String = this.address
): User
每个参数都有默认值——就是原对象对应字段的值。你传了哪个参数,就覆盖哪个;没传的,保持原样。
我在项目中遇到过这样一个场景:订单状态流转,每次只更新状态字段,其他信息(商品、金额、地址)完全不变。用 copy() 配合 when 表达式,代码非常干净:
data class Order(
val id: String,
val items: List<Item>,
val total: Double,
val status: OrderStatus,
val address: String
)
fun Order.transition(newStatus: OrderStatus): Order {
return this.copy(status = newStatus)
}
你想想看,如果不用 copy(),你得手动把 items、total、address 全部再传一遍。万一漏了一个字段,bug 就来了。
小技巧: 如果你经常需要修改同一组字段,可以给数据类加一个扩展函数。比如 fun User.withEmail(newEmail: String) = copy(email = newEmail)。这样调用方更清晰。
3.3 copy与深拷贝的区别:一个常见的坑
嗯,这里要注意。很多初学者以为 copy() 是深拷贝——其实不是。它做的是浅拷贝。
什么叫浅拷贝?就是基本类型字段会复制值,但引用类型字段只复制引用。说白了,两个对象可能共享同一个子对象。
看个例子:
data class Address(
val city: String,
val street: String
)
data class Person(
val name: String,
val address: Address // 引用类型
)
fun main() {
val addr = Address("北京", "长安街")
val p1 = Person("张三", addr)
val p2 = p1.copy() // 浅拷贝
// 注意:p1.address 和 p2.address 是同一个对象
println(p1.address === p2.address) // true
}
为什么会这样?因为 copy() 只是把 address 这个引用复制过去了,并没有递归复制 Address 对象本身。
我曾经在做一个配置管理模块时,就因为这个踩过坑。两个配置对象共享了一个列表引用,一个改了列表内容,另一个也跟着变了。排查了半天才发现是浅拷贝的问题。
避坑指南: 如果你的数据类包含可变对象(如 MutableList、ArrayList、自定义可变类),copy() 后修改子对象会影响原对象。解决方案有两个:
- 尽量使用不可变类型(
List而非MutableList) - 需要深拷贝时,手动实现
copy()或使用序列化方式
那什么时候需要深拷贝?我个人经验是:当你的数据类嵌套了多层,并且你确定要完全独立的两份数据时。比如订单快照、历史版本记录。
手动实现深拷贝可以这样:
data class Address(
val city: String,
val street: String
) {
fun deepCopy() = Address(city, street)
}
data class Person(
val name: String,
val address: Address
) {
fun deepCopy() = Person(name, address.deepCopy())
}
当然,如果嵌套层次太深,这样写也挺累的。我一般建议:优先用不可变设计。如果所有子对象都是不可变的,浅拷贝和深拷贝在效果上就没区别了。
3.4 总结一下
| 特性 | copy() | 深拷贝 |
|---|---|---|
| 复制层级 | 只复制第一层 | 递归复制所有层级 |
| 引用类型 | 共享引用 | 独立对象 |
| 性能 | 快(不递归) | 慢(递归复制) |
| 使用场景 | 不可变架构、状态更新 | 需要完全隔离的数据快照 |
最后说一句:copy() 是 Kotlin 数据类给我的最大惊喜之一。它让不可变编程变得可行,又不像函数式语言那样有学习门槛。你只要记住——它是浅拷贝,然后根据场景选择合适的方式,基本就不会出问题。