De manière générique, un covariant paramètre de type est celui qui est autorisé à varier en bas comme la classe est sous-typés (vous pouvez aussi varier avec le sous-typage, d'où le "co-" préfixe). Plus concrètement:
trait List[+A]
List[Int]
est un sous-type d' List[AnyVal]
car Int
est un sous-type d' AnyVal
. Cela signifie que vous pouvez fournir une instance de List[Int]
lorsqu'une valeur de type List[AnyVal]
est attendue. C'est vraiment une façon très intuitive pour les génériques de travail, mais il s'avère que c'est malsain (pauses le type de système) lorsqu'ils sont utilisés en présence de données mutable. C'est pourquoi les génériques sont des constantes en Java. Bref exemple de troubles à l'aide de Java les tableaux (qui sont à tort covariants):
Object[] arr = new Integer[1];
arr[0] = "Hello, there!";
Nous venons de recevoir une valeur de type String
d'un tableau de type Integer[]
. Pour des raisons qui devraient être évidentes, ce sont de mauvaises nouvelles. Java du type de système permet effectivement présent au moment de la compilation. La JVM va "utilement" jeter un ArrayStoreException
lors de l'exécution. Scala type de système évite ce problème parce que le paramètre type sur l' Array
classe est invariante (déclaration est - [A]
plutôt que d' [+A]
).
Notez qu'il existe un autre type de variance connue comme la contravariance. Ceci est très important car il explique pourquoi la covariance peut causer quelques problèmes. La Contravariance est littéralement à l'opposé de la covariance: paramètres varient à la hausse avec le sous-typage. Il est beaucoup moins fréquent en partie parce qu'il est tellement contre-intuitif, mais il en a une très importante demande: les fonctions.
trait Function1[-P, +R] {
def apply(p: P): R
}
Notez le "-" annotation de variance sur l' P
paramètre de type. Cette déclaration comme un ensemble de moyens qu' Function1
est contravariant en P
et covariants en R
. Ainsi, on peut obtenir les axiomes suivants:
T1' <: T1
T2 <: T2'
---------------------------------------- S-Fun
Function1[T1, T2] <: Function1[T1', T2']
Notez que T1'
doit être un sous-type (ou le même type) T1
, alors que c'est l'inverse pour T2
et T2'
. En anglais, cela peut être lu comme suit:
Une fonction Un est un sous-type d'une autre fonction B si le paramètre de type de Un est un supertype du paramètre type de B alors que le type de retour de l' Un est un sous-type du type de retour de B.
La raison de cette règle est laissé comme exercice au lecteur (indice: une réflexion sur les différents cas que les fonctions sont sous-typés, comme mon exemple du tableau ci-dessus).
Avec votre nouvelle connaissance de la covariance et la contravariance, vous devriez être en mesure de voir pourquoi l'exemple suivant ne compile pas:
trait List[+A] {
def cons(hd: A): List[A]
}
Le problème est qu' A
est covariant, tandis que l' cons
fonction attend son type de paramètre à contravariant. Ainsi, A
est variable dans la mauvaise direction. Fait intéressant, nous avons pu résoudre ce problème en List
contravariant en A
, mais ensuite le type de retour List[A]
serait nulle, comme l' cons
fonction attend son type de retour covariantes.
Nos seules deux options sont a) de rendre A
invariant, la perte de l'agréable, intuitive sous-typage des propriétés de la covariance, ou b) ajouter un local type de paramètre à l' cons
méthode qui définit A
comme limite inférieure:
def cons[B >: A](v: B): List[B]
C'est maintenant valide. Vous pouvez imaginer qu' A
est variable à la baisse, mais B
est capable de faire varier à la hausse à l'égard A
depuis A
est sa limite inférieure. Avec cette déclaration de méthode, nous pouvons avoir l' A
être covariantes et tout fonctionne.
Notez que cette astuce ne fonctionne que si nous retourner une instance de List
qui est spécialisé sur les moins-type B
. Si vous essayez de faire de l' List
mutables, les choses tomber en panne, car en fin de compte vous essayez d'affecter des valeurs de type B
pour une variable de type A
, ce qui est rejetée par le compilateur. Chaque fois que vous avez la mutabilité, vous devez disposer d'un mutateur, ce qui nécessite un paramètre d'une méthode, d'un certain type, qui (avec l'accesseur) implique l'invariance. La Covariance travaille avec des données immuables depuis la seule opération est un accesseur, qui peut être donné un covariant type de retour.