Расширения (extensions) в Kotlin
Аналогично таким языкам программирования, как C# и Gosu, Kotlin позволяет расширять класс путём добавления нового функционала, не наследуясь от такого класса и не используя паттерн "Декоратор". Это реализовано с помощью специальных выражений, называемых расширениями (extensions). Kotlin поддерживает функции-расширения (extension functions) и свойства-расширения (extension properties).
Функции-расширения
Для того чтобы объявить функцию-расширение, нам нужно указать в качестве префикса расширяемый тип – тип, который мы расширяем с помощью этой функции. Следующий пример добавляет функцию 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>
с любым параметром 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".
Расширение Nullable типов
Обратите внимание, что расширения могут быть объявлены для Nullable типов. Такие расширения могут ссылаться на переменные объекта, даже если значение переменной равно null. В таком случае есть возможность провести проверку this == null
внутри тела функции. Благодаря этому метод toString()
в языке Kotlin вызывается без проверки на null – эта проверка проходит внутри функции-расширения.
fun Any?.toString(): String { if (this == null) return "null" // после проверки на null `this` автоматически приводится к не-null типу, // поэтому toString() вызывается уже у класса Any, а не Any? return toString() }
Свойства-расширения
Аналогично функциям, Kotlin поддерживает расширения свойств:
val <T> List<T>.lastIndex: Int get() = size - 1
Так как расширения на самом деле не добавляют никаких членов к классам, свойство-расширение не может иметь теневого поля. Вот почему запрещено использовать инициализаторы для свойств-расширений. Их поведение может быть определено только явным образом, с указанием геттеров/сеттеров.
Пример:
val Foo.bar = 1 // ошибка: запрещено инициализировать значения в свойствах-расширениях
Расширения для Companion объекта
Если у класса есть Companion объект, вы также можете определить функции и свойства расширения для такого объекта:
class MyClass { companion object { } // называется "Companion" } 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 objects), доступ к членам которых может быть произведён без квалификатора. Экземпляр класса, в котором расширение объявлено, называется 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() // вызов функции-расширения } }
В случае конфликта имён между членами классов dispatch receiver'а и extension receiver'а, приоритет имеет extension receiver. Чтобы обратиться к члену класса dispatch receiver, можно использовать синтаксис this с определителем.
class C { fun D.foo() { toString() // вызывает D.toString() this@C.toString() // вызывает C.toString() } }
Расширения, объявленные как члены класса, могут иметь модификатор видимости open и быть переопределены в унаследованных классах. Это означает, что диспетчеризация таких функций является виртуальной по отношению к типу dispatch receiver'а, но статической по отношению к типам extension receiver'ов.
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" - dispatch receiver вычислен виртуально C().caller(D1()) // prints "D.foo in C" - extension receiver вычислен статически
Мотивация
В 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, как автодополнение, не предоставляет нам сколько-нибудь серьёзную помощь в данном случае. Намного лучше, если бы у нас было так:
// Kotlin list.swap(list.binarySearch(otherList.max()), list.max())
Но мы же не хотим реализовывать все возможные методы внутри класса List
, ведь так? Вот для чего и нужны расширения.
Источник: Расширения (extensions)