Дорогие читатели,
Помогите сделать документацию лучше!

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


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

Аналогично таким языкам программирования, как 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 в качестве возврашаемого значения. Такие расширения могут ссылаться на переменные объекта, даже если их значение 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, так? Вот для чего и нужны расширения.