Generalmente, un parametro di tipo covariante è uno che può variare in base alla sottotipo della classe (in alternativa, variare con il sottotipo, da cui il prefisso "co-"). Più concretamente:
trait List[+A]
List[Int]è un sottotipo di List[AnyVal]poiché Intè un sottotipo di AnyVal. Ciò significa che è possibile fornire un'istanza di List[Int]quando List[AnyVal]è previsto un valore di tipo . Questo è davvero un modo molto intuitivo per far funzionare i generici, ma si scopre che è insensato (rompe il sistema dei tipi) se usato in presenza di dati mutabili. Questo è il motivo per cui i generici sono invarianti in Java. Breve esempio di instabilità nell'uso di array Java (che sono erroneamente covarianti):
Object[] arr = new Integer[1];
arr[0] = "Hello, there!";
Abbiamo appena assegnato un valore di tipo Stringa un array di tipo Integer[]. Per ragioni che dovrebbero essere ovvie, questa è una cattiva notizia. Il sistema di tipi Java in realtà lo consente al momento della compilazione. La JVM lancerà "utile" ArrayStoreExceptionin fase di esecuzione. Il sistema di tipi di Scala evita questo problema perché il parametro type sulla Arrayclasse è invariante (dichiarazione [A]invece che [+A]).
Si noti che esiste un altro tipo di varianza noto come contravarianza . Questo è molto importante perché spiega perché la covarianza può causare alcuni problemi. La contraddizione è letteralmente l'opposto della covarianza: i parametri variano verso l'alto con il sottotipo. È molto meno comune in parte perché è così controintuitivo, sebbene abbia un'applicazione molto importante: le funzioni.
trait Function1[-P, +R] {
def apply(p: P): R
}
Notare l' annotazione della varianza " - " sul Pparametro type. Questa dichiarazione nel suo insieme significa che Function1è contraddittoria Pe covariante in R. Pertanto, possiamo derivare i seguenti assiomi:
T1' <: T1
T2 <: T2'
---------------------------------------- S-Fun
Function1[T1, T2] <: Function1[T1', T2']
Si noti che T1'deve essere un sottotipo (o lo stesso tipo) di T1, mentre è l'opposto per T2e T2'. In inglese, questo può essere letto come segue:
Una funzione A è un sottotipo di un'altra funzione B se il tipo di parametro di A è un supertipo del tipo di parametro di B , mentre il tipo di ritorno di A è un sottotipo del tipo restituito di B .
Il motivo di questa regola è lasciato al lettore come esercizio (suggerimento: pensa a casi diversi in quanto le funzioni sono sottotitolate, come il mio esempio di matrice dall'alto).
Con la tua nuova conoscenza della co-e contravarianza, dovresti essere in grado di capire perché il seguente esempio non verrà compilato:
trait List[+A] {
def cons(hd: A): List[A]
}
Il problema è che Aè covariante, mentre la consfunzione prevede che il suo parametro di tipo sia invariante . Quindi, Asta variando la direzione sbagliata. È interessante notare che potremmo risolvere questo problema rendendoci Listcontroversi A, ma il tipo restituito non List[A]sarebbe valido poiché la consfunzione prevede che il suo tipo restituito sia covariante .
Le nostre uniche due opzioni qui sono: a) rendere Ainvarianti, perdendo le proprietà di digitazione di covarianza belle e intuitive, oppure b) aggiungere un parametro di tipo locale al consmetodo che definisce Aun limite inferiore:
def cons[B >: A](v: B): List[B]
Questo è ora valido. Potete immaginare che Astia cambiando verso il basso, ma Bè in grado di variare verso l'alto rispetto a Apoiché Aè il suo limite inferiore. Con questa dichiarazione del metodo, possiamo Aessere covarianti e tutto funziona.
Nota che questo trucco funziona solo se restituiamo un'istanza di Listcui è specializzato il tipo meno specifico B. Se si tenta di rendere Listmutevole, le cose si rompono poiché si finisce per provare ad assegnare valori di tipo Ba una variabile di tipo A, che non è consentita dal compilatore. Ogni volta che si ha una mutabilità, è necessario disporre di un mutatore di qualche tipo, che richiede un parametro di metodo di un certo tipo, che (insieme all'accessorio) implica invarianza. Covariance funziona con dati immutabili poiché l'unica operazione possibile è un accessor, a cui può essere assegnato un tipo di ritorno covariante.
varè impostabile mentrevalnon lo è. È lo stesso motivo per cui le collezioni immutabili di Scala sono covarianti ma non quelle mutabili.