Call data class copy() via reflection
Asked Answered
F

3

12

Is it possible to call the copy() function of a data class via reflection in Kotlin? How can I get a reference to the function itself? Is there a superclass for all data classes?

Foetation answered 27/3, 2018 at 10:57 Comment(0)
N
23

There's no common supertype for all data classes.

Basically, copy is a normal member function which you can call with the Kotlin reflection API as follows:

val person = Person("Jane", 23)
val copy = person::class.memberFunctions.first { it.name == "copy" }
val instanceParam = copy.instanceParameter!!
val ageParam = copy.parameters.first { it.name == "age" }
val result = copy.callBy(mapOf(instanceParam to person, ageParam to 18))
println(result) // Person(name=Jane, age=18)

Make sure you add kotlin-reflect as a dependency.

The example above shows how to omit values for the default parameters – no value is passed for name. If you want to pass all the parameters, this can be done in a simpler way:

val person = Person("Jane", 23)
val copy = person::class.memberFunctions.first { it.name == "copy" }
val result = copy.call(person, person.name, 18)
println(result) // Person(name=Jane, age=18)

Kotlin reflection API is not strictly necessary to call a function if you pass arguments for all of the parameters, you can do that via Java reflection as well:

val person = Person("Jane", 23)
val copy = person::class.java.methods.first { it.name == "copy" }
val result = copy.invoke(person, person.name, 18)
println(result) // Person(name=Jane, age=18)
Nisan answered 27/3, 2018 at 11:7 Comment(1)
That helped me a lot. Thanks!Pyoid
H
3

So, based on https://stackoverflow.com/users/2196460/hotkey's answer above:

fun <T : Any> clone (obj: T): T {
  if (!obj::class.isData) {
    println(obj)
    throw Error("clone is only supported for data classes")
  }

  val copy = obj::class.memberFunctions.first { it.name == "copy" }
  val instanceParam = copy.instanceParameter!!
  return copy.callBy(mapOf(
    instanceParam to obj
  )) as T
}


Haro answered 13/11, 2019 at 16:33 Comment(0)
S
1

This is another version of the suggestions above that allows you to invoke copy() passing specific property values like you can do when calling copy directly:

fun <T : Any> copyDataObject(toCopy: T, vararg properties: Pair<KProperty<*>, Any?>): T {
    val dataClass = toCopy::class
    require(dataClass.isData) { "Type of object to copy must be a data class" }
    val copyFunction = dataClass.memberFunctions.first { it.name == "copy" }
    val parameters = buildMap {
        put(copyFunction.instanceParameter!!, toCopy)
        properties.forEach { (property, value) ->
            val parameter = requireNotNull(
                copyFunction.parameters.firstOrNull { it.name == property.name }
            ) { "Parameter not found for property ${property.name}" }
            value?.let {
                require(
                    parameter.type.isSupertypeOf(it::class.starProjectedType)
                ) { "Incompatible type of value for property ${property.name}" }
            }
            put(parameter, value)
        }
    }
    @Suppress("UNCHECKED_CAST")
    return copyFunction.callBy(parameters) as T
}

With this you should be able to do the following:

val person = Person("Jane", 23, Sex.FEMALE)
val copy = copyDataObject(person, 
    person::name to "Jack",
    person::sex to Sex.MALE
)
println(person) // Person(name=Jane, age=23, sex=FEMALE)
println(copy) // Person(name=Jack, age=23, sex=MALE)

The function also validates if the passed object is a data class (remove this line if not relevant for you) and if the passed values are compatible to the passed properties.

For convenience, I've created an extension function in the place I used this like this:

private fun <T : Any> T.copy(vararg properties: Pair<KProperty<*>, Any?>): T =
    copyDataObject(this, *properties)

So that I could do something like this:

val someGenericPerson: P  // P here is P : Person
someGenericPerson.copy(someGenericPerson::name to "Jack")
Sluiter answered 30/11, 2023 at 14:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.