--- title: Параметрический полиморфизм description: Обобщённые функции и классы, ограничения параметров типа и вывод типов tags: - kotlin - generics --- Дженерики позволяют написать код один раз для любого типа, сохранив проверку типов при компиляции. ```kotlin fun firstOrDefault(items: List, 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 } ``` `` объявляется **перед** именем функции. Компилятор выводит его из аргументов. ## Обобщённые классы ```kotlin class Box5(private val value: T) { fun get(): T = value fun map(f: (T) -> R): Box5 = 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("явно") println(explicit.get().length) // 4 } ``` Метод `map` объявляет **свой** параметр типа `R` - он не связан с `T` класса. Несколько параметров: ```kotlin class Pair2(val first: A, val second: B) { fun swap(): Pair2 = 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 describe(value: T): String = "значение: $value" // только toString fun > maxOf3(a: T, b: T): T = if (a > b) a else b fun sum2(items: List): 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 firstOrNull2(items: List): T? = items.firstOrNull() fun requireFirst(items: List): T = items.first() fun main() { println(firstOrNull2(listOf(null))) // null println(requireFirst(listOf(1, 2))) // 1 } ``` `T : Any` - способ запретить nullable-аргумент типа. ### Несколько ограничений ```kotlin interface Named4 { val name: String } interface Aged { val age: Int } fun 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 List.secondOrNull(): T? = if (size >= 2) this[1] else null fun > List.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 val b = listOf(1, "a") // List val c = listOf(1, 2.0) // List — указали явно val d = mutableListOf() // без аргументов тип не вывести val e = emptyMap() println("${a::class.simpleName} ${b.size} ${c.size} ${d.size} ${e.size}") // ArrayList 2 2 0 0 } ``` Когда аргументов нет или нужен более широкий тип, параметр указывают явно. Вывод работает и через возвращаемое значение: ```kotlin class Box7(private val value: T?) { fun get(): T? = value } fun emptyBox(): Box7 = Box7(null) fun main() { val box: Box7 = emptyBox() // T выведен из типа переменной println(box.get()) // null } ``` ## Обобщённые интерфейсы ```kotlin interface Repository6 { fun find(id: ID): T? fun save(item: T): ID fun all(): List } data class User9(val id: Int, val name: String) class InMemoryUserRepository : Repository6 { private val storage = mutableMapOf() 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 { fun find(id: ID): T? fun save(item: T): ID fun all(): List } data class User10(val id: Int, val name: String) class InMemoryRepository( private val idOf: (T) -> ID, ) : Repository7 { private val storage = mutableMapOf() 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 { 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) // ошибка: нельзя проверить println(ints is List<*>) // true — только «какой-то List» } ``` Это наследие JVM: дженерики появились в Java 5 и реализованы стиранием ради совместимости. Следствия: ```kotlin fun main() { // 1. Нельзя создать массив параметра типа напрямую // fun makeArray(): Array = arrayOf() // ошибка // 2. Нельзя перегрузить по параметру типа // fun show(x: List) = "строки" // fun show(x: List) = "числа" // та же JVM-сигнатура // 3. Непроверяемый каст компилируется с предупреждением val any: Any = listOf(1, 2) @Suppress("UNCHECKED_CAST") val back = any as List // не упадёт здесь try { println(back[0].length) // упадёт здесь } catch (e: ClassCastException) { println("ClassCastException при обращении") } } ``` Обойти стирание в Kotlin можно для inline-функций через `reified`. Подробнее: [Reified и inline](Reified%20и%20inline.md) ## Параметры типа и значения по умолчанию ```kotlin class Cache( private val maxSize: Int = 100, private val storage: MutableMap = 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(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 first(List x)` | `fun first(x: List): T` | > | `` | `` | > | `` | `where T : A, T : B` | > | `new Box<>(1)` (ромб) | `Box(1)` - вывод | > | `List` | `List<*>` | > | `List` | `List` | > | `List` | `List` | > | стирание типов | стирание типов (то же) | > | нет аналога | `reified` в inline-функциях | > > **Синтаксис почти совпадает** - это сознательное решение, чтобы перенос кода был механическим. Два отличия: двоеточие вместо `extends` и `where` для нескольких границ. > > **Вариантность объявляется в месте определения.** В Java `List` пишется каждый раз в сигнатуре метода (use-site variance). В Kotlin `List` объявлено один раз в самом интерфейсе, поэтому `List` **является** `List` автоматически: > ```kotlin > fun printAll(items: List) = items.size > > fun main() { > println(printAll(listOf("a", "b"))) // 2, без всяких ? extends > } > ``` > В Java пришлось бы объявить параметр как `List`. Подробнее: [Вариантность](Вариантность.md) > > **`reified` решает главную проблему стирания.** В Java чтобы узнать тип в рантайме, передают `Class`: > ```java > T parse(String json, Class type) { ... } > parse(json, User.class); > ``` > В Kotlin inline-функция может объявить `reified T` и использовать `T::class` напрямую. Это убирает параметр-костыль из сигнатуры. > > **Массивы дженериков одинаково проблемны.** `new T[]` запрещён в Java, `arrayOf()` - в Kotlin. Обход - `Array` с кастом или `ArrayList`. > > Сравнить: [Параметрический полиморфизм в Java](../../Программирование%20на%20Java/Дженерики/Параметрический%20полиморфизм.md), [Полиморфизм](../../Программирование%20на%20Java/ООП/Полиморфизм/Полиморфизм.md) > [!warning] Подводные камни > **Типы стираются - проверить `is List` нельзя:** > ```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` от `List` в рантайме невозможно. > > **Непроверяемый каст откладывает ошибку:** > ```kotlin > @Suppress("UNCHECKED_CAST") > fun castAll(items: List): List = items as List > > fun main() { > val wrong: List = 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 process3(value: T): String = value.toString() > > fun main() { > println(process3(null)) // null — T выведен как Nothing? > } > ``` > Чтобы запретить это, нужно ``. > > **Перегрузка по параметру типа невозможна** - после стирания сигнатуры совпадают. Обход - `@JvmName`: > ```kotlin > @JvmName("sumInts") > fun sumAll(items: List) = items.sum() > fun sumAll(items: List) = items.sumOf { it.length } > > fun main() { println(sumAll(listOf("ab", "c"))) } // 3 > ``` > > **Массив параметра типа создать нельзя:** > ```kotlin > class Buffer(size: Int) { > @Suppress("UNCHECKED_CAST") > private val items = arrayOfNulls(size) as Array > operator fun set(i: Int, v: T) { items[i] = v } > operator fun get(i: Int): T? = items[i] > } > > fun main() { > val b = Buffer(2) > b[0] = "a" > println(b[0]) // a > } > ``` > Это стандартный обход, он безопасен, пока массив не утекает наружу как `Array`. > > **Вывод типа может дать неожиданно широкий тип:** > ```kotlin > fun main() { > val mixed = listOf(1, "a") // List > val nullable = listOf(1, null) // List > println("${mixed.size} ${nullable.size}") // 2 2 > // mixed[0] + 1 // ошибка: Any не число > } > ``` > > **Ограничение `T : Comparable` не покрывает смешанные типы.** `maxOf3(1, 2.0)` не скомпилируется, потому что `Int` и `Double` не сравнимы между собой. > > **Обобщённое исключение объявить нельзя:** > ```kotlin > // class MyException : Exception() // ошибка: generic class cannot extend Throwable > class MyException(val payload: Any?) : Exception() > > fun main() { println(MyException(1).payload) } // 1 > ``` > Причина - стирание: `catch` не смог бы различить типы параметров. Далее: [Вариантность](Вариантность.md)