Вариантность (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-выражения
- Обобщенные Классы
- Вариантность
- Верхнее Ограничение Типа
- Нижнее Ограничение Типа
- Внутренние классы
- Члены Абстрактного Типа
- Составные Типы
- Самоописываемые типы
- Контекстные параметры, также известные, как неявные параметры
- Неявные Преобразования
- Полиморфные методы
- Выведение Типа
- Операторы
- Вызов по имени
- Аннотации
- Пакеты и Импорт
- Объекты Пакета