2016-02-20 37 views
11

Mam scenariusz w moim kodu, w którym chciałbym klasę zaimplementować interfejs do dwóch odrębnych typów, jak w poniższym przykładzie:Jakikolwiek sposób odziedziczyć z tego samego interfejsu generycznego dwukrotnie (z oddzielnymi typami) w 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 
    } 
} 

Kotlin nie jest zadowolony z tego, cytując:

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

Wiem, że jednym z możliwych rozwiązań jest wdrożenie następującego kodu, w którym implementuję interfejs z <Any>, a następnie sprawdzam typy osobiście i deleguję je do ich funkcji.

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

jednak, że czuje się jakbym usunięcie typu bezpieczeństwa i komunikacji o co klasa jest używana do, i prosi o kłopoty w dół. Czy istnieje lepszy sposób na wdrożenie tego w Kotlin? Dodatkowo - co jest przyczyną tego, że nie jest dozwolone tak, jak wskazałem w pierwszej próbce? Czy interfejsy to nie tylko kontrakt z podpisami, które muszę wprowadzić, czy też jest coś, czego mi brakuje z udziałem generycznych?

Odpowiedz

14

Tak, brakuje jakiegoś ważnego szczegółu implementacji generycznych na JVM: the type erasure. W skrócie, skompilowany kod bajtowy klas w rzeczywistości nie zawiera żadnych informacji o typach ogólnych (z wyjątkiem niektórych metadanych dotyczących faktu, że klasa lub metoda ma charakter ogólny). Wszystkie sprawdzenia typu odbywają się w czasie kompilacji, a potem żadne typy ogólne nie zachowują się w kodzie, jest tylko Object.

Aby odkryć problem w twoim przypadku, po prostu spójrz na kod bajtowy (w IDEA, Tools -> Kotlin -> Show Kotlin Bytecode lub jakiekolwiek inne narzędzie). Rozważmy ten prosty przykład:

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

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

W kodu bajtowego z Converter typ rodzajowy jest wymazane:

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

A oto metody znalezione w kodu bajtowego z 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; 
    ... 

Aby odziedziczyć interfejs Converter, Reverser powinien mieć metodę z ten sam podpis, czyli typ wymazany. Jeśli aktualna metoda implementacji ma inny podpis, zostanie dodana bridge method. Tutaj widzimy, że druga metoda w kodzie bajtowym jest dokładnie metodą mostu (i wywołuje pierwszą).

Tak więc, wiele implementacji interfejsu ogólnego trywialnie kolidowałoby ze sobą, ponieważ może istnieć tylko jedna metoda mostkowa dla określonego podpisu.

Co więcej, gdyby było to możliwe, ani Java, ani Kotlin has method overloading based on return value type, a także niekiedy pojawiałyby się niejasności w argumentach, więc dziedziczenie wielokrotne byłoby dość ograniczone.

Rzeczy zmienią się jednak z Project Valhalla (rektywiści będą zachowywali rzeczywisty typ w środowisku wykonawczym), ale nadal nie oczekuję wielokrotnego dziedziczenia interfejsu.