--- title: Вариантность description: out и in в объявлении класса, ковариантность, контравариантность и правило PECS tags: - kotlin - generics --- Вариантность отвечает на один вопрос: если `String` - подтип `Any`, то является ли `Box` подтипом `Box`? По умолчанию - **нет**. Дженерики инвариантны: ```kotlin class Box6(val value: T) fun main() { val str: Box6 = Box6("текст") // val any: Box6 = str // ошибка: несовместимые типы println(str.value) // текст } ``` Причина видна, если допустить обратное на изменяемой коллекции: ```kotlin fun main() { val strings = mutableListOf("a", "b") // val anys: MutableList = strings // если бы компилировалось... // anys.add(42) // ...сюда положили бы Int // val s: String = strings[0] // ...и здесь получили бы ClassCastException println(strings.size) // 2 } ``` Запрет инвариантности спасает от этой дыры. Но он же мешает там, где изменения нет. ## out - ковариантность `out T` означает, что `T` встречается только **на выходе**: в возвращаемых значениях, но не в параметрах. ```kotlin class Producer(private val value: T) { fun get(): T = value // T на выходе — разрешено // fun set(v: T) { } // ошибка: T на входе запрещён } fun main() { val strings: Producer = Producer("текст") val anys: Producer = strings // работает! println(anys.get()) // текст } ``` Это безопасно: из `Producer` можно только читать, а всякая прочитанная `String` является `Any`. Именно так объявлен `List`: ```kotlin fun printSize(items: List) = items.size fun main() { println(printSize(listOf("a", "b"))) // 2 println(printSize(listOf(1, 2, 3))) // 3 val numbers: List = listOf(1, 2.0, 3L) println(numbers.sumOf { it.toDouble() }) // 6.0 } ``` В stdlib: `List`, `Set`, `Map`, `Iterable`, `Sequence`, `Pair`. ## in - контравариантность `in T` означает, что `T` встречается только **на входе**: в параметрах, но не в возвращаемых типах. ```kotlin class Consumer { fun consume(value: T) { println("принял $value") } // fun produce(): T = TODO() // ошибка: T на выходе запрещён } fun main() { val anyConsumer: Consumer = Consumer() val stringConsumer: Consumer = anyConsumer // работает! stringConsumer.consume("текст") // принял текст } ``` Направление перевёрнуто: `Consumer` является подтипом `Consumer`. Логика простая - тот, кто умеет принимать любой `Any`, справится и со `String`. Классический пример - `Comparator`: ```kotlin fun main() { val anyComparator = Comparator { a, b -> a.hashCode() - b.hashCode() } val strings = mutableListOf("bb", "a") strings.sortWith(anyComparator) // Comparator для List println(strings.size) // 2 val byLength: Comparator = compareBy { it.length } println(listOf("ccc", "a", "bb").sortedWith(byLength)) // [a, bb, ccc] } ``` `sortedWith` принимает `Comparator`, поэтому компаратор по `CharSequence` годится для `List`. В stdlib: `Comparator`, `Comparable`, функциональные типы `(in P) -> out R`. ## Сводка | Объявление | Называется | `T` может быть | Направление подтипов | |---|---|---|---| | `Box` | инвариантность | везде | нет связи | | `Box` | ковариантность | только возврат | `Box` -> `Box` | | `Box` | контравариантность | только параметр | `Box` -> `Box` | ```mermaid graph TD subgraph co["out T: ковариантность"] CS["Producer<String>"] --> CA["Producer<Any>"] end subgraph contra["in T: контравариантность"] KA["Consumer<Any>"] --> KS["Consumer<String>"] end subgraph inv["T: инвариантность"] IS["Box<String>"] IA["Box<Any>"] end ``` ## Функциональные типы Вариантность функциональных типов - самый частый случай, с которым работают, не замечая этого: ```kotlin fun main() { val produceString: () -> String = { "текст" } val produceAny: () -> Any = produceString // возврат ковариантен println(produceAny()) // текст val acceptAny: (Any) -> Unit = { println("принял $it") } val acceptString: (String) -> Unit = acceptAny // параметр контравариантен acceptString("строка") // принял строка } ``` Тип `(P) -> R` объявлен как `Function1`: функция, принимающая больше и возвращающая меньше, подходит везде, где ожидается более узкая. Практическое следствие для API: ```kotlin class EventBus { private val handlers = mutableListOf<(Any) -> Unit>() fun subscribe(handler: (Any) -> Unit) { handlers.add(handler) } fun publish(event: Any) = handlers.forEach { it(event) } } fun main() { val bus = EventBus() bus.subscribe { println("получено: $it") } bus.publish("событие") // получено: событие bus.publish(42) // получено: 42 } ``` ## Когда помечать свой класс Правило простое: если параметр типа используется **только** на выходе - ставьте `out`, только на входе - `in`. Компилятор проверит и подскажет. ```kotlin interface Reader { fun read(): T fun readAll(count: Int): List // List — тоже выход, ок } interface Writer { fun write(value: T) fun writeAll(values: List) // List на входе, ок } interface Storage : Reader, Writer // и то и другое — инвариантен class MemoryStorage(private var value: T) : Storage { override fun read(): T = value override fun readAll(count: Int) = List(count) { value } override fun write(value: T) { this.value = value } override fun writeAll(values: List) { values.lastOrNull()?.let { value = it } } } fun main() { val storage = MemoryStorage("начальное") val reader: Reader = storage // ковариантность работает storage.write("новое") println(reader.read()) // новое println(reader.readAll(2)) // [новое, новое] } ``` Разделение на `Reader` и `Writer` - типичный приём: инвариантный класс реализует оба интерфейса, а клиентам отдаётся вариантный. ## Nothing и вариантность `Nothing` - подтип всех типов, поэтому `List` подходит под любой `List`: ```kotlin fun main() { val empty: List = emptyList() val ints: List = empty // работает благодаря out val strings: List = empty println("${ints.size} ${strings.size}") // 0 0 } ``` Именно поэтому `emptyList()` один на все типы - это синглтон `EmptyList : List`. Подробнее: [Any, Unit, Nothing](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md) Тот же приём в sealed-иерархиях: ```kotlin sealed interface Outcome { data class Ok(val value: T) : Outcome data object Empty : Outcome } fun describe4(o: Outcome): String = when (o) { is Outcome.Ok -> "значение ${o.value}" Outcome.Empty -> "пусто" } fun main() { println(describe4(Outcome.Ok(1))) // значение 1 println(describe4(Outcome.Empty)) // пусто } ``` `Empty` может быть `object`, а не обобщённым классом, только потому что `Outcome` ковариантен. Подробнее: [Sealed class и interface](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Sealed%20class%20и%20interface.md) ## Ограничение out: нельзя принимать T Иногда ковариантный класс всё-таки должен принять `T`. Для этого параметр объявляют с более широкой границей: ```kotlin class ImmutableBox(private val value: T) { fun get(): T = value fun withDefault(default: R): R where R : Any = default fun plus(other: U): List = listOf(value, other) } fun main() { val box: ImmutableBox = ImmutableBox("a") val any: ImmutableBox = box println(any.get()) // a println(box.plus(1)) // [a, 1] } ``` В stdlib этот приём виден в `List.plus`: ```kotlin fun main() { val strings: List = listOf("a") val mixed: List = strings + 1 // plus объявлен как List.plus(element: T) println(mixed) // [a, 1] } ``` Тип `T` здесь выводится как `Any`, а не как `String`, поэтому ковариантность не нарушается. > [!info] Отличия от Java > | Java | Kotlin | > |---|---| > | `List` в каждой сигнатуре | `List` один раз в объявлении | > | `List` | `MutableList` или `in` в объявлении | > | `Collection` | `Collection<*>` | > | нет declaration-site variance | `out`/`in` при объявлении класса | > | PECS - правило для программиста | компилятор проверяет сам | > > **Главное отличие - где объявляется вариантность.** Java знает только use-site variance: интерфейс `List` инвариантен, и каждый метод, принимающий «список чего-то производного», обязан писать wildcard: > ```java > void printAll(Collection items) { ... } > double sum(List numbers) { ... } > void addAll(List dest, List src) { ... } > ``` > В Kotlin `List` объявлен один раз в stdlib, и дальше всё работает само: > ```kotlin > fun sum3(numbers: List) = numbers.sumOf { it.toDouble() } > > fun main() { > println(sum3(listOf(1, 2, 3))) // 6.0 — List подошёл > } > ``` > Это убирает wildcard из подавляющего большинства сигнатур. Use-site variance в Kotlin тоже есть - это [проекции типов](Проекции%20типов.md), но нужны они редко. > > **PECS из правила превратился в проверку компилятора.** В Java «Producer Extends, Consumer Super» - мнемоника, которую программист применяет вручную; забыл - и метод неудобен в использовании, но компилируется. В Kotlin вы помечаете класс `out`, и компилятор откажется принять `T` в параметр: ошибка возникает при объявлении, а не у пользователя API. > > **`Comparator` в Kotlin контравариантен автоматически.** В Java `Comparator` инвариантен, и методы вроде `Collections.sort` объявляют `Comparator`. В Kotlin stdlib `Comparator` уже `in T`, поэтому `sortedWith(comparator)` принимает компаратор по любому супертипу без дополнительных обозначений. > > **Разделение `List`/`MutableList` делает `out` возможным.** В Java `List` содержит `add`, поэтому ковариантным он быть не может в принципе - только через wildcard в месте использования. Kotlin вынес мутаторы в `MutableList` (инвариантный), и `List` стал безопасным по построению. > > Сравнить: [Параметрический полиморфизм в Java](../../Программирование%20на%20Java/Дженерики/Параметрический%20полиморфизм.md), [Полиморфизм](../../Программирование%20на%20Java/ООП/Полиморфизм/Полиморфизм.md) > [!warning] Подводные камни > **`out` запрещает `T` в параметрах - включая неочевидные места:** > ```kotlin > class Holder(private val value: T) { > fun get(): T = value > // fun contains(other: T): Boolean = value == other // ошибка > fun contains(other: Any?): Boolean = value == other // так можно > } > > fun main() { > println(Holder("a").contains("a")) // true > } > ``` > Расширение границы до `Any?` - стандартный обход, но оно теряет проверку типов: `contains(42)` тоже скомпилируется. > > **`private` функции могут нарушать вариантность:** > ```kotlin > class Mutable(private var value: T) { > fun get(): T = value > private fun set(v: T) { value = v } // private — проверка не применяется > } > > fun main() { println(Mutable("a").get()) } // a > ``` > Компилятор разрешает `T` на входе в `private` членах, потому что снаружи они недоступны. Но `private var value: T` - всё ещё изменяемое состояние; если его изменить изнутри класса, ковариантный «снимок» окажется нестабильным. > > **`MutableList` инвариантен, и это не ошибка:** > ```kotlin > fun main() { > val strings = mutableListOf("a") > // val anys: MutableList = strings // не скомпилируется — и правильно > val readOnly: List = strings // а так можно > println(readOnly.size) // 1 > } > ``` > Если нужно передать изменяемую коллекцию «как список чего угодно» - передавайте `List`, а не `MutableList`. > > **Массивы инвариантны в Kotlin и ковариантны в Java:** > ```kotlin > fun main() { > val strings = arrayOf("a", "b") > // val anys: Array = strings // ошибка компиляции > println(strings.size) // 2 > } > ``` > В Java `String[]` является `Object[]`, поэтому `Object[] o = new String[1]; o[0] = 1;` компилируется и бросает `ArrayStoreException` в рантайме. Kotlin закрыл эту дыру ценой того, что `Array` не принимает `Array`. Для чтения используйте `Array`. > > **Вариантность не распространяется на вложенные параметры автоматически:** > ```kotlin > fun main() { > val nested: List> = listOf(listOf("a")) > val widened: List> = nested // работает: оба List ковариантны > println(widened.size) // 1 > > val maps: List> = listOf(mutableListOf("a")) > // val wide: List> = maps // не работает: MutableList инвариантен > println(maps.size) // 1 > } > ``` > > **`in` и `out` легко перепутать направление.** Мнемоника: `out` - значение **выходит** из объекта (producer, можно только читать), `in` - значение **входит** в объект (consumer, можно только писать). Если нужны обе операции, вариантность невозможна. > > **`Map` ковариантен только по значению:** > ```kotlin > fun main() { > val m: Map = mapOf("a" to 1) > val widened: Map = m // ок — V ковариантен > // val keys: Map = m // ошибка — K инвариантен > println(widened.size) // 1 > } > ``` > Ключ инвариантен, потому что он используется на входе в `get(key)`. > > **Ковариантный класс с `var`-полем типа `T` объявить нельзя** - сеттер принимает `T`. Если нужно изменяемое состояние, либо инвариантность, либо приватный сеттер с осознанием последствий. Далее: [Проекции типов](Проекции%20типов.md)