也許你需要這個為數據類生成 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 的話,我們就會發現新實例與舊實例共享了 GitUserProject 的實例,萬一項目的名稱是可以改的:

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())      }  }

你這時候想,雖然 GitUserProject 這兩個數據類內部的字段都是基本類型,用 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 一波吧,千萬別手軟。