2016-02-20 1 views
11

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

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 
    } 
} 

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

ответ

14

Да, вам не хватает важной детали реализации дженериков на JVM: the type erasure. В двух словах, скомпилированный байт-код классов фактически не содержит никакой информации об общих типах (за исключением некоторых метаданных о том, что класс или метод является общим). Вся проверка типов происходит во время компиляции, и после этого в коде не сохраняются общие типы, есть только 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 должен иметь способ с такая же подпись, то есть тип, стертый один. Если у фактического метода реализации есть другая подпись, добавляется bridge method. Здесь мы видим, что второй метод в байт-коде является именно мостовым методом (и он вызывает первый).

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

Кроме того, если бы это было возможно, ни Java, ни Kotlin has method overloading based on return value type, а также были бы неоднозначные аргументы, поэтому множественное наследование было бы весьма ограниченным.

Вещи, однако, будут меняться с Project Valhalla (reified generics сохранит фактический тип во время выполнения), но все же я не ожидал бы нескольких общих наследований интерфейсов.