Combining/merging data classes in Kotlin

二次信任 提交于 2020-04-11 04:19:09

问题


Is there a way to merge kotlin data classes without specifying all the properties?

data class MyDataClass(val prop1: String, val prop2: Int, ...//many props)

with a function with the following signature:

fun merge(left: MyDataClass, right: MyDataClass): MyDataClass

where this function checks each property on both classes and where they are different uses the left parameter to create a new MyDataClass.

Is this possible possible using kotlin-reflect, or some other means?

EDIT: more clarity

Here is a better description of what i want to be able to do

  data class Bob(
        val name: String?,
        val age: Int?,
        val remoteId: String?,
        val id: String)

@Test
fun bob(){

    val original = Bob(id = "local_id", name = null, age = null, remoteId = null)
    val withName = original.copy(name = "Ben")
    val withAge = original.copy(age = 1)
    val withRemoteId = original.copy(remoteId = "remote_id")

    //TODO: merge without accessing all properties
    // val result = 
    assertThat(result).isEqualTo(Bob(id = "local_id", name = "Ben", age=1, remoteId = "remote_id"))
}

回答1:


infix fun <T : Any> T.merge(mapping: KProperty1<T, *>.() -> Any?): T {
    //data class always has primary constructor ---v
    val constructor = this::class.primaryConstructor!!
    //calculate the property order
    val order = constructor.parameters.mapIndexed { index, it -> it.name to index }
                                      .associate { it };

    // merge properties
    @Suppress("UNCHECKED_CAST")
    val merged = (this::class as KClass<T>).declaredMemberProperties
                                           .sortedWith(compareBy{ order[it.name]})
                                           .map { it.mapping() }
                                           .toTypedArray()


    return constructor.call(*merged);
}

Edit

infix fun <T : Any> T.merge(right: T): T {
    val left = this;
    return left merge mapping@ {
        //    v--- implement your own merge strategy
        return@mapping this.get(left) ?: this.get(right);
    };
}

Example

val original = Bob(id = "local_id", name = null, age = null, remoteId = null)
val withName = original.copy(name = "Ben")
val withAge = original.copy(age = 1)
val withRemoteId = original.copy(remoteId = "remote_id")

val result = withName merge withAge merge withRemoteId;



回答2:


If you want to copy values from the right when values in the left are null then you can do the following:

infix 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]!!
        parameter to (property.get(this) ?: property.get(other))
    }
    return primaryConstructor.callBy(args)
}

Usage:

data class MyDataClass(val prop1: String?, val prop2: Int?)
val a = MyDataClass(null, 1)
val b = MyDataClass("b", 2)
val c = a merge b // MyDataClass(prop1=b, prop2=1)



回答3:


Your requirements are exactly the same as copying the left value:

fun merge(left: MyDataClass, right: MyDataClass) = left.copy()

Perhaps one of use isn't properly understanding the other. Please elaborate if this isn't what you want.

Note that since right isn't used, you could make it a vararg and "merge" as many as you like :)

fun merge(left: MyDataClass, vararg right: MyDataClass) = left.copy()

val totallyNewData = merge(data1, data2, data3, data4, ...)

EDIT

Classes in Kotlin don't keep track of their deltas. Think of what you get as you're going through this process. After the first change you have

current = Bob("Ben", null, null, "local_id")
next = Bob(null, 1, null, "local_id")

How is it supposed to know that you want next to apply the change to age but not name? If you're just updating based on nullability, @mfulton has a good answer. Otherwise you need to provide the information yourself.




回答4:


A class-specific way to combine data classes when we can define the fields we want to combine would be:

data class SomeData(val dataA: Int?, val dataB: String?, val dataC: Boolean?) {
    fun combine(newData: SomeData): SomeData {        
        //Let values of new data replace corresponding values of this instance, otherwise fall back on the current values.
        return this.copy(dataA = newData.dataA ?: dataA,
                dataB = newData.dataB ?: dataB,
                dataC = newData.dataC ?: dataC)
    }
}



回答5:


@mfulton26's solution merges properties that are part of primary constructor only. I have extended that to support all properties

inline infix 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]!!
        parameter to (property.get(other) ?: property.get(this))
    }
    val mergedObject = primaryConstructor.callBy(args)
    nameToProperty.values.forEach { it ->
        run {
            val property = it as KMutableProperty<*>
            val value = property.javaGetter!!.invoke(other) ?: property.javaGetter!!.invoke(this)
            property.javaSetter!!.invoke(mergedObject, value)
        }
    }
    return mergedObject
}


来源:https://stackoverflow.com/questions/44566607/combining-merging-data-classes-in-kotlin

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!