Spec-Zone.ru › Kotlin 2

Расширения

Расширения Kotlin позволяют добавлять новые возможности к классу или интерфейсу без использования наследования или шаблонов проектирования, таких как «Декоратор». Они полезны при работе со сторонними библиотеками, которые нельзя изменить напрямую. Создав расширение, вы вызываете его так, будто оно является членом исходного класса или интерфейса.

Наиболее распространённые формы расширений — это функции-расширения и свойства-расширения.

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

Получатели

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

Например, функция-расширение .appendLine() из стандартной библиотеки расширяет класс StringBuilder. В этом случае получателем является экземпляр StringBuilder, а тип получателя — StringBuilder:

fun main() { 
//sampleStart
    // builder is an instance of StringBuilder
    val builder = StringBuilder()
        // Calls .appendLine() extension function on builder
        .appendLine("Hello")
        .appendLine()
        .appendLine("World")
    println(builder.toString())
    // Hello
    //
    // World
}
//sampleEnd

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

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

  • Работы с коллекциями: .map(), .filter(), .reduce(), .fold(), .groupBy().

  • Преобразования в строки: .joinToString().

  • Работы с null-значениями: .filterNotNull().

Чтобы создать собственную функцию-расширение, укажите перед её именем тип получателя, а затем .. В этом примере функция .truncate() расширяет класс String, поэтому тип получателя — String:

fun String.truncate(maxLength: Int): String {
    return if (this.length <= maxLength) this else take(maxLength - 3) + "..."
}

fun main() {
    val shortUsername = "KotlinFan42"
    val longUsername = "JetBrainsLoverForever"

    println("Short username: ${shortUsername.truncate(15)}") 
    // KotlinFan42
    println("Long username:  ${longUsername.truncate(15)}")
    // JetBrainsLov...
}

Функция .truncate() усекает строку, для которой она вызвана, до количества символов, заданного аргументом maxLength, и добавляет многоточие .... Если строка короче maxLength, функция возвращает исходную строку.

В этом примере функция .displayInfo() расширяет интерфейс User:

interface User {
    val name: String
    val email: String
}

fun User.displayInfo(): String = "User(name=$name, email=$email)"

// Inherits from and implements the properties of the User interface
class RegularUser(override val name: String, override val email: String) : User

fun main() {
    val user = RegularUser("Alice", "alice@example.com")
    println(user.displayInfo()) 
    // User(name=Alice, email=alice@example.com)
}

Функция .displayInfo() возвращает строку, содержащую name и email экземпляра RegularUser. Определение расширения интерфейса, как в этом примере, полезно, когда требуется один раз добавить функциональность всем типам, реализующим этот интерфейс.

В этом примере функция .mostVoted() расширяет класс Map<String, Int>:

fun Map<String, Int>.mostVoted(): String? {
    return maxByOrNull { (key, value) -> value }?.key
}

fun main() {
    val poll = mapOf(
        "Cats" to 37,
        "Dogs" to 58,
        "Birds" to 22
    )

    println("Top choice: ${poll.mostVoted()}") 
    // Dogs
}

Функция .mostVoted() перебирает пары «ключ-значение» карты, для которой она вызвана, и использует функцию maxByOrNull(), чтобы вернуть ключ пары с наибольшим значением. Если карта пуста, функция maxByOrNull() возвращает null. Функция mostVoted() использует безопасный вызов ?., чтобы обращаться к свойству key только в том случае, если функция maxByOrNull() возвращает ненулевое значение.

Обобщённые функции-расширения

Чтобы создать обобщённую функцию-расширение, объявите параметр обобщённого типа перед именем функции, чтобы он был доступен в выражении типа получателя. В этом примере функция .endpoints() расширяет List<T>, где T может быть любым типом:

fun <T> List<T>.endpoints(): Pair<T, T> {
    return first() to last()
}

fun main() {
    val cities = listOf("Paris", "London", "Berlin", "Prague")
    val temperatures = listOf(21.0, 19.5, 22.3)

    val cityEndpoints = cities.endpoints()
    val tempEndpoints = temperatures.endpoints()

    println("First and last cities: $cityEndpoints")
    // (Paris, Prague)
    println("First and last temperatures: $tempEndpoints") 
    // (21.0, 22.3)
}

Функция .endpoints() возвращает пару, содержащую первый и последний элементы списка, для которого она вызвана. В теле функции вызываются функции first() и last(), а их возвращаемые значения объединяются в Pair с помощью инфиксной функции to.

Дополнительную информацию об обобщениях см. в разделе обобщённые функции.

Получатели с поддержкой null

Можно определять функции-расширения с типом получателя, допускающим null, что позволяет вызывать их для переменной, даже если её значение равно null. Если получатель — null, то this также является null. Обязательно правильно обрабатывайте null в своих функциях. Например, используйте проверки this == null в телах функций, безопасные вызовы ?. или оператор Элвиса ?:.

В этом примере функцию .toString() можно вызвать без проверки на null, поскольку эта проверка уже выполняется внутри функции-расширения:

fun main() {
    //sampleStart
    // Extension function on nullable Any
    fun Any?.toString(): String {
        if (this == null) return "null"
        // After null check, `this` is smart-cast to non-nullable Any
        // So this call resolves to the regular toString() function
        return toString()
    }
    
    val number: Int? = 42
    val nothing: Any? = null
    
    println(number.toString())
    // 42
    println(nothing.toString()) 
    // null
    //sampleEnd
}

Функции-расширения или функции-члены?

Поскольку вызовы функций-расширений и функций-членов записываются одинаково, как компилятор определяет, какую функцию использовать? Функции-расширения связываются статически: это означает, что компилятор определяет, какую функцию вызвать, на этапе компиляции, исходя из типа получателя. Например:

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

В этом примере компилятор вызывает функцию-расширение Shape.getName(), поскольку параметр shape объявлен как имеющий тип Shape. Поскольку разрешение функций-расширений выполняется статически, компилятор выбирает функцию на основе объявленного типа, а не фактического экземпляра.

Поэтому, несмотря на то что в примере передаётся экземпляр Rectangle, функция .getName() разрешается в Shape.getName(), поскольку переменная объявлена как имеющая тип Shape.

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

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

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

fun main() {
//sampleStart
    class Example {
        fun printFunctionType() { println("Member function") }
    }
    
    // Same name but different signature
    fun Example.printFunctionType(index: Int) { println("Extension function #$index") }
    
    Example().printFunctionType(1)
    // Extension function #1
//sampleEnd
}

В этом примере функции .printFunctionType() передаётся Int, поэтому компилятор выбирает функцию-расширение, соответствующую сигнатуре. Компилятор игнорирует функцию-член, не принимающую аргументов.

Анонимные функции-расширения

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

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

fun main() {
    //sampleStart
    data class Order(val weight: Double)
    val calculateShipping = fun Order.(rate: Double): Double = this.weight * rate
    
    val order = Order(2.5)
    val cost = order.calculateShipping(3.0)
    println("Shipping cost: $cost") 
    // Shipping cost: 7.5
}

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

fun main() {
    val isInRange: Int.(min: Int, max: Int) -> Boolean = { min, max -> this in min..max }

    println(5.isInRange(1, 10))
    // true
    println(20.isInRange(1, 10))
    // false
}

В этом примере переменная isInRange содержит функцию типа Int.(min: Int, max: Int) -> Boolean. Это функция-расширение класса Int, принимающая параметры min и max и возвращающая Boolean.

Тело лямбда-выражения { min, max -> this in min..max } проверяет, входит ли значение Int, для которого вызывается функция, в диапазон между параметрами min и max. Если проверка проходит успешно, лямбда-выражение возвращает true.

Дополнительную информацию см. в разделе Лямбда-выражения и анонимные функции.

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

Kotlin поддерживает свойства-расширения, которые полезны для преобразования данных или создания вспомогательных элементов пользовательского интерфейса, не загромождая класс, с которым вы работаете.

Чтобы создать свойство-расширение, укажите имя класса, который хотите расширить, затем . и имя свойства.

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

data class User(val firstName: String, val lastName: String)

// An extension property to get a username-style email handle
val User.emailUsername: String
    get() = "${firstName.lowercase()}.${lastName.lowercase()}"

fun main() {
    val user = User("Mickey", "Mouse")
    // Calls extension property
    println("Generated email username: ${user.emailUsername}")
    // Generated email username: mickey.mouse
}

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

data class House(val streetName: String)

// Doesn't compile because there is no getter and setter
// var House.number = 1
// Error: Initializers are not allowed for extension properties

// Compiles successfully
val houseNumbers = mutableMapOf<House, Int>()
var House.number: Int
    get() = houseNumbers[this] ?: 1
    set(value) {
        println("Setting house number for ${this.streetName} to $value")
        houseNumbers[this] = value
    }

fun main() {
    val house = House("Maple Street")

    // Shows the default
    println("Default number: ${house.number} ${house.streetName}") 
    // Default number: 1 Maple Street
    
    house.number = 99
    // Setting house number for Maple Street to 99

    // Shows the updated number
    println("Updated number: ${house.number} ${house.streetName}") 
    // Updated number: 99 Maple Street
}

В этом примере геттер использует оператор Элвиса, чтобы вернуть номер дома, если он есть в карте houseNumbers, или 1. Подробнее о том, как писать геттеры и сеттеры, см. в разделе Пользовательские геттеры и сеттеры.

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

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

class Logger {
    companion object { }
}

fun Logger.Companion.logStartupMessage() {
    println("Application started.")
}

fun main() {
    Logger.logStartupMessage()
    // Application started.
}

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

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

  • Класс, в котором объявлено расширение, является получателем диспетчеризации.

  • Тип получателя функции-расширения является получателем расширения.

Рассмотрим пример, в котором класс Connection содержит функцию-расширение для класса Host с именем printConnectionString():

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

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

    // Host is the extension receiver
    fun Host.printConnectionString() {
        // Calls Host.printHostname()
        printHostname() 
        print(":")
        // Calls Connection.printPort()
        // Connection is the dispatch receiver
        printPort()
    }

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

fun main() {
    Connection(Host("kotl.in"), 443).connect()
    // kotl.in:443
    
    // Triggers an error because the extension function isn't available outside Connection
    // Host("kotl.in").printConnectionString()
    // Unresolved reference 'printConnectionString'.
}

В этом примере функция printConnectionString() объявлена внутри класса Connection, поэтому класс Connection является получателем диспетчеризации. Тип получателя функции-расширения — класс Host, поэтому класс Host является получателем расширения.

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

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

Переопределение расширений-членов

Можно объявлять расширения-члены как open и переопределять их в подклассах. Это полезно, если требуется настроить поведение расширения для каждого подкласса. Компилятор обрабатывает каждый тип получателя по-разному:

Тип получателя

Время разрешения

Тип диспетчеризации

Получатель диспетчеризации

Время выполнения

Виртуальная

Получатель расширения

Время компиляции

Статическая

Рассмотрим этот пример: класс User является open, а класс Admin наследуется от него. Класс NotificationSender определяет функции-расширения sendNotification() для классов User и Admin, а класс SpecialNotificationSender переопределяет их:

open class User

class Admin : User()

open class NotificationSender {
    open fun User.sendNotification() {
        println("Sending user notification from normal sender")
    }

    open fun Admin.sendNotification() {
        println("Sending admin notification from normal sender")
    }

    fun notify(user: User) {
        user.sendNotification()
    }
}

class SpecialNotificationSender : NotificationSender() {
    override fun User.sendNotification() {
        println("Sending user notification from special sender")
    }

    override fun Admin.sendNotification() {
        println("Sending admin notification from special sender")
    }
}

fun main() {
    // Dispatch receiver is NotificationSender
    // Extension receiver is User
    // Resolves to User.sendNotification() in NotificationSender
    NotificationSender().notify(User())
    // Sending user notification from normal sender
    
    // Dispatch receiver is SpecialNotificationSender
    // Extension receiver is User
    // Resolves to User.sendNotification() in SpecialNotificationSender
    SpecialNotificationSender().notify(User())
    // Sending user notification from special sender 
    
    // Dispatch receiver is SpecialNotificationSender
    // Extension receiver is User NOT Admin
    // The notify() function declares user as type User
    // Statically resolves to User.sendNotification() in SpecialNotificationSender
    SpecialNotificationSender().notify(Admin())
    // Sending user notification from special sender 
}

Получатель диспетчеризации разрешается во время выполнения с помощью виртуальной диспетчеризации, что упрощает понимание поведения функции main(). Может удивить то, что при вызове функции notify() для экземпляра Admin компилятор выбирает расширение по объявленному типу: user: User, поскольку разрешает получателя расширения статически.

Расширения и модификаторы видимости

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

Например, расширение, объявленное на верхнем уровне файла, может обращаться к другим объявлениям верхнего уровня private в том же файле:

// File: StringUtils.kt

private fun removeWhitespace(input: String): String {
    return input.replace("\\s".toRegex(), "")
}

fun String.cleaned(): String {
    return removeWhitespace(this)
}

fun main() {
    val rawEmail = "  user @example. com  "
    val cleaned = rawEmail.cleaned()
    println("Raw:     '$rawEmail'")
    // Raw:     '  user @example. com  '
    println("Cleaned: '$cleaned'")
    // Cleaned: 'user@example.com'
    println("Looks like an email: ${cleaned.contains("@") && cleaned.contains(".")}") 
    // Looks like an email: true
}

А если расширение объявлено вне типа получателя, оно не может обращаться к членам private или protected получателя:

class User(private val password: String) {
    fun isLoggedIn(): Boolean = true
    fun passwordLength(): Int = password.length
}

// Extension declared outside the class
fun User.isSecure(): Boolean {
    // Can't access password because it's private:
    // return password.length >= 8

    // Instead, we rely on public members:
    return passwordLength() >= 8 && isLoggedIn()
}

fun main() {
    val user = User("supersecret")
    println("Is user secure: ${user.isSecure()}") 
    // Is user secure: true
}

Если расширение помечено как internal, оно доступно только в пределах своего модуля:

// Networking module
// JsonParser.kt
internal fun String.parseJson(): Map<String, Any> {
    return mapOf("fakeKey" to "fakeValue")
}

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

В большинстве случаев расширения определяются на верхнем уровне, непосредственно внутри пакетов:

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

Дополнительную информацию см. в разделе Импорт.

4 ноября 2025 г.
Классы данныхИнтерфейсы

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

Spec-Zone.ru

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