Generics в Kotlin: зачем нужны и как использовать

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение в Generics в Kotlin

Generics (обобщенные типы) — одна из ключевых возможностей статически типизированных языков программирования, включая Kotlin. Они позволяют писать гибкий, переиспользуемый код, который работает с разными типами данных, сохраняя при этом строгую типовую безопасность на этапе компиляции.

Представьте, что вам нужно создать структуру данных для хранения списка элементов. Без generics вам пришлось бы писать отдельные классы для каждого типа (IntList, StringList, UserList). С generics достаточно одного класса MyList<T>, который будет работать с любым типом.

// Без Generics — приходится дублировать код
class IntList(private val items: MutableList<Int>)
class StringList(private val items: MutableList<String>)

// С Generics — один класс для всех типов
class GenericList<T>(private val items: MutableList<T>) {
    fun add(item: T) { items.add(item) }
    fun get(index: Int): T = items[index]
}

В этой статье мы подробно разберем, как работают generics в Kotlin, рассмотрим их синтаксис, варианты использования и продвинутые концепции вроде ковариантности и контравариантности.

Основы работы с Generics

Объявление обобщенных классов и функций

Чтобы объявить обобщенный класс, достаточно указать один или несколько параметров типа в угловых скобках после имени класса:

class Box<T>(var value: T) {
    fun getValue(): T = value
    fun setValue(newValue: T) { value = newValue }
}

// Использование
val intBox = Box<Int>(42)
val stringBox = Box<String>("Hello")

// Kotlin умеет выводить тип, поэтому можно писать короче
val anotherBox = Box("Auto type inference")

Функции тоже могут быть обобщенными. Параметр типа указывается перед именем функции:

fun <T> printItem(item: T) {
    println("Item: $item")
}

fun <T> createPair(first: T, second: T): Pair<T, T> {
    return Pair(first, second)
}

// Использование
printItem(123)
printItem("Text")
val pair = createPair("A", "B")

Ограничения типов (Type Bounds)

Иногда нужно ограничить типы, которые могут быть использованы в generics. Для этого применяются верхние границы (upper bounds) с ключевым словом : Type:

// Только типы, реализующие Comparable
fun <T : Comparable<T>> findMax(a: T, b: T): T {
    return if (a > b) a else b
}

// Несколько ограничений через where
fun <T> processItem(item: T) 
    where T : CharSequence, T : Comparable<T> {
    println("Length: ${item.length}")
    println("Compared: ${item.compareTo(item)}")
}

// Использование
println(findMax(10, 20))        // 20
println(findMax("Apple", "Banana")) // "Banana"
// findMax(10, "Text") // Ошибка компиляции!

Вариативность: in, out и проекция типов

Одна из самых сложных, но важных тем в generics — вариативность (variance). Она определяет, как обобщенные типы соотносятся друг с другом при наследовании.

Инвариантность (по умолчанию)

По умолчанию generics в Kotlin инвариантны. Это значит, что Box<Number> и Box<Int> не являются подтипами друг друга, даже если Int — подтип Number:

val intBox: Box<Int> = Box(42)
// val numBox: Box<Number> = intBox // Ошибка компиляции!

Ковариантность (out)

Модификатор out делает тип ковариантным. Это означает, что Box<Int> становится подтипом Box<Number>. Используется, когда тип T используется только в выходных позициях (возвращаемые значения):

class Producer<out T>(private val item: T) {
    fun produce(): T = item  // T только на выходе
}

val intProducer: Producer<Int> = Producer(100)
val numProducer: Producer<Number> = intProducer // Работает!

// А так нельзя — T используется на входе
// class BadProducer<out T>(item: T) {
//     fun consume(item: T) { } // Ошибка!
// }

Контравариантность (in)

Модификатор in делает тип контравариантным. Box<Number> становится подтипом Box<Int>. Используется, когда T используется только во входных позициях (параметры функций):

class Consumer<in T> {
    fun consume(item: T) { 
        println("Consumed: $item")
    }
}

val numConsumer: Consumer<Number> = Consumer()
val intConsumer: Consumer<Int> = numConsumer // Работает!

// А так нельзя — T используется на выходе
// class BadConsumer<in T> {
//     fun produce(): T { ... } // Ошибка!
// }

Проекция типов (Type Projections)

Кроме объявления вариативности в определении класса, можно использовать проекции в месте использования — это называется use-site variance:

// Ковариантная проекция
fun copy(from: Array<out Any>, to: Array<Any>) {
    for (i in from.indices) {
        to[i] = from[i]
    }
}

// Контравариантная проекция
fun fill(dest: Array<in String>, value: String) {
    for (i in dest.indices) {
        dest[i] = value
    }
}

// Звездочная проекция (star projection) — "любой тип"
fun printLength(list: List<*>) {
    println("Size: ${list.size}")
    // val item: String = list[0] // Ошибка — тип неизвестен
}

Продвинутые возможности: reified и where

Reified — сохранение информации о типе в runtime

Из-за стирания типов (type erasure) в JVM информация о generic-параметрах теряется во время выполнения. Но Kotlin предлагает reified для inline-функций, чтобы обойти это ограничение:

inline fun <                

Блоги

Рекомендаций по книгам