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,359 @@
---
title: Reified и inline
description: Как reified обходит стирание типов, что компилятор делает с inline-функциями и чего reified не умеет
tags:
- kotlin
- generics
---
На JVM параметры типа стираются: в рантайме `List<Int>` и `List<String>` - один класс. Но если функция объявлена `inline`, компилятор подставляет её тело в место вызова, где фактический тип **известен**. Это и позволяет `reified`.
```kotlin
inline fun <reified T> isOfType(value: Any): Boolean = value is T
fun main() {
println(isOfType<String>("текст")) // true
println(isOfType<Int>("текст")) // false
println(isOfType<List<*>>(listOf(1))) // true
}
```
Без `reified` строка `value is T` не компилируется - проверить стёртый тип невозможно.
## Что делает компилятор
```kotlin
inline fun <reified T> nameOf(): String = T::class.simpleName ?: "?"
fun main() {
println(nameOf<String>()) // String
println(nameOf<Int>()) // Int
}
```
В байт-коде вызова `nameOf<String>()` не остаётся: компилятор подставляет тело и заменяет `T` на `String`, получая буквально `String::class.simpleName`. Поэтому `reified` и требует `inline` - без подстановки подставлять нечего.
Следствие: `reified`-функцию нельзя вызвать из Java и нельзя передать `T` динамически.
## Главный случай: парсинг и десериализация
Задача, ради которой `reified` чаще всего и используют:
```kotlin
inline fun <reified T> parseAs(raw: String): T? = when (T::class) {
Int::class -> raw.toIntOrNull() as T?
Long::class -> raw.toLongOrNull() as T?
Double::class -> raw.toDoubleOrNull() as T?
Boolean::class -> raw.toBooleanStrictOrNull() as T?
String::class -> raw as T
else -> null
}
fun main() {
println(parseAs<Int>("42")) // 42
println(parseAs<Int>("abc")) // null
println(parseAs<Double>("1.5")) // 1.5
println(parseAs<Boolean>("true")) // true
println(parseAs<String>("текст")) // текст
}
```
В Java такая функция потребовала бы параметр `Class<T> type` в сигнатуре. Именно это делают Jackson (`readValue(json, User.class)`) и Gson (`fromJson(json, User.class)`); Kotlin-обёртки над ними объявляют `inline fun <reified T> readValue(json: String): T`, и тип выводится из контекста:
```kotlin
// С kotlinx.serialization или Jackson Kotlin module:
// val user: User = mapper.readValue(json) // тип берётся из объявления переменной
```
## filterIsInstance и прочее в stdlib
```kotlin
fun main() {
val mixed: List<Any> = listOf(1, "a", 2.0, 3, "b")
println(mixed.filterIsInstance<Int>()) // [1, 3]
println(mixed.filterIsInstance<String>()) // [a, b]
println(mixed.filterIsInstance<Number>()) // [1, 2.0, 3]
val target = mutableListOf<Int>()
mixed.filterIsInstanceTo(target)
println(target) // [1, 3]
}
```
`filterIsInstance` объявлена как `inline fun <reified R> Iterable<*>.filterIsInstance(): List<R>` - без `reified` поэлементная проверка типа была бы невозможна.
Другие `reified`-функции stdlib:
```kotlin
fun main() {
val array = arrayOf(1, 2, 3) // arrayOf — reified
println(listOf("a", "b").toTypedArray().size) // 2 — тоже reified
println(array.javaClass.simpleName) // Integer[]
val typed: Array<String?> = arrayOfNulls(2)
println(typed.size) // 2
println(emptyArray<Int>().size) // 0
}
```
`toTypedArray` без `reified` не смогла бы создать массив правильного типа - у `Array<T>` в рантайме есть настоящий тип элементов.
## Безопасное приведение
```kotlin
inline fun <reified T> Any?.asOrNull(): T? = this as? T
fun main() {
val value: Any = "текст"
println(value.asOrNull<String>()?.length) // 5
println(value.asOrNull<Int>()) // null — без исключения
}
```
Это удобнее, чем `as? String` в каждом месте, когда тип приходит из параметра обобщённой функции.
Разбор `Map<String, Any>` - типичная задача при работе с конфигами и JSON:
```kotlin
inline fun <reified T> Map<String, Any>.getTyped(key: String): T? = this[key] as? T
fun main() {
val config = mapOf<String, Any>("port" to 8080, "host" to "localhost", "tls" to true)
println(config.getTyped<Int>("port")) // 8080
println(config.getTyped<String>("host")) // localhost
println(config.getTyped<Int>("host")) // null — тип не тот
println(config.getTyped<Int>("нет")) // null — ключа нет
}
```
## reified и Java-рефлексия
```kotlin
inline fun <reified T : Any> newInstance(): T = T::class.java.getDeclaredConstructor().newInstance()
class Service2 {
override fun toString() = "сервис создан"
}
fun main() {
val service: Service2 = newInstance()
println(service) // сервис создан
}
```
`T::class` даёт `KClass<T>`, `T::class.java` - `Class<T>`. Это мост к любому Java API, требующему `Class`:
```kotlin
inline fun <reified T> typeName(): String = T::class.java.name
fun main() {
println(typeName<String>()) // java.lang.String
println(typeName<Int>()) // java.lang.Integer — не int!
println(typeName<List<String>>()) // java.util.List
println(Int::class.java.name) // int — а так примитив
}
```
Две детали из этого примера:
- `T::class` знает только стёртый тип: `List<String>` и `List<Int>` дадут одинаковый результат.
- Параметр типа всегда представлен ссылочным типом, поэтому `T = Int` даёт `java.lang.Integer`, хотя написанный напрямую `Int::class.java` возвращает примитивный `int`.
## Что происходит с inline
`reified` - побочный эффект `inline`, а у `inline` есть своя основная задача: убрать объект лямбды.
```kotlin
inline fun measure(block: () -> Unit): Long {
val start = System.nanoTime()
block()
return System.nanoTime() - start
}
fun main() {
val time = measure { (1..1000).sum() }
println(time > 0) // true
}
```
Тело `block` подставляется прямо в `main` - объекта `Function0` не создаётся. Подробнее об этом и о `noinline`/`crossinline`: [Inline-функции](../Функциональный%20Kotlin/Inline-функции.md)
Для `reified` важно только одно следствие inline: **функция перестаёт существовать как метод**, поэтому её нельзя вызвать не из Kotlin и нельзя взять на неё ссылку.
## Ограничения reified
```kotlin
inline fun <reified T> demo(): String {
val cls = T::class // можно
val arr = arrayOfNulls<T>(1) // можно
// val instance = T() // нельзя: нет конструктора в типе
// val companion = T.Companion // нельзя
return "${cls.simpleName} ${arr.size}"
}
fun main() {
println(demo<String>()) // String 1
}
```
| Можно с `reified T` | Нельзя |
|---|---|
| `value is T`, `value as? T` | `T()` - создать экземпляр |
| `T::class`, `T::class.java` | обращаться к статическим членам `T` |
| `arrayOfNulls<T>()`, `arrayOf<T>()` | различать `List<Int>` и `List<String>` |
| передать `T` в другую `reified`-функцию | вызвать из Java |
| вывести `T` из типа переменной | передать тип как значение в рантайме |
Создание экземпляра требует рефлексии:
```kotlin
inline fun <reified T : Any> createOrNull(): T? = runCatching {
T::class.java.getDeclaredConstructor().newInstance()
}.getOrNull()
class WithDefault { override fun toString() = "создан" }
class WithoutDefault(val x: Int)
fun main() {
println(createOrNull<WithDefault>()) // создан
println(createOrNull<WithoutDefault>()) // null — нет пустого конструктора
}
```
## Проброс reified дальше
```kotlin
inline fun <reified T> logType(value: Any) {
println("${T::class.simpleName}: ${check2<T>(value)}")
}
inline fun <reified T> check2(value: Any): Boolean = value is T
fun main() {
logType<String>("текст") // String: true
logType<Int>("текст") // Int: false
}
```
`reified` передаётся в другие `inline reified`-функции - каждая подстановка сохраняет фактический тип. А вот в обычную обобщённую функцию - нет:
```kotlin
fun <T> notReified(value: Any): Boolean = value != null // T здесь стёрт
inline fun <reified T> caller(value: Any): Boolean = notReified<T>(value)
fun main() {
println(caller<String>("текст")) // true — но проверки типа внутри нет
}
```
## Альтернатива без reified
Когда inline невозможен (рекурсия, большая функция, вызов из Java), тип передают явно:
```kotlin
fun <T : Any> parseWith(raw: String, type: kotlin.reflect.KClass<T>): T? = when (type) {
Int::class -> raw.toIntOrNull() as T?
String::class -> raw as T
else -> null
}
fun main() {
println(parseWith("42", Int::class)) // 42
println(parseWith("текст", String::class)) // текст
}
```
Это ровно Java-подход с `Class<T>`. Он многословнее в вызове, зато функция остаётся обычным методом.
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `<T> T parse(String s, Class<T> type)` | `inline fun <reified T> parse(s: String): T` |
> | `parse(json, User.class)` | `parse<User>(json)` или `val u: User = parse(json)` |
> | `x instanceof T` - невозможно | `x is T` с `reified` |
> | `new T[]` - запрещён | `arrayOf<T>()` с `reified` |
> | `T.class` - невозможно | `T::class` с `reified` |
> | нет inline-функций | `inline` + подстановка на этапе компиляции |
>
> **`reified` устраняет параметр-токен типа.** Это самая заметная разница в API. Jackson в Java:
> ```java
> User user = mapper.readValue(json, User.class);
> List<User> users = mapper.readValue(json, new TypeReference<List<User>>() {});
> ```
> Kotlin-модуль того же Jackson:
> ```kotlin
> // val user: User = mapper.readValue(json)
> // val users: List<User> = mapper.readValue(json)
> ```
> Второй случай особенно показателен: в Java для обобщённого типа приходится создавать анонимный класс `TypeReference`, чтобы тип сохранился в сигнатуре суперкласса - единственное место, где Java не стирает дженерики. Kotlin обходится выводом типа из переменной.
>
> **Стирание никуда не делось.** `reified` работает только потому, что тип подставляется при компиляции. Поэтому `reified T` **не** даёт настоящей информации об аргументах типа:
> ```kotlin
> inline fun <reified T> describeType(): String = T::class.simpleName ?: "?"
>
> fun main() {
> println(describeType<List<String>>()) // List — параметр потерян
> println(describeType<List<Int>>()) // List — то же самое
> }
> ```
> Для полного типа с аргументами нужен `typeOf<T>()` из `kotlin.reflect`, и он требует зависимости `kotlin-reflect`.
>
> **Inline-функций в Java нет.** JVM инлайнит методы на уровне JIT в рантайме, но это оптимизация, невидимая системе типов. Kotlin инлайнит на этапе компиляции, что даёт две вещи сразу: отсутствие объекта лямбды и `reified`. Цена - рост размера байт-кода и невозможность вызвать такую функцию из Java.
>
> **`filterIsInstance` аналога в Java не имеет.** Эквивалент - `stream().filter(X.class::isInstance).map(X.class::cast)`, где класс указывается дважды.
>
> Сравнить: [Параметрический полиморфизм в Java](../../Программирование%20на%20Java/Дженерики/Параметрический%20полиморфизм.md), [Reflection API в Java](../../Программирование%20на%20Java/Рефлексия/Reflection%20API.md)
> [!warning] Подводные камни
> **`reified` без `inline` не компилируется** - это не выбор, а требование. Если функция не может быть inline (рекурсивная, очень большая, публичное API для Java), придётся передавать `KClass<T>` вручную.
>
> **Тип всё равно стёрт внутри аргументов:**
> ```kotlin
> inline fun <reified T> isList(value: Any): Boolean = value is T
>
> fun main() {
> println(isList<List<String>>(listOf(1, 2))) // true — хотя это List<Int>!
> }
> ```
> `is List<String>` в подставленном коде превращается в `is List<*>`. Это молчаливая ошибка: проверка выглядит строгой, но не различает аргументы типа.
>
> **`as T` внутри reified-функции не всегда проверяется:**
> ```kotlin
> inline fun <reified T> forceCast(value: Any): T = value as T
>
> fun main() {
> val list: List<String> = forceCast(listOf(1, 2)) // прошло!
> println(list.size) // 2
> try {
> println(list[0].length)
> } catch (e: ClassCastException) {
> println("упало только при обращении к элементу")
> }
> }
> ```
> Каст к `List<String>` сводится к касту к `List`, который успешен. Ошибка всплывает позже.
>
> **Inline раздувает байт-код.** Тело функции копируется в каждое место вызова. Для большой функции, вызываемой из сотни мест, это заметный рост размера класса - и потеря выигрыша от кеша инструкций процессора. `reified` оправдывает inline только для коротких функций.
>
> **Inline-функцию нельзя вызвать из Java:**
> ```kotlin
> inline fun <reified T> kotlinOnly(): String = T::class.simpleName ?: "?"
>
> fun main() { println(kotlinOnly<Int>()) } // Int
> ```
> Java-код не умеет подставлять тело и не знает про `reified`. Для API, доступного из Java, нужна обычная функция с `Class<T>`. Подробнее: [Kotlin из Java](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md)
>
> **`T()` не работает** - у параметра типа нет конструктора. Варианты: передать фабрику `() -> T` параметром или использовать рефлексию, которая упадёт при отсутствии пустого конструктора.
>
> **Ссылку на inline-функцию взять нельзя:**
> ```kotlin
> inline fun <reified T> typeNameOf(): String = T::class.simpleName ?: "?"
>
> fun main() {
> // val ref = ::typeNameOf // ошибка: нельзя взять ссылку
> println(typeNameOf<Int>()) // Int
> }
> ```
>
> **`reified` требует, чтобы тип был известен в месте вызова.** Передать тип из переменной нельзя:
> ```kotlin
> inline fun <reified T> show2(value: Any) = println(value is T)
>
> fun <U> wrapper(value: Any) {
> // show2<U>(value) // ошибка: U не reified
> show2<Any>(value)
> }
>
> fun main() { wrapper<String>("x") } // true
> ```
> Цепочка `reified` обрывается на первой обычной обобщённой функции.
>
> **`T::class.java` и `Int::class.java` дают разные классы:**
> ```kotlin
> inline fun <reified T> jvmName(): String = T::class.java.name
>
> fun main() {
> println(jvmName<Int>()) // java.lang.Integer
> println(Int::class.java.name) // int
> println(Int::class.javaObjectType.name) // java.lang.Integer
> }
> ```
> Через параметр типа всегда получается обёртка - примитив не может быть аргументом типа. Написанный напрямую `Int::class.java` даёт примитивный `int`, и если результат передаётся в Java-API, различие имеет значение: `Integer.class` и `int.class` - разные объекты, и, например, `getDeclaredMethod` по ним найдёт разные методы. Явные формы - `javaPrimitiveType` и `javaObjectType`. Подробнее: [Платформенные типы](../Синтаксис%20и%20типы/Null-безопасность/Платформенные%20типы.md)
>
> **Изменение тела inline-функции требует перекомпиляции всех вызывающих.** Если функция опубликована в библиотеке, клиенты, собранные со старой версией, продолжат исполнять старое тело - оно физически скопировано в их байт-код. Для публичных API это аргумент против inline.
Далее: [Функции высшего порядка](../Функциональный%20Kotlin/Функции%20высшего%20порядка.md)
@@ -0,0 +1,351 @@
---
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)
@@ -0,0 +1,393 @@
---
title: Параметрический полиморфизм
description: Обобщённые функции и классы, ограничения параметров типа и вывод типов
tags:
- kotlin
- generics
---
Дженерики позволяют написать код один раз для любого типа, сохранив проверку типов при компиляции.
```kotlin
fun <T> firstOrDefault(items: List<T>, default: T): T =
if (items.isEmpty()) default else items[0]
fun main() {
println(firstOrDefault(listOf(1, 2), 0)) // 1
println(firstOrDefault(emptyList(), "нет")) // нет
// println(firstOrDefault(listOf(1), "нет")) // ошибка: T не может быть и Int, и String
}
```
`<T>` объявляется **перед** именем функции. Компилятор выводит его из аргументов.
## Обобщённые классы
```kotlin
class Box5<T>(private val value: T) {
fun get(): T = value
fun <R> map(f: (T) -> R): Box5<R> = Box5(f(value))
override fun toString() = "Box($value)"
}
fun main() {
val intBox = Box5(42)
println(intBox.get() + 1) // 43
println(intBox.map { it.toString() + "!" }) // Box(42!)
val explicit = Box5<String>("явно")
println(explicit.get().length) // 4
}
```
Метод `map` объявляет **свой** параметр типа `R` - он не связан с `T` класса.
Несколько параметров:
```kotlin
class Pair2<A, B>(val first: A, val second: B) {
fun swap(): Pair2<B, A> = Pair2(second, first)
override fun toString() = "($first, $second)"
}
fun main() {
val p = Pair2("ключ", 1)
println(p) // (ключ, 1)
println(p.swap()) // (1, ключ)
}
```
## Ограничения параметра типа
Без ограничения у `T` доступны только методы `Any?`:
```kotlin
fun <T> describe(value: T): String = "значение: $value" // только toString
fun <T : Comparable<T>> maxOf3(a: T, b: T): T = if (a > b) a else b
fun <T : Number> sum2(items: List<T>): Double = items.sumOf { it.toDouble() }
fun main() {
println(describe(listOf(1))) // значение: [1]
println(maxOf3(3, 7)) // 7
println(maxOf3("abc", "abd")) // abd
println(sum2(listOf(1, 2.5, 3L))) // 6.5
}
```
Верхняя граница после двоеточия говорит компилятору, какие методы доступны.
По умолчанию граница - `Any?`, то есть `T` может быть nullable:
```kotlin
fun <T> firstOrNull2(items: List<T>): T? = items.firstOrNull()
fun <T : Any> requireFirst(items: List<T>): T = items.first()
fun main() {
println(firstOrNull2(listOf<String?>(null))) // null
println(requireFirst(listOf(1, 2))) // 1
}
```
`T : Any` - способ запретить nullable-аргумент типа.
### Несколько ограничений
```kotlin
interface Named4 { val name: String }
interface Aged { val age: Int }
fun <T> describe2(value: T): String where T : Named4, T : Aged =
"${value.name}, ${value.age}"
data class Person2(override val name: String, override val age: Int) : Named4, Aged
fun main() {
println(describe2(Person2("Иван", 30))) // Иван, 30
}
```
Больше одного ограничения задаётся через `where` после сигнатуры.
## Обобщённые функции-расширения
```kotlin
fun <T> List<T>.secondOrNull(): T? = if (size >= 2) this[1] else null
fun <T : Comparable<T>> List<T>.isSorted(): Boolean =
zipWithNext().all { (a, b) -> a <= b }
fun main() {
println(listOf(1, 2, 3).secondOrNull()) // 2
println(listOf(1).secondOrNull()) // null
println(listOf(1, 2, 3).isSorted()) // true
println(listOf(3, 1).isSorted()) // false
}
```
Это основной способ добавить обобщённую операцию к существующему типу. Подробнее: [Функции-расширения](../Функциональный%20Kotlin/Функции-расширения.md)
## Вывод типов
```kotlin
fun main() {
val a = listOf(1, 2) // List<Int>
val b = listOf(1, "a") // List<Any>
val c = listOf<Number>(1, 2.0) // List<Number> — указали явно
val d = mutableListOf<String>() // без аргументов тип не вывести
val e = emptyMap<String, Int>()
println("${a::class.simpleName} ${b.size} ${c.size} ${d.size} ${e.size}")
// ArrayList 2 2 0 0
}
```
Когда аргументов нет или нужен более широкий тип, параметр указывают явно.
Вывод работает и через возвращаемое значение:
```kotlin
class Box7<T>(private val value: T?) {
fun get(): T? = value
}
fun <T> emptyBox(): Box7<T> = Box7(null)
fun main() {
val box: Box7<String> = emptyBox() // T выведен из типа переменной
println(box.get()) // null
}
```
## Обобщённые интерфейсы
```kotlin
interface Repository6<T, ID> {
fun find(id: ID): T?
fun save(item: T): ID
fun all(): List<T>
}
data class User9(val id: Int, val name: String)
class InMemoryUserRepository : Repository6<User9, Int> {
private val storage = mutableMapOf<Int, User9>()
override fun find(id: Int) = storage[id]
override fun save(item: User9): Int { storage[item.id] = item; return item.id }
override fun all() = storage.values.toList()
}
fun main() {
val repo = InMemoryUserRepository()
repo.save(User9(1, "Иван"))
println(repo.find(1)?.name) // Иван
println(repo.find(2)) // null
println(repo.all().size) // 1
}
```
Реализация может зафиксировать параметры типа (как здесь) или оставить их обобщёнными:
```kotlin
interface Repository7<T, ID> {
fun find(id: ID): T?
fun save(item: T): ID
fun all(): List<T>
}
data class User10(val id: Int, val name: String)
class InMemoryRepository<T : Any, ID : Any>(
private val idOf: (T) -> ID,
) : Repository7<T, ID> {
private val storage = mutableMapOf<ID, T>()
override fun find(id: ID) = storage[id]
override fun save(item: T): ID {
val id = idOf(item)
storage[id] = item
return id
}
override fun all() = storage.values.toList()
}
fun main() {
val repo = InMemoryRepository<User10, Int> { it.id }
repo.save(User10(1, "Иван"))
println(repo.find(1)?.name) // Иван
}
```
## Стирание типов
Параметры типа существуют только при компиляции. В рантайме их нет:
```kotlin
fun main() {
val ints = listOf(1, 2)
val strings = listOf("a", "b")
println(ints.javaClass == strings.javaClass) // true — один класс
// println(ints is List<Int>) // ошибка: нельзя проверить
println(ints is List<*>) // true — только «какой-то List»
}
```
Это наследие JVM: дженерики появились в Java 5 и реализованы стиранием ради совместимости. Следствия:
```kotlin
fun main() {
// 1. Нельзя создать массив параметра типа напрямую
// fun <T> makeArray(): Array<T> = arrayOf() // ошибка
// 2. Нельзя перегрузить по параметру типа
// fun show(x: List<String>) = "строки"
// fun show(x: List<Int>) = "числа" // та же JVM-сигнатура
// 3. Непроверяемый каст компилируется с предупреждением
val any: Any = listOf(1, 2)
@Suppress("UNCHECKED_CAST")
val back = any as List<String> // не упадёт здесь
try {
println(back[0].length) // упадёт здесь
} catch (e: ClassCastException) {
println("ClassCastException при обращении")
}
}
```
Обойти стирание в Kotlin можно для inline-функций через `reified`. Подробнее: [Reified и inline](Reified%20и%20inline.md)
## Параметры типа и значения по умолчанию
```kotlin
class Cache<K, V>(
private val maxSize: Int = 100,
private val storage: MutableMap<K, V> = LinkedHashMap(),
) {
fun put(key: K, value: V) {
if (storage.size >= maxSize) storage.remove(storage.keys.first())
storage[key] = value
}
fun get(key: K): V? = storage[key]
val size: Int get() = storage.size
}
fun main() {
val cache = Cache<String, Int>(maxSize = 2)
cache.put("a", 1)
cache.put("b", 2)
cache.put("c", 3) // "a" вытеснен
println("${cache.get("a")} ${cache.get("c")} ${cache.size}") // null 3 2
}
```
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `<T> T first(List<T> x)` | `fun <T> first(x: List<T>): T` |
> | `<T extends Number>` | `<T : Number>` |
> | `<T extends A & B>` | `where T : A, T : B` |
> | `new Box<>(1)` (ромб) | `Box(1)` - вывод |
> | `List<?>` | `List<*>` |
> | `List<? extends T>` | `List<out T>` |
> | `List<? super T>` | `List<in T>` |
> | стирание типов | стирание типов (то же) |
> | нет аналога | `reified` в inline-функциях |
>
> **Синтаксис почти совпадает** - это сознательное решение, чтобы перенос кода был механическим. Два отличия: двоеточие вместо `extends` и `where` для нескольких границ.
>
> **Вариантность объявляется в месте определения.** В Java `List<? extends Number>` пишется каждый раз в сигнатуре метода (use-site variance). В Kotlin `List<out E>` объявлено один раз в самом интерфейсе, поэтому `List<String>` **является** `List<Any>` автоматически:
> ```kotlin
> fun printAll(items: List<Any>) = items.size
>
> fun main() {
> println(printAll(listOf("a", "b"))) // 2, без всяких ? extends
> }
> ```
> В Java пришлось бы объявить параметр как `List<? extends Object>`. Подробнее: [Вариантность](Вариантность.md)
>
> **`reified` решает главную проблему стирания.** В Java чтобы узнать тип в рантайме, передают `Class<T>`:
> ```java
> <T> T parse(String json, Class<T> type) { ... }
> parse(json, User.class);
> ```
> В Kotlin inline-функция может объявить `reified T` и использовать `T::class` напрямую. Это убирает параметр-костыль из сигнатуры.
>
> **Массивы дженериков одинаково проблемны.** `new T[]` запрещён в Java, `arrayOf<T>()` - в Kotlin. Обход - `Array<Any?>` с кастом или `ArrayList`.
>
> Сравнить: [Параметрический полиморфизм в Java](../../Программирование%20на%20Java/Дженерики/Параметрический%20полиморфизм.md), [Полиморфизм](../../Программирование%20на%20Java/ООП/Полиморфизм/Полиморфизм.md)
> [!warning] Подводные камни
> **Типы стираются - проверить `is List<Int>` нельзя:**
> ```kotlin
> fun describe3(x: Any): String = when (x) {
> is List<*> -> "список из ${x.size}" // только так
> is Map<*, *> -> "карта"
> else -> "другое"
> }
>
> fun main() {
> println(describe3(listOf(1, 2))) // список из 2
> println(describe3(listOf("a"))) // список из 2? нет — список из 1
> }
> ```
> Отличить `List<Int>` от `List<String>` в рантайме невозможно.
>
> **Непроверяемый каст откладывает ошибку:**
> ```kotlin
> @Suppress("UNCHECKED_CAST")
> fun <T> castAll(items: List<Any>): List<T> = items as List<T>
>
> fun main() {
> val wrong: List<String> = castAll(listOf(1, 2))
> println(wrong.size) // 2 — каст «прошёл»
> try {
> println(wrong[0].length)
> } catch (e: ClassCastException) {
> println("упало только при использовании")
> }
> }
> ```
> `ClassCastException` возникает не в месте каста, а там, где значение используется - отлаживать это тяжело.
>
> **Параметр типа без ограничения почти бесполезен внутри функции.** У `T` доступны только `toString`, `equals`, `hashCode`. Если нужны операции - нужна граница.
>
> **`T` по умолчанию nullable:**
> ```kotlin
> fun <T> process3(value: T): String = value.toString()
>
> fun main() {
> println(process3(null)) // null — T выведен как Nothing?
> }
> ```
> Чтобы запретить это, нужно `<T : Any>`.
>
> **Перегрузка по параметру типа невозможна** - после стирания сигнатуры совпадают. Обход - `@JvmName`:
> ```kotlin
> @JvmName("sumInts")
> fun sumAll(items: List<Int>) = items.sum()
> fun sumAll(items: List<String>) = items.sumOf { it.length }
>
> fun main() { println(sumAll(listOf("ab", "c"))) } // 3
> ```
>
> **Массив параметра типа создать нельзя:**
> ```kotlin
> class Buffer<T>(size: Int) {
> @Suppress("UNCHECKED_CAST")
> private val items = arrayOfNulls<Any?>(size) as Array<T?>
> operator fun set(i: Int, v: T) { items[i] = v }
> operator fun get(i: Int): T? = items[i]
> }
>
> fun main() {
> val b = Buffer<String>(2)
> b[0] = "a"
> println(b[0]) // a
> }
> ```
> Это стандартный обход, он безопасен, пока массив не утекает наружу как `Array<T>`.
>
> **Вывод типа может дать неожиданно широкий тип:**
> ```kotlin
> fun main() {
> val mixed = listOf(1, "a") // List<Any>
> val nullable = listOf(1, null) // List<Int?>
> println("${mixed.size} ${nullable.size}") // 2 2
> // mixed[0] + 1 // ошибка: Any не число
> }
> ```
>
> **Ограничение `T : Comparable<T>` не покрывает смешанные типы.** `maxOf3(1, 2.0)` не скомпилируется, потому что `Int` и `Double` не сравнимы между собой.
>
> **Обобщённое исключение объявить нельзя:**
> ```kotlin
> // class MyException<T> : Exception() // ошибка: generic class cannot extend Throwable
> class MyException(val payload: Any?) : Exception()
>
> fun main() { println(MyException(1).payload) } // 1
> ```
> Причина - стирание: `catch` не смог бы различить типы параметров.
Далее: [Вариантность](Вариантность.md)
@@ -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)