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...
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 :(