394 lines
16 KiB
Markdown
394 lines
16 KiB
Markdown
---
|
||
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)
|