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

Аналогично таким языкам программирования, как C# и Gosu, Kotlin позволяет расширять класс путём добавления нового функционала. Не наследуясь от такого класса и не используя паттерн "Декоратор". Это реализовано с помощью специальных выражений, называемых расширения. Kotlin поддерживает функции-расширения и свойства-расширения.

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

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

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

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

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

Разумеется, эта функция имеет смысл для любого 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 C

class D: C()

fun C.foo() = "c"

fun D.foo() = "d"

fun printFoo(c: C) {
    println(c.foo())
}

printFoo(D())

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

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

class C {
    fun foo() { println("member") }
}

fun C.foo() { println("extension") }

Если мы вызовем c.foo() любого объекта c с типом C, на экран выведется "member", а не "extension".

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

class C {
    fun foo() { println("member") }
}

fun C.foo(i: Int) { println("extension") }

Обращение к C().foo(1) выведет на экран надпись "extension".

Возвращаемое null значение

Обратите внимание, что расширения могут быть объявлены с возможностью получения null в качестве возврашаемого значения. Такие расширения могут ссылаться на переменные объекта, даже если их значение null. В таком случае есть возможность провести проверку this == null внутри тела функции. Благодаря этому метод toString() в языке Koltin вызывается без проверки на null: она проходит внутри функции-расширения.

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

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

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

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

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

Пример:

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

Расширения вспомогательных объектов (ориг.: companion object extensions)

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

class MyClass {
    companion object { }  // называется "сompanion"
}

fun MyClass.Companion.foo() {
    // ...
}

Как и обычные члены вспомогательного объекта, они могут быть вызваны с помощью имени класса в качестве точки доступа:

MyClass.foo()

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

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

package foo.bar
 
fun Baz.goo() { ... } 

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

package com.example.usage

import foo.bar.goo // импортировать все расширения за именем "goo"
                   // или
import foo.bar.*   // импортировать все из "foo.bar"

fun usage(baz: Baz) {
    baz.goo()
)

См. Импорт для более подробной информации.

Объявление расширений в качестве членов класса

Внутри класса вы можете объявить расширение для другого класса. Внутри такого объявления существует несколько неявных приёмников (ориг.:implicit receivers), доступ к членам которых может быть произведён без классификатора. Экземпляр такого класса, к которому относится вызываемое расширение, называется отсылкой (ориг.: dispatch receiver), а экземпляр класса, в котором вызывается расширение называется принимающим расширением (ориг.: extension receiver).

class D {
    fun bar() { ... }
}

class C {
    fun baz() { ... }

    fun D.foo() {
        bar()   // вызывает D.bar
        baz()   // вызывает C.baz
    }

    fun caller(d: D) {
        d.foo()   // вызов функции-расширения
    }
}

В случае конфликта имён между членами класса, к которому отсылается расширение, и членами класса, в котором оно вызывается, в приоритете будут именна класса, принимающего расширение.

class C {
    fun D.foo() {
        toString()         // вызывает D.toString()
        this@C.toString()  // вызывает C.toString()
    }

Расширения, объявленные как члены класса, могут иметь модификатор видимости open и быть переопределены в унаследованных классах. Это означает, что виртуально такая отсылка происходит с учётом типа, к которому она отсылает, но статически - с учётом типа, возвращаемого таким расширением.

open class D {
}

class D1 : D() {
}

open class C {
    open fun D.foo() {
        println("D.foo in C")
    }

    open fun D1.foo() {
        println("D1.foo in C")
    }

    fun caller(d: D) {
        d.foo()   // вызов функции-расширения
    }
}

class C1 : C() {
    override fun D.foo() {
        println("D.foo in C1")
    }

    override fun D1.foo() {
        println("D1.foo in C1")
    }
}

C().caller(D())   // prints "D.foo in C"
C1().caller(D())  // prints "D.foo in C1" - получатель отсылки вычислен виртуально
C().caller(D1())  // prints "D.foo in C" - получатель расширения вычислен статически

Мотивация

В Java мы привыкли к классам с названием "*Utils": FileUtils, StringUtils и т.п. Довольно известным следствием этого является java.util.Collections. Но вот использование таких утилитных классов в своём коде - не самое приятное мероприятие:

// Java
Collections.swap(list, Collections.binarySearch(list, Collections.max(otherList)), Collections.max(list))

Имена таких классов постоянно используются при вызове. Мы можем их статически импортировать и получить что-то типа:

// Java
swap(list, binarySearch(list, max(otherList)), max(list))

Уже лучше, но такой мощный инструмент IDE, как автодополнение, не предоставляет нам сколь-нибудь серьёзную помощь в данном случае. Намного лучше, если бы у нас было:

// Java
list.swap(list.binarySearch(otherList.max()), list.max())

Но мы же не хотим реализовывать все методы класса List, так? Вот для чего и нужны расширения.