Вариантность: ковариантность и контравариантность
Освойте ковариантность и контравариантность, чтобы правильно работать с отношениями подтипов в обобщённых типах.
«Вариантность: ковариантность и контравариантность» — бесплатный урок 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 — локальная установка не требуется.
Все уроки этого курса
- Обобщения и параметры типов
- Вариантность: ковариантность и контравариантность
- Классы типов и неявные параметры