Home > OS >  Kotlin delegate's ReadOnlyProperty with generic type value dose not cast correctly in getValue
Kotlin delegate's ReadOnlyProperty with generic type value dose not cast correctly in getValue

Time:10-24

I am expecting to see the output

black
white

with below code

package delegate

import kotlinx.coroutines.runBlocking
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty

open class Color(private val name: String) {
    override fun toString(): String {
        return name
    }
}

class Black : Color("black")
class White : Color("white")

class ColorCollection {
    private val black = Black()
    private val white = White()
    val list = listOf(black, white)
}

class Palette {
    val black: Black by ColorDelegate()
    val white: White by ColorDelegate()
    val colorCollection = ColorCollection()
}

class ColorDelegate<T> : ReadOnlyProperty<Palette, T> {
    override fun getValue(thisRef: Palette, property: KProperty<*>): T {
        return thisRef.colorCollection.list.mapNotNull { it as? T }.first()
    }
}

fun main() = runBlocking {
    val palette = Palette()
    println(palette.black)
    println(palette.white)
}

However, I only get black output and then Exception in thread "main" java.lang.ClassCastException: delegate.Black cannot be cast to delegate.White. I found that with this line thisRef.colorCollection.list.mapNotNull { it as? T }, I am expecting it only returns the value in the list that can be safely cast to the generic type, otherwise return null. For example, when accessing black delegated property in Palette, I should only see 1 black element returned by thisRef.colorCollection.list.mapNotNull { it as? T },It actually returns two (black and white). it as? T somehow always works regardless of what T is. I also tried putting a breakpoint at that line, tried "abcdef" as T?, it also works, which I expect to see cast exception that String cannot be cast to Black... enter image description hereenter image description here

Is this a bug...?

CodePudding user response:

This is due to type erasure with generics. Casting to a generic type always succeeds because generic types are not known at runtime. This can cause a runtime exception elsewhere if the value is assigned to a variable of a concrete mismatched type or a function it doesn't have is called on it.

Since casting to generic types is dangerous (it silently works, allowing for bugs that occur at a different place in code), there is a compiler warning when you do it like in your code. The warning says "unchecked cast" because the cast occurs without type-checking. When you cast to a concrete type, the runtime checks the type at the cast site and if there is a mismatch it immediately throws ClassCastException, or resolves to null in the case of a safe-cast as?.

Info about type erasure in the Kotlin documentation

CodePudding user response:

Remember that Type Erasure is a thing in Kotlin, so the runtime does not know what the T in it as? T, and hence cannot check the cast for you. Therefore, the cast always succeeds (and something else will fail later down the line). See also this post. IntelliJ should have given you an "unchecked cast" warning here.

So rather than checking the type using T, you can check the type using the property argument:

class ColorDelegate<T> {
    operator fun getValue(thisRef: Palette, property: KProperty<*>) =
        // assuming such an item always exists
        thisRef.colorCollection.list.first { 
            property.returnType.classifier == it::class 
        } as T
}

fun main()  {
    val palette = Palette()
    println(palette.black) // prints "black"
    println(palette.white) // prints "white"
}

Here, I've checked that the class of the returnType of the property (i.e. the property on which you are putting the delegate) is equal to the list element's runtime class. You can also e.g. be more lenient and check isSubclassOf.

Do note that this wouldn't find any element in the list if the property's type was another type parameter, rather than a class, e.g.

class Palette<T> {
    ...
    val foo: T by ColorDelegate()
    ...
}

But alas, that's type erasure for you :(

  • Related