Как правило, параметр ковариантного типа - это параметр, который может изменяться в зависимости от подтипа класса (альтернативно, варьироваться в зависимости от подтипа, отсюда и префикс «co»). Более конкретно:
trait List[+A]
List[Int]
является подтипом, List[AnyVal]
потому что Int
является подтипом AnyVal
. Это означает, что вы можете предоставить случай, List[Int]
когда List[AnyVal]
ожидается значение типа . Это действительно очень интуитивный способ работы генериков, но оказывается, что он неэффективен (нарушает систему типов) при использовании в присутствии изменяемых данных. Вот почему дженерики инвариантны в Java. Краткий пример несостоятельности с использованием массивов Java (которые ошибочно ковариантны):
Object[] arr = new Integer[1];
arr[0] = "Hello, there!";
Мы просто присвоили значение типа String
массиву типов Integer[]
. По причинам, которые должны быть очевидны, это плохие новости. Система типов Java фактически позволяет это во время компиляции. JVM «услужливо» сгенерирует ArrayStoreException
во время выполнения. Система типов Scala предотвращает эту проблему, потому что параметр типа в Array
классе является инвариантным ( [A]
вместо объявления [+A]
).
Обратите внимание, что существует другой тип дисперсии, известный как контравариантность . Это очень важно, поскольку объясняет, почему ковариация может вызвать некоторые проблемы. Контрвариация буквально противоположность ковариации: параметры изменяются вверх с подтипами. Это гораздо реже, отчасти потому, что оно настолько нелогично, хотя у него есть одно очень важное приложение: функции.
trait Function1[-P, +R] {
def apply(p: P): R
}
Обратите внимание на аннотацию " - " для P
параметра типа. Это объявление в целом означает, что Function1
является контравариантным в P
и ковариантным в R
. Таким образом, мы можем вывести следующие аксиомы:
T1' <: T1
T2 <: T2'
---------------------------------------- S-Fun
Function1[T1, T2] <: Function1[T1', T2']
Обратите внимание, что это T1'
должен быть подтип (или тот же тип) T1
, тогда как для T2
и T2'
. На английском языке это можно прочитать следующим образом:
Функция является подтипом другой функции B , если типа параметра А является супертипом типа параметра В то время как тип возвращаемого А является подтипом типа возвращаемого B .
Причина этого правила оставлена читателю в качестве упражнения (подсказка: подумайте о разных случаях, когда функции имеют подтипы, как в примере с моим массивом выше).
С вашими новыми знаниями о со-и контравариантности вы сможете понять, почему следующий пример не скомпилируется:
trait List[+A] {
def cons(hd: A): List[A]
}
Проблема в том, что A
она ковариантна, тогда как cons
функция ожидает, что ее параметр типа будет инвариантным . Таким образом, A
меняется неправильное направление. Интересно, что мы могли бы решить эту проблему, сделав ее List
контравариантной A
, но тогда возвращаемый тип List[A]
был бы недействительным, поскольку cons
функция ожидает, что ее возвращаемый тип будет ковариантным .
Здесь есть только два варианта: а) сделать A
инвариант, потеряв приятные, интуитивно понятные свойства ковариации подтипирования, или б) добавить параметр локального типа в cons
метод, который определяет A
как нижнюю границу:
def cons[B >: A](v: B): List[B]
Теперь это действительно. Вы можете себе представить , что A
в той или иной вниз, но B
может изменяться вверх относительно , A
так как A
это его нижняя граница. С этим объявлением метода мы можем A
быть ковариантными, и все работает.
Обратите внимание, что этот прием работает только в том случае, если мы возвращаем экземпляр List
, специализирующийся на менее конкретном типе B
. Если вы попытаетесь сделать List
изменчивым, все пойдет не так, как вы пытаетесь присвоить значения типа B
переменной типа A
, что запрещено компилятором. Всякий раз, когда у вас есть изменчивость, вам нужен какой-то мутатор, для которого требуется параметр метода определенного типа, который (вместе с аксессором) подразумевает инвариантность. Covariance работает с неизменяемыми данными, поскольку единственной возможной операцией является метод доступа, которому может быть задан ковариантный тип возврата.
var
что устанавливается, аval
нет. Это та же самая причина, по которой неизменные коллекции scala являются ковариантными, а изменяемые - нет.