也許你需要這個為數據類生成 DeepCopy 方法的庫
- 2020 年 2 月 20 日
- 筆記
前不久 JetBrains 在北京搞了一次技術大會,我在演講當中提到了一個叫 DeepCopy 的庫,那麼我們今天就來詳細說說它。
我們有什麼拷貝的需求?
我們知道 Kotlin 的 data class 出廠自帶了一套 copy
方法,這個 copy
方法呢,就是實打實的一個淺拷貝,例如:
data class GitUser(val name: String)
它的 copy
方法其實就相當於:
data class GitUser(val name: String) { fun copy(name: String = this.name) = GitUser(name) }
如果成員是 val
那倒也還好,可如果是另一個數據類呢?
data class GitUser(val name: String) data class Project(val name: String) data class Owner(val gitUser: GitUser, val project: Project)
我們如果需要 copy
一下 Owner
的話,我們就會發現新實例與舊實例共享了 GitUser
和 Project
的實例,萬一項目的名稱是可以改的:
data class Project(var name: String)
新實例的 project
的名稱我想更改一下,結果發現老實例的也被改了。
所以你需要一個 DeepCopy 的方法:
data class Owner(val gitUser: GitUser, val project: Project){ fun deepCopy(gitUser: GitUser = this.gitUser, project: Project = this.project): Owner { return Owner(gitUser.copy(), project.copy()) } }
你這時候想,雖然 GitUser
和 Project
這兩個數據類內部的字段都是基本類型,用 copy
進行複製似乎也問題不大,可如果它們的成員當中也存在數據類呢?所以,你需要為每一個數據類定製一個 deepCopy
方法。。。
來來來,先實現個簡單的
天哪。那豈不是要寫死了。
不過問題不大,我們總是會想辦法讓編譯器或者運行時來幫我們搞定一切,於是我想到了要不要寫個 Kotlin 的編譯期插件呢,正巧 KotlinConf 也有人分享了一下這個話題,不過由於目前這套機制還沒有正式開放,就算我寫了你們也不敢用,更何況我還不會寫呢ψ(`∇´)ψ
於是我想到了最簡單的,用反射!用反射再配合 Kotlin 最優秀的特性之一的擴展方法,我們就可以為所有的類無縫提供一個 deepCopy
的擴展方法,當然,我們的目標是為數據類服務,所以其他類調用這個方法我們一概直接返回(~ ̄▽ ̄)~
fun <T : Any> T.deepCopy(): T { //①判斷是否為數據類,不是的話直接返回 if (!this::class.isData) { return this } //②數據類一定有主構造器,不用怕,這裡放心使用 !! 來轉為非空類型 return this::class.primaryConstructor!!.let { primaryConstructor -> primaryConstructor.parameters .map { parameter -> val value = (this::class as KClass<T>).declaredMemberProperties.first { it.name == parameter.name }.get(this) //③如果主構造器參數類型為數據類,遞歸調用 if ((parameter.type.classifier as? KClass<*>)?.isData == true) { parameter to value?.deepCopy() } else { parameter to value } } .toMap() .let(primaryConstructor::callBy) } }
看上去很簡單吧!正好秀一波反射的肌肉( ̄▽ ̄)~*
其實反射還真挺簡單的,不會反射的小夥伴們也不要害怕,現在大家都覺得反射有性能問題而不敢用,不會就不會吧(╯°□°)╯︵┻━┻
這個庫我已經扔到 jcenter,因此你可以通過添加依賴來使用它:
compile 'com.bennyhuo.kotlin:deepcopy-reflect:1.1.0'
當然,這裡由於是運行時才知道類型的構造器參數列表,因此沒辦法添加默認參數。但註解處理器可以呀,不信你瞧——
來我們再試試註解處理器
註解處理器實現理論上是可以的。額,實際上當然也是可以的,不然這在 JetBrains 大會上講過的東西要是不行的話,我可丟不起那人。。。
不過有些細節需要注意,我們在 Java 編譯期的角度是無法認知哪些是數據類的,也沒有什麼所謂的主構造器一說,所以我們需要通過 Metadata
來獲取到這些信息。
讀取 Metadata
需要用到下面這個框架,其實 Kotlin 反射跟這個原理一樣,不同之處在於反射在運行時讀取,我們則在編譯時讀取:
compile "org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.0.4"
讀取的方法我就不細說啦,後面我會提供源碼,大家有興趣可以花兩分鐘詳細閱讀下ヽ(;´Д`)ノ
需要提一句的是,我們通過註解處理器生成的 deepCopy
可以添加默認參數,這裡有不少細節需要處理,也是得益於 Metadata
的信息。
下面給大家看看例子吧:
首先添加依賴,配置註解處理器:
repositories { jcenter() //kotlinx-metadata-jvm 目前部署到了這個倉庫 maven { url "https://kotlin.bintray.com/kotlinx/" } } ... apply plugin: "kotlin-kapt" ... dependencies { kapt 'com.bennyhuo.kotlin:deepcopy-compiler:1.1.0' compile 'com.bennyhuo.kotlin:deepcopy-annotations:1.1.0' }
接着為我們的數據類配置註解:
@DeepCopy data class GitUser(val name: String) @DeepCopy data class Project(val name: String) @DeepCopy data class Owner(val gitUser: GitUser, val project: Project)
build 一下,生成了下面的擴展函數:
fun Owner.deepCopy(gitUser: GitUser = this.gitUser, project: Project = this.project): Owner = Owner(gitUser.deepCopy(), project.deepCopy())
我們看到,這已經跟出廠自帶的 copy
很像了,不同之處就是我們會遞歸的檢查哪些數據類被標註為 DeepCopy
,如果標註,就遞歸調用對應的 deepCopy
函數。
其實如果不添加默認值,這個註解處理器非常容易寫的,因為它不需要處理泛型,不需要處理與 Java 的類型映射,也不需要處理別名,一氣呵成,就像這樣:
fun Owner.deepCopy(): Owner = Owner(gitUser.deepCopy(), project.deepCopy())
可是一旦加上了默認值,那就意味着我們需要為函數添加參數,那麼我們就需要搞清楚參數的類型,是否協變,等等。但仍然問題不大,經過一下午的折騰(耽誤了我看 KPL 季後賽了都),我們支持了參數類型有泛型實參,形參,星投影,甚至泛型參數嵌套,型變,例如:
@DeepCopy data class GenericParameter(val map: HashMap<String, List<String>>) @DeepCopy data class GenericParameterT<K: Number, V>(val map: HashMap<K, V>) @DeepCopy data class StarProjection(val map: List<Map<*, String>>) @DeepCopy data class Variances1(val map: HashMap<String, out List<Number>>)
如果還有哪些情況沒有覆蓋到,那麼儘管給我開 Issue 就好啦。
項目詳情
這個項目在 11月17日的 JetBrains 大會上我已經提到過了,項目在 Github 上,地址:https://github.com/enbandari/KotlinDeepCopy,我知道公眾號沒法點地址,因此點擊閱讀原文,就可以很方便的找到它。
別猶豫了,Star 一波吧,千萬別手軟。