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?.

Информация об удалении типа в документации Kotlin

Другие вопросы по тегам