Любой способ наследовать от одного и того же универсального интерфейса дважды (с отдельными типами) в 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 все изменится (реализованные дженерики сохранят фактический тип во время выполнения), но все же я не ожидаю множественного наследования универсального интерфейса.