Kotlin guide done. (20.10.26)

This commit is contained in:
2026-10-20 12:54:25 +03:00
parent da3ad9dd1a
commit 6853ada348
81 changed files with 30112 additions and 0 deletions
@@ -0,0 +1,361 @@
---
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)