Files
obsidian/Kotlin/Дженерики/Проекции типов.md
T

332 lines
16 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: Вариантность в месте использования, star-проекция и ограничения, которые она накладывает
tags:
- kotlin
- generics
---
[Вариантность](Вариантность.md) объявляется при определении класса. Но инвариантный класс изменить нельзя - ни свой `MutableList`, ни чужой из библиотеки. Для этого существуют проекции: вариантность в **месте использования**.
```kotlin
fun copy(from: MutableList<out Any>, to: MutableList<Any>) {
for (item in from) to.add(item)
}
fun main() {
val source = mutableListOf("a", "b")
val target = mutableListOf<Any>(1)
copy(source, target)
println(target) // [1, a, b]
}
```
Без `out` в сигнатуре `MutableList<String>` не подошёл бы под `MutableList<Any>`. С `out` компилятор принимает любой `MutableList<T>`, где `T` - подтип `Any`, но взамен запрещает использовать `from` на запись.
## out-проекция
```kotlin
fun sumAll2(numbers: MutableList<out Number>): Double {
// numbers.add(1) // ошибка: add принимает Nothing
return numbers.sumOf { it.toDouble() }
}
fun main() {
println(sumAll2(mutableListOf(1, 2, 3))) // 6.0
println(sumAll2(mutableListOf(1.5, 2.5))) // 4.0
}
```
Внутри функции `MutableList<out Number>` ведёт себя как `List<Number>`: читать можно, писать нельзя. Формально тип параметра `add` становится `Nothing` - значения такого типа не существует, поэтому вызвать его невозможно.
## in-проекция
Зеркальный случай - нужно только писать:
```kotlin
fun fillWithOnes(target: MutableList<in Int>) {
target.add(1)
target.add(2)
// val x: Int = target[0] // ошибка: читается как Any?
val any: Any? = target[0]
println("первый: $any")
}
fun main() {
val numbers = mutableListOf<Number>(0.5)
fillWithOnes(numbers)
println(numbers) // [0.5, 1, 2]
val anys = mutableListOf<Any>("x")
fillWithOnes(anys)
println(anys) // [x, 1, 2]
}
```
`MutableList<in Int>` принимает `MutableList<Int>`, `MutableList<Number>`, `MutableList<Any>` - любой список, в который `Int` поместится. Чтение даёт `Any?`, потому что компилятор не знает фактический тип элементов.
| Проекция | Можно | Нельзя | Принимает |
|---|---|---|---|
| `MutableList<out T>` | читать как `T` | писать | `MutableList<подтип T>` |
| `MutableList<in T>` | писать `T` | читать как `T` (только `Any?`) | `MutableList<супертип T>` |
| `MutableList<T>` | всё | - | только точно `MutableList<T>` |
Это ровно PECS из Java: **P**roducer - `out`, **C**onsumer - `in`.
## Копирование с обеими проекциями
```kotlin
fun <T> transfer(from: MutableList<out T>, to: MutableList<in T>) {
for (item in from) to.add(item)
}
fun main() {
val source = mutableListOf(1, 2, 3)
val target = mutableListOf<Number>(0.0)
transfer(source, target)
println(target) // [0.0, 1, 2, 3]
}
```
Здесь `from` - производитель, `to` - потребитель. Такая сигнатура максимально гибкая: подойдёт любая пара списков, где элементы источника помещаются в приёмник.
## Star-проекция
Когда параметр типа не важен вообще, пишут `*`:
```kotlin
fun describeSize(collection: Collection<*>): String = "элементов: ${collection.size}"
fun main() {
println(describeSize(listOf(1, 2))) // элементов: 2
println(describeSize(setOf("a"))) // элементов: 1
println(describeSize(mapOf("a" to 1).values)) // элементов: 1
}
```
`*` - это не «любой тип», а «неизвестный, но конкретный тип». Правила такие же, как у проекций, применённые сразу в обе стороны:
```kotlin
fun main() {
val list: MutableList<*> = mutableListOf(1, 2)
println(list.size) // 2 — не зависит от T
val item: Any? = list[0] // чтение даёт Any?
println(item) // 1
// list.add(3) // ошибка: параметр имеет тип Nothing
list.removeAt(0) // не требует T — разрешено
println(list) // [2]
}
```
| Объявление | `*` означает |
|---|---|
| `Foo<out T : TUpper>` | чтение как `TUpper` |
| `Foo<in T>` | запись как `Nothing` (невозможна) |
| `Foo<T>` | чтение `TUpper`, запись `Nothing` |
```kotlin
fun main() {
val map: Map<*, *> = mapOf("a" to 1)
println(map.size) // 1
println(map.keys.first()) // a — тип Any?
println(map.values.first()) // 1 — тип Any?
val function: (Nothing) -> Any? = { "любая функция" }
println(function::class.simpleName) // null — у лямбды нет имени класса
}
```
## Star-проекция и проверка типов
Главное применение `*` - проверки `is`, где из-за стирания типов иначе никак:
```kotlin
fun classify(value: Any): String = when (value) {
is List<*> -> "список из ${value.size}"
is Map<*, *> -> "карта из ${value.size}"
is Set<*> -> "множество"
is Array<*> -> "массив из ${value.size}"
else -> "что-то ещё"
}
fun main() {
println(classify(listOf(1, 2))) // список из 2
println(classify(mapOf("a" to 1))) // карта из 1
println(classify(arrayOf(1, 2, 3))) // массив из 3
println(classify(42)) // что-то ещё
}
```
`is List<Int>` написать нельзя - компилятор откажется, потому что проверить это в рантайме невозможно. Подробнее о стирании: [Параметрический полиморфизм](Параметрический%20полиморфизм.md)
Безопасная работа с содержимым после такой проверки:
```kotlin
fun sumIfNumbers(value: Any): Double? {
if (value !is List<*>) return null
return value.filterIsInstance<Number>().sumOf { it.toDouble() }
}
fun main() {
println(sumIfNumbers(listOf(1, 2.5))) // 3.5
println(sumIfNumbers(listOf(1, "не число"))) // 1.0 — лишнее отфильтровано
println(sumIfNumbers("строка")) // null
}
```
`filterIsInstance` проверяет каждый элемент по отдельности - это единственный честный способ сузить тип элементов после стирания.
## Когда проекция, когда параметр типа
```kotlin
fun printFirst(items: List<*>) = println(items.firstOrNull())
fun <T> firstOrNull3(items: List<T>): T? = items.firstOrNull()
fun main() {
printFirst(listOf("a")) // a — но тип потерян
val value: String? = firstOrNull3(listOf("a"))
println(value?.length) // 1 — тип сохранён
}
```
Правило: если тип нужен в возвращаемом значении - объявляйте `<T>`. Проекция подходит, когда тип внутри функции действительно не используется.
```kotlin
fun <T> copyAll(from: List<T>, to: MutableList<in T>) {
to.addAll(from)
}
fun countAll(collections: List<Collection<*>>): Int = collections.sumOf { it.size }
fun main() {
val target = mutableListOf<Any>()
copyAll(listOf(1, 2), target)
println(target) // [1, 2]
println(countAll(listOf(listOf(1), setOf("a", "b")))) // 3
}
```
## Проекции в своих классах
```kotlin
class Container<T>(private val items: MutableList<T>) {
fun add(item: T) { items.add(item) }
fun get(index: Int): T = items[index]
val size: Int get() = items.size
fun addAllFrom(other: Container<out T>) {
repeat(other.size) { items.add(other.get(it)) }
}
}
fun main() {
val numbers = Container<Number>(mutableListOf(0))
val ints = Container(mutableListOf(1, 2))
numbers.addAllFrom(ints) // Container<Int> подошёл под Container<out Number>
println(numbers.size) // 3
println(numbers.get(1)) // 1
}
```
Без `out` в `addAllFrom` пришлось бы либо делать класс ковариантным (невозможно - есть `add`), либо требовать точного совпадения типов.
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `List<? extends T>` | `List<out T>` |
> | `List<? super T>` | `List<in T>` |
> | `List<?>` | `List<*>` |
> | `Class<?>` | `KClass<*>` |
> | wildcard нужен почти всегда | проекция нужна редко |
> | `? extends` на чтение, `? super` на запись | то же самое |
>
> **Это тот же механизм, что wildcard в Java.** `MutableList<out Number>` и `List<? extends Number>` означают одно и то же: неизвестный подтип, доступный только на чтение. Семантика, ограничения и правило PECS совпадают дословно.
>
> **Разница в том, как часто это нужно.** В Java `List` инвариантен, поэтому wildcard приходится писать почти в каждой сигнатуре, принимающей коллекцию:
> ```java
> double sum(Collection<? extends Number> items) { ... }
> void addAll(List<? super String> dest) { ... }
> ```
> В Kotlin `List<out E>` уже ковариантен, и проекция нужна только для изменяемых коллекций и собственных инвариантных классов:
> ```kotlin
> fun sum4(items: Collection<Number>) = items.sumOf { it.toDouble() }
>
> fun main() { println(sum4(listOf(1, 2))) } // 3.0
> ```
> Практическое следствие: `out`/`in` в сигнатурах Kotlin встречаются на порядок реже, чем wildcard в Java. Если вы пишете проекцию - стоит сначала проверить, не подойдёт ли read-only `List`.
>
> **Синтаксис короче и симметричен объявлению.** В Java `extends`/`super` в wildcard никак не связаны с объявлением класса. В Kotlin те же `out`/`in`, что и в declaration-site variance, что делает связь очевидной: проекция - это «применить вариантность здесь, раз её нет в классе».
>
> **`List<*>` и `List<?>` ведут себя одинаково**, но `*` в Kotlin учитывает верхнюю границу параметра. Для `class Box<T : Number>` чтение из `Box<*>` даёт `Number`, а не `Object`:
> ```kotlin
> class NumberBox<T : Number>(val value: T)
>
> fun main() {
> val box: NumberBox<*> = NumberBox(42)
> val n: Number = box.value // Number, а не Any?
> println(n.toDouble()) // 42.0
> }
> ```
> Java в этой ситуации тоже выводит границу, но менее последовательно - через capture conversion, которая не всегда видна в сообщениях об ошибках.
>
> Сравнить: [Параметрический полиморфизм в Java](../../Программирование%20на%20Java/Дженерики/Параметрический%20полиморфизм.md)
> [!warning] Подводные камни
> **`out`-проекция запрещает запись полностью:**
> ```kotlin
> fun broken(items: MutableList<out Number>) {
> // items.add(1) // ошибка даже для Int
> // items[0] = 1 // тоже ошибка
> items.removeAt(0) // а это можно — не требует T
> println(items.size)
> }
>
> fun main() { broken(mutableListOf(1, 2)) } // 1
> ```
> Запрещены только методы, принимающие `T`. `clear`, `removeAt`, `size` работают - это иногда удивляет, потому что коллекция вроде бы «только для чтения».
>
> **`in`-проекция теряет тип при чтении:**
> ```kotlin
> fun readBack(items: MutableList<in String>) {
> items.add("новый")
> val value = items.last() // Any?, не String
> println(value.toString().length) // приходится приводить
> }
>
> fun main() { readBack(mutableListOf("a")) } // 6
> ```
>
> **Проекция `*` не равна `Any?`:**
> ```kotlin
> fun main() {
> val stars: MutableList<*> = mutableListOf(1, 2)
> val anys: MutableList<Any?> = mutableListOf(1, 2)
>
> // stars.add(3) // ошибка
> anys.add(3) // работает
> println("${stars.size} ${anys.size}") // 2 3
> }
> ```
> `MutableList<Any?>` - конкретный тип, в который можно класть что угодно. `MutableList<*>` - неизвестный тип, в который нельзя класть ничего.
>
> **`Array<*>` и `Array<Any?>` тоже разные** - последний не примет `Array<String>`, потому что массивы инвариантны.
>
> **После `is List<*>` элементы имеют тип `Any?`:**
> ```kotlin
> fun process4(value: Any) {
> if (value is List<*>) {
> // value.sumOf { it } // ошибка: it — Any?
> println(value.filterIsInstance<Int>().sum())
> }
> }
>
> fun main() { process4(listOf(1, 2, "x")) } // 3
> ```
> Умное приведение даёт только `List<Any?>`. Содержимое нужно проверять поэлементно.
>
> **`is List<Int>` не компилируется, а `as List<Int>` компилируется с предупреждением** - и откладывает `ClassCastException` до момента использования:
> ```kotlin
> fun main() {
> val any: Any = listOf("a", "b")
> @Suppress("UNCHECKED_CAST")
> val ints = any as List<Int>
> println(ints.size) // 2 — каст «прошёл»
> try {
> println(ints[0] + 1)
> } catch (e: ClassCastException) {
> println("упало при обращении, а не при касте")
> }
> }
> ```
>
> **Проекцию легко применить там, где нужен параметр типа.** `fun first(items: List<*>): Any?` теряет тип результата; `fun <T> first(items: List<T>): T?` его сохраняет. Проекция - для случаев, когда тип внутри функции не нужен совсем.
>
> **Проекция `out` на уже ковариантном типе бессмысленна.** `List<out String>` компилируется, но ничего не добавляет к `List<String>`, потому что `List` уже объявлен как `List<out E>`. Компилятор выдаёт предупреждение о лишней проекции.
>
> **Внутри класса `this` не проецируется.** Если метод принимает `Container<out T>`, то `other.add(x)` запрещён, но `this.add(x)` работает - проекция применена только к параметру.
Далее: [Reified и inline](Reified%20и%20inline.md)