16 KiB
title, description, tags
| title | description | tags | ||
|---|---|---|---|---|
| Параметрический полиморфизм | Обобщённые функции и классы, ограничения параметров типа и вывод типов |
|
Дженерики позволяют написать код один раз для любого типа, сохранив проверку типов при компиляции.
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> объявляется перед именем функции. Компилятор выводит его из аргументов.
Обобщённые классы
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 класса.
Несколько параметров:
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?:
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:
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-аргумент типа.
Несколько ограничений
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 после сигнатуры.
Обобщённые функции-расширения
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
}
Это основной способ добавить обобщённую операцию к существующему типу. Подробнее: Функции-расширения
Вывод типов
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
}
Когда аргументов нет или нужен более широкий тип, параметр указывают явно.
Вывод работает и через возвращаемое значение:
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
}
Обобщённые интерфейсы
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
}
Реализация может зафиксировать параметры типа (как здесь) или оставить их обобщёнными:
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) // Иван
}
Стирание типов
Параметры типа существуют только при компиляции. В рантайме их нет:
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 и реализованы стиранием ради совместимости. Следствия:
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
Параметры типа и значения по умолчанию
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 : Bnew 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). В KotlinList<out E>объявлено один раз в самом интерфейсе, поэтомуList<String>являетсяList<Any>автоматически:fun printAll(items: List<Any>) = items.size fun main() { println(printAll(listOf("a", "b"))) // 2, без всяких ? extends }В Java пришлось бы объявить параметр как
List<? extends Object>. Подробнее: Вариантность
reifiedрешает главную проблему стирания. В Java чтобы узнать тип в рантайме, передаютClass<T>:<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, Полиморфизм
[!warning] Подводные камни Типы стираются - проверить
is List<Int>нельзя: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>в рантайме невозможно.Непроверяемый каст откладывает ошибку:
@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:fun <T> process3(value: T): String = value.toString() fun main() { println(process3(null)) // null — T выведен как Nothing? }Чтобы запретить это, нужно
<T : Any>.Перегрузка по параметру типа невозможна - после стирания сигнатуры совпадают. Обход -
@JvmName:@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Массив параметра типа создать нельзя:
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>.Вывод типа может дать неожиданно широкий тип:
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не сравнимы между собой.Обобщённое исключение объявить нельзя:
// class MyException<T> : Exception() // ошибка: generic class cannot extend Throwable class MyException(val payload: Any?) : Exception() fun main() { println(MyException(1).payload) } // 1Причина - стирание:
catchне смог бы различить типы параметров.
Далее: Вариантность