Вариантность (Variances) - это указание определенной специфики взаимосвязи между связанными типами. Scala поддерживает вариантную аннотацию типов у обобщенных классов, что позволяет им быть ковариантными, контрвариантными или инвариантными (если нет никакого указания на вариантность). Использование вариантности в системе типов позволяет устанавливать понятные взаимосвязи между сложными типами, в то время как отсутствие вариантности может ограничить повторное использование абстракции класса.
class Foo[+A] // ковариантный класс
class Bar[-A] // контравариантный класс
class Baz[A] // инвариантный класс
Инвариантность
По умолчанию параметры типа в Scala инвариантны: отношения подтипа между параметрами типа не отражаются в параметризованном типе. Чтобы понять, почему это работает именно так, рассмотрим простой параметризованный тип, изменяемый контейнер.
class Box[A](var content: A)
Мы собираемся поместить в него значения типа Animal
(животное). Этот тип определяется следующим образом:
abstract class Animal {
def name: String
}
case class Cat(name: String) extends Animal
case class Dog(name: String) extends Animal
abstract class Animal:
def name: String
case class Cat(name: String) extends Animal
case class Dog(name: String) extends Animal
Можно сказать, что Cat
(кот) - это подтип Animal
, Dog
(собака) - также подтип Animal
.
Это означает, что следующее допустимо и пройдет проверку типов:
val myAnimal: Animal = Cat("Felix")
А контейнеры?
Является ли Box[Cat]
подтипом Box[Animal]
, как Cat
подтип Animal
?
На первый взгляд может показаться, что это правдоподобно,
но если мы попытаемся это сделать, компилятор сообщит об ошибке:
val myCatBox: Box[Cat] = new Box[Cat](Cat("Felix"))
val myAnimalBox: Box[Animal] = myCatBox // не компилируется
val myAnimal: Animal = myAnimalBox.content
val myCatBox: Box[Cat] = Box[Cat](Cat("Felix"))
val myAnimalBox: Box[Animal] = myCatBox // не компилируется
val myAnimal: Animal = myAnimalBox.content
Почему это может быть проблемой? Мы можем достать из контейнера кота, и это все еще животное, не так ли? Ну да. Но это не все, что мы можем сделать. Мы также можем заменить в контейнере кота другим животным.
myAnimalBox.content = Dog("Fido")
Теперь в контейнере для животных есть собака. Все в порядке, вы можете поместить собак в контейнеры для животных, потому что собаки — это животные. Но наш контейнер для животных — это контейнер для котов! Нельзя поместить собаку в контейнер с котом. Если бы мы могли, а затем попытались достать кота из нашего кошачьего контейнера, он оказался бы собакой, нарушающей целостность типа.
val myCat: Cat = myCatBox.content // myCat стал бы собакой Fido!
Из этого мы должны сделать вывод, что между Box[Cat]
и Box[Animal]
не может быть отношения подтипа,
хотя между Cat
и Animal
это отношение есть.
Ковариантность
Проблема, с которой мы столкнулись выше, заключается в том, что, поскольку мы можем поместить собаку в контейнер для животных, контейнер для кошек не может быть контейнером для животных.
Но что, если мы не сможем поместить собаку в контейнер? Тогда мы бы могли просто вернуть нашего кота, и это не проблема, чтобы можно было следовать отношениям подтипа. Оказывается, это действительно то, что мы можем сделать.
class ImmutableBox[+A](val content: A)
val catbox: ImmutableBox[Cat] = new ImmutableBox[Cat](Cat("Felix"))
val animalBox: ImmutableBox[Animal] = catbox // теперь код компилируется
class ImmutableBox[+A](val content: A)
val catbox: ImmutableBox[Cat] = ImmutableBox[Cat](Cat("Felix"))
val animalBox: ImmutableBox[Animal] = catbox // теперь код компилируется
Мы говорим, что ImmutableBox
ковариантен в A
- на это указывает +
перед A
.
Более формально это дает нам следующее отношение:
если задано некоторое class Cov[+T]
, то если A
является подтипом B
, то Cov[A]
является подтипом Cov[B]
.
Это позволяет создавать очень полезные и интуитивно понятные отношения подтипов с помощью обобщения.
В следующем менее надуманном примере метод printAnimalNames
принимает список животных в качестве аргумента
и печатает их имена с новой строки.
Если бы List[A]
не был бы ковариантным, последние два вызова метода не компилировались бы,
что сильно ограничивало бы полезность метода printAnimalNames
.
def printAnimalNames(animals: List[Animal]): Unit =
animals.foreach {
animal => println(animal.name)
}
val cats: List[Cat] = List(Cat("Whiskers"), Cat("Tom"))
val dogs: List[Dog] = List(Dog("Fido"), Dog("Rex"))
// печатает: "Whiskers", "Tom"
printAnimalNames(cats)
// печатает: "Fido", "Rex"
printAnimalNames(dogs)
Контрвариантность
Мы видели, что можем достичь ковариантности, убедившись, что не сможем поместить что-то в ковариантный тип, а только что-то получить.
Что, если бы у нас было наоборот, что-то, что можно положить, но нельзя вынуть?
Такая ситуация возникает, если у нас есть что-то вроде сериализатора, который принимает значения типа A
и преобразует их в сериализованный формат.
abstract class Serializer[-A] {
def serialize(a: A): String
}
val animalSerializer: Serializer[Animal] = new Serializer[Animal] {
def serialize(animal: Animal): String = s"""{ "name": "${animal.name}" }"""
}
val catSerializer: Serializer[Cat] = animalSerializer
catSerializer.serialize(Cat("Felix"))
abstract class Serializer[-A]:
def serialize(a: A): String
val animalSerializer: Serializer[Animal] = Serializer[Animal]():
def serialize(animal: Animal): String = s"""{ "name": "${animal.name}" }"""
val catSerializer: Serializer[Cat] = animalSerializer
catSerializer.serialize(Cat("Felix"))
Мы говорим, что Serializer
контравариантен в A
, и на это указывает -
перед A
.
Более общий сериализатор является подтипом более конкретного сериализатора.
Более формально это дает нам обратное отношение: если задано некоторое class Contra[-T]
,
то если A
является подтипом B
, Contra[B]
является подтипом Contra[A]
.
Неизменность и вариантность
Неизменяемость является важной частью проектного решения, связанного с использованием вариантности.
Например, коллекции Scala систематически различают изменяемые и неизменяемые коллекции.
Основная проблема заключается в том, что ковариантная изменяемая коллекция может нарушить безопасность типов.
Вот почему List
- ковариантная коллекция, а scala.collection.mutable.ListBuffer
- инвариантная коллекция.
List
- это коллекция в package scala.collection.immutable
, поэтому она гарантированно будет неизменяемой для всех.
Принимая во внимание, что ListBuffer
изменяем, то есть вы можете обновлять, добавлять или удалять элементы ListBuffer
.
Чтобы проиллюстрировать проблему ковариантности и изменчивости, предположим,
что ListBuffer
ковариантен, тогда следующий проблемный пример скомпилируется (на самом деле он не компилируется):
import scala.collection.mutable.ListBuffer
val bufInt: ListBuffer[Int] = ListBuffer[Int](1,2,3)
val bufAny: ListBuffer[Any] = bufInt
bufAny(0) = "Hello"
val firstElem: Int = bufInt(0)
Если бы приведенный выше код был бы возможен, то вычисление firstElem
завершилась бы ошибкой с ClassCastException
,
потому что bufInt(0)
теперь содержит String
, а не Int
.
Инвариантность ListBuffer
означает, что ListBuffer[Int]
не является подтипом ListBuffer[Any]
,
несмотря на то, что Int
является подтипом Any
,
и поэтому bufInt
не может быть присвоен в качестве значения bufAny
.
Сравнение с другими языками
В языках, похожих на Scala, разные способы поддержки вариантности. Например, указания вариантности в Scala очень похожи на то, как это делается в C#, где такие указания добавляются при объявлении абстракции класса (вариантность при объявлении). Однако в Java, указание вариантности задается непосредственно при использовании абстракции класса (вариантность при использовании).
Тенденция Scala к неизменяемым типам делает ковариантные и контравариантные типы более распространенными, чем в других языках, поскольку изменяемый универсальный тип должен быть инвариантным.
Contributors to this page:
Contents
- Введение
- Основы
- Единобразие типов
- Классы
- Значения Параметров По умолчанию
- Именованные Аргументы
- Трейты
- Кортежи
- Композиция классов с трейтами
- Функции Высшего Порядка
- Вложенные Методы
- Множественные списки параметров (Каррирование)
- Классы Образцы
- Сопоставление с примером
- Объекты Одиночки
- Регулярные Выражения
- Объект Экстрактор
- Сложные for-выражения
- Обобщенные Классы
- Вариантность
- Верхнее Ограничение Типа
- Нижнее Ограничение Типа
- Внутренние классы
- Члены Абстрактного Типа
- Составные Типы
- Самоописываемые типы
- Контекстные параметры, также известные, как неявные параметры
- Неявные Преобразования
- Полиморфные методы
- Выведение Типа
- Операторы
- Вызов по имени
- Аннотации
- Пакеты и Импорт
- Объекты Пакета