Scala for Backend Engineering & Functional Programming · Урок

Вариантность: ковариантность и контравариантность

Освойте ковариантность и контравариантность, чтобы правильно работать с отношениями подтипов в обобщённых типах.

Урок 2 из 311 шагов

«Вариантность: ковариантность и контравариантность» — бесплатный урок Scala for Backend Engineering & Functional Programming на CoddyKit. Это урок 2 из 3. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Scala for Backend Engineering & Functional Programming, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Scala for Backend Engineering & Functional Programming содержит 3 уроков всего.

Что такое вариантность типов

В Scala, если существует иерархия типов (например, Cat — подтип Animal), как ведут себя обобщённые типы?

Считается ли List[Cat] подтипом List[Animal]? По умолчанию — не всегда!

Вариантность типов позволяет определить, как отношения подтипов сохраняются или обращаются для обобщённых типов. Это крайне важно для написания гибкого и безопасного кода.

Ковариантность: типы-производители (+T)

Ковариантность обозначается символом + перед параметром типа (например, trait Box[+T]).

  • Если A — подтип B, то Box[A] становится подтипом Box[B].
  • Думайте о ковариантных типах как о «производителях» T. Они могут использоваться только в выходных позициях, например в качестве возвращаемых типов методов.
  • Поэтому, если требуется Box[Animal], можно передать Box[Cat], поскольку он «производит» более конкретное значение: Cat является Animal.

Ковариантность в действии

Попробуйте запустить этот пример ковариантного трейта Producer:

class Animal
class Cat extends Animal

trait Producer[+T] {
  def produce: T // Output position
}

class CatProducer extends Producer[Cat] {
  def produce: Cat = new Cat
}

object Main {
  def main(args: Array[String]): Unit = {
    val catProducer: Producer[Cat] = new CatProducer
    // Because Producer is covariant, Producer[Cat] is a subtype of Producer[Animal]
    val animalProducer: Producer[Animal] = catProducer 
    println("Assigned CatProducer to AnimalProducer.")
    println(s"Produced: ${animalProducer.produce.getClass.getSimpleName}")
  }
}

Когда использовать ковариантность

Ковариантность безопасна, когда обобщённый тип только «производит» значения типа T или никогда не принимает T в качестве аргумента.

  • Неизменяемые коллекции, такие как List[+T], — отличный пример. Можно рассматривать List[Cat] как List[Animal], поскольку из него можно только «читать» значения типа Animal или его подтипов.
  • Нельзя добавить произвольный объект Animal в List[Cat], если список рассматривается как List[Animal], и это сохраняет безопасность типов.

Контравариантность: типы-потребители (-T)

Контравариантность обозначается символом - перед параметром типа (например, trait Consumer[-T]).

  • Если A — подтип B, то Consumer[B] становится подтипом Consumer[A].
  • Думайте о контравариантных типах как о «потребителях» T. Они могут использоваться только во входных позициях, например в качестве типов параметров методов.
  • Поэтому, если требуется Consumer[Cat], можно передать Consumer[Animal], поскольку он «потребляет» более общий тип: он может работать с любым Animal, включая Cat.

Контравариантность в действии

Попробуйте запустить этот пример контравариантного трейта Consumer:

class Animal
class Cat extends Animal

trait Consumer[-T] {
  def consume(item: T): Unit // Input position
}

class AnimalConsumer extends Consumer[Animal] {
  def consume(item: Animal): Unit = {
    println(s"Consumed an animal: ${item.getClass.getSimpleName}")
  }
}

object Main {
  def main(args: Array[String]): Unit = {
    val animalConsumer: Consumer[Animal] = new AnimalConsumer
    // Because Consumer is contravariant, Consumer[Animal] is a subtype of Consumer[Cat]
    val catConsumer: Consumer[Cat] = animalConsumer 
    catConsumer.consume(new Cat)
    println("Assigned AnimalConsumer to CatConsumer.")
  }
}

Когда использовать контравариантность

Контравариантность безопасна, когда обобщённый тип только «потребляет» значения типа T или никогда не возвращает T.

  • Распространённый пример — функции, а именно тип входного параметра. Если функция может обрабатывать любой Animal (Animal => Unit), она, конечно, сможет обработать и Cat. Поэтому (Animal => Unit) является подтипом (Cat => Unit).
  • Это обеспечивает большую гибкость при передаче функций в качестве аргументов.

Инвариантность: поведение по умолчанию

Если не указать + или -, параметр типа является инвариантным. В Scala это поведение по умолчанию.

  • Box[A] является подтипом Box[B] только в том случае, если A и B — в точности один и тот же тип.
  • Это часто необходимо для изменяемых коллекций (например, Array[T]), чтобы избежать проблем с безопасностью типов, поскольку из них можно читать значения разных подтипов и записывать их.
class Food
class Apple extends Food

// Invariant Box
class Box[T](val item: T) {
  def getContent: T = item
}

object Main {
  def main(args: Array[String]): Unit = {
    val appleBox = new Box(new Apple)
    // The following line would cause a compile error:
    // val foodBox: Box[Food] = appleBox 
    println(s"An Apple Box contains: ${appleBox.getContent.getClass.getSimpleName}")
    println("Box[Apple] is NOT a subtype of Box[Food] (invariant).")
    println("The types must match exactly for invariant types.")
  }
}

Функции: ковариантность и контравариантность

Типы функций Scala, Function1[-A, +B], элегантно объединяют оба вида вариантности:

  • Входной параметр A является контравариантным (-A). Это означает, что функцию, принимающую более общий тип (например, Animal), можно использовать там, где ожидается функция, принимающая более конкретный тип (например, Cat).
  • Возвращаемый тип B является ковариантным (+B). Это означает, что функцию, возвращающую более конкретный тип (например, Cat), можно использовать там, где ожидается функция, возвращающая более общий тип (например, Animal).
class Vehicle
class Car extends Vehicle

object Main {
  def main(args: Array[String]): Unit = {
    // Contravariance for input: (Vehicle => Unit) is a subtype of (Car => Unit)
    val printVehicle: Vehicle => Unit = (v: Vehicle) => println(s"Printing vehicle: ${v.getClass.getSimpleName}")
    val printCar: Car => Unit = printVehicle // OK: A general printer can print a specific car
    printCar(new Car)

    // Covariance for output: (() => Car) is a subtype of (() => Vehicle)
    val getCar: () => Car = () => new Car
    val getVehicle: () => Vehicle = getCar // OK: A specific producer can fulfill a general request
    println(s"Got vehicle: ${getVehicle().getClass.getSimpleName}")
  }
}

Быстрая проверка: правила вариантности

Рассмотрим следующий трейт:

trait Handler[T] {
  def handle(item: T): Unit
}

Чтобы Handler[Animal] можно было использовать там, где ожидается Handler[Cat] (а Cat расширяет Animal), какую аннотацию вариантности должен иметь T?

Повторение: мастерство вариантности

Вы освоили вариантность в Scala! Кратко повторим:

  • Ковариантность (+T): позволяет Container[Subtype] быть подтипом Container[Supertype]. Полезна для типов-«производителей», которые только возвращают T.
  • Контравариантность (-T): позволяет Container[Supertype] быть подтипом Container[Subtype]. Полезна для типов-«потребителей», которые принимают T на вход.
  • Инвариантность: поведение по умолчанию. Типы должны полностью совпадать.

Понимание вариантности помогает создавать более гибкий и безопасный обобщённый код на Scala!

Можно начать бесплатно

Изучай Scala с ИИ-репетитором — бесплатно

Пиши и запускай код прямо в браузере, получай мгновенную помощь от ИИ-репетитора 24/7 и продолжи учиться на сайте или в приложении.

Курсы
39
Уроки
143

Часто задаваемые вопросы

Урок «Вариантность: ковариантность и контравариантность» бесплатный?

Да — полный текст урока «Вариантность: ковариантность и контравариантность» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Scala for Backend Engineering & Functional Programming, подпишись на CoddyKit PRO. Курс Scala for Backend Engineering & Functional Programming содержит 3 уроков всего.

Чему я научусь в уроке «Вариантность: ковариантность и контравариантность»?

Освойте ковариантность и контравариантность, чтобы правильно работать с отношениями подтипов в обобщённых типах. Ты практикуешь Scala for Backend Engineering & Functional Programming с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать Scala for Backend Engineering & Functional Programming?

Предыдущий опыт не требуется. Scala for Backend Engineering & Functional Programming на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 3.

Сколько времени занимает урок «Вариантность: ковариантность и контравариантность»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке Scala for Backend Engineering & Functional Programming?

Да. Каждый урок Scala for Backend Engineering & Functional Programming включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Обобщения и параметры типов
  2. Вариантность: ковариантность и контравариантность
  3. Классы типов и неявные параметры
← Назад к Scala for Backend Engineering & Functional Programming