Kotlin guide done. (20.10.26)

This commit is contained in:
2026-10-20 12:54:25 +03:00
parent da3ad9dd1a
commit 6853ada348
81 changed files with 30112 additions and 0 deletions
@@ -0,0 +1,331 @@
---
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)