Files
obsidian/Kotlin/Дженерики/Параметрический полиморфизм.md
T

394 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Параметрический полиморфизм
description: Обобщённые функции и классы, ограничения параметров типа и вывод типов
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)