【有趣的 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())
}

以上代码,运行结果是什么?可选项:

  1. one, two
  2. one, two, oops
  3. UnsupportedOperationException
  4. will not compile

思考一下,记录下你心中的答案。

分析

Kotlin 编译器帮 data class 生成的 copy 函数是深拷贝还是浅拷贝?

  • 若是深拷贝,答案就是 one, two
  • 若是浅拷贝,答案就是 one, two, oops

深拷贝、浅拷贝

深拷贝和浅拷贝只针对ObjectArray这样的引用数据类型。

  • 浅拷贝(Shadow Clone)
    浅拷贝只复制对象应用,即指向对象的指针,而不复制对象本身,新旧对象共享同一块内存。

android kotlin数据库 kotlin clone_java

android kotlin数据库 kotlin clone_浅拷贝_02

android kotlin数据库 kotlin clone_浅拷贝_03

android kotlin数据库 kotlin clone_android kotlin数据库_04

  • 深拷贝(Deep Clone)
    深拷贝会另外创建一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。

android kotlin数据库 kotlin clone_kotlin_05

android kotlin数据库 kotlin clone_android kotlin数据库_06

android kotlin数据库 kotlin clone_kotlin_07

android kotlin数据库 kotlin clone_kotlin_08

图片来自: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()

但是,componentNcopy()函数不允许被覆写。更重要的一点,copy() 函数完成的是浅拷贝

因此题目中,c1c2 对象中成员变量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