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

Что такое in в Generic?

3.0 Senior🔥 111 комментариев
#Kotlin основы#Архитектура и паттерны

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

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

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

Введение в ключевое слово in в дженериках (вариативность)

В Kotlin (и с оговорками в Java) ключевое слово in в контексте обобщённого программирования (Generics) — это модификатор вариативности (variance), который делает тип-параметр контравариантным (contravariant). Это позволяет определять безопасные с точки зрения типов отношения подстановки между обобщёнными типами, когда они используются в определённом контексте — преимущественно как «потребители» (consumers) или «входные» параметры.

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

Классический пример — интерфейс Comparator или Consumer. Объект, который может сравнивать элементы более общего типа, может быть безопасно использован для сравнения элементов более конкретного типа.

// Объявление контравариантного типа-параметра с 'in'
interface Consumer<in T> {
    fun consume(item: T)
}

// Более общий потребитель
val anyConsumer: Consumer<Any> = object : Consumer<Any> {
    override fun consume(item: Any) {
        println(item.toString())
    }
}

// Более конкретный потребитель
val stringConsumer: Consumer<String> = anyConsumer // КОРРЕКТНО! Благодаря 'in'

// Мы можем присвоить Consumer<Any> переменной типа Consumer<String>,
// потому что любой String является Any.
// Метод consume в anyConsumer ожидает Any, а мы передадим String — это безопасно.
stringConsumer.consume("Hello")

Сравнение с out (ковариантность)

Чтобы понять in, полезно сравнить его с антагонистом — модификатором out:

  • out T (Ковариантность): Объявляет тип как «производитель» (producer) или «выходной». Вы можете безопасно использовать Producer<Cat> там, где ожидается Producer<Animal> (если Cat — подтип Animal). Тип-параметр может появляться только в возвращаемой позиции (после return).
  • in T (Контравариантность): Объявляет тип как «потребитель» (consumer) или «входной». Вы можете безопасно использовать Consumer<Animal> там, где ожидается Consumer<Cat> (обратное направление!). Тип-параметр может появляться только в входной позиции (как параметр функции).
// Пример сравнения out и in
interface Producer<out T> { // Ковариантный производитель
    fun produce(): T
}

interface Consumer<in T> {  // Контравариантный потребитель
    fun consume(item: T)
}

fun main() {
    val catProducer: Producer<Cat> = ... // Предположим, есть реализация
    val animalProducer: Producer<Animal> = catProducer // OK благодаря 'out'

    val animalConsumer: Consumer<Animal> = ... // Предположим, есть реализация
    val catConsumer: Consumer<Cat> = animalConsumer // OK благодаря 'in'
}

Ограничения (позиционность)

Модификатор in накладывает строгое ограничение: тип-параметр T, объявленный как in, может использоваться только как входной параметр (аргумент) функций внутри этого класса/интерфейса. Его нельзя использовать в возвращаемом типе (public-свойствах типа val, методах, возвращающих T). Это гарантирует, что мы не сможем «прочитать» объект неожиданного типа, нарушив безопасность.

interface Problematic<in T> {
    fun consume(item: T) // OK
    // fun getItem(): T // ОШИБКА! Тип-параметр T объявлен как 'in',
                         // поэтому он не может фигурировать в возвращаемой позиции.
}

In-позиция в Kotlin vs Use-site variance в Java

В Kotlin вариативность (in/out) обычно объявляется непосредственно в объявлении типа (declaration-site variance), как в примерах выше. Это более лаконично и выразительно.

В Java подобного ключевого слова нет, но аналогичный эффект достигается через use-site variance с использованием ограниченных wildcards ? super T.

// Java-эквивалент контравариантности (Consumer<in String> в Kotlin)
Consumer<? super String> consumer;

// Эквивалент ковариантности (Producer<out String> в Kotlin)
Producer<? extends String> producer;

Типичные use-cases

  1. Коллекции как потребители: MutableList<in String> может быть присвоен MutableList<CharSequence>. В такую коллекцию можно записывать (add) строки, но при чтении (get) вы получите тип Any?/CharSequence?, а не String, потому что компилятор не знает точный тип хранимых объектов.

    fun addStrings(list: MutableList<in String>) {
        list.add("Text") // OK
        val item: Any? = list[0] // Чтение даёт тип верхней границы (Any?)
    }
    
  2. Обратные вызовы (Callbacks) и обработчики (Handlers): Интерфейсы, которые только получают данные для обработки.

  3. Сравнение (Comparable/Comparator): Comparator<Any> может сравнивать любые объекты, поэтому его можно использовать как Comparator<String>.

Итог

Ключевое слово in — это мощный инструмент системы типов Kotlin, который:

  • Делает тип-параметр контравариантным.
  • Позволяет безопасно подставлять более общий обобщённый тип вместо более конкретного (в отличие от out).
  • Ограничивает использование типа-параметра только входными позициями (параметры функций), превращая класс в «потребителя».
  • Повышает гибкость API, позволяя писать более универсальный код, сохраняя при этом стабильную типобезопасность на этапе компиляции. Понимание in и out критически важно для эффективной работы с коллекциями, callback-ами и проектирования расширяемых библиотек.