scalabook

Форк
0
/
types-variance.md 
184 строки · 10.0 Кб

Ковариантность типов

Вариантность параметра типа управляет подтипом параметризованных типов (таких, как классы или 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
    .

Ссылки:

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.