например. почему
val list:List[Any] = List[Int](1,2,3)
работает, но
val arr:Array[Any] = Array[Int](1,2,3)
не работает (поскольку массивы являются инвариантными). Каков желаемый эффект от этого дизайнерского решения?
например. почему
val list:List[Any] = List[Int](1,2,3)
работает, но
val arr:Array[Any] = Array[Int](1,2,3)
не работает (поскольку массивы являются инвариантными). Каков желаемый эффект от этого дизайнерского решения?
Потому что иначе это нарушит безопасность типов. Если нет, вы сможете сделать что-то вроде этого:
val arr:Array[Int] = Array[Int](1,2,3)
val arr2:Array[Any] = arr
arr2(0) = 2.54
и компилятор не может его поймать.
С другой стороны, списки являются неизменяемыми, поэтому вы не можете добавить то, что не является Int
Это потому, что списки неизменяемы, а массивы изменяемы.
Разница в том, что List
являются неизменяемыми, а Array
являются изменяемыми.
Чтобы понять, почему изменчивость определяет дисперсию, подумайте о создании измененной версии List
- позвоните ей MutableList
. Мы также будем использовать некоторые типы примеров: базовый класс Animal
и 2 подкласса с именем Cat
и Dog
.
trait Animal {
def makeSound: String
}
class Cat extends Animal {
def makeSound = "meow"
def jump = // ...
}
class Dog extends Animal {
def makeSound = "bark"
}
Обратите внимание, что Cat
имеет еще один метод (jump
), чем Dog
.
Затем определите функцию, которая принимает измененный список животных и изменяет список:
def mindlessFunc(xs: MutableList[Animal]) = {
xs += new Dog()
}
Теперь ужасные вещи произойдут, если вы передадите список кошек в функцию:
val cats = MutableList[Cat](cat1, cat2)
val horror = mindlessFunc(cats)
Если бы мы использовали небрежный язык программирования, во время компиляции это будет игнорироваться. Тем не менее, наш мир не рухнет, если мы получим доступ только к списку кошек, используя следующий код:
cats.foreach(c => c.makeSound)
Но если мы это сделаем:
cats.foreach(c => c.jump)
Произойдет ошибка во время выполнения. С Scala запись такого кода предотвращается, потому что компилятор будет жаловаться.
Обычный ответ, который следует дать, заключается в том, что изменчивость в сочетании с ковариацией приведет к нарушению безопасности типов. Для коллекций это можно рассматривать как фундаментальную истину. Но теория действительно применима к любому родовому типу, а не только к коллекциям типа List
и Array
, и нам не нужно вообще пытаться рассуждать о изменчивости.
Реальный ответ связан с тем, как типы функций взаимодействуют с подтипированием. Короче говоря, если параметр типа используется как возвращаемый тип, он ковариантен. С другой стороны, если параметр типа используется как тип аргумента, он контравариантен. Если он используется как возвращаемый тип и как тип аргумента, он является инвариантным.
Посмотрите на документацию для Array[T]
. Два очевидных метода для поиска - для поиска и обновления:
def apply(i: Int): T
def update(i: Int, x: T): Unit
В первом методе T
есть возвращаемый тип, а во втором T
- тип аргумента. Правила дисперсии диктуют, что T
должен быть инвариантным.
Мы можем сравнить документацию для List[A]
, чтобы понять, почему она ковариантна. Смутно мы находим эти методы, которые аналогичны методам Array[T]
:
def apply(n: Int): A
def ::(x: A): List[A]
Так как A
используется как возвращаемый тип, а как тип аргумента, мы ожидаем, что A
будет инвариантным, так же как T
для Array[T]
. Однако, в отличие от Array[T]
, документация лежит нам о типе ::
. Ложь достаточно хороша для большинства вызовов этого метода, но недостаточно хороша, чтобы решить дисперсию A
. Если мы расширим документацию для этого метода и нажмем "Полная подпись", нам будет показана правда:
def ::[B >: A](x: B): List[B]
Итак, A
фактически не отображается как тип аргумента. Вместо этого B
(который может быть любым супертипом A
) является типом аргумента. Это не помещает никаких ограничений на A
, так что это действительно может быть ковариантным. Любой метод на List[A]
, который имеет A
как тип аргумента, является аналогичной ложью (мы можем сказать, потому что эти методы отмечены как [use case]
).