Avatar ·

Что означает ключевое слово out в Kotlin и как его использовать с дженериками?

Привет! Я начал изучать Kotlin после Java и столкнулся с непонятной для меня конструкцией. В Java я привык к дженерикам с wildcard'ами типа `? extends T` и `? super T`. Но в Kotlin я вижу ключевые слова `in` и `out` прямо в объявлении класса. Например, я наткнулся на такой код: ```kotlin interface Source { fun get(): T } ``` Что значит `out` перед `T`? Я попробовал написать свой класс без `out`: ```kotlin class Box(private val item: T) { fun get(): T = item fun set(newItem: T) { /* ... */ } } ``` И всё работает. Но если я добавлю `out`, то компилятор ругается на метод `set`. Почему? И зачем вообще нужен `out`? В каких ситуациях его следует применять? Как это связано с принципом PECS (Producer Extends, Consumer Super) из Java? Объясните, пожалуйста, на простых примерах, чтобы я понял, когда и зачем использовать `out` в Kotlin. <<>>

Что такое out в Kotlin: объявление вариантности (declaration-site variance)

Ключевое слово out в Kotlin — это способ объявить ковариантность (covariance) прямо на месте объявления класса или интерфейса (declaration-site variance). Это аналог wildcard ? extends T в Java, но применяется один раз — при объявлении типа, а не при каждом использовании.

Если класс помечен как out T, это означает, что T может быть только на выходе (out-позиции): возвращаемые типы методов. Вы не можете использовать T в качестве входного параметра (in-позиции). Компилятор следит за этим.

Вот почему ваш код с set(newItem: T) не работает с out: вы пытаетесь использовать T как параметр, что запрещено.

// ❌ Ошибка компиляции
class Box<out T>(private val item: T) {
    fun get(): T = item
    fun set(newItem: T) { } // Ошибка! T в in-позиции
}

Зачем нужен out? Пример из реальной жизни

Представьте, что у вас есть иерархия животных:

open class Animal
class Dog : Animal()
class Cat : Animal()

И интерфейс-производитель (только читает данные):

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

class DogProducer : Producer<Dog> {
    override fun produce(): Dog = Dog()
}

class CatProducer : Producer<Cat> {
    override fun produce(): Cat = Cat()
}

Благодаря out мы можем сделать так:

fun feedAnimal(producer: Producer<Animal>) {
    val animal = producer.produce()
    println("Кормим $animal")
}

fun main() {
    val dogProducer = DogProducer()
    feedAnimal(dogProducer) // ✅ Работает! Producer<Dog> — это Producer<Animal>
}

Без out такой код не скомпилировался бы, потому что Producer<Dog> не является подтипом Producer<Animal>.

Правило: Producer = out, Consumer = in

Это прямое соответствие принципу PECS из Java:

  • Producer (производит данные, отдаёт их наружу) → используйте out
  • Consumer (потребляет данные, принимает их внутрь) → используйте in

Типичные примеры из стандартной библиотеки Kotlin:

// List — производитель, только читает элементы
public interface List<out E> : Collection<E> {
    override val size: Int
    override fun get(index: Int): E  // E в out-позиции
    // Нет метода set() — поэтому List ковариантен
}

// MutableList — и производитель, и потребитель
public interface MutableList<E> : List<E>, MutableCollection<E> {
    override fun set(index: Int, element: E): E  // E и in, и out — поэтому не out
}

Когда использовать out?

  • Когда ваш класс/интерфейс только отдаёт объекты типа T, но никогда их не принимает
  • Примеры: неизменяемые коллекции (List, Set), фабрики, провайдеры данных, итераторы
  • Это даёт гибкость: можно передать List<Dog> туда, где ожидается List<Animal>

Важное ограничение: var-свойства

Свойства с var тоже не могут быть типа out T, потому что var подразумевает сеттер (in-позиция):

class Bad<out T>(var item: T) // ❌ Ошибка! var имеет и get, и set

А val — можно, так как у него только геттер (out-позиция).

Итог: out — это способ сказать компилятору: "Я обещаю, что этот класс будет только отдавать объекты типа T, никогда их не принимать. Доверься мне и разреши ковариантность".

<<>>

Альтернативный взгляд: out как аналог ? extends T в Java

Если вы пришли из Java, запомните простое соответствие:

  • out T в Kotlin = ? extends T в Java (ковариантность)
  • in T в Kotlin = ? super T в Java (контравариантность)

Разница в том, что в Java вы пишете wildcard при каждом использовании типа:

// Java: wildcard на месте использования
List<? extends Animal> animals = new ArrayList<Dog>();
// Нельзя добавить Dog — компилятор не знает точный тип
// animals.add(new Dog()); // Ошибка компиляции

В Kotlin вы объявляете вариантность один раз в определении класса:

// Kotlin: declaration-site variance
interface Producer<out T> {
    fun produce(): T
}

// И теперь везде Producer<Dog> — это Producer<Animal>
fun use(producer: Producer<Animal>) { }

Это удобнее: не нужно повторять wildcard в каждом методе. Однако если вы не можете изменить исходный класс (например, из библиотеки Java), используйте use-site variance:

// Kotlin: use-site variance (аналог Java wildcard)
fun process(list: List<out Animal>) { // можно передать List<Dog>
    val animal = list[0] // работает
    // list.add(Dog()) — нельзя, как в Java
}

Таким образом, out — это не магия, а просто способ явно сказать компилятору: "Этот тип будет только читаться, поэтому разреши подстановку подтипов".