Обобщения

Обобщенные классы и функции

Последнее обновление: 04.03.2024

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

Какие задачи решают обобщения? Допустим, у нас есть следующий класс

Обобщенные типы

Обобщенные типы (generic types) представляют типы, в которых типы объектов параметризированы. Что это значит? Рассмотрим следующий класс:

class Person<T>(val id: T, val name: String)

Класс Person использует параметр T. Параметры указываются после имени класса в угловых скобках. Данный параметр будет представлять некоторый тип данных, который на момент определения класса неизвестен.

В первичном конструкторе определяется свойство id, которое представляет идентификатор. Оно представляет тип, который передается через параметр T. На момент определения класса Person мы не знаем, что это будет за тип.

Само название параметра произвольное (если оно не совпадает с ключевыми словами). Но нередко используется T как сокращение от слова type.

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

fun main() {

    val tom: Person<Int> = Person(367, "Tom")
    val bob: Person<String> = Person("A65", "Bob")

    println("${tom.id} - ${tom.name}")
    println("${bob.id} - ${bob.name}")
}

class Person<T>(val id: T, val name: String)

Для типизации объекта после названия типа в угловых скобках указывается конкретный тип:

val tom: Person<Int>

В данном случае мы говорим, что параметр T фактически будет представлять тип Int. Поэтому в конструктор объекта Person для свойства id необходимо передать числовое значение Int:

Person(367, "Tom")

Второй объект типизируется типом String, поэтому в конструкторе для свойства id передается строка:

val bob: Person<String> = Person("A65", "Bob")

Если конструктор использует параметр T, то в принципе мы можем не указывать, каким типом типизируется объект - данный тип будет выводиться из типа параметра конструктора:

val tom = Person(367, "Tom")
val bob = Person("A65", "Bob")

При этом параметры типа могут широко применяться внутри класса, не только при определении свойств, но и в функциях:

fun main() {

    val tom = Person("qwrtf2", "Tom")
    tom.checkId("qwrtf2")   // The same
    tom.checkId("q34tt")    // Different
}

class Person<T>(val id: T, val name: String){

    fun checkId(_id: T){
        if(id == _id){
            println("The same")
        }
        else{
            println("Different")
        }
    }
}

Здесь класс Person определяет функцию checkId(), которая проверяет, равен ли id значению параметра _id. При этом параметр _id имеет тип T - то есть он будет представлять тот же тип, что и свойство id.

Стоит отметить, что generic-типы широко используются в Kotlin. Самый показательный пример, который представлен классом - Array<T>. Параметр класса определяет, элементы какого типа массив будет хранить:

val people: Array<String> = arrayOf("Tom", "Bob", "Sam")
val numbers: Array<Int> = arrayOf(1, 2, 3, 4)

Применение нескольких параметров

Можно одновременно использовать несколько параметров:

fun main() {

    var word1: Word<String, String> = Word("one", "один")
    var word2: Word<String, Int> = Word("two", 2)

    println("${word1.source} - ${word1.target}")    // one - один
    println("${word2.source} - ${word2.target}")    // two - 2
}

class Word<K, V>(val source: K, var target: V)

В данном случае класс Word применяет два параметра - K и V. При создании объекта Word эти параметры могут представлять один и тот же тип, а могут представлять и разные типы.

Обобщенные функции

Функции, как и классы, могут быть обобщенными.

fun main() {

    display("Hello Kotlin")
    display(1234)
    display(true)
}
fun <T> display(obj: T){
    println(obj)
}

Функция display() параметризирована параметром T. Параметр также указывается в угловых скобках после слова fun и перед названием функции. Функция принимает один параметр типа T и выводит его значение на консоль. И при использовании функции мы можем передавать в нее данные любых типов.

Другой более практический пример - определим функцию, которая будет возвращать наибольший массив:

fun main() {

    val arr1 = getBiggest(arrayOf(1,2,3,4), arrayOf(3, 4, 5, 6, 7, 7))
    arr1.forEach { item -> print("$item ") }    // 3  4  5  6  7  7

    println()
    
    val arr2 = getBiggest(arrayOf("Tom", "Sam", "Bob"), arrayOf("Kate", "Alice"))
    arr2.forEach { item -> print("$item ") }    // Tom  Sam  Bob
}

fun <T> getBiggest(args1: Array<T>, args2: Array<T>): Array<T>{
    if(args1.size > args2.size) return args1
    return  args2
}

Здесь функция getBiggest() в качестве параметров принимает два массива. При этом мы точно не значем, объекты какого типа эти массивы будут содержать. Однако оба массива типизированы параметром T, что гарантирует, что оба массива будут хранить объекты одного и того же типа. Внутри функции сравниваем размер массивов с помощью их свойства size и возвращаем наибольший массив.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850