Kotlin Academy · Урок

Вариативность на месте объявления: in и out

Правильно используйте ковариантные (out) и контравариантные (in) параметры типов.

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

«Вариативность на месте объявления: in и out» — бесплатный урок Kotlin Academy на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Kotlin Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Kotlin Academy содержит 4 уроков всего.

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

В Kotlin List<String> является подтипом List<Any>, потому что List объявлен с out T. Без аннотаций вариантности это было бы невозможно.

val strings: List<String> = listOf("a", "b")
val anys: List<Any> = strings  // OK because List<out T>
// MutableList<String> is NOT a subtype of MutableList<Any>:
// val m: MutableList<Any> = mutableListOf("x") // compile error
fun main() { println(anys) }

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

out T означает, что класс может только создавать значения T (возвращать их), но не принимать их. Поэтому Producer<Dog> является подтипом Producer<Animal>.

interface Producer<out T> {
    fun produce(): T
}
class DogProducer : Producer<String> {
    override fun produce() = "Woof!"
}
fun sound(p: Producer<Any>) = println(p.produce())
fun main() {
    val dog = DogProducer()
    sound(dog)  // OK: Producer<String> is a subtype of Producer<Any>
}

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

in T означает, что класс может только принимать значения T (получать их как параметры), но не создавать их. Поэтому Consumer<Animal> является подтипом Consumer<Dog>.

interface Consumer<in T> {
    fun consume(item: T)
}
class Printer : Consumer<Any> {
    override fun consume(item: Any) = println(item)
}
fun feedDog(consumer: Consumer<String>) = consumer.consume("Dog")
fun main() {
    val printer = Printer()
    feedDog(printer)  // OK: Consumer<Any> is a subtype of Consumer<String>
}

Ограничение out: нет позиции in

При использовании out T T не может находиться в позиции параметра (in). Компилятор проверяет это правило.

interface ReadOnly<out T> {
    fun get(): T          // OK: out position
    // fun set(t: T) {}  // Error: T in in-position violates out variance
}
class Box<out T>(private val value: T) : ReadOnly<T> {
    override fun get() = value
}
fun main() {
    val box: ReadOnly<Any> = Box<String>("hello")
    println(box.get())
}

Ограничение in: нет позиции out

При использовании in T T не может находиться в позиции возвращаемого значения (out).

interface WriteOnly<in T> {
    fun set(t: T)        // OK: in position
    // fun get(): T {}  // Error: T in out-position violates in variance
}
class Sink<in T> : WriteOnly<T> {
    override fun set(t: T) = println("Received: $t")
}
fun main() {
    val sink: WriteOnly<String> = Sink<Any>()
    sink.set("hello")
}

Примеры из стандартной библиотеки Kotlin

Comparable<in T> контравариантен: объект Comparable<Number> может сравнивать любой подтип Number. List<out T> ковариантен: список кошек является списком животных.

fun sortNumbers(list: List<Number>) = list.sortedWith(compareBy { it.toDouble() })
fun main() {
    val ints: List<Int> = listOf(3, 1, 2)
    val sorted = sortNumbers(ints)  // OK: List<Int> is List<Number>
    println(sorted)
}

Инвариантность: MutableList

MutableList<T> инвариантен: он может и создавать, и принимать значения T, поэтому между разными типами T нет отношения подтипов.

fun addNumber(list: MutableList<Number>) { list.add(1.5) }
fun main() {
    val ints = mutableListOf<Int>(1, 2)
    // addNumber(ints) // compile error: MutableList<Int> is not MutableList<Number>
    val nums = mutableListOf<Number>(1, 2)
    addNumber(nums)   // OK
    println(nums)
}

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

Если изменить класс нельзя, используйте вариантность на месте использования: укажите out в месте вызова, чтобы представить тип как ковариантный.

fun copy(from: MutableList<out Any>, to: MutableList<Any>) {
    for (item in from) to.add(item)
}
fun main() {
    val src = mutableListOf("a", "b", "c")
    val dest = mutableListOf<Any>()
    copy(src, dest)
    println(dest)
}

Практика: шаблон Repository

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

interface Repository<out T> {
    fun findAll(): List<T>
    fun findById(id: Int): T?
}
interface Writer<in T> {
    fun save(entity: T)
}
interface ReadWriteRepo<T> : Repository<T>, Writer<T>
// ReadWriteRepo<User> is neither sub nor super of ReadWriteRepo<Admin>

Руководство по выбору вариантности

Выбирайте вариантность в зависимости от использования T: out = производитель (только возвращает T), in = потребитель (только принимает T), инвариантность = оба варианта (MutableList, Channel).

// Quick mental model:
// out T: source/producer  — List, Flow, Sequence
// in  T: sink/consumer    — Comparable, Continuation
// invariant: read+write   — MutableList, Channel, MutableStateFlow
fun main() {
    val nums: List<Number> = listOf(1, 2, 3)  // List is out
    println(nums)
}

Инвариантный класс с функцией out

Даже в инвариантном классе можно использовать out на месте использования для отдельных параметров функции.

class Stack<T>(private val items: MutableList<T> = mutableListOf()) {
    fun push(item: T) = items.add(item)
    fun pop(): T? = items.removeLastOrNull()
}
fun printAll(stack: Stack<out Any>) {
    // can only read, not write
    println(stack.pop())
}
fun main() {
    val s = Stack<String>()
    s.push("hello")
    printAll(s)
}

Быстрая проверка

Что означает out T у параметра типа?

Итоги

out T (ковариантность) позволяет использовать отношение подтипов при создании значений. in T (контравариантность) позволяет использовать отношение подтипов при приёме значений. У инвариантных типов, которые и читаются, и записываются, нет отношения подтипов для разных T.

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

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

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

Курсы
51
Уроки
203

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

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

Да — полный текст урока «Вариативность на месте объявления: in и out» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Kotlin Academy, подпишись на CoddyKit PRO. Курс Kotlin Academy содержит 4 уроков всего.

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

Правильно используйте ковариантные (out) и контравариантные (in) параметры типов. Ты практикуешь Kotlin Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать Kotlin Academy?

Предыдущий опыт не требуется. Kotlin Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.

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

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

Можно ли писать и запускать код в этом уроке Kotlin Academy?

Да. Каждый урок Kotlin Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

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

  1. Обобщённые функции и ограничения типов с where
  2. Вариативность на месте объявления: in и out
  3. Звёздная проекция и случаи применения *
  4. Стирание типов и параметры типов reified
← Назад к Kotlin Academy