← Назад к вопросам

Как работать с вариантностью

2.4 Senior🔥 141 комментариев
#JVM и память#Kotlin основы

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Работа с вариантностью в Kotlin

Вариантность (variance) — это концепция, определяющая, как наследование типов параметров влияет на наследование обобщённых (generic) типов. В Kotlin вариантность контролируется на уровне объявления (declaration-site) и использования (use-site) и является ключевой для создания типобезопасных и гибких API.

Основные виды вариантности

  1. Инвариантность (Invariance) — тип по умолчанию. Не позволяет подставлять производные типы.
  2. Ковариантность (Covariance) — сохраняет иерархию наследования (out).
  3. Контравариантность (Contravariance) — обращает иерархию наследования (in).

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

Используется, когда обобщённый тип является производителем (producer) данных — только возвращает значения. Это безопасно, так как потребитель получит значение родительского типа.

interface Producer<out T> {
    fun produce(): T
}

open class Animal
class Dog : Animal()

fun main() {
    val dogProducer: Producer<Dog> = object : Producer<Dog> {
        override fun produce(): Dog = Dog()
    }
    
    // Ковариантность позволяет присвоить Producer<Dog> в Producer<Animal>
    val animalProducer: Producer<Animal> = dogProducer
    val animal: Animal = animalProducer.produce()
}

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

Используется, когда обобщённый тип является потребителем (consumer) — только принимает значения. Это безопасно, так как функция ожидает родительский тип, а получит производный.

interface Consumer<in T> {
    fun consume(item: T)
}

open class Animal
class Dog : Animal()

fun main() {
    val animalConsumer: Consumer<Animal> = object : Consumer<Animal> {
        override fun consume(item: Animal) {
            println("Consuming animal")
        }
    }
    
    // Контравариантность позволяет присвоить Consumer<Animal> в Consumer<Dog>
    val dogConsumer: Consumer<Dog> = animalConsumer
    dogConsumer.consume(Dog()) // Безопасно: Dog является Animal
}

Вариантность на месте использования

Когда вариантность не объявлена в определении типа, её можно указать при использовании через проекции (type projections):

class Box<T>(var item: T)

fun copy(from: Box<out Animal>, to: Box<in Animal>) {
    // 'from' — ковариантна, можно только читать
    val animal: Animal = from.item
    
    // 'to' — контравариантна, можно только писать
    to.item = animal
}

// Использование
val dogBox = Box(Dog())
val animalBox = Box<Animal>(Animal())
copy(dogBox, animalBox) // Безопасно благодаря проекциям

Звёздная проекция (Star Projection)

Используется, когда тип неизвестен, но нужно обеспечить безопасность:

fun printItems(list: List<*>) {
    // Можно читать как Any?, но нельзя писать
    for (item in list) {
        println(item)
    }
}

// Эквивалентно List<out Any?>

Практические примеры

Коллекции в Kotlin:

  • List<out T> — ковариантна, так как только для чтения
  • MutableList<T> — инвариантна, так как и читает, и пишет
  • Comparable<in T> — контравариантна, потребляет T для сравнения

Собственная реализация с вариантностью:

interface Repository<out T : Entity> {
    fun getAll(): List<T>
    // Нельзя объявить функцию с параметром T
}

interface Validator<in T> {
    fun validate(entity: T): Boolean
    // Нельзя возвращать T
}

Правила безопасности

  1. Ковариантные типы (out) могут использоваться только в исходящих (out) позициях — возвращаемые значения
  2. Контравариантные типы (in) могут использоваться только во входящих (in) позициях — параметры функций
  3. Нарушение приводит к ошибке компиляции: Type parameter T is declared as 'out' but occurs in 'in' position

Итог

Вариантность в Kotlin — мощный инструмент для:

  • Обеспечения типобезопасности при работе с обобщёнными типами
  • Создания гибких API, работающих с иерархиями классов
  • Предотвращения runtime ошибок через compile-time проверки
  • Реализации паттернов производитель/потребитель

Понимание вариантности критически важно для эффективной работы с коллекциями, DSL, реактивными потоками и другими продвинутыми возможностями Kotlin.

Как работать с вариантностью | PrepBro