Kotlin 中的深度合并数据类

Tom*_*man 3 reflection kotlin data-class

如何在 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")
Run Code Online (Sandbox Code Playgroud)

相关:在 Kotlin 中组合/合并数据类

Cza*_*zar 5

贴出的代码有几个问题,

  1. 不必要的具体化和内联
  2. 当检测到类型 isData 而不是将属性的值合并thisother被调用时,它变成了无限递归。
  3. get 由于方差,不能用于 KProperty1<out T, Any?>
  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!")
Run Code Online (Sandbox Code Playgroud)