Generics в Kotlin: зачем нужны и как использовать
При разработке на Kotlin вы часто сталкиваетесь с необходимостью писать код, который может работать с разными типами данных. Вместо того чтобы создавать отдельные функции или классы для Int, String, Double и так далее, можно использовать механизм Generics (обобщения). Эта статья объяснит, что такое Generics в Kotlin, зачем они нужны и как их применять на практике.
Что такое Generics и зачем они нужны?
Generics (обобщения) — это инструмент, который позволяет писать классы, интерфейсы и функции, работающие с произвольными типами данных. Представьте, что вы создаете коробку, в которую можно положить что угодно: книгу, телефон или еду. Вместо того чтобы делать отдельную коробку для каждого предмета, вы делаете одну универсальную.
Основные преимущества использования Generics:
- Типобезопасность (Type Safety) — компилятор проверяет типы на этапе компиляции, что предотвращает ошибки времени выполнения.
- Повторное использование кода — один раз написанный код работает с любыми типами.
- Читаемость — код становится более понятным, так как явно указывается, с какими типами он работает.
Без Generics вам пришлось бы использовать Any и постоянно приводить типы, что чревато ошибками. С Generics компилятор сам следит за корректностью типов.
Основы Generics в Kotlin: параметры типа
В Kotlin Generics реализуются через параметры типа. Они указываются в угловых скобках <>. Чаще всего используются буквы T (Type), E (Element), K (Key), V (Value), но можно использовать любые имена.
Generic-функции
Начнем с простого примера — функции, которая возвращает переданный ей аргумент:
fun <T> identity(value: T): T {
return value
}
fun main() {
val number: Int = identity(42)
val text: String = identity("Привет")
println(number) // 42
println(text) // Привет
}
Здесь <T> перед именем функции объявляет параметр типа. Тип T выводится автоматически из аргумента, поэтому можно не указывать его явно.
Generic-классы
Классы тоже могут быть обобщенными. Создадим простой контейнер:
class Box<T>(private val item: T) {
fun getItem(): T {
return item
}
}
fun main() {
val stringBox = Box("Книга")
val intBox = Box(123)
println(stringBox.getItem()) // Книга
println(intBox.getItem()) // 123
}
Класс Box<T> может хранить объект любого типа. При создании экземпляра мы указываем конкретный тип, и компилятор гарантирует, что getItem() вернет именно этот тип.
Ограничения типов (Type Bounds)
Иногда нужно ограничить типы, которые могут быть использованы в Generic. Например, чтобы функция работала только с числами. Для этого используются ограничения (bounds) с ключевым словом :.
Верхняя граница (Upper Bound)
fun <T : Number> sum(a: T, b: T): Double {
return a.toDouble() + b.toDouble()
}
fun main() {
println(sum(10, 20)) // 30.0
println(sum(3.5, 2.5)) // 6.0
// println(sum("A", "B")) // Ошибка компиляции
}
Здесь T : Number означает, что тип T должен быть наследником класса Number. Int, Double, Float подходят, а String — нет.
Множественные ограничения
Если нужно несколько ограничений, используйте where:
interface Printable {
fun print()
}
fun <T> printItem(item: T) where T : Number, T : Printable {
item.print()
println(item.toDouble())
}
В этом примере T должен одновременно наследовать Number и реализовывать Printable.
Вариативность (Variance): in и out
Вариативность определяет, как обобщенные типы соотносятся друг с другом. В Kotlin есть два ключевых слова: out (ковариантность) и in (контравариантность).
Ковариантность (out)
Если класс производит значения типа T (только возвращает их), используйте out. Это означает, что Box<Dog> можно рассматривать как Box<Animal>.
open class Animal
class Dog : Animal()
class Producer<out T>(private val item: T) {
fun produce(): T = item
}
fun main() {
val dogProducer: Producer<Dog> = Producer(Dog())
val animalProducer: Producer<Animal> = dogProducer // Работает!
val animal: Animal = animalProducer.produce()
}
Контравариантность (in)
Если класс потребляет значения типа T (только принимает их), используйте in. Тогда Consumer<Animal> можно использовать как Consumer<Dog>.
class Consumer<in T> {
fun consume(item: T) {
println("Потреблено: $item")
}
}
fun main() {
val animalConsumer: Consumer<Animal> = Consumer()
val dogConsumer: Consumer<Dog> = animalConsumer // Работает!
dogConsumer.consume(Dog())
}
Инвариантность (по умолчанию)
Если не указано in или out, тип инвариантен. Это значит, что Box<Dog> и Box<Animal> — совершенно разные типы, и их нельзя присваивать друг другу.
Reified типы и inline-функции
В Kotlin есть специальная возможность — reified типы. Обычно во время выполнения информация о