Что такое variance (+A, -A) и зачем она нужна?
Вариантность (variance) — это концепция, описывающая, как параметризованные типы ведут себя при наследовании. В Scala она используется для управления совместимостью обобщённых типов (generic types), когда один тип является подтипом другого. Вариантность влияет на то, можно ли использовать, скажем, List[Dog] там, где ожидается List[Animal].
В Scala есть три вида вариантности:
1. Ковариантность (+A)
Ковариантность означает: если Dog — подтип Animal, то Box[Dog] — подтип Box[Animal].
Это полезно, когда параметр типа используется только для чтения, и экземпляр не должен модифицировать объект типа A.
Пример:
class Animal
class Dog extends Animal
class Cage\[+A\](val animal: A)
val dogCage: Cage\[Dog\] = new Cage(new Dog)
val animalCage: Cage\[Animal\] = dogCage // OK
Таким образом, ковариантность позволяет передавать Cage[Dog] туда, где ожидается Cage[Animal].
Важно: ковариантные типы не могут иметь var-поля с типом A, потому что это может привести к нарушению типобезопасности. Только val допустим.
2. Контрвариантность (-A)
Контрвариантность означает: если Dog — подтип Animal, то Handler[Animal] — подтип Handler[Dog].
Это используется, когда параметр типа применяется только для записи, например, в функциях, которые принимают значения, но не возвращают их.
Пример:
class Animal
class Dog extends Animal
class Handler\[-A\] {
def handle(animal: A): Unit = println(animal)
}
val animalHandler: Handler\[Animal\] = new Handler\[Animal\]
val dogHandler: Handler\[Dog\] = animalHandler // OK
Контрвариантность особенно полезна для функциональных аргументов. Например, обработчики событий, логеры, валидаторы и другие объекты, которые "потребляют" значения.
3. Инвариантность (без + или -)
Если параметр типа A указан без + или -, то тип Box[Dog] не совместим с Box[Animal], даже если Dog подтип Animal.
Пример:
class Box\[A\](val value: A)
val dogBox: Box\[Dog\] = new Box(new Dog)
val animalBox: Box\[Animal\] = dogBox // Ошибка компиляции
Это наиболее безопасное поведение, применяемое по умолчанию, чтобы избежать нежелательных последствий при комбинировании чтения и записи.
4. Почему это важно
Представим следующую ситуацию:
class Animal
class Dog extends Animal
class Cat extends Animal
class Cage\[+A\](val a: A)
def putCatInCage(cage: Cage\[Cat\]) = ???
val dogCage: Cage\[Dog\] = new Cage(new Dog)
val animalCage: Cage\[Animal\] = dogCage // OK, если +A
putCatInCage(animalCage) // Проблема!
Если Cage ковариантен, то Cage[Dog] можно подставить туда, где ожидается Cage[Animal], и тогда кто-то может положить туда Cat, а это нарушает ожидания.
Поэтому вариантность помогает сохранить типобезопасность при наследовании и обобщении.
5. Функции и вариантность
Функции в Scala — особый случай. Их сигнатура:
A => B
-
Аргумент A — контрвариантен (-A)
-
Результат B — ковариантен (+B)
Это логично: если функция может принимать Animal, то она точно справится с Dog, а если она возвращает Dog, это допустимо там, где ожидается Animal.
6. Как выбрать нужную вариантность при проектировании классов
-
Если ваш тип только читает значения параметра типа (например, коллекции, обработчики событий), скорее всего, подойдёт ковариантность +A.
-
Если ваш тип только пишет значения параметра (например, логгеры, буферы записи), вероятно, нужна контрвариантность -A.
-
Если ваш тип и читает, и пишет, либо изменяет значение параметра типа — используйте инвариантность (никакого + или -).
7. Ограничения
Scala позволяет использовать вариантность только в положениях, где она безопасна. Компилятор анализирует, где используется параметр типа — на стороне входа (контрвариантно) или выхода (ковариантно), и запрещает использование варианта в противоположной позиции.
Например:
class Box\[+A\](var value: A) // Ошибка: нельзя var с ковариантным типом
8. Вариантность в стандартной библиотеке
-
List[+A] — ковариантна
-
Function1[-A, +B] — контрвариантна по входу, ковариантна по выходу
-
Option[+A] — ковариантна
-
Array[A] — инвариантна, потому что позволяет изменять элементы
Таким образом, вариантность в Scala — это мощный инструмент типобезопасности и обобщённого проектирования. Понимание её принципов важно при работе с коллекциями, API-интерфейсами и функциями высшего порядка.