ReadOnlyProperty делегата Kotlin с дозой значения общего типа некорректно приводится в getValue
Я ожидаю увидеть результат
black
white
с кодом ниже
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)
}
Однако я получаю только черный цвет, а затем
Exception in thread "main" java.lang.ClassCastException: delegate.Black cannot be cast to delegate.White
. Я обнаружил, что с этой строкой я ожидаю, что она вернет только значение в списке, которое можно безопасно привести к универсальному типу, в противном случае вернет null. Например, при доступе к делегированному свойству черного цвета в палитре я должен видеть только 1 черный элемент, возвращаемый
thisRef.colorCollection.list.mapNotNull { it as? T }
, Он фактически возвращает два (черный и белый).
it as? T
как-то всегда работает независимо от того, что такое Т. Я также попытался поставить точку останова в этой строке, попробовал «abcdef» как T ?, он также работает, и я ожидаю увидеть исключение приведения, которое String не может быть преобразовано в Black ...
Это ошибка ...?
2 ответа
Помните, что Type Erasure - это вещь в Kotlin, поэтому среда выполнения не знает, что внутри
it as? T
, и, следовательно, не может проверить состав для вас. Следовательно, приведение всегда выполняется успешно (и что-то еще не сработает позже). См. Также этот пост . IntelliJ должен был выдать здесь предупреждение о "непроверенном приведении".
Поэтому вместо проверки типа с помощью
T
, вы можете проверить тип с помощью
property
аргумент:
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"
}
Здесь я проверил, что класс
returnType
свойства (т. е. свойства, на которое вы помещаете делегата) совпадает с классом времени выполнения элемента списка. Вы также можете, например, проявить снисходительность и проверить
isSubclassOf
.
Обратите внимание, что при этом не будет найдено ни одного элемента в списке, если тип свойства был другим параметром типа, а не классом, например
class Palette<T> {
...
val foo: T by ColorDelegate()
...
}
Но увы, это стирание типа для вас :(
Это происходит из-за стирания типов с помощью дженериков. Приведение к универсальному типу всегда выполняется успешно, поскольку универсальные типы неизвестны во время выполнения. Это может вызвать исключение времени выполнения в другом месте, если значение присваивается переменной конкретного несоответствующего типа или для нее вызывается функция, которой у нее нет.
Поскольку приведение к универсальным типам опасно (оно работает незаметно, допуская ошибки, которые возникают в другом месте кода), компилятор выдает предупреждение, когда вы делаете это, как в своем коде. В предупреждении написано «непроверенное приведение», поскольку приведение выполняется без проверки типов. Когда вы приводите к конкретному типу, среда выполнения проверяет тип на сайте приведения и, если есть несоответствие, немедленно генерирует ClassCastException или разрешает значение null в случае безопасного приведения
as?
.