Files
obsidian/Kotlin/Дженерики/Вариантность.md
T

352 lines
18 KiB
Markdown
Raw Blame History

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