I want to implement a function that compares two objects of the same type based on their Comparable properties, such as:
fun <T:Any> reflectCompare(a: T, b: T) : Int {
a::class
.memberProperties
.intersect(b::class.memberProperties.toSet())
.forEach { prop ->
val pa = prop.getter.call(a)
if(pa is Comparable<*>) {
val cmp = pa.compareTo(prop.getter.call(b)) // ERROR: Type mismatch
if(cmp != 0) return cmp
}
}
return 0
}
However, how can I achieve this without resorting to an unsafe cast to Comparable<Any>?
For instance, in the above implementation, using
prop.getter.call(b)as an argument topa.compareTo(...)yields aType mismatch: inferred type is Any? but Nothing was expectederror.Moreover, replacing
if(pa is Comparable<*>)withif(pa is Comparable<Any?>)isn't viable. Due to type erasure, it's impossible to check the type argument withis.Even though
filterIsInstanceappears safe and compiles without errors, it's not entirely safe. Despite its outward appearance, it's not truly safe.
The following alternative implementation for reflectCompare compiles without errors but fails to provide the intended behavior:
fun <T:Any> reflectCompare(a: T, b: T) : Int {
a::class
.memberProperties
.intersect(b::class.memberProperties.toSet())
.filterIsInstance<KProperty1<T, Comparable<Any?>>>()
.forEach { prop ->
val pa = prop.getter.call(a)
val cmp = pa.compareTo(prop.getter.call(b))
if(cmp != 0) return cmp
}
return 0
}
If used with a class with incomparable properties, it will attempt to cast the value of that property to Comparable, resulting in an error. For example, the following test fails with: ClassCastException: class Residence cannot be cast to class java.lang.Comparable.
class Individual(val name: String, val addr: Residence)
class Residence(val nr: Int, val street: String)
@Test fun testCompareReflect() {
val a = Individual("Bart", Residence(45, "Pink Street"))
val b = Individual("Bart", Residence(23, "Soho"))
compare(a, b) // ClassCastException: class Residence cannot be cast to class java.lang.Comparable
}
Upon examining the implementation of filterIsInstance, it becomes clear that it doesn't truly provide the expected behavior. It uses if (element is R), where R takes KProperty1<T, Comparable<Any?>> from the above example. Due to type erasure, it merely checks if it's a KProperty, neglecting the type arguments. Perhaps filterIsInstance should warn about this biased behavior, but @kotlin.internal.NoInfer R on the type parameter R obscures this error.