Ковариантность типов
Вариантность параметра типа управляет подтипом параметризованных типов (таких, как классы или trait-ы).
Чтобы разобраться в вариантности, рассмотрим следующий пример типов:
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
В целом существует три режима вариантности (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 then b1 else b2
Напомним, что отношения подтипов между типами следующие: Book <: Buyable <: Item
.
Мы не можем передать Pipeline[Book]
методу oneOf
,
потому что в реализации oneOf
мы вызываем p1
и p2
со значением типа Buyable
.
Pipeline[Book]
ожидает Book
, что потенциально может вызвать runtime error.
Мы не можем передать Pipeline[Item]
, потому что вызов process
обещает вернуть Item
;
однако мы должны вернуть Buyable
.
Почему Инвариант?
На самом деле тип Pipeline
должен быть инвариантным,
так как он использует свой параметр типа T
и в качестве аргумента, и в качестве типа возвращаемого значения.
По той же причине некоторые типы в библиотеке коллекций Scala, такие как Array
или Set
,
также являются инвариантными.
Ковариантные типы
В отличие от Pipeline
, который является инвариантным,
тип Producer
помечается как ковариантный (covariant) путем добавления к параметру типа префикса +
.
Это допустимо, так как параметр типа используется только в качестве типа возвращаемого значения.
Пометка типа как ковариантного означает, что мы можем передать (или вернуть) Producer[Book]
там,
где ожидается Producer[Buyable]
. И на самом деле, это разумно.
Тип Producer[Buyable].make
только обещает вернуть Buyable
.
Но для пользователей make
, так же допустимо принять Book
, который является подтипом 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
Тип аргумента 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
.
Ссылки: