Ответ 1
В общем случае параметр ковариантного типа - это тот, который разрешается изменять, поскольку класс подтипирован (в качестве альтернативы, изменяется с подтипированием, следовательно, префикс "ко-префикс" ). Более конкретно:
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 предотвращает эту проблему, поскольку параметр type в классе 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) сделать инвариант A
, потерять приятные, интуитивные свойства типизации ковариации, или b) добавить параметр локального типа в метод cons
, который определяет A
как нижняя граница:
def cons[B >: A](v: B): List[B]
Теперь это действует. Вы можете себе представить, что A
меняется вниз, но B
может меняться вверх относительно A
, так как A
является его нижней границей. С этим объявлением метода мы можем иметь A
быть ковариантным и все получится.
Обратите внимание, что этот трюк работает только в том случае, если мы возвращаем экземпляр List
, который специализируется на менее конкретном типе B
. Если вы пытаетесь сделать List
изменчивым, все ломается, поскольку вы пытаетесь присвоить значения типа B
переменной типа A
, которая не разрешена компилятором. Всякий раз, когда у вас есть изменчивость, вам нужно иметь какой-то мутатор, для которого требуется параметр метода определенного типа, который (вместе с аксессуаром) подразумевает инвариантность. Ковариация работает с неизменяемыми данными, поскольку единственной возможной операцией является аксессор, которому может быть присвоен ковариантный тип возврата.