2016-02-19 14 views
7

Sto provando a scrivere una funzione che confronta due elenchi di comparabili. I comparables possono essere di vari tipi purché gli elementi nelle stesse posizioni nelle due liste da confrontare siano confrontabili. Esempio:Confronto di elenchi comparabili in Kotlin

val list1 = ArrayList<Comparable<*>>() 
    val list2 = ArrayList<Comparable<*>>() 

    list1.add(10) 
    list1.add("xyz") 
    list1.add('a') 

    list2.add(10) 
    list2.add("xyz") 
    list2.add('b') 

    println(compare(list1, list2)) 

Questo dovrebbe stampare -1 perché

  • 10 == 10
  • "xyz" == "xyz"
  • 'a' < 'b'

e quindi lista1 < lista2.

Ecco il codice che ho messo insieme con un po 'di un processo di tentativi ed errori visto che sono un po' confuso su come farmaci generici funzionano in questo caso specifico:

fun <T> compare(list1: List<Comparable<T>>, list2: List<Comparable<T>>): Int { 
    for (i in 0..Math.max(list1.size, list2.size) - 1) { 
     val elem1 = if (i < list1.size) list1[i] else null 
     val elem2 = if (i < list2.size) list2[i] else null 

     if (elem1 == null && elem2 == null) 
      return 0 

     if (elem1 == null) 
      return -1 

     if (elem2 == null) 
      return 1 

     @Suppress("UNCHECKED_CAST") 
     val comparisonResult = elem1.compareTo(elem2 as T) 

     if (comparisonResult != 0) 
      return comparisonResult 
    } 

    return 0 
} 

E questo in realtà si compila e funziona come previsto, ma ci sono alcune cose di cui sono perplesso.

Il mio primo tentativo è stato con la seguente firma del metodo:

fun compare(list1: List<Comparable<*>>, list2: List<Comparable<*>>): Int 

Questo non ha però compilare. Perché? E come si differenzia questa dichiarazione dall'altra?

In secondo luogo, se provo a confrontare elenchi con valori incomparabili nelle posizioni corrispondenti, viene visualizzato un errore di tipo cast. Ad esempio, quando si confrontano [1,1] a [1, "abc"], ottengo

java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer 

Questo si pone apparentemente tipo colato in

elem1.compareTo(elem2 as T) 

cosa mi lascia perplesso: Come è il T risolto con Intero qui? In effetti, sono sorpreso che questo in realtà compili.

E terzo, c'è un modo per sbarazzarsi del cast incontrollato? Ho provato

if (elem2 !is T) 
    // throw Exception 

ma che non è stato compilato. Perché? Sembra che in qualche modo si sappia che T è pensato per essere un numero intero in questa iterazione, quindi perché non riesco a digitare il controllo su di esso?

risposta

9

Comparable è un'interfaccia controvariante per il suo parametro tipo T. I valori di tipo T sono consentiti solo a in -posizioni, ovvero come parametri dei metodi di classe e non come valori di ritorno.

interface Comparable<in T> { 
    abstract operator fun compareTo(other: T): Int 
} 

Una stella-proiezione di tipo controvariante è equivalente a quel tipo parametrizzate con Nothing, così Comparable<*> è in realtà un Comparable<in Nothing>. Significa che una volta che si ha un paragone di tipo sconosciuto, non è possibile confrontarlo in modo sicuro con qualsiasi cosa eccetto il valore del tipo Nothing, che è noto per non avere valori.:)

È possibile riscontrare le conseguenze di tale mancanza di sicurezza se si tenta di confrontare un Int con un String. Non è il elem2 as T lancia ClassCastException (è davvero un cast non controllato come l'avvertenza che hai soppresso stati), è l'implementazione di String.compareTo che lancia, quando incontra qualcosa che non è un String.

Tornando alla domanda, è possibile implementare tale confronto di elenchi con l'aiuto della funzione libreria kotlin.comparisons.compareValues. Sa come gestire i null e nasconde il brutto cast incontrollato all'interno.

import kotlin.comparisons.* 

fun compareLists(list1: List<Comparable<*>>, list2: List<Comparable<*>>): Int { 
    for (i in 0..Math.min(list1.size, list2.size)-1) { 
     val elem1 = list1[i] 
     val elem2 = list2[i] 

     if (elem1.javaClass != elem2.javaClass) { 
      TODO("Decide what to do when you encounter values of different classes") 
     } 

     compareValues(elem1, elem2).let { 
      if (it != 0) return it 
     } 
    } 
    return compareValues(list1.size, list2.size) 
} 

nota, a causa del tipo di cancellazione in generici assicurando valori hanno la stessa classe (elem1.javaClass == elem2.javaClass) non significa necessariamente i valori possono essere confrontati con sicurezza. Ad esempio List<Int> e List<String> entrambi hanno la stessa classe List.

+0

Grazie mille, Ilya, per la spiegazione dettagliata e una soluzione più elegante. Molto utile! –