362 lines
18 KiB
Markdown
362 lines
18 KiB
Markdown
---
|
||
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 <T> List<T>.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 <T> List<T>.penultimate(): T? = getOrNull(size - 2)
|
||
|
||
fun <T : Comparable<T>> List<T>.isSorted(): Boolean = zipWithNext().all { (a, b) -> a <= b }
|
||
|
||
fun <T> 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<Int>.sumOfSquares(): Int = sumOf { it * it }
|
||
fun List<String>.longest(): String? = maxByOrNull { it.length }
|
||
|
||
fun main() {
|
||
println(listOf(1, 2, 3).sumOfSquares()) // 14
|
||
println(listOf("a", "ccc", "bb").longest()) // ccc
|
||
}
|
||
```
|
||
Так же работает `Iterable<Int>.sum()` в stdlib - одна функция на каждый числовой тип.
|
||
|
||
## Расширения на функциональных типах
|
||
```kotlin
|
||
fun <T, R> ((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<String>, 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)
|