Обычно параметр типа ковариантный - это параметр, который может изменяться в зависимости от подтипа класса (альтернативно, варьироваться в зависимости от подтипа, отсюда и префикс "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'
он является противоположным. На английском языке это можно прочитать следующим образом:
Функция A является подтипом другой функции B , если тип параметра A является супертипом типа параметра B , в то время как тип возврата A является подтипом типа возврата 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 работает с неизменяемыми данными, поскольку единственной возможной операцией является метод доступа, которому может быть задан ковариантный тип возврата.