Home > database >  What is the type of a Kotlin 'data class'?
What is the type of a Kotlin 'data class'?

Time:07-24

I have a situation where I need to create a copy of data class object. I don't know in advance which of the many data classes I have will come in into the function. I do know, however, that only data classes will be used as input to this function.

This is what didn't work:

fun doSomething(obj: Any): Any {
  obj.copy(...) // <- there's no 'copy' on Any
  ...
}

This is what I really like to do:

fun doSomething(obj: KAnyDataClass): KAnyDataClass {
  obj.copy(...) // <- works, data classes have a 'copy' method
  ...
}

CodePudding user response:

I'm not a Kotlin developer, but it looks like the language does not support dynamic dispatch or traits. You might find success with the dynamic type, which just turns off the type-checker so it won't yell at you for using a method that it doesn't know about. However this opens up the possibility of a runtime error if you pass an argument that actually doesn't have that method.

CodePudding user response:

There is no class or interface for data classes, but we know from the documentation of data classes that there are derived functions componentN and copy in each data class.

We can use that knowledge to write an abstract copy method that calls the copy method of a given arbitrary data class using reflection:

fun <T : Any> copy(data: T, vararg override: Pair<Int, Any?>): T {
    val kClass = data::class
    if (!kClass.isData) error("expected a data class")
    val copyFun = kClass.functions.first { it.name == "copy" }
    checkParameters(override, kClass)
    val vals = determineComponentValues(copyFun, kClass, override, data)
    @Suppress("UNCHECKED_CAST")
    return copyFun.call(data, *vals) as T
}

/** check if override of parameter has the right type and nullability */
private fun <T : Any> checkParameters(
    override: Array<out Pair<Int, Any?>>,
    kClass: KClass<out T>
) {
    override.forEach { (index, value) ->
        val expectedType = kClass.functions.first { it.name == "component${index   1}" }.returnType
        if (value == null) {
            if (!kClass.functions.first { it.name == "component${index   1}" }.returnType.isMarkedNullable) {
                error("value for parameter $index is null but parameter is not nullable")
            }
        } else {
            if (!expectedType.jvmErasure.isSuperclassOf(value::class))
                error("wrong type for parameter $index: expected $expectedType but was ${value::class}")
        }
    }
}

/** determine for each componentN the value from override or data element */
private fun <T : Any> determineComponentValues(
    copyFun: KFunction<*>,
    kClass: KClass<out T>,
    override: Array<out Pair<Int, Any?>>,
    data: T
): Array<Any?> {
    val vals = (1 until copyFun.parameters.size)
        .map { "component$it" }
        .map { name -> kClass.functions.first { it.name == name } }
        .mapIndexed { index, component ->
            override.find { it.first == index }.let { if (it !== null) it.second else component.call(data) }
        }
        .toTypedArray()
    return vals
}

Since this copy function is a generic, it is not possible to specify overloads in the usual way, but I tried to support it in another way. Let's say we have a data class and element

data class Example(
    val a: Int,
    val b: String,
)

val example = Example(1, "x")

We can create a copy of example with copy(example) that has the same elements as the original.

If we want to override the first element, we cannot write copy(example, a = 2), but we can write copy(example, 0 to 2), saying that we want to override the first component with value 2.

Analogously we can write copy(example, 0 to 3, 1 to "y") to specify that we want to change the first and the second component.

I am not sure if this works for all cases since I just wrote it, but it should be a good start to work with.

  • Related