Глубокое слияние классов данных в Kotlin - PullRequest
0 голосов
/ 28 января 2020

Как я могу сделать рекурсивное / глубокое слияние двух классов данных в 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")

относящиеся: Объединение / объединение классов данных в Kotlin

1 Ответ

1 голос
/ 29 января 2020

Было несколько проблем в опубликованном коде,

  1. излишнее преобразование и вставка
  2. при обнаружении типа isData вместо слияния значений свойства сливаются в this с other был вызван, поэтому он стал бесконечной рекурсией.
  3. get не может использоваться в KProperty1 из-за дисперсии
  4. некоторых не-идиоматических c вещей, которые работают, но могут быть лучше

Вот исправленная версия. Для производства я бы добавил несколько проверок и сообщений об ошибках, но это должно работать для «счастливого пути» и, надеюсь, даст вам основу для построения:


import kotlin.reflect.KClass
import kotlin.reflect.KParameter
import kotlin.reflect.KProperty1
import kotlin.reflect.full.declaredMemberProperties
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
)

fun <T> mergeValues(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 { 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<*>
        if (type.isData) mergeValues(property, this, other) else lastNonNull(property, this, other)
    }
    return primaryConstructor.callBy(args)
}

// verification

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

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

println("Works!")
...