Что означает ключевое слово 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 — это не магия, а просто способ явно сказать компилятору: "Этот тип будет только читаться, поэтому разреши подстановку подтипов".