【问题标题】:How to merge two objects of a class with nullable fields, keeping the non-null values?如何将一个类的两个对象与可空字段合并,保持非空值?
【发布时间】:2020-05-05 05:23:24
【问题描述】:

给定一个包含一堆成员的类,我想合并它的两个实例。结果实例应保留两个输入中的每一个的非空值。如果两个非空值相互矛盾,则应引发异常。

我当前的实现有些效果,但扩展性不好:

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),那就太好了。

【问题讨论】:

    标签: class generics kotlin reflection null


    【解决方案1】:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-10-04
      • 1970-01-01
      • 2014-05-20
      • 1970-01-01
      • 2023-03-16
      • 1970-01-01
      • 2021-01-08
      • 1970-01-01
      相关资源
      最近更新 更多