Spec-Zone.ru › Kotlin 1.4

Расширения

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

Расширяемые функции

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

fun MutableList<Int>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this' corresponds to the list
    this[index1] = this[index2]
    this[index2] = tmp
}

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

val list = mutableListOf(1, 2, 3)
list.swap(0, 2) // 'this' inside 'swap()' will hold the value of 'list'

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

fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this' corresponds to the list
    this[index1] = this[index2]
    this[index2] = tmp
}

Мы объявляем обобщённый параметр типа перед именем функции, чтобы он был доступен в выражении типа получателя. См. Обобщённые функции.

Расширения разрешаются статически

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

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

fun main() {
//sampleStart
    open class Shape
    
    class Rectangle: Shape()
    
    fun Shape.getName() = "Shape"
    
    fun Rectangle.getName() = "Rectangle"
    
    fun printClassName(s: Shape) {
        println(s.getName())
    }    
    
    printClassName(Rectangle())
//sampleEnd
}

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

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

fun main() {
//sampleStart
    class Example {
        fun printFunctionType() { println("Class method") }
    }
    
    fun Example.printFunctionType() { println("Extension function") }
    
    Example().printFunctionType()
//sampleEnd
}

Этот код выводит "Метод класса".

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

fun main() {
//sampleStart
    class Example {
        fun printFunctionType() { println("Class method") }
    }
    
    fun Example.printFunctionType(i: Int) { println("Extension function") }
    
    Example().printFunctionType(1)
//sampleEnd
}

Получатель с типом Nullable

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

fun Any?.toString(): String {
    if (this == null) return "null"
    // after the null check, 'this' is autocast to a non-null type, so the toString() below
    // resolves to the member function of the Any class
    return toString()
}

Расширяемые свойства

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

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

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

Пример:

val House.number = 1 // error: initializers are not allowed for extension properties

Расширения объекта-компаньона

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

class MyClass {
    companion object { }  // will be called "Companion"
}

fun MyClass.Companion.printCompanion() { println("companion") }

fun main() {
    MyClass.printCompanion()
}

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

Большую часть времени мы определяем расширения на верхнем уровне — непосредственно под пакетами:

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()
}

См. Импорты для получения дополнительной информации.

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

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

class Host(val hostname: String) {
    fun printHostname() { print(hostname) }
}

class Connection(val host: Host, val port: Int) {
     fun printPort() { print(port) }

     fun Host.printConnectionString() {
         printHostname()   // calls Host.printHostname()
         print(":")
         printPort()   // calls Connection.printPort()
     }

     fun connect() {
         /*...*/
         host.printConnectionString()   // calls the extension function
     }
}

fun main() {
    Connection(Host("kotl.in"), 443).connect()
    //Host("kotl.in").printConnectionString(443)  // error, the extension function is unavailable outside Connection
}

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

class Connection {
    fun Host.getConnectionString() {
        toString()         // calls Host.toString()
        this@Connection.toString()  // calls Connection.toString()
    }
}

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

open class Base { }

class Derived : Base() { }

open class BaseCaller {
    open fun Base.printFunctionInfo() {
        println("Base extension function in BaseCaller")
    }

    open fun Derived.printFunctionInfo() {
        println("Derived extension function in BaseCaller")
    }

    fun call(b: Base) {
        b.printFunctionInfo()   // call the extension function
    }
}

class DerivedCaller: BaseCaller() {
    override fun Base.printFunctionInfo() {
        println("Base extension function in DerivedCaller")
    }

    override fun Derived.printFunctionInfo() {
        println("Derived extension function in DerivedCaller")
    }
}

fun main() {
    BaseCaller().call(Base())   // "Base extension function in BaseCaller"
    DerivedCaller().call(Base())  // "Base extension function in DerivedCaller" - dispatch receiver is resolved virtually
    DerivedCaller().call(Derived())  // "Base extension function in DerivedCaller" - extension receiver is resolved statically
}

Примечание по видимостью

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

  • Расширение, объявленное на верхнем уровне файла, имеет доступ к другим private объявлениям на верхнем уровне в том же файле;
  • Если расширение объявлено вне типа получателя, такое расширение не может получить доступ к членам private получателя.

© 2010–2020 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/reference/extensions.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API