Что скрывается за словом out в Kotlin? Отвечаем на популярные вопросы
Если вы когда-нибудь писали на Kotlin дженерики и сталкивались с непонятной ошибкой компиляции, скорее всего, вы встречали слово out. Этот маленький модификатор вызывает много споров даже у опытных разработчиков. Мы собрали самые частые вопросы с Stack Overflow и превратили их в понятное руководство. Сегодня вы узнаете, что такое out, зачем он нужен и как не запутаться в ковариантности.
Давайте разберемся раз и навсегда — без скучной теории, только живой код и практика.
Почему Kotlin придумал out? (Краткий экскурс в историю)
В Java есть wildcard — знак вопроса ? extends T. Он означает, что коллекция может содержать объекты типа T или его подтипов. Проблема в том, что Java не запрещает вам случайно положить в такую коллекцию неправильный объект — вы получите ошибку только в рантайме. Kotlin решил эту проблему на уровне типов.
Ключевое слово out в Kotlin — это декларация ковариантности. Если вы пишете Source<out T>, это значит: «Я могу читать объекты типа T из этого Source, но не могу в него писать». Компилятор следит за этим строго.
Вопрос 1: Чем out отличается от in?
Самый частый вопрос на Stack Overflow. Давайте представим коробку:
out— коробка, из которой можно только доставать вещи (производит).in— коробка, в которую можно только складывать вещи (потребляет).
Пример:
// Ковариантность: можем читать, но не писать
interface Producer<out T> {
fun produce(): T
}
// Контравариантность: можем писать, но не читать
interface Consumer<in T> {
fun consume(item: T)
}
fun main() {
val stringProducer: Producer<String> = object : Producer<String> {
override fun produce(): String = "Hello"
}
val anyProducer: Producer<Any> = stringProducer // Работает! out позволяет
println(anyProducer.produce())
}
Обратите внимание: Producer<out T> позволяет присвоить Producer<String> переменной типа Producer<Any>. Без out это было бы невозможно.
Вопрос 2: Почему Kotlin запрещает fun set(value: T) при out?
Это классическая ловушка. Если у вас объявлен out, компилятор не позволит иметь функции, которые принимают T в качестве параметра. Почему?
Представьте, что у вас есть MutableList<out Number>. Вы можете положить в него Int, но компилятор не знает, что это за список на самом деле. Может, это MutableList<Double>? Если бы разрешили запись, вы бы сломали типы. out гарантирует безопасность: вы можете только читать.
// Не скомпилируется!
interface BadList<out T> {
fun get(index: Int): T // OK
fun add(item: T) // Ошибка! T находится в in-позиции
}
Решение: если вам нужно и читать, и писать — используйте in и out по отдельности, либо инвариантный тип (без модификатора).
Вопрос 3: Как out связан с wildcard ? extends из Java?
Прямое соответствие: Source<out T> в Kotlin = Source<? extends T> в Java. Но есть важное отличие:
- В Java wildcard — это использование типа (use-site variance).
- В Kotlin
out— это объявление типа (declaration-site variance).
Пример на Java:
// Java: используем wildcard при объявлении переменной
List<? extends Number> numbers = new ArrayList<Integer>();
numbers.add(42); // Ошибка компиляции! (но не всегда очевидная)
В Kotlin то же самое, но проще:
// Kotlin: объявляем ковариантность один раз в интерфейсе
interface Source<out T> {
fun nextT(): T
}
fun demo(strs: Source<String>) {
val objects: Source<Any> = strs // Это безопасно и разрешено
}
Kotlin избавляет вас от необходимости писать ? extends в каждом месте использования.
Вопрос 4: Можно ли использовать out с изменяемыми коллекциями?
Короткий ответ: да, но осторожно. Если вы объявите MutableList<out T>, вы не сможете вызывать add(), но сможете читать. На практике это используется редко, потому что теряется смысл изменяемой коллекции.
fun process(list: MutableList<out Number>) {
// val item: Number = list[0] // OK
// list.add(42) // Ошибка!
}
Лучше использовать List<out T> (он уже ковариантен по умолчанию) или MutableList<T> без модификаторов.
Вопрос 5: Как out влияет на производительность?
Никак. out — это исключительно информация для компилятора. Во время выполнения (runtime) все дженерики стираются, и модификаторы не влияют на скорость работы. Единственное — вы получаете более безопасный код без лишних проверок типов.
Практический пример: ковариантный интерфейс для чтения данных
Допустим, вы пишете библиотеку для работы с разными источниками данных. Используйте out для интерфейса, который только читает:
interface DataReader<out T> {
fun read(): T
fun readAll(): List<T>
}
class StringReader : DataReader<String> {
override fun read(): String = "kotlin"
override fun readAll(): List<String> = listOf("kotlin", "java")
}
class IntReader : DataReader<Int> {
override fun read(): Int = 42
override fun readAll(): List<Int> = listOf(1, 2, 3)
}
fun printAll(reader: DataReader<Any>) {
println(reader.read())
}
fun main() {
printAll(StringReader()) // Работает благодаря out
printAll(IntReader()) // И это тоже
}
Без out пришлось бы писать отдельные функции для каждого типа или использовать звездочку * (проекцию).
Типичные ошибки и как их избежать
- Ошибка: «Type parameter T