Котлин: обработка звездных дженериков с помощью токенов типов

Рассмотрим следующий универсальный класс, который «реифицируется» с помощью токена типа:

class Gen<X : Any>(val token: KClass<X>) {
    fun doSomething(x: X) {
        // ...
    }
}

Теперь Gen используется гетерогенно с использованием звездообразной проекции, но токен типа можно использовать для соответствия подходящим значениям:

fun m(g: Gen<*>, x: Any) {
    if (g.token == x::class) {
        g.doSomething(x)
    }
}

Это не компилируется. Введение следующего непроверяемого приведения приводит к компиляции кода, но это небезопасно, поскольку в значение приведения могут быть переданы неправильные входные данные (текущий код на самом деле не генерирует ClassCastException, но его можно легко расширить для этого).

fun m(g: Gen<*>, x: Any) {
    if (g.token == x::class) {
        g as Gen<Any>
        g.doSomething(x)
        g.doSomething(8) // potential ClassCastException
    }
}

Это единственный способ справиться с этим или есть более безопасные способы сделать это?


Уточнение

Введение «динамического типа» улучшает ситуацию

fun <T : Any> m(g: Gen<*>, x: T) {
    if (g.token == x::class) {
        g as Gen<T>

        g.doSomething(x) // ok
        g.doSomething(8) // does not compile
    }
}

Условие в if вместе с приведением отражает назначение токена типа, и после этого мы снова находимся в «безопасном» коде. Однако было бы желательно, чтобы бизнес-логика была отделена от проверки/приведения и чтобы последнюю можно было каким-то образом инкапсулировать. Кажется, это не так-то просто. Например, следующий подход является «небезопасным».

fun <T : Any> Gen<*>.ifMatches(x: T, code: (Gen<T>) -> Unit) {
    if (token == x::class) {
        code(this as Gen<T>)
    }
}

fun main() {
    val g: Gen<*> = Gen(String::class)
    val x: Any = "hello"
    g.ifMatches(x) { it.doSomething(8) } // potential ClassCastException
}  
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
0
59
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Как правило, код, который понимает логику внутри Gen и выполняет непроверяемые приведения, должен быть «близок» к нему. Это может быть функция-член или функция расширения, предоставляемая тем же компонентом. Во многих случаях нет необходимости использовать какие-либо сложные способы обеспечения безопасности типов, поскольку код, выполняющий непроверяемое приведение типов, уже дважды проверен разработчиком на корректность.

Но если нам нужно предоставить некоторые утилиты для обеспечения безопасности типов, например, для потребителей нашего класса, то в зависимости от случая мы можем применить некоторые методы.

Если нам нужно предоставить типобезопасную функцию doSomething для пользователей Gen, мы можем легко ее реализовать:

fun Gen<*>.doSomethingIfMatches(x: Any) {
    if (token == x::class) {
        @Suppress("UNCHECKED_CAST")
        (this as Gen<Any>).doSomething(x)
    }
}

Если мы реализуем внутреннюю функцию, близкую к Gen, то есть нас, как правило, устраивает непроверяемое приведение типов, но мы хотели бы ограничить нетипизированный код только началом функции, а затем следовать типобезопасным кодом, мы можем сделать универсальная функция и используйте T как «динамический тип»:

fun <T : Any> m(g: Gen<*>, x: T) {
    if (g.token == x::class) {
        @Suppress("UNCHECKED_CAST")
        g as Gen<T>
        g.doSomething(x)
        g.doSomething(8) // compile error
    }
}

Если у нас есть несколько функций, потребляющих или возвращающих X, и нам нужна утилита для переключения из среды нетипизированного кода в типизированную, мы можем сделать это:

fun main() {
    val g: Gen<*> = Gen(String::class)
    g.asTypedOrNull<String>()?.doSomething("hello") // executes
    g.asTypedOrNull<Int>()?.doSomething(42) // doesn't execute
}

@Suppress("UNCHECKED_CAST")
inline fun <reified T : Any> Gen<*>.asTypedOrNull(): Gen<T>? = (this as Gen<T>).takeIf { token == T::class }

Обратите внимание, что в отличие от других примеров, в приведенном выше примере используется тип времени компиляции, а не тип времени выполнения.

Если нам нужно основывать логику на типах времени выполнения, мы могли бы подумать об утилите, которая принимает лямбда-выражение и выполняет его условно, передавая Gen и x соответствующих типов. Однако я не знаю, как представить такой логин в Котлине. Это должно быть что-то похожее на:

fun main() {
    val g: Gen<*> = Gen(String::class)
    g.ifMatches("hello") { doSomething(it) } // executes
    g.ifMatches(42) { doSomething(it) } // doesn't execute
}

inline fun Gen<*>.ifMatches(x: Any, block: <T> Gen<T>.(T) -> Unit): Unit { // syntax error
    if (token == x::class) {
        @Suppress("UNCHECKED_CAST")
        (this as Gen<T>).block(x as T)
    }
}

Но, очевидно, это не компилируется. Мы не можем сделать весь ifMatches универсальным, потому что таким образом вызывающий элемент управляет T, а это не то, что нам нужно.

Спасибо за объяснения. Это позволило мне подумать дальше и уточнить цели моего вопроса, см. «Уточнение».

Werner Thumann 24.05.2024 20:50

Ааа, вы правы, последний пример неверен. Я удалю это из своего ответа.

broot 24.05.2024 22:52

Отлично, благодаря нашему взаимодействию, думаю, я нашел для себя решение, см. мой ответ. Если вы увидите дальнейшие улучшения, дайте мне знать.

Werner Thumann 25.05.2024 11:44
Ответ принят как подходящий

Думаю, благодаря Бруту я нашел решение, которое искал.

interface TypedCode {
    fun <T : Any> Gen<T>.invoke(value: T) {
}

fun Gen<*>.runIfMatches(value: Any, code: TypedCode) {
    if (token == value::class) {
        this as Gen<Any>
        with(code) { invoke(value) }
    } 
}

fun main() {
    val g: Gen<*> = Gen(String::class)
    val a: Any = "hello"
    g.runIfMatches(a, object : TypedCode {
        override fun <T : Any> Gen<T>.invoke(value: T) {
            doSomething(value) // compiles and executes
        }
    })
    g.runIfMatches(a, object : TypedCode {
        override fun <T : Any> Gen<T>.invoke(value: T) {
            doSomething(8) // does not compile
        }
    })
    g.runIfMatches(8, object : TypedCode {
        override fun <T : Any> Gen<T>.invoke(value: T) {
            doSomething(value) // compiles but does not execute
        }
    })
}

Функция runIfMatches инкапсулирует гарантии, предоставляемые токеном типа среды выполнения, в (обязательно) небезопасный код. Бизнес-логика отделена от этого и работает в безопасной среде.

Синтаксически это не идеально, но, вероятно, это ограничение языка.

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