type | layout | category | title | url |
---|---|---|---|---|
doc |
reference |
Syntax |
Расширения |
Аналогично таким языкам программирования, как C# и Gosu, Kotlin позволяет расширять класс путём добавления нового функционала. Не наследуясь от такого класса и не используя паттерн "Декоратор". Это реализовано с помощью специальных выражений, называемых расширения. Kotlin поддерживает функции-расширения и свойства-расширения.
Для того, чтобы объявить функцию-расширение, нам нужно указать в качестве префикса расширяемый тип, то есть тип, который мы расширяем. Следующий пример добавляет функцию 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 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. В таком случае есть возможность провести проверку 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 Foo.bar = 1 // ошибка: запрещено инициализировать значения
// в свойствах-расширениях
Если у класса есть вспомогательный объект, вы также можете определить функции и свойства расширения для такого объекта:
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() // вызов функции-расширения
}
}
В случае конфликта имён между членами классов диспетчера приёмников и приёмников расширения, приоритет имеет приёмник расширения. Чтобы обратиться к члену класса диспетчера приёмников, можно использовать синтаксис this с квалификатором.
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, как автодополнение, не предоставляет нам сколь-нибудь серьёзную помощь в данном случае. Намного лучше, если бы у нас было:
// Kotlin
list.swap(list.binarySearch(otherList.max()), list.max())
Но мы же не хотим реализовывать все возможные методы внутри класса List
, ведь так? Вот для чего и нужны расширения.