De manière générique, un paramètre de type covariant est un paramètre qui peut varier vers le bas lorsque la classe est sous-typée (en variante, varier avec le sous-typage, d'où le préfixe "co-"). Plus concrètement:
trait List[+A]
List[Int]
est un sous-type de List[AnyVal]
parce que Int
est un sous-type de AnyVal
. Cela signifie que vous pouvez fournir une instance de List[Int]
quand une valeur de type List[AnyVal]
est attendue. C'est vraiment un moyen très intuitif pour les génériques de fonctionner, mais il s'avère qu'il n'est pas sain (casse le système de types) lorsqu'il est utilisé en présence de données mutables. C'est pourquoi les génériques sont invariants en Java. Bref exemple de dysfonctionnement à l'aide de tableaux Java (qui sont à tort covariants):
Object[] arr = new Integer[1];
arr[0] = "Hello, there!";
Nous venons d'attribuer une valeur de type String
à un tableau de type Integer[]
. Pour des raisons qui devraient être évidentes, ce sont de mauvaises nouvelles. Le système de types de Java permet en fait cela au moment de la compilation. La JVM lancera "utilement" un ArrayStoreException
at runtime. Le système de type de Scala évite ce problème car le paramètre de type de la Array
classe est invariant (la déclaration est [A]
plutôt que [+A]
).
Notez qu'il existe un autre type de variance appelé contravariance . Ceci est très important car cela explique pourquoi la covariance peut causer certains problèmes. La contravariance est littéralement l'opposé de la covariance: les paramètres varient à la hausse avec le sous-typage. C'est beaucoup moins courant en partie parce qu'il est tellement contre-intuitif, bien qu'il ait une application très importante: les fonctions.
trait Function1[-P, +R] {
def apply(p: P): R
}
Notez l' annotation de variance " - " sur le P
paramètre de type. Cette déclaration dans son ensemble signifie qu'elle Function1
est contravariante dans P
et covariante dans R
. Ainsi, nous pouvons dériver les axiomes suivants:
T1' <: T1
T2 <: T2'
---------------------------------------- S-Fun
Function1[T1, T2] <: Function1[T1', T2']
Notez qu'il T1'
doit s'agir d'un sous-type (ou du même type) de T1
, alors que c'est l'inverse pour T2
et T2'
. En anglais, cela peut se lire comme suit:
Une fonction A est un sous - type d' une autre fonction B si le type de paramètre A est un supertype du type de paramètre de B pendant que le type de retour de A est un sous - type du type de retour de B .
La raison de cette règle est laissée comme exercice au lecteur (indice: pensez à différents cas car les fonctions sont sous-typées, comme mon exemple de tableau ci-dessus).
Avec vos nouvelles connaissances sur la co- et la contravariance, vous devriez être en mesure de voir pourquoi l'exemple suivant ne se compilera pas:
trait List[+A] {
def cons(hd: A): List[A]
}
Le problème est que A
c'est covariant, alors que la cons
fonction s'attend à ce que son paramètre de type soit invariant . Ainsi, A
fait varier la mauvaise direction. Il est intéressant de noter que nous pourrions résoudre ce problème en rendant List
contravariant dans A
, mais le type de retour List[A]
serait alors invalide car la cons
fonction s'attend à ce que son type de retour soit covariant .
Nos deux seules options ici sont: a) rendre A
invariant, en perdant les propriétés de sous-typage intuitives et intéressantes de la covariance, ou b) ajouter un paramètre de type local à la cons
méthode qui définit A
comme une limite inférieure:
def cons[B >: A](v: B): List[B]
Ceci est maintenant valide. Vous pouvez imaginer que cela A
varie vers le bas, mais B
est capable de varier à la hausse par rapport à A
puisque A
c'est sa limite inférieure. Avec cette déclaration de méthode, nous pouvons A
être covariants et tout fonctionne.
Notez que cette astuce ne fonctionne que si nous retournons une instance List
dont est spécialisée sur le type moins spécifique B
. Si vous essayez de rendre List
mutable, les choses échouent puisque vous finissez par essayer d'attribuer des valeurs de type B
à une variable de type A
, ce qui est interdit par le compilateur. Chaque fois que vous avez une mutabilité, vous devez avoir un mutateur quelconque, qui nécessite un paramètre de méthode d'un certain type, ce qui (avec l'accesseur) implique l'invariance. La covariance fonctionne avec des données immuables puisque la seule opération possible est un accesseur, auquel un type de retour covariant peut être attribué.
var
c'est réglable alors queval
ne l'est pas. C'est la même raison pour laquelle les collections immuables de scala sont covariantes, mais les collections mutables ne le sont pas.