Как правило, параметр ковариантного типа - это параметр, который может изменяться в зависимости от подтипа класса (альтернативно, варьироваться в зависимости от подтипа, отсюда и префикс «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 являются ковариантными, а изменяемые - нет.