【有趣的 Kotlin 】系列,通过解题加深自己对 Kotlin 的理解。
0x0B:Copy
data class Container(val list: MutableList<String>)
fun main(args: Array<String>) {
val list = mutableListOf("one", "two")
val c1 = Container(list)
val c2 = c1.copy()
list += "oops"
println(c2.list.joinToString())
}
以上代码,运行结果是什么?可选项:
- one, two
- one, two, oops
- UnsupportedOperationException
- will not compile
思考一下,记录下你心中的答案。
分析
Kotlin 编译器帮 data class
生成的 copy
函数是深拷贝还是浅拷贝?
- 若是深拷贝,答案就是 one, two
- 若是浅拷贝,答案就是 one, two, oops
深拷贝、浅拷贝
深拷贝和浅拷贝只针对Object
和Array
这样的引用数据类型。
- 浅拷贝(Shadow Clone)
浅拷贝只复制对象应用,即指向对象的指针,而不复制对象本身,新旧对象共享同一块内存。
- 深拷贝(Deep Clone)
深拷贝会另外创建一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
图片来自:https://stackoverflow.com/questions/184710/what-is-the-difference-between-a-deep-copy-and-a-shallow-copy
Data Class
Kotlin中,编译器会帮助 data class
生成如下成员函数:
-
equals()
和hashCode()
toString()
componentN
copy()
但是,componentN
和 copy()
函数不允许被覆写。更重要的一点,copy()
函数完成的是浅拷贝。
因此题目中,c1
和 c2
对象中成员变量list
指向同一份内存空间(即变量list
),所以当list
改变时,由于两个对象中的成员变量均会改变。
所以,正确答案为 :
选项 2 :one, two, oops
延伸
虽然 data class
的设计初衷是帮助开发者持有一些不可变的数据,以便区分数据类和业务类,但是 Kotlin 在编译上并未做严格的限制,再加上copy
函数浅拷贝的问题,程序上很容易出现与题目中相似的问题。开发者在抱怨设计缺陷的同时,也在积极寻找解决办法。
noCopy
一个从数据类中删除了 copy
方法的编译器插件。源码地址:https://github.com/AhmedMourad0/no-copy。
添加依赖
buildscript {
repositories {
mavenCentral()
// Or
maven { url "https://plugins.gradle.org/m2/" }
}
dependencies {
classpath "dev.ahmedmourad.nocopy:nocopy-gradle-plugin:1.4.0"
}
}
plugins {
id "dev.ahmedmourad.nocopy.nocopy-gradle-plugin" version "1.4.0"
}
使用
通过 @NoCopy
注解实现功能
@NoCopy
data class User(val name: String, val phoneNumber: String)
User("Ahmed", "+201234567890").copy(phoneNumber = "Happy birthday!") // Unresolved reference: copy
deepCopy
BennyHuo 老师为你解忧,提供两种深拷贝的方法:一种基于 Kotlin 反射,一种基于 KAPT。源码地址:https://github.com/bennyhuo/KotlinDeepCopy
反射
- 添加依赖
implementation("com.bennyhuo.kotlin.reflect:deepcopy-reflect:1.5.0")
- 使用
data class Speaker(val name: String, val age: Int)
data class Talk(val name: String, val speaker: Speaker)
class DeepCopyTest {
@Test
fun test() {
val talk = Talk("DataClass in Action", Speaker("Benny Huo", 30))
val newTalk = talk.deepCopy()
assert(talk == newTalk)
assert(talk !== newTalk)
}
}
KAPT
- 添加依赖
apply plugin: "kotlin-kapt"
...
dependencies {
kapt("com.bennyhuo.kotlin.apt:deepcopy-compiler:1.5.0")
implementation("com.bennyhuo.kotlin.apt:deepcopy-runtime:1.5.0")
}
- 添加
@DeepCopy
注解
@DeepCopy
data class Speaker(val name: String, val age: Int)
@DeepCopy
data class Talk(val name: String, val speaker: Speaker)
- 使用
fun Talk.deepCopy(name: String = this.name, speaker: Speaker = this.speaker): Talk = Talk(name, speaker.deepCopy())fun Speaker.deepCopy( name: String = this.name, age: Int = this.age, company: Company = this.company): Speaker = Speaker(name, age, company.deepCopy())
注意:如果成员属性是使用
@DeepCopy
注释的数据类,程序将会递归调用deepCopy
。