Kotlin guide done. (20.10.26)
This commit is contained in:
@@ -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<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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user