Расширения классов #

Васильев Андрей Михайлович, 2024

Версии презентации

Расширения (extensions) #

Kotlin позволяет расширять класс путём добавления нового функционала без необходимости наследования от такого класса и использования паттернов, таких как Decorator. Это реализовано с помощью специальных выражений, называемых расширения.

Например, вы можете написать новые функции для класса из сторонней библиотеки, которую вы не можете изменить.

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

Существуют также свойства расширения, которые позволяют определять новые свойства для существующих классов.

Функции-расширения #

Для того чтобы объявить функцию-расширение, укажите в качестве префикса расширяемый тип, то есть тип, который мы расширяем. Следующий пример добавляет функцию swap к MutableList<Int>:

fun MutableList<Int>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this' даёт ссылку на список
    this[index1] = this[index2]
    this[index2] = tmp
}

Ключевое слово this внутри функции-расширения соотносится с объектом расширяемого типа (этот тип ставится перед точкой). Теперь мы можем вызывать такую функцию в любом MutableList<Int>.

val list = mutableListOf(1, 2, 3)
list.swap(0, 2) // 'this' внутри 'swap()' будет содержать значение 'list'

Обобщённая функция-расширение #

Следующая функция имеет смысл для любого MutableList<T>, и вы можете сделать её обобщённой:

fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this' относится к списку
    this[index1] = this[index2]
    this[index2] = tmp
}

Вам нужно объявлять обобщённый тип-параметр перед именем функции для того, чтобы он был доступен в получаемом типе-выражении

Расширения вычисляются статически #

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

Расширения имеют статическую диспетчеризацию: это значит, что вызванная функция-расширение определяется типом её выражения, из которого она вызвана, а не типом выражения, вычисленным в ходе выполнения программы, как при вызове виртуальных функций.

open class Shape
class Rectangle: Shape()

fun Shape.getName() = "Shape"
fun Rectangle.getName() = "Rectangle"

fun printClassName(s: Shape) {
    println(s.getName())
}

printClassName(Rectangle())

Этот пример выведет нам Shape на экран потому, что вызванная функция-расширение зависит только от объявленного параметризованного типа s, который является Shape-классом.

Приоритет функций-расширений #

Если в классе есть и функция-член, и функция-расширение с тем же возвращаемым типом, таким же именем и применяется с такими же аргументами, то функция-член имеет более высокий приоритет.

class Example {
    fun printFunctionType() { println("Class method") }
}

fun Example.printFunctionType() { println("Extension function") }

Example().printFunctionType()

Этот код выведет Class method.

Различные сигнатуры #

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

class Example {
    fun printFunctionType() { println("Class method") }
}

fun Example.printFunctionType(i: Int) { println("Extension function #$i") }

Example().printFunctionType(1)

Обращение к Example().printFunctionType(1) выведет на экран надпись Extension function #1.

Расширение null-допустимых типов #

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

Благодаря этому метод toString() в Kotlin вызывается без проверки на null: она проходит внутри функции-расширения.

fun Any?.toString(): String {
    if (this == null) return "null"
    // после проверки на null, `this` автоматически приводится к не-null типу,
    // поэтому toString() обращается (ориг.: resolves) к функции-члену класса Any
    return toString()
}

Свойства-расширения #

Аналогично функциям, Kotlin поддерживает расширения свойств.

val <T> List<T>.lastIndex: Int
    get() = size - 1

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

Пример:

val House.number = 1 // ошибка: запрещено инициализировать значения
                     // в свойствах-расширениях

Область видимости расширений #

В большинстве случаев вы определяете расширения на верхнем уровне, непосредственно в разделе пакетов.

package org.example.declarations

fun List<String>.getLongestString() { /*...*/}

Для того, чтобы использовать такое расширение вне пакета, в котором оно было объявлено, импортируйте его на месте вызова.

package org.example.usage

import org.example.declarations.getLongestString

fun main() {
    val list = listOf("red", "green", "blue")
    list.getLongestString()
}