Files
obsidian/Kotlin/Функциональный Kotlin/Функции-расширения.md
T

362 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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)