352 lines
18 KiB
Markdown
352 lines
18 KiB
Markdown
---
|
||
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<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<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)
|