--- title: Функции-расширения description: Добавление методов к готовым типам, статическое разрешение и свойства-расширения tags: - kotlin - functional --- Функция-расширение добавляет метод к типу, который вы не контролируете - без наследования и без обёрток. ```kotlin fun String.isPalindrome(): Boolean { val clean = lowercase().filter { it.isLetter() } return clean == clean.reversed() } fun main() { println("Довод".isPalindrome()) // true println("А роза упала на лапу Азора".isPalindrome()) // true println("Привет".isPalindrome()) // false } ``` Внутри расширения `this` - это объект, на котором вызвали функцию (его называют **получателем**, receiver). `this` можно опускать: `lowercase()` - это `this.lowercase()`. ## Это обычная статическая функция Расширение не меняет класс и не попадает в него. Компилятор превращает его в статический метод с получателем первым параметром: ```kotlin fun Int.double(): Int = this * 2 fun main() { println(5.double()) // 10 — выглядит как метод // В байт-коде: Файл.double(5) } ``` Из этого следуют все особенности расширений - и удобные, и опасные. **Нет доступа к `private`:** ```kotlin class Account(private val balance: Long, val owner: String) // fun Account.canWithdraw(x: Long) = balance >= x // ошибка: balance недоступен fun Account.describe() = "счёт $owner" // public — можно fun main() { println(Account(100, "Иван").describe()) // счёт Иван } ``` Расширение - внешний код, инкапсуляция работает. Это принципиальное ограничение: расширение не заменяет метод класса, когда нужен доступ к состоянию. **Разрешение статическое, не виртуальное:** ```kotlin open class Shape class Circle : Shape() fun Shape.name() = "фигура" fun Circle.name() = "круг" fun main() { val shape: Shape = Circle() println(shape.name()) // фигура — тип переменной, не объекта! println(Circle().name()) // круг } ``` Это главная ловушка расширений. Выбор функции делается компилятором по **объявленному** типу, поэтому полиморфизма здесь нет. Подробнее: [Полиморфизм](../ООП/Полиморфизм.md) **Метод класса всегда выигрывает:** ```kotlin class Printer { fun show() = "метод класса" } fun Printer.show() = "расширение" // warning: extension is shadowed by a member fun main() { println(Printer().show()) // метод класса } ``` Это гарантия совместимости: добавление метода в класс не ломает код, но молча меняет поведение вызова. Компилятор предупреждает. ## Расширения на nullable-типах ```kotlin fun String?.orPlaceholder(): String = if (isNullOrBlank()) "(пусто)" else this fun main() { println("текст".orPlaceholder()) // текст println(null.orPlaceholder()) // (пусто) println(" ".orPlaceholder()) // (пусто) } ``` Расширение на `T?` можно вызвать на `null` без `?.` - внутри `this` просто может быть `null`. Так устроены `isNullOrEmpty`, `orEmpty` и `toString` в stdlib: ```kotlin fun main() { val s: String? = null println(s.isNullOrEmpty()) // true — без ?. ! println(s.orEmpty().length) // 0 println(s.toString()) // null — строка "null" } ``` ## Свойства-расширения ```kotlin val String.firstWord: String get() = substringBefore(" ") val List.secondOrNull: T? get() = getOrNull(1) val Int.isEven: Boolean get() = this % 2 == 0 fun main() { println("привет мир".firstWord) // привет println(listOf(1, 2, 3).secondOrNull) // 2 println(4.isEven) // true } ``` У свойства-расширения **не может быть поля** - только геттер (и сеттер, если получатель изменяемый): ```kotlin var StringBuilder.lastChar: Char get() = this[length - 1] set(value) { setCharAt(length - 1, value) } fun main() { val sb = StringBuilder("Котлин") println(sb.lastChar) // н sb.lastChar = '!' println(sb) // Котли! } ``` Хранить состояние в расширении нельзя: `val Int.counter = 0` не скомпилируется, потому что хранить его негде. ## Расширения на дженериках ```kotlin fun List.penultimate(): T? = getOrNull(size - 2) fun > List.isSorted(): Boolean = zipWithNext().all { (a, b) -> a <= b } fun T.alsoPrint(label: String): T = also { println("$label: $it") } fun main() { println(listOf(1, 2, 3).penultimate()) // 2 println(listOf(1, 2, 3).isSorted()) // true val x = 42.alsoPrint("значение") // значение: 42 println(x + 1) // 43 } ``` Расширение на `T` без ограничений применимо к любому типу - именно так объявлены все [scope-функции](Scope-функции.md). Расширение можно объявить на конкретной параметризации: ```kotlin fun List.sumOfSquares(): Int = sumOf { it * it } fun List.longest(): String? = maxByOrNull { it.length } fun main() { println(listOf(1, 2, 3).sumOfSquares()) // 14 println(listOf("a", "ccc", "bb").longest()) // ccc } ``` Так же работает `Iterable.sum()` в stdlib - одна функция на каждый числовой тип. ## Расширения на функциональных типах ```kotlin fun ((T) -> R).withLogging(name: String): (T) -> R = { arg -> println("$name($arg)") this(arg) } fun main() { val double: (Int) -> Int = { it * 2 } val logged = double.withLogging("double") println(logged(21)) // double(21) // 42 } ``` Функциональный тип - обычный тип, расширения на нём работают так же. ## Расширения на объектах-компаньонах ```kotlin class Celsius(val value: Double) { companion object {} override fun toString() = "$value°C" } fun Celsius.Companion.fromFahrenheit(f: Double) = Celsius((f - 32) * 5 / 9) fun main() { println(Celsius.fromFahrenheit(68.0)) // 20.0°C } ``` Так к типу добавляют фабричный метод, вызываемый через имя класса. Два условия: `companion object` должен быть объявлен в классе (пустого объявления достаточно) и конструктор должен быть доступен - расширение на `Companion` остаётся внешним кодом и приватный конструктор не увидит. Подробнее: [Object и companion object](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md) ## Где объявлять ```kotlin class Order6(val items: List, val total: Long) { // расширение внутри класса — видно только внутри private fun String.shorten() = take(3) fun summary() = items.joinToString { it.shorten() } } fun main() { println(Order6(listOf("молоко", "хлеб"), 100).summary()) // мол, хле } ``` Расширение, объявленное внутри класса, имеет два получателя: `this` расширения и `this` класса. Это основа DSL - подробнее: [Receivers и context parameters](Receivers%20и%20context%20parameters.md) Для расширений верхнего уровня принято заводить файл по теме: ```kotlin // StringExtensions.kt fun String.toSlug() = lowercase().replace(Regex("[^a-zа-я0-9]+"), "-").trim('-') fun main() { println("Привет, Мир!".toSlug()) // привет-мир } ``` ## Когда расширение, когда метод | Ситуация | Выбор | |---|---| | нужен доступ к `private`-состоянию | метод класса | | поведение - часть сути типа | метод класса | | нужен полиморфизм (`override`) | метод класса | | тип чужой (stdlib, библиотека) | расширение | | утилита, нужная не всем пользователям типа | расширение | | адаптация типа к своему слою (DTO -> модель) | расширение | | логика зависит от внешних сервисов | расширение или отдельный класс | Практическое правило: расширение не должно притворяться частью типа, если оно меняет его инварианты. Для чистых преобразований и утилит это идеальный инструмент. > [!info] Отличия от Java > | Java | Kotlin | > |---|---| > | `StringUtils.isPalindrome(s)` | `s.isPalindrome()` | > | класс `*Utils` со статикой | расширения в файле | > | `default`-метод в интерфейсе | расширение на интерфейсе | > | наследование или декоратор | расширение | > | нет аналога | свойства-расширения | > > **Расширение - это и есть статический утилитный метод, но с синтаксисом метода.** В Java накопление утилит приводит к классам вроде `StringUtils`, `CollectionUtils`, `DateUtils`, и вызов читается наизнанку: `StringUtils.capitalize(StringUtils.trim(s))`. В Kotlin то же самое читается слева направо: > ```kotlin > fun String.capitalizeFirst() = replaceFirstChar { it.uppercase() } > > fun main() { > println(" привет ".trim().capitalizeFirst()) // Привет > } > ``` > Разницы в байт-коде нет: и там и там статический метод. Отличается только порядок чтения и то, что IDE подсказывает расширения в автодополнении по типу. > > **`default`-методы требуют владения интерфейсом.** В Java добавить поведение к `List` можно только если вы автор `List`. Расширение объявляется где угодно - поэтому `List` в Kotlin имеет ~150 операций, не входящих в интерфейс. > > **Вызов из Java выглядит как статика.** Расширение `fun String.toSlug()` в файле `StringExtensions.kt` компилируется в `StringExtensionsKt.toSlug(String)`: > ```kotlin > @file:JvmName("StringUtils") > package demo > > fun String.toSlug() = lowercase() > ``` > Java-код вызовет `StringUtils.toSlug(s)`. То есть для Java-клиентов это ровно привычный утилитный класс. Подробнее: [Kotlin из Java](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md) > > **Lombok-подобных расширений в Java нет вообще.** Попытки добиться похожего (манифольд, плагины компилятора) остаются экзотикой, потому что требуют нестандартного компилятора. > > Сравнить: [Функции в Java](../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Функции.md), [Интерфейсы в Java](../../Программирование%20на%20Java/ООП/Интерфейсы.md) > [!warning] Подводные камни > **Разрешение статическое - полиморфизма нет:** > ```kotlin > open class Animal > class Dog : Animal() > > fun Animal.sound() = "..." > fun Dog.sound() = "гав" > > fun makeSound(a: Animal) = a.sound() > > fun main() { > println(Dog().sound()) // гав > println(makeSound(Dog())) // ... — выбрано по типу параметра! > } > ``` > Это самая частая ошибка с расширениями. Если поведение должно зависеть от фактического типа - нужен метод класса или `when` по типу. > > **Метод класса перекрывает расширение молча для вызывающего:** > ```kotlin > class Box8 { > fun size() = 1 > } > > fun Box8.size() = 999 > > fun main() { println(Box8().size()) } // 1 > ``` > Опаснее другое: если библиотека добавит метод `size()` в новой версии, ваше расширение перестанет вызываться после обновления - без ошибки компиляции. > > **Нет доступа к `private` и `protected`:** > ```kotlin > class Counter2 { > private var count = 0 > fun increment() { count++ } > fun value() = count > } > > // fun Counter2.reset() { count = 0 } // ошибка > fun Counter2.incrementTwice() { increment(); increment() } > > fun main() { > val c = Counter2() > c.incrementTwice() > println(c.value()) // 2 > } > ``` > > **Расширение на `T?` скрывает проверку на null:** > ```kotlin > fun String?.safeLength(): Int = this?.length ?: 0 > > fun main() { > val s: String? = null > println(s.safeLength()) // 0 — без ?. и без предупреждений > } > ``` > Читатель видит `s.safeLength()` и может решить, что `s` не может быть `null`. Для такой функции нужно очень явное имя. > > **Свойство-расширение не хранит состояние:** > ```kotlin > // val Int.cached: Int = compute() // ошибка: initializer not allowed > val Int.computed: Int get() = this * 2 // только геттер > > fun main() { println(2.computed) } // 4 > ``` > Геттер вызывается на каждое обращение. Для дорогих вычислений это неожиданная стоимость - `val` выглядит как поле, но является функцией. > > **Расширения не участвуют в `when` по типу и в рефлексии:** > ```kotlin > fun Any.describe5() = "объект" > > fun main() { > println("x".describe5()) // объект > println(String::class.members.any { it.name == "describe5" }) // false > } > ``` > Для фреймворков, работающих через рефлексию (сериализация, ORM), расширения невидимы. > > **Расширение на `Any` засоряет автодополнение:** > ```kotlin > fun Any?.debug(): String = "значение: $this" > > fun main() { println(1.debug()) } // значение: 1 > ``` > Такая функция появится в подсказках для **каждого** типа в проекте. Расширения на `Any` оправданы только в stdlib-подобных случаях. > > **Одинаковые расширения из разных пакетов конфликтуют.** Если два импорта дают `String.toSlug()`, компилятор потребует явного импорта или алиаса (`import a.toSlug as aSlug`). Для библиотек это аргумент за специфичные имена. > > **Расширение не наследуется и не переопределяется.** `override fun String.foo()` невозможно: у расширений нет виртуальной таблицы. > > **Расширение на платформенном типе может получить `null` вопреки сигнатуре:** > ```kotlin > fun String.shout() = uppercase() + "!" > > fun main() { > println("да".shout()) // ДА! > } > ``` > Если такое расширение вызвать на значении, пришедшем из Java-метода без аннотаций, проверка на `null` сработает на входе и даст NPE внутри расширения - со стеком, указывающим не на источник проблемы. Подробнее: [Платформенные типы](../Синтаксис%20и%20типы/Null-безопасность/Платформенные%20типы.md) Далее: [Scope-функции](Scope-функции.md)