Как объединить два объекта класса с полями, допускающими значение NULL, сохранив ненулевые значения? - PullRequest
0 голосов
/ 05 мая 2020

Учитывая класс с кучей членов, я хотел бы объединить два его экземпляра. Результирующий экземпляр должен сохранять ненулевые значения каждого из двух входов. Если два ненулевых значения противоречат друг другу, должно возникнуть исключение.

Моя текущая реализация работает, но плохо масштабируется:

import kotlin.reflect.KProperty1
import kotlin.reflect.full.memberProperties
import kotlin.test.fail

class Thing(
    val a: Int,
    var b: String,
    val c: Int? = null,
    val d: Boolean? = null,
    val e: Long? = null,
    val f: String? = null,
    val g: String? = null
)

private fun <T> mergeThingProperty(property: KProperty1<Thing, *>, a: Thing, b: Thing): T {
    val propA = property.get(a)
    val propB = property.get(b)
    val mergedValue = if (propA != null && propB == null) {
        propA
    } else if (propA == null && propB != null) {
        propB
    } else if (propA != null && propB != null) {
        if (propA != propB) {
            throw RuntimeException("Can not merge Thing data on property ${property.name}: $propA vs. $propB.")
        } else {
            propA
        }
    } else {
        null
    }
    @Suppress("UNCHECKED_CAST")
    return mergedValue as T
}

fun mergeTwoThings(thing1: Thing, thing2: Thing): Thing {
    val properties = Thing::class.memberProperties.associateBy { it.name }
    val propertyMissingMsg = "Missing value in Thing properties"
    return Thing(
        mergeThingProperty(properties["a"] ?: error(propertyMissingMsg), thing1, thing2),
        mergeThingProperty(properties["b"] ?: error(propertyMissingMsg), thing1, thing2),
        mergeThingProperty(properties["c"] ?: error(propertyMissingMsg), thing1, thing2),
        mergeThingProperty(properties["d"] ?: error(propertyMissingMsg), thing1, thing2),
        mergeThingProperty(properties["e"] ?: error(propertyMissingMsg), thing1, thing2),
        mergeThingProperty(properties["f"] ?: error(propertyMissingMsg), thing1, thing2),
        mergeThingProperty(properties["g"] ?: error(propertyMissingMsg), thing1, thing2)
    )
}

fun main() {
    val result1 = mergeTwoThings(Thing(a = 42, b = "foo"), Thing(a = 42, b = "foo", c = 23))
    assert(result1.c == 23)
    assert(result1.d == null)

    try {
        mergeTwoThings(Thing(a = 42, b = "foo"), Thing(a = 42, b = "bar"))
        fail("An exception should have been thrown.")
    } catch (ex: RuntimeException) {
    }
}

Как избежать повторения вручную каждый член (в настоящее время находится в mergeTwoThings)?

Кроме того, было бы неплохо, если бы мне не требовалось непроверенное приведение (в настоящее время в mergeThingProperty).

1 Ответ

0 голосов
/ 05 мая 2020

callBy может использоваться для вызова функции (например, конструктора класса) с аргументами, предоставленными в карте. Тогда решение выглядит следующим образом:

import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.full.valueParameters
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.fail

class Thing(
    val a: Int,
    var b: String,
    val c: Int? = null,
    val d: Boolean? = null,
    val e: Long? = null,
    val f: String? = null,
    val g: String? = null
)

inline fun <reified T : Any> getPrimaryConstructor() =
    T::class.primaryConstructor
        ?: throw RuntimeException("${T::class.qualifiedName} does not have a primary constructor.")

inline fun <reified T : Any> doConstructorParametersMatchMembers() =
    T::class
        .declaredMemberProperties
        .map { Pair(it.name, it.returnType) }
        .toSet() ==
            getPrimaryConstructor<T>()
                .valueParameters.map { Pair(it.name, it.type) }
                .toSet()

/**
 * Returns the single element, or `null` if the collection is empty, or throws an exception if the collection has more than one element.
 */
fun <T> Iterable<T>.nullOrExactlySingle() =
    when (toList().size) {
        0 -> null
        1 -> single()
        else -> throw IllegalArgumentException("Collection has more than one element.")
    }

inline fun <reified T : Any> mergeTwoObjects(a: T, b: T): T {
    assert(doConstructorParametersMatchMembers<T>()) {
        "Constructor parameters of ${T::class.qualifiedName} does not match its members."
    }
    val arguments = T::class
        .declaredMemberProperties
        .associateBy { it.name }
        .mapValues {
            listOfNotNull(
                it.value.get(a),
                it.value.get(b)
            )
                .toSet()
                .nullOrExactlySingle()
        }
        .filterValues { it != null }

    return getPrimaryConstructor<T>()
        .callBy(getPrimaryConstructor<T>()
            .valueParameters
            .associateWith {
                arguments[it.name]
            })
}

fun mergeTwoThings(thing1: Thing, thing2: Thing) = mergeTwoObjects(thing1, thing2)

fun main() {
    val result1 = mergeTwoThings(
        Thing(a = 42, b = "foo"),
        Thing(a = 42, b = "foo", c = 23)
    )
    assertEquals(23, result1.c)
    assertNull(result1.d)

    try {
        mergeTwoThings(
            Thing(a = 42, b = "foo"),
            Thing(a = 42, b = "bar")
        )
        fail("An exception should have been thrown.")
    } catch (ex: IllegalArgumentException) {
    }
}

...