Любой способ наследовать от одного и того же универсального интерфейса дважды (с отдельными типами) в Kotlin?

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

interface Speaker<T> {
    fun talk(value: T)
}

class Multilinguist : Speaker<String>, Speaker<Float> {
    override fun talk(value: String) {
        println("greetings")
    }

    override fun talk(value: Float) {
        // Do something fun like transmit it along a serial port
    }
}

Котлин не доволен этим, ссылаясь на:

Type parameter T of 'Speaker' has inconsistent values: kotlin.String, kotlin.Float
A supertype appears twice

Я знаю, что одним из возможных решений является реализация следующего кода, где я реализую интерфейс с <Any> а затем сам проверяю типы и делегирую их функциям.

interface Speaker<T> {
    fun talk(value: T)
}

class Multilinguist : Speaker<Any> {
    override fun talk(value: Any) {
        when (value) {
            is String ->
                internalTalk(value)
            is Float ->
                internalTalk(value)
        } 
    }

    fun internalTalk(value: String) {
        println(value)
    }

    fun internalTalk(value: Float) {
        // Do something fun like transmit it along a serial port
    }
}

Тем не менее, мне кажется, что я убираю безопасность типов и общение о том, для чего используется класс, и в будущем я хочу решить проблему. Есть ли лучший способ реализовать это в Kotlin? Кроме того - в чем причина того, что это не разрешено так, как я указал в первом примере? Разве интерфейсы - это не просто контракт подписей, которые я должен внедрить, или я что-то упускаю из-за использования дженериков здесь?

1 ответ

Решение

Да, вам не хватает важной детали реализации обобщений в JVM: стирание типов. В двух словах, скомпилированный байт-код классов на самом деле не содержит никакой информации об универсальных типах (за исключением некоторых метаданных о том, что класс или метод являются универсальными). Вся проверка типов происходит во время компиляции, и после этого в коде не сохраняются универсальные типы, просто Object,

Чтобы обнаружить проблему в вашем случае, просто посмотрите на байт-код (в IDEA, Tools -> Kotlin -> Show Kotlin Bytecodeили любой другой инструмент). Давайте рассмотрим этот простой пример:

interface Converter<T> {
    fun convert(t: T): T
}

class Reverser(): Converter<String> {
    override fun convert(t: String) = t.reversed()
}

В байт-коде Converter универсальный тип стирается:

// access flags 0x401
// signature (TT;)TT;
// declaration: T convert(T)
public abstract convert(Ljava/lang/Object;)Ljava/lang/Object;

И вот методы, найденные в байт-коде Reverser:

// access flags 0x1
public convert(Ljava/lang/String;)Ljava/lang/String;
    ...

// access flags 0x1041
public synthetic bridge convert(Ljava/lang/Object;)Ljava/lang/Object;
    ...
    INVOKEVIRTUAL Reverser.convert (Ljava/lang/String;)Ljava/lang/String;
    ...

Наследовать Converter интерфейс, Reverser должен иметь метод с той же сигнатурой, то есть стертый тип. Если фактический метод реализации имеет другую подпись, добавляется метод моста. Здесь мы видим, что вторым методом в байт-коде является именно метод моста (и он вызывает первый).

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

Более того, если бы это было возможно, ни у Java, ни у Kotlin нет перегрузки методов, основанной на типе возвращаемого значения, и иногда в аргументах также будет неоднозначность, поэтому множественное наследование будет весьма ограниченным.

Однако с Project Valhalla все изменится (реализованные дженерики сохранят фактический тип во время выполнения), но все же я не ожидаю множественного наследования универсального интерфейса.

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