Вариантность (variance) параметра типа управляет подтипом параметризованных типов (таких, как классы или трейты).
Чтобы объяснить вариантность, давайте рассмотрим следующие определения типов:
trait Item { def productNumber: String }
trait Buyable extends Item { def price: Int }
trait Book extends Buyable { def isbn: String }
Предположим также следующие параметризованные типы:
// пример инвариантного типа
trait Pipeline[T] {
def process(t: T): T
}
// пример ковариантного типа
trait Producer[+T] {
def make: T
}
// пример контрвариантного типа
trait Consumer[-T] {
def take(t: T): Unit
}
// пример инвариантного типа
trait Pipeline[T]:
def process(t: T): T
// пример ковариантного типа
trait Producer[+T]:
def make: T
// пример контрвариантного типа
trait Consumer[-T]:
def take(t: T): Unit
В целом существует три режима вариантности (variance):
- инвариант (invariant) — значение по умолчанию, написанное как
Pipeline[T]
- ковариантный (covariant) — помечен знаком
+
, напримерProducer[+T]
- контравариантный (contravariant) — помечен знаком
-
, как вConsumer[-T]
Подробнее рассмотрим, что означает и как используется эта аннотация.
Инвариантные типы
По умолчанию такие типы, как Pipeline
, инвариантны в своем аргументе типа (в данном случае T
).
Это означает, что такие типы, как Pipeline[Item]
, Pipeline[Buyable]
и Pipeline[Book]
, не являются подтипами друг друга.
И это правильно! Предположим, что следующий метод использует два значения (b1
, b2
) типа Pipeline[Buyable]
и передает свой аргумент b
методу process
при его вызове на b1
и b2
:
def oneOf(
p1: Pipeline[Buyable],
p2: Pipeline[Buyable],
b: Buyable
): Buyable = {
val b1 = p1.process(b)
val b2 = p2.process(b)
if (b1.price < b2.price) b1 else b2
}
def oneOf(
p1: Pipeline[Buyable],
p2: Pipeline[Buyable],
b: Buyable
): Buyable =
val b1 = p1.process(b)
val b2 = p2.process(b)
if b1.price < b2.price then b1 else b2
Теперь вспомните, что у нас есть следующие отношения подтипов между нашими типами:
Book <: Buyable <: Item
Мы не можем передать Pipeline[Book]
методу oneOf
,
потому что в реализации oneOf
мы вызываем p1
и p2
со значением типа Buyable
.
Pipeline[Book]
ожидает Book
, что потенциально может вызвать ошибку времени выполнения.
Мы не можем передать Pipeline[Item]
, потому что вызов process
обещает вернуть Item
;
однако мы должны вернуть Buyable
.
Почему Инвариант?
На самом деле тип Pipeline
должен быть инвариантным,
так как он использует свой параметр типа T
и в качестве аргумента, и в качестве типа возвращаемого значения.
По той же причине некоторые типы в библиотеке коллекций Scala, такие как Array
или Set
, также являются инвариантными.
Ковариантные типы
В отличие от Pipeline
, который является инвариантным,
тип Producer
помечается как ковариантный (covariant) путем добавления к параметру типа префикса +
.
Это допустимо, так как параметр типа используется только в качестве типа возвращаемого значения.
Пометка типа как ковариантного означает, что мы можем передать (или вернуть) Producer[Book]
там,
где ожидается Producer[Buyable]
. И на самом деле, это разумно.
Тип Producer[Buyable].make
только обещает вернуть Buyable
.
Но для пользователей make
, так же допустимо принять Book
, который является подтипом Buyable
,
то есть это по крайней мере Buyable
.
Это иллюстрируется следующим примером, где функция makeTwo
ожидает Producer[Buyable]
:
def makeTwo(p: Producer[Buyable]): Int =
p.make.price + p.make.price
Допустимо передать в makeTwo
производителя книг:
val bookProducer: Producer[Book] = ???
makeTwo(bookProducer)
Вызов price
в рамках makeTwo
по-прежнему действителен и для Book
.
Ковариантные типы для неизменяемых контейнеров
Ковариантность чаще всего встречается при работе с неизменяемыми контейнерами, такими как List
, Seq
, Vector
и т.д.
Например, List
и Vector
определяются приблизительно так:
class List[+A] ...
class Vector[+A] ...
Таким образом, можно использовать List[Book]
там, где ожидается List[Buyable]
.
Это также интуитивно имеет смысл: если ожидается коллекция вещей, которые можно купить,
то вполне допустимо получить коллекцию книг.
В примере выше у книг есть дополнительный метод isbn
, но дополнительные возможности можно игнорировать.
Контравариантные типы
В отличие от типа Producer
, который помечен как ковариантный,
тип Consumer
помечен как контравариантный (contravariant) путем добавления к параметру типа префикса -
.
Это допустимо, так как параметр типа используется только в позиции аргумента.
Пометка его как контравариантного означает, что можно передать (или вернуть) Consumer[Item]
там,
где ожидается Consumer[Buyable]
.
То есть у нас есть отношение подтипа Consumer[Item] <: Consumer[Buyable]
.
Помните, что для типа Producer
все было наоборот, и у нас был Producer[Buyable] <: Producer[Item]
.
И в самом деле, это разумно. Метод Consumer[Item].take
принимает Item
.
Как вызывающий take
, мы также можем предоставить Buyable
, который будет с радостью принят Consumer[Item]
,
поскольку Buyable
— это подтип Item
, то есть, по крайней мере, Item
.
Контравариантные типы для потребителей
Контравариантные типы встречаются гораздо реже, чем ковариантные типы. Как и в нашем примере, вы можете думать о них как о «потребителях». Наиболее важным типом, помеченным как контравариантный, с которым можно столкнуться, является тип функций:
trait Function[-A, +B] {
def apply(a: A): B
}
trait Function[-A, +B]:
def apply(a: A): B
Тип аргумента A
помечен как контравариантный A
— он использует значения типа A
.
Тип результата B
, напротив, помечен как ковариантный — он создает значения типа B
.
Вот несколько примеров, иллюстрирующих отношения подтипов, вызванные аннотациями вариантности функций:
val f: Function[Buyable, Buyable] = b => b
// OK - допустимо вернуть Buyable там, где ожидается Item
val g: Function[Buyable, Item] = f
// OK - допустимо передать аргумент Book туда, где ожидается Buyable
val h: Function[Book, Buyable] = f
Резюме
В этом разделе были рассмотрены три различных вида вариантности:
- Producers обычно ковариантны и помечают свой параметр типа со знаком
+
. Это справедливо и для неизменяемых коллекций. - Consumers обычно контравариантны и помечают свой параметр типа со знаком
-
. - Типы, которые являются одновременно производителями и потребителями,
должны быть инвариантными и не требуют какой-либо маркировки для параметра своего типа.
В эту категорию, в частности, попадают изменяемые коллекции, такие как
Array
.
Contributors to this page:
Contents
- Введение
- Возможности Scala
- Почему Scala 3?
- Почувствуй Scala
- Пример 'Hello, World!'
- REPL
- Переменные и типы данных
- Структуры управления
- Моделирование данных
- Методы
- Функции первого класса
- Одноэлементные объекты
- Коллекции
- Контекстные абстракции
- Верхнеуровневые определения
- Обзор
- Первый взгляд на типы
- Интерполяция строк
- Структуры управления
- Моделирование предметной области
- Инструменты
- Моделирование ООП
- Моделирование ФП
- Методы
- Особенности методов
- Main методы в Scala 3
- Обзор
- Функции
- Анонимные функции
- Параметры функции
- Eta расширение
- Функции высшего порядка
- Собственный map
- Создание метода, возвращающего функцию
- Обзор
- Пакеты и импорт
- Коллекции в Scala
- Типы коллекций
- Методы в коллекциях
- Обзор
- Функциональное программирование
- Что такое функциональное программирование?
- Неизменяемые значения
- Чистые функции
- Функции — это значения
- Функциональная обработка ошибок
- Обзор
- Типы и система типов
- Определение типов
- Параметризованные типы
- Пересечение типов
- Объединение типов
- Алгебраические типы данных
- Вариантность
- Непрозрачные типы
- Структурные типы
- Зависимые типы функций
- Другие типы
- Контекстные абстракции
- Методы расширения
- Параметры контекста
- Контекстные границы
- Given импорты
- Классы типов
- Многостороннее равенство
- Неявное преобразование типов
- Обзор
- Параллелизм
- Scala утилиты
- Сборка и тестирование проектов Scala с помощью Sbt
- Рабочие листы
- Взаимодействие с Java