在 Kotlin 中深度合并数据 类

Deep merging data classes in Kotlin

如何在 Kotlin 中对两个数据进行递归/深度合并 类?像这样:

import kotlin.reflect.*
import kotlin.reflect.full.*

data class Address(
  val street: String? = null,
  val zip: String? = null
)

data class User(
  val name: String? = null,
  val age: Int? = null,
  val address: Address? = null
)

inline fun <reified T : Any> T.merge(other: T): T {
  val nameToProperty = T::class.declaredMemberProperties.associateBy { it.name }
  val primaryConstructor = T::class.primaryConstructor!!
  val args = primaryConstructor.parameters.associate { parameter ->
    val property = nameToProperty[parameter.name]!!
    val type = property.returnType.classifier as KClass<*>
    if (type.isData) {
      parameter to this.merge(other) //inline function can't be recursive
    } else {
      parameter to (property.get(other) ?: property.get(this))
    }
  }
  return primaryConstructor.callBy(args)
}

val u1 = User(name = "Tiina", address = Address(street = "Hämeenkatu"))
val u2 = User(age = 23, address = Address(zip = "33100"))

u1.merge(u2)
// expected: User(age = 23, name= "Tiina", address = Address(zip = "33100", street = "Hämeenkatu")

相关:

发布的代码中有几个问题,

  1. 不必要的具体化和内联
  2. 当检测到类型 isData 而不是合并 this 上 属性 的值时调用了 other,因此它变成了无限递归。
  3. get 不能用于 KProperty1 因为方差
  4. 一些非惯用的东西有效,但可以做得更好

这是固定版本。对于生产,我会添加一些检查和错误消息,但这应该适用于“快乐之路”,并希望为您提供构建的基础:

import kotlin.reflect.KClass
import kotlin.reflect.KParameter
import kotlin.reflect.KProperty1
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.full.primaryConstructor

data class Address(
    val street: String? = null,
    val zip: String? = null
)

data class User(
    val name: String? = null,
    val age: Int? = null,
    val address: Address? = null,
    val map: Map<String, Int>? = null
)

fun <T> mergeData(property: KProperty1<out T, Any?>, left: T, right: T): Any? {
    val leftValue = property.getter.call(left)
    val rightValue = property.getter.call(right)
    return rightValue?.let {
        if ((property.returnType.classifier as KClass<*>).isSubclassOf(Map::class)) (leftValue as? Map<*, *>)?.plus(it as Map<*, *>)
        else leftValue?.merge(it)
    } ?: rightValue ?: leftValue
}

fun <T> lastNonNull(property: KProperty1<out T, Any?>, left: T, right: T) =
    property.getter.call(right) ?: property.getter.call(left)

fun <T : Any> T.merge(other: T): T {
    val nameToProperty = this::class.declaredMemberProperties.associateBy { it.name }
    val primaryConstructor = this::class.primaryConstructor!!
    val args: Map<KParameter, Any?> = primaryConstructor.parameters.associateWith { parameter ->
        val property = nameToProperty[parameter.name]!!
        val type = property.returnType.classifier as KClass<*>
        when {
            type.isData || type.isSubclassOf(Map::class) -> mergeData(property, this, other)
            else -> lastNonNull(property, this, other)
        }
    }
    return primaryConstructor.callBy(args)
}


// verification

val u1 = User(name = "Tiina", address = Address(street = "Hämeenkatu"), map = mapOf("a" to 1))
val u2 = User(age = 23, address = Address(zip = "33100"), map = mapOf("b" to 2))

check(
    u1.merge(u2) == User(
        age = 23,
        name = "Tiina",
        address = Address(zip = "33100", street = "Hämeenkatu"),
        map = mapOf("a" to 1,"b" to 2)
    )
) {
    "doesn't work"
}

println("Works!")