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,444 @@
---
title: DSL
description: Из чего собирается предметно-ориентированный язык на Kotlin и когда он оправдан
tags:
- kotlin
- functional
- dsl
---
DSL (domain-specific language) на Kotlin - это обычный Kotlin-код, который выглядит как декларативное описание. Отдельного механизма для этого нет: DSL собирается из четырёх уже знакомых возможностей.
| Возможность | Что даёт |
|---|---|
| [лямбда с получателем](Receivers%20и%20context%20parameters.md) `T.() -> Unit` | блок, внутри которого доступны методы объекта |
| [лямбда последним аргументом](../Синтаксис%20и%20типы/Работа%20с%20данными/Лямбды%20и%20функциональные%20типы.md) | `html { }` вместо `html({ })` |
| [функции-расширения](Функции-расширения.md) | методы только внутри DSL |
| [операторы и `infix`](Инфиксные%20и%20операторные%20функции.md) | `"a" to 1`, `x shouldBe y` |
## Минимальный пример
```kotlin
class Pizza {
private val toppings = mutableListOf<String>()
var size = "средняя"
fun topping(name: String) { toppings.add(name) }
override fun toString() = "$size пицца: ${toppings.joinToString()}"
}
fun pizza(block: Pizza.() -> Unit): Pizza = Pizza().apply(block)
fun main() {
val order = pizza {
size = "большая"
topping("сыр")
topping("грибы")
}
println(order) // большая пицца: сыр, грибы
}
```
Три строки инфраструктуры (`fun pizza(block: ...) = Pizza().apply(block)`) превращают класс в DSL. Это весь базовый приём.
## Вложенные блоки
```kotlin
@DslMarker
annotation class HtmlDsl
@HtmlDsl
class Tag(private val name: String) {
private val children = mutableListOf<Tag>()
private val attributes = mutableMapOf<String, String>()
private var text: String? = null
fun attr(key: String, value: String) { attributes[key] = value }
operator fun String.unaryPlus() { text = this }
fun tag(name: String, block: Tag.() -> Unit = {}) {
children.add(Tag(name).apply(block))
}
fun div(block: Tag.() -> Unit) = tag("div", block)
fun p(block: Tag.() -> Unit) = tag("p", block)
fun render(): String {
val attrs = attributes.entries.joinToString("") { " ${it.key}=\"${it.value}\"" }
val inner = text ?: children.joinToString("") { it.render() }
return "<$name$attrs>$inner</$name>"
}
}
fun html(block: Tag.() -> Unit): String = Tag("html").apply(block).render()
fun main() {
val page = html {
div {
attr("class", "container")
p { +"Первый абзац" }
p { +"Второй абзац" }
}
}
println(page)
// <html><div class="container"><p>Первый абзац</p><p>Второй абзац</p></div></html>
}
```
Разбор приёмов:
- `Tag.() -> Unit` даёт вложенность: внутри `div { }` доступны методы `Tag`
- `operator fun String.unaryPlus()` даёт синтаксис `+"текст"` для содержимого
- `@DslMarker` запрещает вызвать `div { }` изнутри `div { }` через внешний получатель
- значение по умолчанию `block: Tag.() -> Unit = {}` разрешает теги без тела
## Типобезопасность
Главное отличие Kotlin-DSL от строковых шаблонов: ошибки видит компилятор.
```kotlin
class Query {
private var table: String? = null
private val conditions = mutableListOf<String>()
private var limit: Int? = null
fun from(name: String) { table = name }
fun where(condition: String) { conditions.add(condition) }
fun limit(n: Int) { limit = n }
fun build(): String {
val t = requireNotNull(table) { "не указана таблица" }
return buildString {
append("SELECT * FROM $t")
if (conditions.isNotEmpty()) append(" WHERE ${conditions.joinToString(" AND ")}")
limit?.let { append(" LIMIT $it") }
}
}
}
fun query(block: Query.() -> Unit): String = Query().apply(block).build()
fun main() {
println(query {
from("users")
where("age > 18")
where("active = true")
limit(10)
})
// SELECT * FROM users WHERE age > 18 AND active = true LIMIT 10
try {
query { where("x = 1") }
} catch (e: IllegalArgumentException) {
println(e.message) // не указана таблица
}
}
```
Проверка обязательных полей здесь в рантайме. Вынести её в компиляцию можно через типы-состояния:
```kotlin
class EmptyQuery {
fun from(table: String) = TableQuery(table)
}
class TableQuery(private val table: String) {
private val conditions = mutableListOf<String>()
fun where(condition: String): TableQuery = apply { conditions.add(condition) }
fun build(): String =
"SELECT * FROM $table" +
if (conditions.isEmpty()) "" else " WHERE ${conditions.joinToString(" AND ")}"
}
fun main() {
println(EmptyQuery().from("orders").where("total > 100").build())
// SELECT * FROM orders WHERE total > 100
// println(EmptyQuery().build()) // ошибка компиляции: метода нет
}
```
Такой приём (phantom types) делает невозможное состояние непредставимым, но усложняет код. Для большинства DSL достаточно `require` в `build()`.
## Коллекции внутри DSL
```kotlin
class Routes {
private val routes = mutableListOf<Pair<String, String>>()
infix fun String.handledBy(handler: String) { routes.add(this to handler) }
fun group(prefix: String, block: Routes.() -> Unit) {
val nested = Routes().apply(block)
nested.routes.forEach { (path, handler) -> routes.add("$prefix$path" to handler) }
}
fun render() = routes.joinToString("\n") { "${it.first} -> ${it.second}" }
}
fun routes(block: Routes.() -> Unit) = Routes().apply(block).render()
fun main() {
println(routes {
"/" handledBy "HomeController"
group("/api") {
"/users" handledBy "UsersController"
"/orders" handledBy "OrdersController"
}
})
// / -> HomeController
// /api/users -> UsersController
// /api/orders -> OrdersController
}
```
`infix` расширение внутри класса (`String.handledBy`) существует только внутри блока `routes { }` - снаружи такого метода у `String` нет.
## Готовые DSL в stdlib
```kotlin
fun main() {
val text = buildString {
append("Привет")
appendLine(", мир")
append("Вторая строка")
}
println(text)
val list = buildList {
add(1)
addAll(listOf(2, 3))
}
println(list) // [1, 2, 3]
val map = buildMap<String, Int> {
put("a", 1)
putAll(mapOf("b" to 2))
}
println(map) // {a=1, b=2}
}
```
Эти функции - ровно тот же приём `T.() -> Unit` + `apply`.
Числовые и временные DSL:
```kotlin
import kotlin.time.Duration.Companion.seconds
import kotlin.time.Duration.Companion.minutes
fun main() {
val timeout = 2.minutes + 30.seconds
println(timeout) // 2m 30s
println(timeout.inWholeSeconds) // 150
println((1..10 step 2).toList()) // [1, 3, 5, 7, 9]
println((10 downTo 1 step 3).toList()) // [10, 7, 4, 1]
}
```
`2.minutes` - свойство-расширение на `Int`. Такой «единичный» DSL делается одной строкой и заметно улучшает читаемость конфигураций.
## Известные DSL в экосистеме
| DSL | Для чего |
|---|---|
| Gradle Kotlin DSL | сборка проекта, `build.gradle.kts` |
| Ktor | маршруты и конфигурация сервера |
| kotlinx.html | генерация HTML |
| Exposed | SQL-запросы с типобезопасностью |
| Kotest | описание тестов и матчеры |
| Compose | описание UI |
Пример в стиле Gradle - разбор см. [Gradle и Kotlin DSL](../Инструментарий/Gradle%20и%20Kotlin%20DSL.md):
```kotlin
class Dependencies {
private val list = mutableListOf<String>()
fun implementation(dep: String) { list.add("implementation $dep") }
fun testImplementation(dep: String) { list.add("testImplementation $dep") }
fun render() = list.joinToString("\n")
}
class Project2 {
private var name = "unnamed"
private var deps = Dependencies()
fun name(value: String) { name = value }
fun dependencies(block: Dependencies.() -> Unit) { deps = Dependencies().apply(block) }
fun render() = "project $name\n${deps.render()}"
}
fun project(block: Project2.() -> Unit) = Project2().apply(block).render()
fun main() {
println(project {
name("demo")
dependencies {
implementation("kotlin-stdlib")
testImplementation("kotest")
}
})
// project demo
// implementation kotlin-stdlib
// testImplementation kotest
}
```
## Когда DSL оправдан
| Признак | Вывод |
|---|---|
| структура описывается многократно, вид стабилен | DSL уместен |
| вложенная иерархия (UI, конфиг, разметка) | DSL уместен |
| писать будут люди, не знающие внутренностей | DSL уместен |
| альтернатива - длинный конструктор или `Map<String, Any>` | DSL уместен |
| одно-два места использования | обычный код или `data class` |
| нужна сериализация/валидация схемы | формат данных (JSON/YAML) + парсер |
| структура меняется каждую неделю | DSL придётся переписывать |
Главная цена DSL - его нужно изучать. Для внутреннего инструмента, который используют трое, именованные аргументы `data class` часто выигрывают:
```kotlin
data class ServerConfig(
val host: String = "localhost",
val port: Int = 8080,
val tls: Boolean = false,
val timeoutSeconds: Int = 30,
)
fun main() {
val config = ServerConfig(host = "example.com", port = 443, tls = true)
println(config)
// ServerConfig(host=example.com, port=443, tls=true, timeoutSeconds=30)
}
```
Это читается не хуже DSL, проверяется компилятором лучше, бесплатно получает `copy`, `equals` и сериализацию. DSL нужен, когда структура **вложенная** и **повторяющаяся** - там, где `data class` требует громоздких вложенных конструкторов.
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | fluent builder с `return this` | `T.() -> Unit` |
> | XML/YAML/properties для конфигурации | типобезопасный DSL |
> | анонимный класс с методами | лямбда с получателем |
> | строковая конкатенация SQL | DSL с проверкой типов |
> | нет аналога | `@DslMarker` |
>
> **В Java DSL ограничен цепочками методов.** Максимум, что доступно - fluent interface:
> ```java
> Html.create()
> .div().attr("class", "container")
> .p().text("абзац").end()
> .end()
> .render();
> ```
> Вложенность приходится выражать парами `.div()`/`.end()`, и компилятор не проверит их баланс. В Kotlin вложенность выражается вложенностью блоков - забыть «закрывающий тег» физически невозможно.
>
> **Это причина, по которой Gradle перешёл на Kotlin.** `build.gradle` на Groovy был динамическим: опечатка в имени свойства обнаруживалась при запуске сборки, автодополнения не было. `build.gradle.kts` - обычный Kotlin-код, поэтому IDE знает все доступные методы, а ошибка в имени не компилируется. Цена - более медленная первая сборка (скрипт нужно скомпилировать).
>
> **Конфигурация в коде вместо формата данных.** В Java-мире окружение описывают в XML (Spring, Maven) или YAML (Spring Boot). Kotlin-DSL даёт проверку типов и переиспользование (функции, циклы, условия прямо в конфиге), но теряет главное свойство данных: конфигурацию нельзя прочитать и изменить без компилятора. Для конфигурации, которую правят администраторы, формат данных остаётся правильным выбором.
>
> **Лямбда с получателем - ключевая возможность, которой нет в Java.** Без неё все остальные приёмы (операторы, расширения) не складываются в DSL, поэтому в Java аналога попросту не существует.
>
> Сравнить: [Builder](../../Программирование%20на%20Java/Паттерны%20проектирования/Builder.md), [Интерфейсы в Java](../../Программирование%20на%20Java/ООП/Интерфейсы.md)
> [!warning] Подводные камни
> **Без `@DslMarker` внешний получатель виден изнутри:**
> ```kotlin
> class Node {
> val children = mutableListOf<String>()
> fun child(name: String) { children.add(name) }
> fun nested(block: Node.() -> Unit) { children.add(Node().apply(block).children.toString()) }
> }
>
> fun main() {
> val n = Node().apply {
> child("a")
> nested { child("b") }
> }
> println(n.children) // [a, [b]]
> }
> ```
> Здесь всё верно, но стоит добавить метод, которого нет у внутреннего уровня, и вызов молча уйдёт на внешний. `@DslMarker` стоит ставить сразу.
>
> **DSL скрывает порядок выполнения:**
> ```kotlin
> class Steps {
> private val log = mutableListOf<String>()
> fun step(name: String) { log.add(name) }
> fun render() = log.joinToString(" -> ")
> }
>
> fun main() {
> val s = Steps().apply {
> step("первый")
> step("второй")
> }
> println(s.render()) // первый -> второй
> }
> ```
> Блок выглядит декларативно, но это императивный код: вызовы выполняются по порядку, и переставить строки - значит изменить результат. Пользователь DSL об этом не догадывается.
>
> **Изменяемое состояние в билдере переиспользуется:**
> ```kotlin
> class Builder2 {
> private val items = mutableListOf<String>()
> fun item(x: String) { items.add(x) }
> fun build() = items.toList()
> }
>
> fun main() {
> val b = Builder2()
> b.item("a")
> val first = b.build()
> b.item("b")
> val second = b.build()
> println("$first / $second") // [a] / [a, b]
> }
> ```
> Если билдер доступен снаружи, повторный `build()` даёт другой результат. Делайте билдер одноразовым: создавайте его внутри функции-входа и не возвращайте.
>
> **`var` в DSL без валидации допускает некорректное состояние:**
> ```kotlin
> class Window {
> var width = 0
> var height = 0
> fun area() = width * height
> }
>
> fun window(block: Window.() -> Unit) = Window().apply(block)
>
> fun main() {
> println(window { width = -5; height = 10 }.area()) // -50
> }
> ```
> Нужна проверка в `build()`/`apply` или сеттер с `require`.
>
> **Вложенные лямбды с получателем трудно отлаживать.** Стек вызовов состоит из `invoke`-методов сгенерированных лямбд, а точка останова внутри блока не показывает, какому уровню она принадлежит. Для глубоких DSL это заметная проблема.
>
> **DSL без документации непригоден.** Пользователь видит `pizza { }` и не знает, какие методы доступны - IDE подскажет, но ревью в браузере и примеры в README необходимы. Это основной скрытый расход на DSL.
>
> **Методы DSL попадают в автодополнение не там, где ожидалось.** Если расширение объявлено на верхнем уровне (а не внутри класса DSL), оно появится везде:
> ```kotlin
> // плохо: operator fun String.unaryPlus() на верхнем уровне сломает код,
> // где +"строка" означает что-то другое
> class Container2 {
> private val parts = mutableListOf<String>()
> operator fun String.unaryPlus() { parts.add(this) } // только внутри Container2
> fun render() = parts.joinToString()
> }
>
> fun container(block: Container2.() -> Unit) = Container2().apply(block).render()
>
> fun main() { println(container { +"a"; +"b" }) } // a, b
> ```
> Объявляйте операторы DSL **внутри** класса-контекста.
>
> **`apply` возвращает билдер, а не результат:**
> ```kotlin
> class Doc {
> private val lines = mutableListOf<String>()
> fun line(x: String) { lines.add(x) }
> fun render() = lines.joinToString("\n")
> }
>
> fun docWrong(block: Doc.() -> Unit) = Doc().apply(block) // возвращает Doc
> fun docRight(block: Doc.() -> Unit) = Doc().apply(block).render() // возвращает String
>
> fun main() {
> println(docWrong { line("a") }::class.simpleName) // Doc
> println(docRight { line("a") }) // a
> }
> ```
> Утечка билдера наружу позволяет менять «готовый» объект после сборки.
>
> **DSL - не замена валидации данных.** Типобезопасность проверяет структуру, а не смысл: `port = 70000` скомпилируется. Проверки диапазонов, обязательных полей и взаимных зависимостей нужно писать вручную. Подробнее: [Проверки и контракты](../Исключения/Проверки%20и%20контракты.md)
>
> **DSL тяжело версионировать.** Переименование метода - несовместимое изменение для всех пользователей, а `@Deprecated` на методах внутри класса-контекста работает, но предупреждения в больших блоках теряются.
Далее: [Паттерны проектирования](Паттерны%20проектирования.md)
@@ -0,0 +1,322 @@
---
title: Inline-функции
description: Подстановка тела на этапе компиляции, noinline и crossinline, нелокальный return и цена inline
tags:
- kotlin
- functional
- performance
---
`inline` говорит компилятору: не вызывай эту функцию, а подставь её тело в место вызова. Для обычных функций это почти бесполезно (JIT справляется сам), а для функций с лямбдами - убирает объект лямбды.
```kotlin
inline fun measure(block: () -> Unit): Long {
val start = System.nanoTime()
block()
return System.nanoTime() - start
}
fun main() {
val time = measure { (1..1000).sum() }
println(time > 0) // true
}
```
После компиляции в `main` не остаётся ни вызова `measure`, ни объекта `Function0` - там просто `val start = ...; (1..1000).sum(); ...`.
## Что именно экономится
Без `inline` на каждый вызов создаётся объект, реализующий `Function0`:
```kotlin
fun notInlined(block: () -> Int): Int = block()
inline fun inlined(block: () -> Int): Int = block()
fun main() {
var captured = 0
repeat(3) { notInlined { captured + 1 } } // лямбда захватывает captured -> новый объект каждый раз
repeat(3) { inlined { captured + 1 } } // объектов нет вообще
println(notInlined { 42 }) // 42
println(inlined { 42 }) // 42
}
```
Лямбда **без захвата** компилируется в синглтон, поэтому объект создаётся один раз - тут `inline` экономит только вызов. Лямбда **с захватом** создаёт объект на каждое вычисление, и вот здесь разница существенна.
Поэтому все операции над коллекциями в stdlib объявлены `inline`:
```kotlin
fun main() {
val sum = listOf(1, 2, 3).filter { it > 1 }.sumOf { it * 2 }
println(sum) // 10
}
```
В байт-коде это два обычных цикла `for` с телами лямбд внутри. Ни одного объекта лямбды и ни одного виртуального вызова - именно поэтому короткие цепочки в Kotlin обычно быстрее Java-стримов.
## Нелокальный return
Второе следствие подстановки: из inline-лямбды можно выйти из внешней функции.
```kotlin
inline fun <T> List<T>.forEachInline(action: (T) -> Unit) { for (item in this) action(item) }
fun findFirstNegative(numbers: List<Int>): Int? {
numbers.forEachInline { if (it < 0) return it } // выход из findFirstNegative
return null
}
fun main() {
println(findFirstNegative(listOf(1, 2, -3, 4))) // -3
println(findFirstNegative(listOf(1, 2))) // null
}
```
Это работает, потому что `return` оказывается физически внутри `findFirstNegative`. В не-inline функции такой `return` запрещён:
```kotlin
fun <T> List<T>.forEachSlow(action: (T) -> Unit) { for (item in this) action(item) }
fun firstNegative2(numbers: List<Int>): Int? {
numbers.forEachSlow { if (it < 0) return@forEachSlow } // только локальный выход
return null
}
fun main() { println(firstNegative2(listOf(-1))) } // null
```
Именно поэтому `forEach` из stdlib ведёт себя как `continue`, а не как `break`:
```kotlin
fun main() {
listOf(1, 2, 3).forEach {
if (it == 2) return@forEach // пропустить элемент
print("$it ")
}
println() // 1 3
}
```
## noinline
Если лямбду нужно сохранить или передать дальше, подставить её нельзя - у подставленного тела нет объекта:
```kotlin
inline fun register(
name: String,
noinline handler: (String) -> Unit,
validator: (String) -> Boolean,
): (String) -> Unit {
println("регистрирую $name, валиден: ${validator(name)}")
return handler // возвращаем объект — нужен noinline
}
fun main() {
val h = register("событие", handler = { println("обработано: $it") }, validator = { it.isNotEmpty() })
h("данные")
// регистрирую событие, валиден: true
// обработано: данные
}
```
Без `noinline` компилятор откажется: «Illegal usage of inline-parameter». Правило: лямбду, которая только вызывается внутри функции, можно инлайнить; лямбду, которую сохраняют в поле, возвращают или передают в не-inline функцию - нельзя.
## crossinline
Промежуточный случай: лямбда вызывается не напрямую, а внутри другой лямбды или объекта.
```kotlin
inline fun runLater(crossinline block: () -> Unit): Runnable = Runnable { block() }
fun main() {
val task = runLater { println("выполнено позже") }
task.run() // выполнено позже
}
```
`crossinline` означает «инлайнить можно, но нелокальный `return` запрещён». Логика такая: тело лямбды подставляется внутрь `Runnable`, а оттуда выйти из вызывающей функции физически невозможно - она уже завершилась.
```kotlin
inline fun repeatTwice(crossinline block: () -> Unit) {
val wrapped = { block(); block() }
wrapped()
}
fun demo() {
repeatTwice { println("раз") }
// repeatTwice { return } // ошибка: нелокальный return запрещён
}
fun main() { demo() } // раз \n раз
```
| Модификатор | Лямбда подставляется | Нелокальный `return` | Можно сохранить/передать |
|---|---|---|---|
| (по умолчанию) | да | да | нет |
| `crossinline` | да | нет | только внутрь другой лямбды |
| `noinline` | нет | нет | да |
## reified
Третье следствие подстановки: в месте вызова известен фактический тип, поэтому параметр типа можно «материализовать».
```kotlin
inline fun <reified T> filterByType(items: List<Any>): List<T> = items.filterIsInstance<T>()
fun main() {
val mixed = listOf(1, "a", 2.0, 3)
println(filterByType<Int>(mixed)) // [1, 3]
println(filterByType<String>(mixed)) // [a]
}
```
Это основная причина объявлять `inline` функцию без лямбд вообще. Подробнее: [Reified и inline](../Дженерики/Reified%20и%20inline.md)
## Inline-свойства
`inline` применим и к аксессорам:
```kotlin
class Config2(val map: Map<String, String>) {
inline val isEmpty: Boolean get() = map.isEmpty()
val host: String
inline get() = map["host"] ?: "localhost"
}
fun main() {
val c = Config2(mapOf("host" to "example.com"))
println("${c.isEmpty} ${c.host}") // false example.com
}
```
Обратите внимание: `map` объявлен `val` без `private`. Для `private val` сниппет не скомпилируется - на inline-аксессоры действует то же правило, что на inline-функции: подставленное тело окажется в чужом классе, где приватный член недоступен.
Выгода минимальна - JIT инлайнит такие геттеры и сам. Реально `inline`-свойства нужны редко: когда геттер должен использовать `reified` или когда свойство - часть публичного API, которое не хочется оставлять виртуальным.
## Когда inline оправдан
| Случай | Нужен ли `inline` |
|---|---|
| функция принимает лямбду, которая вызывается сразу | да |
| нужен `reified` | да, обязательно |
| нужен нелокальный `return` из лямбды | да, обязательно |
| функция без лямбд и без `reified` | нет, JIT справится |
| тело большое (десятки строк) | нет, байт-код раздуется |
| функция рекурсивная | нельзя |
| публичное API библиотеки | осторожно - см. подводные камни |
## Что происходит с байт-кодом
```kotlin
inline fun twice(block: () -> Unit) { block(); block() }
fun main() {
var count = 0
twice { count++ }
twice { count += 10 }
println(count) // 22
}
```
Тело `twice` скопировано в `main` дважды, и каждая копия содержит своё тело лямбды - итого четыре инкремента в коде. Для короткой функции это дешевле вызова; для функции на сто строк, вызываемой из пятидесяти мест, это пять тысяч строк байт-кода.
Компилятор предупреждает, когда `inline` бесполезен:
```kotlin
inline fun uselessInline(x: Int): Int = x * 2 // warning: expected performance impact is insignificant
fun main() { println(uselessInline(2)) } // 4
```
Такое предупреждение - сигнал убрать `inline`, если не нужен `reified`.
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | нет inline на уровне языка | `inline fun` |
> | JIT инлайнит в рантайме | компилятор инлайнит в байт-код |
> | лямбда всегда объект | при `inline` объекта нет |
> | `return` из лямбды невозможен | нелокальный `return` при `inline` |
> | `Class<T>` параметром | `reified T` |
> | `final` подсказывает JIT | `inline` - директива компилятору |
>
> **Java инлайнит в рантайме, Kotlin - при компиляции.** HotSpot инлайнит горячие методы агрессивнее, чем любой компилятор: он знает фактические типы, статистику ветвлений и может деоптимизировать решение. Поэтому `inline` в Kotlin - не про скорость исполнения тела, а про три вещи, недоступные JIT: отсутствие объекта лямбды, нелокальный `return` и `reified`.
>
> **Escape analysis частично закрывает ту же проблему в Java.** Если JIT доказывает, что объект лямбды не покидает метод, он может не выделять его в куче (scalar replacement). Но это оптимизация «на удачу»: она применяется не всегда и ломается при полиморфном вызове. `inline` в Kotlin гарантирован и виден в байт-коде.
>
> **Нелокального `return` в Java нет вообще.** Из тела `forEach` нельзя выйти из внешнего метода - приходится либо возвращаться к обычному `for`, либо бросать исключение, либо использовать `findFirst` на стриме. Kotlin позволяет писать абстракции обхода, которые ведут себя как встроенный цикл:
> ```kotlin
> inline fun <T> Iterable<T>.firstMatching(p: (T) -> Boolean): T? {
> for (item in this) if (p(item)) return item
> return null
> }
>
> fun main() { println(listOf(1, 2, 3).firstMatching { it > 1 }) } // 2
> ```
>
> **Цена - совместимость библиотек.** В Java изменение тела метода не требует перекомпиляции клиентов. В Kotlin тело inline-функции физически скопировано в байт-код клиента, поэтому исправление в библиотеке не подействует до пересборки вызывающего кода. Для публичного API это серьёзный аргумент, и stdlib помечает такие функции `@PublishedApi`, когда они обращаются к internal-коду.
>
> **Inline-функцию нельзя вызвать из Java** с сохранением смысла: Java-компилятор не умеет подставлять тело, он вызовет сгенерированный метод как обычный. `reified` при этом не работает.
>
> Сравнить: [JIT в Java](../../Программирование%20на%20Java/JVM/JIT.md), [Байт-код](../../Программирование%20на%20Java/JVM/Байт-код.md)
> [!warning] Подводные камни
> **Inline раздувает байт-код.** Тело копируется в каждое место вызова. Для больших функций это рост размера класса, давление на кеш инструкций процессора и более медленная загрузка классов. `inline` оправдан для коротких функций - в stdlib почти все inline-функции занимают несколько строк.
>
> **Изменение тела inline-функции не действует без перекомпиляции клиентов:**
> ```kotlin
> inline fun version() = "v1" // если выпустить v2, клиенты со старой сборкой увидят v1
>
> fun main() { println(version()) } // v1
> ```
> Это ломает обычные ожидания от семантического версионирования библиотек.
>
> **Inline-функция не видит `private` членов своего класса:**
> ```kotlin
> class Service3 {
> private val secret = "скрыто"
> // inline fun leak() = secret // ошибка: public-inline не может обращаться к private
> fun ok() = secret
> internal inline fun fine() = ok() // а так можно
> }
>
> fun main() { println(Service3().fine()) } // скрыто
> ```
> Причина: тело окажется в чужом классе, где `private` недоступен. Обход - `@PublishedApi internal` на члене.
>
> **Рекурсивную функцию нельзя сделать inline:**
> ```kotlin
> // inline fun fact(n: Int): Int = if (n <= 1) 1 else n * fact(n - 1) // ошибка
> fun fact(n: Int): Int = if (n <= 1) 1 else n * fact(n - 1)
>
> fun main() { println(fact(5)) } // 120
> ```
> Подстановка была бы бесконечной.
>
> **Лямбду из inline-параметра нельзя сохранить:**
> ```kotlin
> inline fun broken(block: () -> Unit) {
> // val saved = block // ошибка: Illegal usage of inline-parameter
> block()
> }
>
> inline fun fixed(noinline block: () -> Unit): () -> Unit = block
>
> fun main() {
> broken { println("ок") }
> fixed { println("сохранено") }()
> }
> ```
>
> **`crossinline` запрещает нелокальный `return`, и ошибка возникает у пользователя функции**, а не у автора. Если лямбда оборачивается в `Runnable`, объект или другую лямбду - `crossinline` обязателен, и вызывающий код теряет возможность выйти из своей функции.
>
> **Нелокальный `return` из inline-лямбды пропускает код после вызова:**
> ```kotlin
> inline fun withLogging(block: () -> Unit) {
> println("до")
> block()
> println("после") // не выполнится при return из лямбды
> }
>
> fun run1() {
> withLogging { return }
> }
>
> fun main() { run1(); println("конец") } // до \n конец
> ```
> Строка «после» потерялась. Если в функции есть освобождение ресурсов, его нужно ставить в `finally` - иначе нелокальный `return` его обойдёт:
> ```kotlin
> inline fun safeLogging(block: () -> Unit) {
> println("до")
> try { block() } finally { println("после") }
> }
>
> fun run2() { safeLogging { return } }
>
> fun main() { run2() } // до \n после
> ```
> Это важнее, чем кажется: `use`, `withLock` и транзакционные обёртки обязаны использовать `finally` именно по этой причине.
>
> **`inline` на функции без лямбд вызывает предупреждение** и почти не влияет на производительность. Единственные оправдания - `reified` и необходимость подставить тело в клиентский код.
>
> **Внутри inline-лямбды нельзя использовать `suspend`-вызовы, если не помечено специально.** Для функций вроде `measure` это выглядит рабочим, но обёртка над `suspend`-кодом требует либо `suspend inline`, либо `crossinline` и явного учёта контекста. Подробнее: [Suspend-функции](../Корутины/Suspend-функции.md)
>
> **Инлайн не убирает упаковку примитивов в функциональных типах автоматически** - но при полной подстановке упаковки не остаётся, потому что лямбды как объекта нет. А вот с `noinline` параметром `(Int) -> Int` упаковка сохраняется.
>
> **Профилируйте, прежде чем добавлять `inline` вручную.** Накладные расходы лямбды измеримы только в горячем коде с миллионами вызовов. В обычном бизнес-коде преждевременный `inline` даёт лишь раздутый байт-код и проблемы совместимости.
Далее: [Функции-расширения](Функции-расширения.md)
@@ -0,0 +1,439 @@
---
title: Receivers и context parameters
description: Лямбды с получателем, несколько получателей в области видимости, DslMarker и context parameters вместо context receivers
tags:
- kotlin
- functional
---
Получатель (receiver) - это объект, доступный в блоке кода как `this` без явного указания. На этом механизме построены расширения, scope-функции, билдеры и все DSL в Kotlin.
```kotlin
fun StringBuilder.addLine(text: String) {
append(text) // this.append — this есть неявно
append("\n")
}
fun main() {
val sb = StringBuilder()
sb.addLine("первая")
sb.addLine("вторая")
print(sb)
// первая
// вторая
}
```
## Функциональный тип с получателем
Тип `T.() -> R` - это функция, внутри которой `this` имеет тип `T`:
```kotlin
fun buildText(block: StringBuilder.() -> Unit): String {
val sb = StringBuilder()
sb.block() // вызываем лямбду с sb как получателем
return sb.toString()
}
fun main() {
val text = buildText {
append("Привет") // this == StringBuilder
append(", ")
append("мир")
}
println(text) // Привет, мир
}
```
Разница с обычным `(StringBuilder) -> Unit` только в том, как обращаться к объекту:
```kotlin
fun withReceiver(block: StringBuilder.() -> Unit) = StringBuilder().apply(block).toString()
fun withParameter(block: (StringBuilder) -> Unit) = StringBuilder().also(block).toString()
fun main() {
println(withReceiver { append("через this") }) // через this
println(withParameter { it.append("через it") }) // через it
}
```
Типы взаимозаменяемы при передаче: лямбда с получателем подходит там, где ожидается функция с параметром, и наоборот - компилятор конвертирует их сам.
Так устроены все scope-функции:
```kotlin
// В stdlib:
// inline fun <T, R> T.run(block: T.() -> R): R = block()
// inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }
// inline fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block()
// inline fun <T, R> T.let(block: (T) -> R): R = block(this)
// inline fun <T> T.also(block: (T) -> Unit): T { block(this); return this }
```
`run`/`apply`/`with` принимают `T.() -> R`, `let`/`also` - `(T) -> R`. Вот и всё различие между `this` и `it` в [scope-функциях](Scope-функции.md).
## Два получателя одновременно
Расширение, объявленное внутри класса, имеет **два** получателя: экземпляр класса (dispatch receiver) и объект расширения (extension receiver).
```kotlin
class HtmlBuilder {
private val sb = StringBuilder()
fun String.tag(name: String) { // this: String, this@HtmlBuilder: HtmlBuilder
sb.append("<$name>").append(this).append("</$name>")
}
fun build(block: HtmlBuilder.() -> Unit): String {
block()
return sb.toString()
}
}
fun main() {
val html = HtmlBuilder().build {
"Заголовок".tag("h1")
"Текст".tag("p")
}
println(html) // <h1>Заголовок</h1><p>Текст</p>
}
```
Внутри `tag` доступны оба: `this` (или `this@tag`) - строка, `this@HtmlBuilder` - билдер. Приоритет у ближайшего, то есть у extension receiver.
## Квалифицированный this
Когда получателей несколько, их различают метками:
```kotlin
class Outer2 {
val name = "внешний"
inner class Inner3 {
val name = "внутренний"
fun show(): String {
val local = "локальный"
return "$name / ${this@Inner3.name} / ${this@Outer2.name} / $local"
}
}
}
fun main() {
println(Outer2().Inner3().show())
// внутренний / внутренний / внешний / локальный
}
```
То же в лямбдах с получателем:
```kotlin
fun main() {
val result = StringBuilder().apply {
append("a")
"b".run {
this@apply.append(this) // this — String "b", this@apply — StringBuilder
this@apply.append("c")
}
}
println(result) // abc
}
```
Метка берётся из имени функции, принимающей лямбду. Поэтому для двух вложенных `run` метка `this@run` указала бы на **внутренний** - ближайший одноимённый. Когда уровней больше одного, выбирайте разные scope-функции или явные имена параметров; если имени нет (анонимная функция), доступ только через параметры.
## Вложенные билдеры и DslMarker
Проблема вложенных получателей: внутренний блок видит и внешний.
```kotlin
class Table {
private val rows = mutableListOf<String>()
fun row(block: Row.() -> Unit) { rows.add(Row().apply(block).render()) }
fun render() = rows.joinToString("\n")
}
class Row {
private val cells = mutableListOf<String>()
fun cell(text: String) { cells.add(text) }
fun render() = cells.joinToString(" | ")
}
fun table(block: Table.() -> Unit) = Table().apply(block).render()
fun main() {
println(table {
row {
cell("a")
cell("b")
// row { } // компилируется! внешний получатель видим
}
row { cell("c") }
})
// a | b
// c
}
```
Вложенный `row { }` внутри `row` - логическая ошибка, но компилятор её не видит. Решение - аннотация `@DslMarker`:
```kotlin
@DslMarker
annotation class TableDsl
@TableDsl
class Table2 {
private val rows = mutableListOf<String>()
fun row(block: Row2.() -> Unit) { rows.add(Row2().apply(block).render()) }
fun render() = rows.joinToString("\n")
}
@TableDsl
class Row2 {
private val cells = mutableListOf<String>()
fun cell(text: String) { cells.add(text) }
fun render() = cells.joinToString(" | ")
}
fun table2(block: Table2.() -> Unit) = Table2().apply(block).render()
fun main() {
println(table2 {
row {
cell("a")
// row { } // теперь ошибка компиляции
}
})
// a
}
```
`@DslMarker` запрещает неявный доступ к внешнему получателю того же «семейства». Явный `this@table2.row { }` при этом остаётся доступен. Подробнее: [DSL](DSL.md)
## Получатель в расширении для DSL
Полезный приём - расширение, доступное только внутри определённого контекста:
```kotlin
class Validator {
private val errors = mutableListOf<String>()
fun String.required(field: String) {
if (isBlank()) errors.add("$field обязателен")
}
fun String.maxLength(field: String, max: Int) {
if (length > max) errors.add("$field длиннее $max")
}
fun result(): List<String> = errors
}
fun validate(block: Validator.() -> Unit): List<String> = Validator().apply(block).result()
fun main() {
val errors = validate {
"".required("имя")
"очень длинное значение".maxLength("описание", 5)
"ок".required("статус")
}
println(errors) // [имя обязателен, описание длиннее 5]
}
```
`required` существует только внутри `validate { }` - снаружи такого расширения на `String` нет. Это способ не засорять глобальное пространство имён.
## Context parameters
Начиная с Kotlin 2.2 (стабилизированы в 2.4) есть **контекстные параметры** - неявные зависимости, которые передаются по области видимости, а не через аргументы. Они заменили экспериментальные context receivers.
```kotlin
interface Logger2 {
fun log(message: String)
}
context(logger: Logger2)
fun processOrder(id: Int) {
logger.log("обработка заказа $id")
logger.log("заказ $id готов")
}
class ConsoleLogger : Logger2 {
override fun log(message: String) = println("[LOG] $message")
}
fun main() {
with(ConsoleLogger()) {
processOrder(42)
}
// [LOG] обработка заказа 42
// [LOG] заказ 42 готов
}
```
Отличия от получателя:
- контекстный параметр **именованный** (`logger`), а не безымянный `this`
- он не участвует в разрешении вызовов: `log("x")` без `logger.` не скомпилируется
- таких параметров может быть несколько
- он не делает функцию расширением - вызов остаётся `processOrder(42)`
```kotlin
interface Transaction { fun commit() }
interface Metrics { fun count(name: String) }
context(tx: Transaction, metrics: Metrics)
fun saveUser(name: String) {
metrics.count("user.save")
println("сохранён $name")
tx.commit()
}
fun main() {
val tx = object : Transaction { override fun commit() = println("транзакция закрыта") }
val metrics = object : Metrics { override fun count(name: String) = println("метрика: $name") }
with(tx) {
with(metrics) {
saveUser("Иван")
}
}
// метрика: user.save
// сохранён Иван
// транзакция закрыта
}
```
Задача, которую это решает: передать «окружение» (логгер, транзакцию, локаль, конфигурацию) через несколько уровней вызовов, не протаскивая параметр руками и не используя глобальное состояние.
Контекстный параметр можно объявить и у свойства:
```kotlin
interface Locale2 { val code: String }
context(locale: Locale2)
val greeting: String
get() = if (locale.code == "ru") "Привет" else "Hello"
fun main() {
val ru = object : Locale2 { override val code = "ru" }
val en = object : Locale2 { override val code = "en" }
println(with(ru) { greeting }) // Привет
println(with(en) { greeting }) // Hello
}
```
| | Параметр | Получатель (`T.()`) | Контекстный параметр |
|---|---|---|---|
| вызов | `f(x, dep)` | `x.f()` | `f(x)`, зависимость из области |
| доступ внутри | по имени | `this`, неявно | по имени |
| сколько | сколько угодно | один | несколько |
| влияет на разрешение вызовов | нет | да | нет |
| видно в сигнатуре вызова | да | да | нет |
Практическое правило: получатель - когда объект **главный** в операции (`sb.append`), контекстный параметр - когда это **служебная** зависимость (логгер, транзакция), обычный параметр - когда это данные.
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `this` только в методах своего класса | `this` в любой лямбде с получателем |
> | билдер с `return this` | `T.() -> Unit` |
> | передача зависимостей аргументами или DI-контейнером | контекстные параметры |
> | `ThreadLocal` для «окружения» | контекстные параметры |
> | нет аналога | `@DslMarker` |
>
> **Лямбда с получателем аналога в Java не имеет.** Это и есть причина, по которой DSL на Kotlin (Gradle Kotlin DSL, HTML-билдеры, конфигурация Ktor) выглядят как отдельный язык, а в Java ограничиваются цепочками методов:
> ```java
> new StringBuilder().append("a").append("b").toString();
> ```
> В Kotlin блок кода сам становится областью, где доступны методы объекта - без повторения имени переменной и без `return this` в каждом методе класса.
>
> **Контекстные параметры решают задачу, для которой в Java используют `ThreadLocal` или DI.** Типичный Java-подход к «окружению» - либо протащить параметр через все слои, либо положить его в `ThreadLocal` (как делают Spring-транзакции и MDC в логировании), либо внедрить через контейнер. Первый вариант шумный, второй - неявное глобальное состояние, ломающееся при переходе между потоками, третий требует фреймворка. Контекстный параметр проверяется компилятором: функция не вызовется без нужного контекста:
> ```kotlin
> interface Tenant { val id: String }
>
> context(tenant: Tenant)
> fun currentTenantId(): String = tenant.id
>
> fun main() {
> val t = object : Tenant { override val id = "acme" }
> println(with(t) { currentTenantId() }) // acme
> // println(currentTenantId()) // ошибка компиляции: нет контекста
> }
> ```
> Для корутин у той же задачи есть другое решение - `CoroutineContext`. Подробнее: [Диспетчеры и контекст](../Корутины/Диспетчеры%20и%20контекст.md)
>
> **Квалифицированный `this@Label` обобщает Java-овский `Outer.this`.** В Java `Outer.this` работает только для вложенных классов; в Kotlin метки действуют и для лямбд, и для расширений, поэтому вложенные области разрешаются одним механизмом.
>
> Сравнить: [Вложенные классы в Java](../../Программирование%20на%20Java/ООП/Вложенные%20классы.md), [Builder](../../Программирование%20на%20Java/Паттерны%20проектирования/Builder.md)
> [!warning] Подводные камни
> **Внутренний получатель затеняет внешний:**
> ```kotlin
> fun main() {
> val result = StringBuilder("внешний").run {
> StringBuilder("внутренний").run {
> toString() // внутренний — не тот, что ожидали?
> }
> }
> println(result) // внутренний
> }
> ```
> При вложенности больше одного уровня используйте `this@label` или обычные переменные.
>
> **Без `@DslMarker` вложенные билдеры принимают неверный код:**
> ```kotlin
> class Menu {
> private val items = mutableListOf<String>()
> fun item(name: String) { items.add(name) }
> fun submenu(block: Menu.() -> Unit) { items.add("[${Menu().apply(block).items.joinToString()}]") }
> fun render() = items.joinToString(", ")
> }
>
> fun main() {
> val m = Menu().apply {
> item("файл")
> submenu { item("открыть") }
> }
> println(m.render()) // файл, [открыть]
> }
> ```
> Здесь `item` внутри `submenu` попадает во внутреннее меню - как и задумано. Но если имена методов у разных уровней совпадают лишь частично, вызов молча уйдёт не туда, куда ожидал автор.
>
> **Получатель и параметр с одинаковым именем:**
> ```kotlin
> class Box9(val value: Int)
>
> fun Box9.compare(value: Int) = "${this.value} vs $value"
>
> fun main() {
> println(Box9(1).compare(2)) // 1 vs 2
> }
> ```
> Без `this.` обращение уходит к параметру - ближайшая область выигрывает. Это корректно, но при чтении легко ошибиться.
>
> **Лямбда с получателем скрывает, откуда взялся метод:**
> ```kotlin
> fun main() {
> val text = buildString {
> append("a")
> appendLine("b")
> insert(0, "!") // метод StringBuilder, не видно из вызова
> }
> println(text.trim()) // !ab
> }
> ```
> В длинном блоке непонятно, какие функции доступны - помогает только IDE. Для публичных DSL это аргумент за короткие блоки и явные имена.
>
> **Контекстные параметры не участвуют в разрешении вызовов:**
> ```kotlin
> interface Printer2 { fun show(m: String) }
>
> context(printer: Printer2)
> fun demo2() {
> printer.show("через имя") // так
> // show("без имени") // ошибка: не найдено
> }
>
> fun main() {
> with(object : Printer2 { override fun show(m: String) = println(m) }) { demo2() }
> }
> ```
> Это сознательное отличие от отменённых context receivers: там метод вызывался без префикса, что делало код неоднозначным.
>
> **Контекст передаётся через `with`, и это выглядит странно.** Чтобы вызвать функцию с контекстным параметром, нужно внести значение в область видимости - обычно через `with`. Для нескольких контекстов получается вложенность `with(a) { with(b) { ... } }`, которая читается хуже, чем явные аргументы. Для двух зависимостей обычный параметр часто честнее.
>
> **Контекстные параметры - свежая фича.** Они стабилизированы недавно, поддержка в инструментах и библиотеках неполная, а до них была экспериментальная версия (context receivers) с другим синтаксисом, которую убрали. Код из старых статей (`context(Logger)` без имени параметра) не компилируется. Подробнее: [Компилятор K2](../Инструментарий/Компилятор%20K2.md)
>
> **Из Java функции с контекстными параметрами выглядят как функции с дополнительными аргументами** - контекст становится обычным параметром в начале списка. Никакой «неявности» для Java-клиента нет. Подробнее: [Kotlin из Java](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md)
>
> **Расширение внутри класса недоступно снаружи:**
> ```kotlin
> class Scope {
> fun String.shout() = uppercase() + "!"
> fun use() = "привет".shout()
> }
>
> fun main() {
> println(Scope().use()) // ПРИВЕТ!
> // println("привет".shout()) // ошибка: вне Scope расширения нет
> }
> ```
> Это полезное свойство (ограничение области), но в документации к DSL его нужно объяснять - иначе пользователь не поймёт, почему метод «исчезает».
>
> **`this` в лямбде с получателем нельзя переназначить.** Если нужно и внешний объект, и внутренний одновременно, единственный способ - метки или явный параметр: `fun with2(block: (Outer, Inner) -> Unit)`.
Далее: [DSL](DSL.md)
@@ -0,0 +1,441 @@
---
title: Scope-функции
description: let, run, apply, also, with - различия, выбор и типичные ошибки
tags:
- kotlin
- functional
---
Пять функций stdlib, которые выполняют блок кода в контексте объекта. Делают они почти одно и то же, различаясь двумя вещами: как доступен объект (`this` или `it`) и что возвращается (объект или результат блока).
| Функция | Объект внутри | Возвращает | Расширение |
|---|---|---|---|
| `let` | `it` | результат блока | да |
| `run` | `this` | результат блока | да |
| `run` (без получателя) | - | результат блока | нет |
| `apply` | `this` | сам объект | да |
| `also` | `it` | сам объект | да |
| `with` | `this` | результат блока | нет (принимает аргумент) |
```kotlin
data class Person3(var name: String, var age: Int)
fun main() {
val p = Person3("Иван", 30)
val letResult = p.let { "${it.name} ${it.age}" } // it, результат блока
val runResult = p.run { "$name $age" } // this, результат блока
val applyResult = p.apply { age = 31 } // this, сам объект
val alsoResult = p.also { println("создан ${it.name}") } // it, сам объект
val withResult = with(p) { "$name $age" } // this, результат блока
println(letResult) // Иван 30
println(runResult) // Иван 30
println(applyResult) // Person3(name=Иван, age=31)
println(alsoResult === p) // true
println(withResult) // Иван 31
}
```
```mermaid
graph TD
A["Нужна scope-функция"] --> B{"Что возвращать?"}
B -->|"сам объект"| C{"Как обращаться?"}
B -->|"результат блока"| D{"Как обращаться?"}
C -->|"this - настройка полей"| E["apply"]
C -->|"it - побочный эффект"| F["also"]
D -->|"it - преобразование"| G["let"]
D -->|"this - много обращений"| H["run"]
D -->|"объект уже есть, не цепочка"| I["with"]
```
## let
Основное применение - работа с nullable и преобразование значения:
```kotlin
fun main() {
val name: String? = "Иван"
val length = name?.let { it.length } ?: 0
println(length) // 4
name?.let { println("имя: $it") } // имя: Иван
val empty: String? = null
empty?.let { println("не выполнится") }
println("после") // после
}
```
`?.let { }` - идиоматичная замена `if (x != null)`, когда внутри нужно значение. Подробнее: [Операторы null-безопасности](../Синтаксис%20и%20типы/Null-безопасность/Операторы%20null-безопасности.md)
Второе применение - ограничить область видимости промежуточного значения:
```kotlin
fun main() {
val result = listOf(1, 2, 3, 4)
.filter { it % 2 == 0 }
.let { evens -> "чётных: ${evens.size}, сумма: ${evens.sum()}" }
println(result) // чётных: 2, сумма: 6
}
```
Переменная `evens` не утекает наружу. Явное имя параметра вместо `it` здесь делает код понятнее.
## run
То же, что `let`, но объект доступен как `this` - удобно, когда обращений к нему много:
```kotlin
data class Config3(val host: String, val port: Int, val tls: Boolean)
fun main() {
val url = Config3("example.com", 8443, true).run {
val scheme = if (tls) "https" else "http"
"$scheme://$host:$port"
}
println(url) // https://example.com:8443
}
```
`run` без получателя - просто блок, возвращающий значение:
```kotlin
fun main() {
val value = run {
val a = 2
val b = 3
a * b
}
println(value) // 6
}
```
Это способ вычислить `val` несколькими строками, не заводя функцию. Полезно в инициализации свойств.
## apply
Возвращает сам объект - значит, предназначен для настройки:
```kotlin
class Request2 {
var url: String = ""
var method: String = "GET"
val headers = mutableMapOf<String, String>()
override fun toString() = "$method $url $headers"
}
fun main() {
val request = Request2().apply {
url = "https://example.com"
method = "POST"
headers["Content-Type"] = "application/json"
}
println(request) // POST https://example.com {Content-Type=application/json}
}
```
Это замена builder-паттерна для классов с изменяемыми свойствами. Подробнее: [Паттерны проектирования](Паттерны%20проектирования.md)
Частый случай - настройка при возврате:
```kotlin
fun buildList2(): List<Int> = mutableListOf<Int>().apply {
add(1)
add(2)
addAll(listOf(3, 4))
}
fun main() {
println(buildList2()) // [1, 2, 3, 4]
}
```
Хотя для коллекций в stdlib уже есть `buildList`, который чище.
## also
Возвращает объект, доступ через `it` - для побочных эффектов, не меняющих объект:
```kotlin
fun main() {
val numbers = listOf(3, 1, 2)
.also { println("исходный: $it") }
.sorted()
.also { println("отсортированный: $it") }
.filter { it > 1 }
.also { println("отфильтрованный: $it") }
println(numbers)
// исходный: [3, 1, 2]
// отсортированный: [1, 2, 3]
// отфильтрованный: [2, 3]
// [2, 3]
}
```
`also` вставляется в цепочку, ничего в ней не меняя - идеально для логирования и отладки.
Второе применение - валидация и регистрация при создании:
```kotlin
class Resource(val name: String) {
fun close() = println("закрыт $name")
}
fun main() {
val registry = mutableListOf<Resource>()
val r = Resource("файл").also { registry.add(it) }
println("${r.name}, в реестре: ${registry.size}") // файл, в реестре: 1
}
```
## with
Единственная не-расширение: объект передаётся аргументом.
```kotlin
fun main() {
val sb = StringBuilder()
with(sb) {
append("Привет")
append(", ")
append("мир")
append("!")
}
println(sb) // Привет, мир!
}
```
`with(x) { }` читается как «с объектом x сделай» и применяется, когда объект уже есть в переменной и цепочка не нужна. Результат - значение блока:
```kotlin
data class Point2(val x: Int, val y: Int)
fun main() {
val distance = with(Point2(3, 4)) {
kotlin.math.sqrt((x * x + y * y).toDouble())
}
println(distance) // 5.0
}
```
## Как выбирать
Практические правила, покрывающие почти все случаи:
| Задача | Функция |
|---|---|
| выполнить код, если значение не `null` | `?.let { }` |
| преобразовать значение в другое | `let` |
| настроить свойства нового объекта | `apply` |
| залогировать значение в середине цепочки | `also` |
| вычислить значение из нескольких полей объекта | `run` |
| сгруппировать вызовы на существующем объекте | `with` |
| вычислить `val` из нескольких строк | `run { }` без получателя |
Выбор между `it` и `this` - вопрос читаемости: `this` короче при многих обращениях к полям, `it` яснее, когда объект используется один-два раза или когда внутри есть другой `this`.
## Вложенность и затенение
```kotlin
data class Inner(val value: Int)
data class Outer(val inner: Inner, val name: String)
fun main() {
val o = Outer(Inner(42), "внешний")
// плохо: два this, непонятно какой
val bad = o.run { inner.run { "$value" } }
println(bad) // 42
// хорошо: явные имена
val good = o.let { outer -> outer.inner.let { inner -> "${outer.name}: ${inner.value}" } }
println(good) // внешний: 42
}
```
Вложенные scope-функции - главный источник нечитаемого кода на Kotlin. Правило: вложенность больше одного уровня - заводите явные имена параметров или обычные переменные.
## Scope-функции и null
```kotlin
fun main() {
val maybe: String? = "текст"
println(maybe?.let { it.length }) // 5 — блок выполнится, если не null
println(maybe?.run { length }) // 5 — то же
println(maybe.let { it?.length }) // 5 — блок выполнится всегда, it nullable
val nothing: String? = null
println(nothing?.let { it.length }) // null
println(nothing.let { it?.length }) // null — но блок выполнился!
}
```
Разница в месте `?.`: `x?.let { }` не вызывает блок при `null`, `x.let { }` вызывает всегда с nullable `it`. Это часто путают.
## takeIf и takeUnless
Родственные функции - фильтр для одного значения:
```kotlin
fun main() {
val number = 42
println(number.takeIf { it > 0 }) // 42
println(number.takeIf { it > 100 }) // null
println(number.takeUnless { it > 0 }) // null
val name = " "
println(name.takeIf { it.isNotBlank() } ?: "(по умолчанию)") // (по умолчанию)
val config = mapOf("port" to "8080")
val port = config["port"]?.takeIf { it.isNotBlank() }?.toIntOrNull() ?: 80
println(port) // 8080
}
```
`takeIf` превращает условие в nullable-значение, которое дальше обрабатывается через `?:` и `?.`. Это заменяет `if`-цепочки в конвейерах преобразования.
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `if (x != null) { ... }` | `x?.let { ... }` |
> | `Optional.map` | `let` |
> | `Optional.ifPresent` | `?.let` или `?.also` |
> | builder с `return this` | `apply` |
> | промежуточная переменная | `let`/`run` |
> | `Objects.requireNonNullElse(x, y)` | `x ?: y` |
> | нет аналога | `also` в середине цепочки |
> | нет аналога | `with` |
>
> **Scope-функции заменяют `Optional` и промежуточные переменные.** В Java цепочка преобразований с проверками на null выглядит так:
> ```java
> String result = Optional.ofNullable(user)
> .map(User::getAddress)
> .map(Address::getCity)
> .orElse("неизвестно");
> ```
> В Kotlin `?.` и `?:` делают то же без обёртки, а `let` подключается, когда нужен не просто вызов метода:
> ```kotlin
> data class Address(val city: String)
> data class User11(val address: Address?)
>
> fun main() {
> val user: User11? = User11(Address("Москва"))
> val result = user?.address?.city?.let { it.uppercase() } ?: "неизвестно"
> println(result) // МОСКВА
> }
> ```
> Принципиальная разница: `Optional` - объект-обёртка со своей стоимостью и своим API, а `?.` - синтаксис над обычной ссылкой. Поэтому `Optional` в Kotlin не используют вообще.
>
> **`apply` заменяет builder-паттерн для изменяемых объектов.** В Java чтобы настраивать объект цепочкой, класс должен возвращать `this` из каждого сеттера - это решение автора класса. `apply` работает с любым классом, включая чужой:
> ```kotlin
> fun main() {
> val props = java.util.Properties().apply {
> setProperty("host", "localhost")
> setProperty("port", "8080")
> }
> println(props.getProperty("host")) // localhost
> }
> ```
> `Properties` из JDK не имеет fluent-API, но с `apply` ведёт себя как будто имеет.
>
> **`also` в цепочке аналога в Java не имеет.** `Stream.peek` похож, но работает только внутри стрима и его использование не рекомендуется. `also` применим к любому значению.
>
> **Это не магия, а обычные inline-расширения.** `let` объявлена как `inline fun <T, R> T.let(block: (T) -> R): R = block(this)` - одна строка. Поэтому накладных расходов нет: после компиляции остаётся только тело блока. Подробнее: [Inline-функции](Inline-функции.md)
>
> Сравнить: [Функции в Java](../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Функции.md), [Builder](../../Программирование%20на%20Java/Паттерны%20проектирования/Builder.md)
> [!warning] Подводные камни
> **`apply` возвращает объект, даже если блок что-то вычислил:**
> ```kotlin
> fun main() {
> val wrong = StringBuilder().apply { append("текст"); length }
> println(wrong) // текст — не 5!
> val right = StringBuilder().run { append("текст"); length }
> println(right) // 5
> }
> ```
> Перепутать `apply` и `run` легко, а ошибка типов возникает далеко от места.
>
> **`let` на nullable без `?.` выполняется всегда:**
> ```kotlin
> fun main() {
> val x: String? = null
> x.let { println("выполнилось, it = $it") } // выполнилось, it = null
> x?.let { println("не выполнится") }
> }
> ```
>
> **Вложенные `this` и `it` путаются:**
> ```kotlin
> data class Address2(var city: String)
> data class User12(var name: String, val address: Address2)
>
> fun main() {
> val u = User12("Иван", Address2("Москва"))
> u.apply {
> name = "Пётр"
> address.apply {
> city = "Казань"
> // name = "x" // name внешнего this — компилируется, но запутанно
> }
> }
> println(u) // User12(name=Пётр, address=Address2(city=Казань))
> }
> ```
> Внутренний блок видит и свой `this`, и внешний. Компилятор разрешает обращаться к членам обоих, и ошибку заметить трудно.
>
> **`also` и `apply` не защищают от изменения объекта.** Оба возвращают **тот же** объект, а не копию:
> ```kotlin
> data class Counter3(var value: Int)
>
> fun main() {
> val c = Counter3(1)
> val same = c.also { it.value = 99 }
> println("${c.value} ${same === c}") // 99 true
> }
> ```
>
> **`?.let` на цепочке проверяет только последнее звено:**
> ```kotlin
> data class Inner2(val value: Int?)
>
> fun main() {
> val i: Inner2? = Inner2(null)
> println(i?.let { it.value?.plus(1) }) // null
> println(i?.value?.plus(1)) // null — короче и яснее
> }
> ```
> Когда достаточно `?.`, `let` только добавляет шум. Используйте `let`, если внутри нужно больше одного действия.
>
> **Цепочка из scope-функций читается хуже обычного кода:**
> ```kotlin
> fun main() {
> val bad = "42".let { it.toInt() }.let { it * 2 }.also { println(it) }.let { "$it" }
> println(bad) // 84 \n 84
>
> val good = ("42".toInt() * 2).toString()
> println(good) // 84
> }
> ```
> Scope-функции - инструмент, а не стиль. Если без них короче - пишите без них.
>
> **`run` без получателя путается с `run` на объекте.** `x.run { }` и `run { }` - разные функции с одним именем; при чтении диффа разница в одну точку.
>
> **Нелокальный `return` внутри scope-функции выходит из внешней функции:**
> ```kotlin
> fun process5(value: String?): String {
> value?.let {
> if (it.isBlank()) return "пусто" // выход из process5!
> }
> return "обработано: $value"
> }
>
> fun main() {
> println(process5(" ")) // пусто
> println(process5("текст")) // обработано: текст
> }
> ```
> Это работает благодаря `inline`, но для читателя неочевидно: `return` внутри лямбды выглядит как возврат из лямбды.
>
> **`takeIf` возвращает nullable - и это заразно:**
> ```kotlin
> fun main() {
> val port: Int? = 8080.takeIf { it > 1024 }
> // println(port + 1) // ошибка: Int? не складывается
> println(port?.plus(1)) // 8081
> }
> ```
> Каждый `takeIf` добавляет в тип `?`, который нужно где-то разрешить.
>
> **`apply` на `val`-свойствах бесполезен:**
> ```kotlin
> data class Immutable(val a: Int, val b: Int)
>
> fun main() {
> val x = Immutable(1, 2).apply {
> // a = 10 // ошибка: val
> }
> println(x) // Immutable(a=1, b=2)
> println(Immutable(1, 2).copy(a = 10)) // Immutable(a=10, b=2)
> }
> ```
> Для неизменяемых классов нужен `copy`, а не `apply`. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md)
Далее: [Инфиксные и операторные функции](Инфиксные%20и%20операторные%20функции.md)
@@ -0,0 +1,504 @@
---
title: Инфиксные и операторные функции
description: infix-нотация, перегрузка операторов, invoke, деструктуризация и делегирование свойств
tags:
- kotlin
- functional
---
Kotlin позволяет задать для своих типов тот же синтаксис, что у встроенных: `a + b`, `a[i]`, `a()`, `a in b`. Делается это обычными функциями с модификатором `operator` или `infix`.
## infix
```kotlin
infix fun Int.упаковатьВ(unit: String): String = "$this $unit"
data class Money(val amount: Long, val currency: String)
infix fun Long.of(currency: String) = Money(this, currency)
fun main() {
println(5 упаковатьВ "кг") // 5 кг
println(100L of "RUB") // Money(amount=100, currency=RUB)
println(100L.of("RUB")) // Money(amount=100, currency=RUB) — обычный вызов тоже работает
}
```
Требования к `infix`: ровно один параметр, не `vararg`, без значения по умолчанию, и функция должна быть методом или расширением.
В stdlib `infix` используется для операций, читающихся как фраза:
```kotlin
fun main() {
println(setOf(1, 2) union setOf(2, 3)) // [1, 2, 3]
println(setOf(1, 2) intersect setOf(2, 3)) // [2]
println("a" to 1) // (a, 1)
println(5 shl 2) // 20
println(5 and 3) // 1
println(1 until 5) // 1..4
println(5 downTo 1) // 5 downTo 1 step 1
println(1..10 step 2) // 1..9 step 2
}
```
Приоритет инфиксных функций ниже арифметики, но выше сравнений - это источник ошибок:
```kotlin
infix fun Int.plusTen(x: Int) = this + x + 10
fun main() {
println(1 plusTen 2 * 3) // 17 — это 1 plusTen (2*3)
println((1 plusTen 2) * 3) // 39
}
```
При сомнении ставьте скобки.
## Перегрузка операторов
Каждый оператор отображается в функцию с фиксированным именем:
| Оператор | Функция |
|---|---|
| `a + b` | `a.plus(b)` |
| `a - b` | `a.minus(b)` |
| `a * b` | `a.times(b)` |
| `a / b` | `a.div(b)` |
| `a % b` | `a.rem(b)` |
| `+a` / `-a` | `a.unaryPlus()` / `a.unaryMinus()` |
| `!a` | `a.not()` |
| `a++` / `a--` | `a.inc()` / `a.dec()` |
| `a += b` | `a.plusAssign(b)` или `a = a.plus(b)` |
| `a == b` | `a?.equals(b) ?: (b === null)` |
| `a > b` | `a.compareTo(b) > 0` |
| `a[i]` | `a.get(i)` |
| `a[i] = v` | `a.set(i, v)` |
| `a()` | `a.invoke()` |
| `a in b` | `b.contains(a)` |
| `a..b` | `a.rangeTo(b)` |
| `a..<b` | `a.rangeUntil(b)` |
| `for (x in a)` | `a.iterator()` |
| `val (x, y) = a` | `a.component1()`, `a.component2()` |
Полный пример - вектор:
```kotlin
data class Vec(val x: Double, val y: Double) {
operator fun plus(other: Vec) = Vec(x + other.x, y + other.y)
operator fun minus(other: Vec) = Vec(x - other.x, y - other.y)
operator fun times(scalar: Double) = Vec(x * scalar, y * scalar)
operator fun unaryMinus() = Vec(-x, -y)
operator fun get(index: Int): Double = when (index) {
0 -> x
1 -> y
else -> throw IndexOutOfBoundsException("индекс $index")
}
val length: Double get() = kotlin.math.sqrt(x * x + y * y)
}
fun main() {
val a = Vec(1.0, 2.0)
val b = Vec(3.0, 4.0)
println(a + b) // Vec(x=4.0, y=6.0)
println(b - a) // Vec(x=2.0, y=2.0)
println(a * 2.0) // Vec(x=2.0, y=4.0)
println(-a) // Vec(x=-1.0, y=-2.0)
println(b[0]) // 3.0
println(b.length) // 5.0
}
```
Умножение на скаляр справа - метод класса; чтобы работало и слева, нужно расширение:
```kotlin
data class Vec2(val x: Double, val y: Double) {
operator fun times(scalar: Double) = Vec2(x * scalar, y * scalar)
}
operator fun Double.times(v: Vec2) = v * this
fun main() {
val v = Vec2(1.0, 2.0)
println(v * 3.0) // Vec2(x=3.0, y=6.0)
println(3.0 * v) // Vec2(x=3.0, y=6.0)
}
```
## Операторы как расширения
Оператор можно добавить к чужому типу:
```kotlin
operator fun String.times(n: Int) = repeat(n)
operator fun <T> List<T>.get(range: IntRange): List<T> = subList(range.first, range.last + 1)
fun main() {
println("ab" * 3) // ababab
println(listOf(1, 2, 3, 4, 5)[1..3]) // [2, 3, 4]
}
```
Так в stdlib добавлены `Map.get` через `[]`, `List.plus` и операции над множествами.
## invoke
`operator fun invoke` позволяет вызывать объект как функцию:
```kotlin
class Multiplier(private val factor: Int) {
operator fun invoke(x: Int) = x * factor
}
fun main() {
val triple = Multiplier(3)
println(triple(5)) // 15
println(triple.invoke(5)) // 15 — то же самое
}
```
Практическое применение - объект с состоянием, который ведёт себя как функция:
```kotlin
class RateLimiter(private val maxCalls: Int) {
private var calls = 0
operator fun invoke(): Boolean {
calls++
return calls <= maxCalls
}
}
fun main() {
val limiter = RateLimiter(2)
println("${limiter()} ${limiter()} ${limiter()}") // true true false
}
```
`invoke` на `companion object` даёт фабрику с синтаксисом конструктора:
```kotlin
class Email private constructor(val value: String) {
companion object {
operator fun invoke(raw: String): Email? =
if (raw.contains("@")) Email(raw.trim().lowercase()) else null
}
override fun toString() = value
}
fun main() {
println(Email(" Ivan@Example.COM ")) // ivan@example.com
println(Email("не почта")) // null
}
```
Это способ сделать «конструктор, возвращающий null» - настоящий конструктор так не может.
## Сравнение: compareTo
```kotlin
class Version(val major: Int, val minor: Int) : Comparable<Version> {
override fun compareTo(other: Version): Int =
compareValuesBy(this, other, { it.major }, { it.minor })
override fun toString() = "$major.$minor"
}
fun main() {
val a = Version(1, 5)
val b = Version(2, 0)
println(a < b) // true
println(a >= Version(1, 5)) // true
println(listOf(b, a, Version(1, 9)).sorted()) // [1.5, 1.9, 2.0]
println(maxOf(a, b)) // 2.0
println(a in Version(1, 0)..Version(1, 9)) // true
}
```
Одна функция `compareTo` даёт `<`, `>`, `<=`, `>=`, сортировку, `maxOf`/`minOf` и диапазоны. `compareValuesBy` сравнивает по нескольким ключам по порядку.
## in: contains
```kotlin
data class DateRange(val from: Int, val to: Int) {
operator fun contains(day: Int) = day in from..to
}
fun main() {
val week = DateRange(1, 7)
println(3 in week) // true
println(10 in week) // false
println(10 !in week) // true — !in получается автоматически
}
```
## Итератор
```kotlin
class Countdown(private val from: Int) {
operator fun iterator(): Iterator<Int> = object : Iterator<Int> {
private var current = from
override fun hasNext() = current > 0
override fun next() = current--
}
}
fun main() {
for (i in Countdown(3)) print("$i ")
println() // 3 2 1
}
```
`iterator` можно добавить и расширением - тогда `for` заработает на чужом типе:
```kotlin
operator fun ClosedRange<java.time.LocalDate>.iterator(): Iterator<java.time.LocalDate> =
object : Iterator<java.time.LocalDate> {
private var current = start
override fun hasNext() = current <= endInclusive
override fun next() = current.also { current = current.plusDays(1) }
}
fun main() {
val from = java.time.LocalDate.of(2026, 1, 1)
val to = java.time.LocalDate.of(2026, 1, 3)
for (d in from..to) print("${d.dayOfMonth} ")
println() // 1 2 3
}
```
## get и set
```kotlin
class Matrix(private val rows: Int, private val cols: Int) {
private val data = DoubleArray(rows * cols)
operator fun get(i: Int, j: Int): Double = data[i * cols + j]
operator fun set(i: Int, j: Int, value: Double) { data[i * cols + j] = value }
override fun toString() = (0 until rows).joinToString("\n") { i ->
(0 until cols).joinToString(" ") { j -> this[i, j].toString() }
}
}
fun main() {
val m = Matrix(2, 2)
m[0, 0] = 1.0
m[1, 1] = 2.0
println(m[0, 0]) // 1.0
println(m)
// 1.0 0.0
// 0.0 2.0
}
```
`get` и `set` принимают любое число аргументов - это даёт многомерную индексацию, которой нет в Java.
## plusAssign против plus
```kotlin
class Basket {
private val items = mutableListOf<String>()
operator fun plusAssign(item: String) { items.add(item) }
override fun toString() = items.toString()
}
fun main() {
val basket = Basket()
basket += "хлеб"
basket += "молоко"
println(basket) // [хлеб, молоко]
}
```
Если объявлены оба (`plus` и `plusAssign`) и переменная `var` - компилятор сообщит об неоднозначности. Правило: `plusAssign` для изменяемых коллекций-подобных типов, `plus` для значений.
## Деструктуризация
```kotlin
class Pair3(val first: Int, val second: String) {
operator fun component1() = first
operator fun component2() = second
}
fun main() {
val (num, text) = Pair3(1, "один")
println("$num = $text") // 1 = один
}
```
`data class` объявляет `componentN` автоматически. Подробнее: [Деструктуризация](../Синтаксис%20и%20типы/Работа%20с%20данными/Деструктуризация.md)
## Делегирование свойств
Операторы `getValue`/`setValue` позволяют подменить реализацию свойства:
```kotlin
import kotlin.reflect.KProperty
class Trimmed {
private var value: String = ""
operator fun getValue(thisRef: Any?, property: KProperty<*>): String = value
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) {
value = newValue.trim()
}
}
class Form {
var login: String by Trimmed()
}
fun main() {
val f = Form()
f.login = " ivan "
println("[${f.login}]") // [ivan]
}
```
Подробнее о делегатах и готовых реализациях (`lazy`, `observable`, `map`): [Делегирование](../ООП/Делегирование.md)
## Когда перегружать оператор
| Ситуация | Стоит ли |
|---|---|
| математический тип (вектор, матрица, деньги, дата) | да |
| коллекция-подобный тип (`[]`, `in`, `for`) | да |
| сравнение по естественному порядку | да, через `Comparable` |
| «красивее выглядит» | нет |
| смысл оператора неочевиден (`user + role`) | нет |
| операция может бросить исключение неожиданно | нет |
Критерий простой: оператор должен означать то же, что для встроенных типов. `+` обязан быть коммутативным по смыслу и не иметь побочных эффектов; `==` - соответствовать `hashCode`.
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | перегрузки операторов нет | `operator fun` |
> | `a.add(b)` | `a + b` |
> | `list.get(i)` | `list[i]` |
> | `map.put(k, v)` | `map[k] = v` |
> | `a.compareTo(b) < 0` | `a < b` |
> | `a.equals(b)` | `a == b` |
> | `BigDecimal.add().multiply()` | `a + b * c` |
> | инфиксной нотации нет | `infix fun` |
>
> **Java не поддерживает перегрузку операторов принципиально** - кроме `+` для строк, который встроен в компилятор. Это сознательное решение дизайнеров Java: операторы с произвольным смыслом усложняют чтение. Kotlin выбрал компромисс: перегрузка есть, но набор операторов фиксирован и имена функций заданы жёстко, так что `plus` всегда означает `+` и нельзя придумать свой оператор.
>
> **Разница заметнее всего на числовых типах.** `BigDecimal` в Java:
> ```java
> BigDecimal result = a.multiply(b).add(c).divide(d, RoundingMode.HALF_UP);
> ```
> В Kotlin те же операторы уже объявлены в stdlib как расширения:
> ```kotlin
> import java.math.BigDecimal
>
> fun main() {
> val a = BigDecimal("10.5")
> val b = BigDecimal("2")
> println(a + b) // 12.5
> println(a * b) // 21.0
> println(a > b) // true
> }
> ```
> Для денежных расчётов это главный практический выигрыш: формула в коде выглядит как формула.
>
> **`==` в Kotlin - это `equals`, а не сравнение ссылок.** Самое важное отличие на практике: в Java `a == b` для объектов сравнивает ссылки, и забытый `.equals()` - классическая ошибка со строками. В Kotlin `==` вызывает `equals` (с корректной обработкой null), а для ссылок есть отдельный `===`:
> ```kotlin
> fun main() {
> val a = StringBuilder("x").toString()
> val b = StringBuilder("x").toString()
> println(a == b) // true — по значению
> println(a === b) // false — разные объекты
> }
> ```
>
> **`infix` даёт читаемость без DSL-библиотек.** В Java тестовые фреймворки имитируют инфиксную запись цепочкой методов: `assertThat(x).isEqualTo(y)`. В Kotlin это записывается как `x shouldBe y` - подробнее: [Kotest](../Тестирование/Kotest.md)
>
> **`invoke` аналога не имеет.** В Java объект нельзя вызвать как функцию; ближайшее - интерфейс с методом `apply`, вызываемый явно.
>
> Сравнить: [Операторы и выражения в Java](../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовый%20синтаксис/Операторы%20и%20выражения.md), [Класс Object](../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовые%20типы%20ООП/Класс%20Object.md)
> [!warning] Подводные камни
> **Приоритет `infix` ниже арифметики:**
> ```kotlin
> infix fun Int.append(x: Int) = "$this$x"
>
> fun main() {
> println(1 append 2 + 3) // 15 — это 1 append (2+3)
> println((1 append 2) + 3) // 123
> }
> ```
> Выражение выглядит как последовательность слева направо, но арифметика вычисляется первой.
>
> **`infix` нельзя использовать без получателя и с `this` неявно:**
> ```kotlin
> class Calc {
> infix fun add(x: Int) = x + 1
> fun demo(): Int {
> // return add 5 // ошибка
> return this add 5 // так можно
> }
> }
>
> fun main() { println(Calc().demo()) } // 6
> ```
>
> **`plus` не меняет объект, `plusAssign` меняет - и перепутать легко:**
> ```kotlin
> fun main() {
> var list1 = listOf(1, 2)
> list1 += 3 // новый список: plus + присваивание
> println(list1) // [1, 2, 3]
>
> val list2 = mutableListOf(1, 2)
> list2 += 3 // изменение на месте: plusAssign
> println(list2) // [1, 2, 3]
> }
> ```
> Результат одинаковый, стоимость разная: первый вариант копирует весь список.
>
> **Объявление и `plus`, и `plusAssign` даёт ошибку компиляции при `var`:**
> ```kotlin
> class Both(val v: Int) {
> operator fun plus(x: Int) = Both(v + x)
> operator fun plusAssign(x: Int) { }
> }
>
> fun main() {
> val b = Both(1)
> b += 1 // выбран plusAssign — для val это единственный вариант
> println(b.v) // 1
> }
> ```
> Для `var`-переменной компилятор не сможет выбрать между `b = b + 1` и `b.plusAssign(1)`.
>
> **`inc`/`dec` должны возвращать новый объект:**
> ```kotlin
> class Counter4(val value: Int) {
> operator fun inc() = Counter4(value + 1)
> }
>
> fun main() {
> var c = Counter4(0)
> c++
> println(c.value) // 1
> }
> ```
> Если `inc` изменит объект и вернёт `this`, постфиксный `c++` сломается: он должен вернуть **старое** значение, а вернёт изменённое.
>
> **`equals` нельзя объявить расширением:**
> ```kotlin
> class NoEquals(val v: Int)
>
> // operator fun NoEquals.equals(other: Any?) = ... // не заработает
>
> fun main() {
> println(NoEquals(1) == NoEquals(1)) // false — сравнение ссылок из Any
> }
> ```
> `equals` должен быть `override` в классе: он объявлен в `Any`, а расширение не переопределяет член. Подробнее: [equals, hashCode и toString](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/equals,%20hashCode%20и%20toString.md)
>
> **`compareTo` обязан быть согласован с `equals`.** Если `a.compareTo(b) == 0`, но `a != b`, то `TreeSet` и `sortedSetOf` начнут терять элементы - они используют сравнение, а не равенство.
>
> **`get` без `set` создаёт асимметричный API:**
> ```kotlin
> class ReadOnlyBox(private val items: List<Int>) {
> operator fun get(i: Int) = items[i]
> }
>
> fun main() {
> val b = ReadOnlyBox(listOf(1, 2))
> println(b[0]) // 1
> // b[0] = 5 // ошибка: нет set
> }
> ```
> Это нормально (так работает `List`), но пользователь увидит `[]` и может ожидать присваивания.
>
> **Оператор на расширении не виден, если есть член класса** - действует то же правило затенения, что у обычных расширений.
>
> **Операторы с неочевидным смыслом ухудшают код:**
> ```kotlin
> data class User13(val name: String, val roles: List<String>)
>
> // operator fun User13.plus(role: String) = copy(roles = roles + role)
> fun User13.withRole(role: String) = copy(roles = roles + role)
>
> fun main() {
> println(User13("Иван", emptyList()).withRole("админ"))
> // User13(name=Иван, roles=[админ])
> }
> ```
> `user + "админ"` короче, но непонятно: что значит «сложить пользователя со строкой»? Именованный метод честнее.
>
> **`invoke` на обычном классе запутывает:** `service()` не говорит, что делает объект. Оправдано для функциональных обёрток (`UseCase`, `Validator`), где у объекта ровно одна операция.
>
> **Нельзя придумать новый оператор.** Набор фиксирован: `+`, `-`, `*`, `/`, `%`, `[]`, `()`, `in`, `..`, сравнения, `++`/`--`, унарные. Произвольные символы вроде `<*>` или `|>` недоступны - только `infix`-функция со словесным именем.
Далее: [Receivers и context parameters](Receivers%20и%20context%20parameters.md)
@@ -0,0 +1,577 @@
---
title: Паттерны проектирования
description: Какие паттерны GoF встроены в язык, какие упростились и какие остались нужны
tags:
- kotlin
- functional
- patterns
---
Многие паттерны проектирования - это обходные приёмы для того, чего не хватает языку. Kotlin часть этих возможностей встроил, поэтому соответствующие паттерны сводятся к одной конструкции.
| Паттерн | В Kotlin |
|---|---|
| Singleton | `object` |
| Builder | именованные аргументы, `apply`, DSL |
| Factory Method | функция верхнего уровня или `invoke` в companion |
| Strategy | функциональный тип |
| Decorator | делегирование `by` |
| Observer | `Flow`, `StateFlow` или список лямбд |
| Template Method | функция высшего порядка |
| Command | лямбда |
| Adapter | функции-расширения |
| Visitor | `sealed` + `when` |
| Prototype | `copy()` у `data class` |
| Null Object | nullable-тип и `?:` |
| Facade, Repository, Proxy | остаются как есть |
## Singleton
```kotlin
object Settings {
private val values = mutableMapOf<String, String>()
var theme: String = "light"
fun put(key: String, value: String) { values[key] = value }
fun get(key: String): String? = values[key]
}
fun main() {
Settings.theme = "dark"
Settings.put("lang", "ru")
println("${Settings.theme} ${Settings.get("lang")}") // dark ru
}
```
`object` - потокобезопасный ленивый синглтон: экземпляр создаётся при первом обращении, инициализация защищена загрузчиком классов. Никакого `getInstance()`, `volatile` и двойной проверки блокировки. Подробнее: [Object и companion object](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md)
Цена та же, что у любого синглтона: глобальное состояние, которое трудно подменить в тестах. Если объект нужно мокировать - он должен реализовать интерфейс и внедряться параметром.
## Builder
Три варианта по возрастанию сложности. Первый - именованные аргументы с значениями по умолчанию:
```kotlin
data class HttpRequest(
val url: String,
val method: String = "GET",
val timeoutMs: Int = 5_000,
val headers: Map<String, String> = emptyMap(),
val retries: Int = 0,
)
fun main() {
val r = HttpRequest(url = "https://example.com", method = "POST", retries = 3)
println(r)
// HttpRequest(url=https://example.com, method=POST, timeoutMs=5000, headers={}, retries=3)
}
```
Это закрывает подавляющее большинство случаев, ради которых пишут builder. Плюс к этому `copy` даёт модификацию:
```kotlin
data class HttpRequest(
val url: String,
val method: String = "GET",
val headers: Map<String, String> = emptyMap(),
)
fun main() {
val base = HttpRequest("https://example.com")
val withAuth = base.copy(headers = mapOf("Authorization" to "Bearer x"))
println(withAuth.headers) // {Authorization=Bearer x}
println(base.headers) // {} — оригинал цел
}
```
Второй вариант - `apply` для классов с изменяемыми свойствами (включая чужие):
```kotlin
fun main() {
val sb = StringBuilder().apply {
append("строка")
append(" ")
append(42)
}
println(sb) // строка 42
}
```
Третий - DSL, когда структура вложенная:
```kotlin
class Form2 {
private val fields = mutableListOf<String>()
fun field(name: String, required: Boolean = false) {
fields.add(if (required) "$name*" else name)
}
fun render() = fields.joinToString(", ")
}
fun form(block: Form2.() -> Unit) = Form2().apply(block).render()
fun main() {
println(form {
field("имя", required = true)
field("телефон")
})
// имя*, телефон
}
```
Подробнее: [DSL](DSL.md)
## Factory Method
```kotlin
sealed interface Shape2 {
data class Circle2(val radius: Double) : Shape2
data class Square(val side: Double) : Shape2
}
fun shapeOf(type: String, size: Double): Shape2? = when (type) {
"circle" -> Shape2.Circle2(size)
"square" -> Shape2.Square(size)
else -> null
}
fun main() {
println(shapeOf("circle", 2.0)) // Circle2(radius=2.0)
println(shapeOf("unknown", 1.0)) // null
}
```
Отдельный класс-фабрика не нужен: функция верхнего уровня делает то же. Когда фабрика должна выглядеть как конструктор, помогает `invoke` в companion:
```kotlin
class PositiveNumber private constructor(val value: Int) {
companion object {
operator fun invoke(raw: Int): PositiveNumber? =
if (raw > 0) PositiveNumber(raw) else null
}
override fun toString() = "+$value"
}
fun main() {
println(PositiveNumber(5)) // +5
println(PositiveNumber(-1)) // null
}
```
Настоящий конструктор не может вернуть `null` или кешированный экземпляр - здесь это возможно, а вызов выглядит как `PositiveNumber(5)`. Подробнее: [Инфиксные и операторные функции](Инфиксные%20и%20операторные%20функции.md)
## Strategy
Классический паттерн - интерфейс с одним методом и набор реализаций. В Kotlin это функциональный тип:
```kotlin
class Checkout(private val discount: (Long) -> Long) {
fun total(amount: Long) = discount(amount)
}
fun main() {
val none = Checkout { it }
val tenPercent = Checkout { it * 90 / 100 }
val fixed = Checkout { (it - 100).coerceAtLeast(0) }
println("${none.total(1000)} ${tenPercent.total(1000)} ${fixed.total(1000)}")
// 1000 900 900
}
```
Стратегии становятся значениями: их можно хранить в `Map`, выбирать по ключу, комбинировать:
```kotlin
fun main() {
val strategies: Map<String, (Long) -> Long> = mapOf(
"none" to { it },
"ten" to { it * 90 / 100 },
"half" to { it / 2 },
)
val chosen = strategies["ten"] ?: strategies.getValue("none")
println(chosen(1000)) // 900
val combined: (Long) -> Long = { strategies.getValue("ten")(strategies.getValue("half")(it)) }
println(combined(1000)) // 450
}
```
Когда у стратегии больше одной операции - интерфейс остаётся правильным выбором.
## Decorator
```kotlin
interface Repository8 {
fun find(id: Int): String?
}
class DatabaseRepository : Repository8 {
override fun find(id: Int): String? = if (id == 1) "Иван" else null
}
class CachingRepository(private val delegate: Repository8) : Repository8 by delegate {
private val cache = mutableMapOf<Int, String?>()
override fun find(id: Int): String? = cache.getOrPut(id) {
println("запрос в БД: $id")
delegate.find(id)
}
}
class LoggingRepository(private val delegate: Repository8) : Repository8 by delegate
fun main() {
val repo = LoggingRepository(CachingRepository(DatabaseRepository()))
println(repo.find(1)) // запрос в БД: 1 \n Иван
println(repo.find(1)) // Иван — из кеша
}
```
`by delegate` генерирует делегирующие реализации для всех методов интерфейса, а переопределить нужно только изменённые. В Java каждый декоратор обязан вручную прописать все методы интерфейса. Подробнее: [Делегирование](../ООП/Делегирование.md)
## Observer
Простая версия - список лямбд:
```kotlin
class Publisher<T> {
private val subscribers = mutableListOf<(T) -> Unit>()
fun subscribe(handler: (T) -> Unit): () -> Unit {
subscribers.add(handler)
return { subscribers.remove(handler) } // функция отписки
}
fun publish(event: T) = subscribers.forEach { it(event) }
}
fun main() {
val publisher = Publisher<String>()
val unsubscribe = publisher.subscribe { println("получено: $it") }
publisher.subscribe { println("второй: $it") }
publisher.publish("событие 1")
// получено: событие 1
// второй: событие 1
unsubscribe()
publisher.publish("событие 2")
// второй: событие 2
}
```
Возврат функции отписки вместо метода `unsubscribe(handler)` избавляет от хранения ссылки на обработчик - частая причина утечек в Java.
Для реальных задач вместо своей реализации берут `StateFlow` или `SharedFlow`: там уже решены обратное давление, конкурентность и жизненный цикл подписки. Подробнее: [StateFlow и SharedFlow](../Корутины/StateFlow%20и%20SharedFlow.md)
Для одного наблюдаемого свойства есть готовый делегат:
```kotlin
import kotlin.properties.Delegates
class Model {
var status: String by Delegates.observable("новый") { _, old, new ->
println("статус: $old -> $new")
}
}
fun main() {
val m = Model()
m.status = "в работе" // статус: новый -> в работе
m.status = "готов" // статус: в работе -> готов
}
```
## Template Method
Паттерн, где базовый класс задаёт скелет алгоритма, а подклассы заполняют шаги. В Kotlin это функция с лямбдами:
```kotlin
fun <T> processFile(
lines: List<String>,
parse: (String) -> T?,
onError: (String) -> Unit = { println("пропущено: $it") },
): List<T> = lines.mapNotNull { line ->
parse(line) ?: null.also { onError(line) }
}
fun main() {
val result = processFile(
lines = listOf("1", "два", "3"),
parse = { it.toIntOrNull() },
)
println(result)
// пропущено: два
// [1, 3]
}
```
Наследование не нужно: шаги передаются параметрами. Это и проще (нет иерархии), и гибче (шаги можно комбинировать в рантайме).
## Command
```kotlin
class History {
private val undoStack = mutableListOf<() -> Unit>()
fun execute(action: () -> Unit, undo: () -> Unit) {
action()
undoStack.add(undo)
}
fun undo() { undoStack.removeLastOrNull()?.invoke() }
}
fun main() {
val text = StringBuilder()
val history = History()
history.execute({ text.append("Привет") }, { text.setLength(0) })
println(text) // Привет
history.undo()
println("[$text]") // []
}
```
Интерфейс `Command` с методом `execute` заменяется лямбдой. Если у команды есть состояние или несколько операций - `data class` с методами честнее.
## Visitor
Паттерн нужен там, где над закрытой иерархией выполняют разные операции. В Kotlin это `sealed` + `when`:
```kotlin
sealed interface Expr {
data class Num(val value: Int) : Expr
data class Add(val left: Expr, val right: Expr) : Expr
data class Mul(val left: Expr, val right: Expr) : Expr
}
fun eval(e: Expr): Int = when (e) {
is Expr.Num -> e.value
is Expr.Add -> eval(e.left) + eval(e.right)
is Expr.Mul -> eval(e.left) * eval(e.right)
}
fun render(e: Expr): String = when (e) {
is Expr.Num -> e.value.toString()
is Expr.Add -> "(${render(e.left)} + ${render(e.right)})"
is Expr.Mul -> "${render(e.left)} * ${render(e.right)}"
}
fun main() {
val expr = Expr.Add(Expr.Num(2), Expr.Mul(Expr.Num(3), Expr.Num(4)))
println(render(expr)) // (2 + 3 * 4)
println(eval(expr)) // 14
}
```
Новая операция - новая функция, менять классы не нужно. Компилятор проверяет полноту `when`, поэтому добавление варианта в `sealed` сразу покажет все места, требующие правки - то, чего Visitor добивается ценой двойной диспетчеризации. Подробнее: [Sealed class и interface](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Sealed%20class%20и%20interface.md)
## Prototype и Null Object
```kotlin
data class Config4(val host: String, val port: Int, val debug: Boolean = false)
fun main() {
val prod = Config4("example.com", 443)
val staging = prod.copy(host = "staging.example.com", debug = true)
println(staging)
// Config4(host=staging.example.com, port=443, debug=true)
}
```
`copy` - это Prototype, встроенный в `data class`. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md)
Null Object создавался, чтобы избежать проверок на `null`. Nullable-типы решают ту же задачу явнее:
```kotlin
interface Logger3 { fun log(m: String) }
fun process6(data: String, logger: Logger3? = null) {
logger?.log("начало")
println("обработано: $data")
logger?.log("конец")
}
fun main() {
process6("x") // обработано: x
process6("y", object : Logger3 { override fun log(m: String) = println("[$m]") })
// [начало]
// обработано: y
// [конец]
}
```
Пустая реализация-заглушка не нужна: `?.` не вызывает метод при `null`. Подробнее: [Операторы null-безопасности](../Синтаксис%20и%20типы/Null-безопасность/Операторы%20null-безопасности.md)
## Что остаётся нужным
Паттерны, описывающие **архитектуру**, а не обход ограничений языка, остаются актуальными: Repository, Facade, Adapter между слоями, Strategy с несколькими методами, State-машина, Chain of Responsibility.
```kotlin
sealed interface OrderState {
data object New : OrderState
data object Paid : OrderState
data class Shipped(val track: String) : OrderState
data object Cancelled : OrderState
}
fun next(state: OrderState, event: String): OrderState = when (state) {
OrderState.New -> when (event) {
"pay" -> OrderState.Paid
"cancel" -> OrderState.Cancelled
else -> state
}
OrderState.Paid -> if (event == "ship") OrderState.Shipped("TRACK-1") else state
is OrderState.Shipped -> state
OrderState.Cancelled -> state
}
fun main() {
var state: OrderState = OrderState.New
for (event in listOf("pay", "ship", "cancel")) {
state = next(state, event)
println("$event -> $state")
}
// pay -> Paid
// ship -> Shipped(track=TRACK-1)
// cancel -> Shipped(track=TRACK-1)
}
```
Здесь `sealed` + `when` не отменяют паттерн State, а дают ему надёжную реализацию: невозможное состояние не создать, а забытый переход не скомпилируется.
> [!info] Отличия от Java
> | Паттерн в Java | В Kotlin |
> |---|---|
> | `private static final INSTANCE` + `getInstance()` | `object` |
> | класс `Builder` с 10 методами и `build()` | именованные аргументы |
> | `interface Strategy` + 3 класса | `(T) -> R` |
> | декоратор с ручным проксированием всех методов | `by delegate` |
> | `List<Listener>` + `addListener`/`removeListener` | лямбды или `Flow` |
> | `abstract class` с `protected abstract` шагами | функция с лямбдами |
> | `interface Visitor` с `visitX` на каждый тип | `sealed` + `when` |
> | `clone()` или конструктор копирования | `copy()` |
> | `NullLogger implements Logger {}` | `Logger?` + `?.` |
>
> **Главная разница - в количестве кода на тот же смысл.** Builder для класса с пятью полями в Java - это ~60 строк (вложенный класс, поле на каждый параметр, метод на каждый параметр, `build()`). В Kotlin это объявление `data class` с значениями по умолчанию, то есть ноль дополнительных строк. То же с Singleton (было: приватный конструктор, статическое поле, `getInstance`, возможно двойная проверка блокировки; стало: `object`) и Strategy (было: интерфейс плюс класс на каждую реализацию; стало: лямбда).
>
> **Декоратор - самый показательный случай.** В Java, чтобы задекорировать интерфейс с 15 методами, нужно написать 15 делегирующих методов, из которых интересен один. Отсюда классы вроде `HttpServletRequestWrapper` в стандартной библиотеке - их пишут специально, чтобы наследоваться. В Kotlin `by delegate` делает это автоматически:
> ```kotlin
> interface Big {
> fun a(): Int
> fun b(): Int
> fun c(): Int
> }
>
> class Impl : Big {
> override fun a() = 1
> override fun b() = 2
> override fun c() = 3
> }
>
> class OnlyBChanged(d: Big) : Big by d {
> override fun b() = 99
> }
>
> fun main() {
> val x = OnlyBChanged(Impl())
> println("${x.a()} ${x.b()} ${x.c()}") // 1 99 3
> }
> ```
>
> **Паттерны не исчезли - исчез их boilerplate.** Смысл Strategy (вынести изменяемое поведение) остался и в Kotlin, просто выражается функциональным типом. Полезно знать паттерны именно как словарь: «здесь Strategy» понятнее, чем «здесь лямбда в конструкторе».
>
> **Некоторые паттерны в Kotlin не нужны вообще.** Null Object существовал, чтобы не писать `if (x != null)`; при наличии `?.` он только добавляет класс. Double-Checked Locking для синглтона не нужен, потому что `object` и `lazy` уже потокобезопасны.
>
> Сравнить: [Singleton в Java](../../Программирование%20на%20Java/Паттерны%20проектирования/Singleton.md), [Builder](../../Программирование%20на%20Java/Паттерны%20проектирования/Builder.md)
> [!warning] Подводные камни
> **`object` - глобальное состояние, которое не подменить в тестах:**
> ```kotlin
> object Clock2 {
> fun now(): Long = System.currentTimeMillis()
> }
>
> interface Clock3 { fun now(): Long }
>
> class Service4(private val clock: Clock3) {
> fun timestamp() = clock.now()
> }
>
> fun main() {
> val fake = object : Clock3 { override fun now() = 1_000L }
> println(Service4(fake).timestamp()) // 1000 — тест детерминирован
> println(Clock2.now() > 0) // true — а это не подменить
> }
> ```
> Между `object` и интерфейсом с внедрением выбирайте второе, если поведение нужно будет подменить.
>
> **`object` с изменяемым состоянием не потокобезопасен.** Создание экземпляра защищено, а доступ к полям - нет:
> ```kotlin
> object Counter5 {
> var count = 0 // гонка при обращении из нескольких потоков
> }
>
> fun main() {
> Counter5.count++
> println(Counter5.count) // 1
> }
> ```
>
> **Стратегия-лямбда теряет имя в стеке и логах:**
> ```kotlin
> fun main() {
> val strategies: Map<String, (Int) -> Int> = mapOf("double" to { it * 2 })
> val f = strategies.getValue("double")
> println(f::class.simpleName?.contains("Lambda")) // true — имя сгенерировано
> }
> ```
> Вместо осмысленного имени получается что-то вроде `MainKt$$Lambda/0x00001e0001040620` - с адресом, меняющимся от запуска к запуску. Отладка «какая стратегия сработала» требует хранить имя рядом. Если это важно - интерфейс или `data class` с полем `name`.
>
> **`by delegate` делегирует только методы интерфейса.** `equals`, `hashCode` и `toString` берутся из `Any`, а не из делегата:
> ```kotlin
> interface Named5 { val name: String }
> data class Impl2(override val name: String) : Named5
>
> class Wrapper(d: Named5) : Named5 by d
>
> fun main() {
> val w = Wrapper(Impl2("x"))
> println(w.name) // x
> println(w.toString().startsWith("Wrapper")) // true — не делегировано
> println(w == Wrapper(Impl2("x"))) // false
> }
> ```
>
> **Делегат фиксируется при создании:**
> ```kotlin
> interface Source { fun value(): Int }
>
> class Changing(private var inner: Source) : Source by inner {
> fun replace(newInner: Source) { inner = newInner }
> }
>
> fun main() {
> val first = object : Source { override fun value() = 1 }
> val second = object : Source { override fun value() = 2 }
> val c = Changing(first)
> c.replace(second)
> println(c.value()) // 1 — делегирует первому!
> }
> ```
> `by` запоминает ссылку в момент конструирования. Для подменяемого делегата нужно явное проксирование метода.
>
> **Свой Observer течёт по памяти:**
> ```kotlin
> class Leaky3 {
> private val handlers = mutableListOf<(Int) -> Unit>()
> fun subscribe(h: (Int) -> Unit) { handlers.add(h) }
> fun count() = handlers.size
> }
>
> fun main() {
> val p = Leaky3()
> repeat(3) { p.subscribe { } }
> println(p.count()) // 3 — и никогда не уменьшится
> }
> ```
> Без механизма отписки подписчики живут до конца жизни издателя и держат свои замыкания. Поэтому для событий лучше `Flow` с привязкой к области корутины. Подробнее: [Структурированная конкурентность](../Корутины/Структурированная%20конкурентность.md)
>
> **Лямбда в коллекции не сравнивается:**
> ```kotlin
> fun main() {
> val handlers = mutableListOf<() -> Unit>()
> val h = { println("x") }
> handlers.add(h)
> println(handlers.remove(h)) // true — та же ссылка
> handlers.add { println("x") }
> println(handlers.remove { println("x") }) // false — другой объект
> }
> ```
> Отписка по «такой же» лямбде не работает - нужна именно та ссылка или токен отписки.
>
> **Builder через `apply` на `val`-полях невозможен** - нужны `var`, а значит объект изменяем после сборки. Для неизменяемого результата используйте `data class` с конструктором или отдельный билдер, возвращающий immutable-объект.
>
> **Именованные аргументы не защищают от неверного сочетания.** `HttpRequest(url = "x", retries = -5)` скомпилируется. Валидацию нужно писать в `init`:
> ```kotlin
> data class Retry(val attempts: Int) {
> init { require(attempts >= 0) { "attempts не может быть отрицательным" } }
> }
>
> fun main() {
> println(Retry(3))
> try { Retry(-1) } catch (e: IllegalArgumentException) { println(e.message) }
> // Retry(attempts=3)
> // attempts не может быть отрицательным
> }
> ```
> Подробнее: [Проверки и контракты](../Исключения/Проверки%20и%20контракты.md)
>
> **Применение паттерна там, где он не нужен, остаётся главной проблемой** - и язык от этого не спасает. Фабрика для класса с одним конструктором, Strategy с единственной реализацией и Repository поверх одного SQL-запроса добавляют уровни без выгоды. Критерий прежний: паттерн оправдан, когда вариативность уже существует, а не когда она предполагается.
Далее: [Корутины](../Корутины/Корутины.md)
@@ -0,0 +1,392 @@
---
title: Функции высшего порядка
description: Функции, принимающие и возвращающие функции, композиция, каррирование и мемоизация
tags:
- kotlin
- functional
---
Функция высшего порядка - это функция, которая принимает функцию в параметре или возвращает функцию. В Kotlin функциональный тип (`(Int) -> String`) полноправный, поэтому ничего особенного для этого не нужно.
```kotlin
fun applyTwice(x: Int, f: (Int) -> Int): Int = f(f(x))
fun main() {
println(applyTwice(3) { it * 2 }) // 12
println(applyTwice(3) { it + 10 }) // 23
}
```
Возврат функции:
```kotlin
fun multiplier(factor: Int): (Int) -> Int = { it * factor }
fun main() {
val triple = multiplier(3)
println(triple(5)) // 15
println(multiplier(10)(7)) // 70
}
```
`multiplier(3)` возвращает лямбду, которая **захватила** `factor`. Это замыкание, и оно работает так же, как в любом функциональном языке. Подробнее о синтаксисе: [Лямбды и функциональные типы](../Синтаксис%20и%20типы/Работа%20с%20данными/Лямбды%20и%20функциональные%20типы.md)
## Зачем это нужно
Функция высшего порядка позволяет вынести **изменяемую часть** алгоритма в параметр. Классический пример - повторяющаяся обвязка:
```kotlin
fun <T> retry(times: Int, block: () -> T): T {
var lastError: Throwable? = null
repeat(times) { attempt ->
try {
return block()
} catch (e: Exception) {
lastError = e
println("попытка ${attempt + 1} не удалась")
}
}
throw IllegalStateException("все $times попыток провалились", lastError)
}
fun main() {
var counter = 0
val result = retry(3) {
counter++
if (counter < 3) error("ещё не готово")
"успех на попытке $counter"
}
println(result) // успех на попытке 3
}
```
Логика повторов написана один раз; что именно повторять - решает вызывающий. Без функциональных типов пришлось бы заводить интерфейс с одним методом и анонимный класс.
Та же идея для измерения, транзакций и логирования:
```kotlin
inline fun <T> measured(name: String, block: () -> T): T {
val start = System.nanoTime()
try {
return block()
} finally {
println("$name: ${(System.nanoTime() - start) / 1_000} мкс")
}
}
fun main() {
val sum = measured("сумма") { (1..100_000).sumOf { it.toLong() } }
println(sum) // 5000050000
}
```
`finally` здесь важен: время выводится и при исключении. `inline` убирает объект лямбды - подробнее: [Inline-функции](Inline-функции.md)
## Функциональные типы как параметры
```kotlin
fun processNumbers(
numbers: List<Int>,
filter: (Int) -> Boolean = { true },
transform: (Int) -> String = { it.toString() },
): List<String> = numbers.filter(filter).map(transform)
fun main() {
println(processNumbers(listOf(1, 2, 3))) // [1, 2, 3]
println(processNumbers(listOf(1, 2, 3), filter = { it > 1 })) // [2, 3]
println(processNumbers(listOf(1, 2), transform = { "№$it" })) // [№1, №2]
}
```
Функциональный параметр может иметь значение по умолчанию - это заменяет перегрузки.
Nullable функциональный тип требует скобок:
```kotlin
fun handle(value: Int, onSuccess: ((Int) -> Unit)? = null): Int {
onSuccess?.invoke(value)
return value
}
fun main() {
println(handle(1)) // 1
println(handle(2) { println("получено $it") }) // получено 2 \n 2
}
```
`(Int) -> Unit?` - это функция, возвращающая `Unit?`. Скобки меняют смысл принципиально.
## Композиция
```kotlin
infix fun <A, B, C> ((A) -> B).then(next: (B) -> C): (A) -> C = { next(this(it)) }
fun main() {
val parse: (String) -> Int = { it.trim().toInt() }
val double: (Int) -> Int = { it * 2 }
val format: (Int) -> String = { "результат: $it" }
val pipeline = parse then double then format
println(pipeline(" 21 ")) // результат: 42
}
```
В stdlib готовой композиции нет - её пишут сами в три строки. Направление `then` (слева направо) читается лучше математического `compose`, но встречаются оба:
```kotlin
infix fun <A, B, C> ((B) -> C).compose(before: (A) -> B): (A) -> C = { this(before(it)) }
fun main() {
val inc: (Int) -> Int = { it + 1 }
val double: (Int) -> Int = { it * 2 }
println((inc compose double)(5)) // 11 — сначала double, потом inc
println((double compose inc)(5)) // 12 — наоборот
}
```
## Каррирование и частичное применение
```kotlin
fun <A, B, C> curry(f: (A, B) -> C): (A) -> (B) -> C = { a -> { b -> f(a, b) } }
fun main() {
val sum: (Int, Int) -> Int = { a, b -> a + b }
val curried = curry(sum)
println(curried(2)(3)) // 5
val add10 = curried(10)
println(add10(5)) // 15
println(listOf(1, 2, 3).map(add10)) // [11, 12, 13]
}
```
На практике частичное применение чаще делают прямо лямбдой - это короче и понятнее:
```kotlin
fun log(level: String, module: String, message: String) = "[$level][$module] $message"
fun main() {
val errorInAuth: (String) -> String = { log("ERROR", "auth", it) }
println(errorInAuth("неверный пароль")) // [ERROR][auth] неверный пароль
}
```
Ещё проще - значения по умолчанию и именованные аргументы, которые в Kotlin закрывают большинство задач, решаемых каррированием в других языках. Подробнее: [Функции](../Синтаксис%20и%20типы/Работа%20с%20данными/Функции.md)
## Мемоизация
```kotlin
fun <A, R> memoize(f: (A) -> R): (A) -> R {
val cache = mutableMapOf<A, R>()
return { arg -> cache.getOrPut(arg) { f(arg) } }
}
fun main() {
var calls = 0
val slowSquare = memoize<Int, Int> { calls++; it * it }
println(slowSquare(4)) // 16
println(slowSquare(4)) // 16
println(slowSquare(5)) // 25
println("вызовов: $calls") // вызовов: 2
}
```
`getOrPut` делает это в одну строку. Ограничения такого кеша: он не ограничен по размеру и не потокобезопасен - для продакшена нужен `ConcurrentHashMap` или готовый кеш.
Рекурсия с мемоизацией требует ссылки на саму функцию:
```kotlin
fun main() {
val cache = mutableMapOf(0 to 0L, 1 to 1L)
fun fib(n: Int): Long = cache.getOrPut(n) { fib(n - 1) + fib(n - 2) }
println(fib(50)) // 12586269025
println(cache.size) // 51
}
```
Локальная функция может вызывать себя, а лямбда - нет: внутри `val f = { ... f(...) }` имя `f` ещё не инициализировано.
## Функции, возвращающие функции
Типичный случай - настраиваемый валидатор:
```kotlin
fun lengthBetween(min: Int, max: Int): (String) -> String? = { value ->
when {
value.length < min -> "короче $min символов"
value.length > max -> "длиннее $max символов"
else -> null
}
}
fun validateAll(value: String, vararg rules: (String) -> String?): List<String> =
rules.mapNotNull { it(value) }
fun main() {
val notBlank: (String) -> String? = { if (it.isBlank()) "пустое значение" else null }
println(validateAll("ab", notBlank, lengthBetween(3, 10))) // [короче 3 символов]
println(validateAll("привет", notBlank, lengthBetween(3, 10))) // []
println(validateAll(" ", notBlank)) // [пустое значение]
}
```
Правила - обычные значения: их можно хранить в списке, передавать и комбинировать. Это основная практическая выгода функций высшего порядка.
## Ссылки на функции вместо лямбд
```kotlin
fun isEven(x: Int) = x % 2 == 0
fun main() {
val numbers = listOf(1, 2, 3, 4)
println(numbers.filter { isEven(it) }) // [2, 4] — лямбда-обёртка
println(numbers.filter(::isEven)) // [2, 4] — ссылка напрямую
println(numbers.map(Int::toString)) // [1, 2, 3, 4] — ссылка на метод
println(listOf("1", "2").map(String::toInt)) // [1, 2]
val strings = listOf("bb", "a", "ccc")
println(strings.sortedBy(String::length)) // [a, bb, ccc]
}
```
Ссылка `::isEven` короче и не создаёт лишнего уровня вложенности. Подробнее: [Лямбды и функциональные типы](../Синтаксис%20и%20типы/Работа%20с%20данными/Лямбды%20и%20функциональные%20типы.md)
## Лямбда с получателем
Особый вид функционального типа - `T.() -> R`. Внутри такой лямбды `this` - это `T`:
```kotlin
fun buildMessage(build: StringBuilder.() -> Unit): String {
val sb = StringBuilder()
sb.build()
return sb.toString()
}
fun main() {
val text = buildMessage {
append("Привет") // this == StringBuilder
append(", ")
append("мир")
}
println(text) // Привет, мир
}
```
На этом построены все билдеры Kotlin, включая `buildList` и `apply`. Подробнее: [Receivers и context parameters](Receivers%20и%20context%20parameters.md), [DSL](DSL.md)
## Карта раздела
| Заметка | О чём |
|---|---|
| [Inline-функции](Inline-функции.md) | как убрать накладные расходы лямбд |
| [Функции-расширения](Функции-расширения.md) | добавление методов к готовым типам |
| [Scope-функции](Scope-функции.md) | `let`, `run`, `apply`, `also`, `with` |
| [Инфиксные и операторные функции](Инфиксные%20и%20операторные%20функции.md) | свой синтаксис для своих типов |
| [Receivers и context parameters](Receivers%20и%20context%20parameters.md) | неявный `this` и контекстные зависимости |
| [DSL](DSL.md) | сборка всего перечисленного в язык под задачу |
| [Паттерны проектирования](Паттерны%20проектирования.md) | что от паттернов осталось в Kotlin |
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `Function<T, R>`, `BiFunction`, `Supplier`, `Consumer` | один синтаксис `(T) -> R` |
> | `Predicate<T>` | `(T) -> Boolean` |
> | `Runnable` | `() -> Unit` |
> | `IntFunction`, `ToIntFunction`, ... (40+ интерфейсов) | нет, но есть упаковка |
> | `f.apply(x)` | `f(x)` |
> | `f.andThen(g)` | своя `then` |
> | лямбда - объект | лямбда инлайнится при `inline` |
> | нет аналога | лямбда с получателем `T.() -> R` |
>
> **Функциональный тип вместо интерфейса.** В Java функция передаётся как реализация интерфейса, и интерфейс нужно выбрать: `Function<T,R>` для одного аргумента, `BiFunction` для двух, `Supplier` без аргументов, `Consumer` без результата, `Predicate` для `boolean`, плюс примитивные специализации - в `java.util.function` их больше сорока. В Kotlin тип записывается напрямую:
> ```kotlin
> fun demo(
> supplier: () -> Int,
> consumer: (Int) -> Unit,
> predicate: (Int) -> Boolean,
> biFunction: (Int, Int) -> Int,
> ) {
> val v = supplier()
> consumer(v)
> println("${predicate(v)} ${biFunction(v, v)}")
> }
>
> fun main() { demo({ 2 }, { }, { it > 1 }, { a, b -> a + b }) } // true 4
> ```
> Под капотом это всё равно интерфейсы `Function0`, `Function1`, ... `Function22`, но в коде их не видно. Три с лишним аргумента в Java требуют собственного интерфейса - в Kotlin предел 22.
>
> **Вызов через `invoke`, а не `apply`.** В Java функциональный объект вызывают методом интерфейса: `f.apply(x)`, `p.test(x)`, `c.accept(x)`, `s.get()` - имя зависит от интерфейса. В Kotlin оператор `()` работает для любого функционального типа, потому что все они объявляют `operator fun invoke`.
>
> **Композиции в stdlib нет.** Java даёт `Function.andThen`, `Function.compose`, `Predicate.and/or/negate`. Kotlin их не предоставляет сознательно: обычно короче написать одну лямбду, чем собирать её из частей. Когда композиция действительно нужна, она пишется в одну строку - как `then` выше.
>
> **Лямбда с получателем аналога в Java не имеет.** Это основа билдеров и DSL в Kotlin; в Java эквивалента нет даже близко, поэтому билдеры там пишут методами, возвращающими `this`.
>
> Сравнить: [Функции в Java](../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Функции.md), [Интерфейсы в Java](../../Программирование%20на%20Java/ООП/Интерфейсы.md)
> [!warning] Подводные камни
> **Лямбда без `inline` создаёт объект на каждый вызов:**
> ```kotlin
> fun notInlined(block: () -> Unit) { block() }
> inline fun inlined(block: () -> Unit) { block() }
>
> fun main() {
> repeat(3) { notInlined { } } // объект Function0 на каждый вызов
> repeat(3) { inlined { } } // объектов нет
> println("готово")
> }
> ```
> Лямбда без захвата переменных кешируется компилятором в синглтон, а лямбда с захватом - нет. В горячем цикле это заметно. Подробнее: [Inline-функции](Inline-функции.md)
>
> **`return` внутри лямбды возвращает из внешней функции - но только в inline:**
> ```kotlin
> inline fun forEachInline(items: List<Int>, action: (Int) -> Unit) { for (i in items) action(i) }
>
> fun findFirstEven(items: List<Int>): Int? {
> forEachInline(items) { if (it % 2 == 0) return it } // выход из findFirstEven
> return null
> }
>
> fun main() { println(findFirstEven(listOf(1, 3, 4, 6))) } // 4
> ```
> В не-inline функции такой `return` не скомпилируется - нужен `return@label`. Это разное поведение для внешне одинакового кода.
>
> **Nullable функциональный тип требует скобок:**
> ```kotlin
> fun main() {
> val a: ((Int) -> Unit)? = null // функция может быть null
> val b: (Int) -> Unit? = { } // функция возвращает Unit?
> println("${a == null} ${b(1)}") // true kotlin.Unit
> }
> ```
> Без скобок получается совсем другой тип, и ошибка проявится далеко от объявления.
>
> **Вызов nullable-функции только через `invoke`:**
> ```kotlin
> fun main() {
> val f: ((Int) -> Int)? = { it * 2 }
> // println(f?(1)) // синтаксическая ошибка
> println(f?.invoke(1)) // 2
> }
> ```
>
> **Захваченная `var` меняется после создания лямбды:**
> ```kotlin
> fun main() {
> var counter = 0
> val show = { println("counter = $counter") }
> counter = 10
> show() // counter = 10, а не 0
> }
> ```
> В отличие от Java, Kotlin разрешает захват изменяемых переменных - они оборачиваются в объект-ссылку. Удобно, но лямбда перестаёт быть чистой функцией и становится зависимой от момента вызова.
>
> **Лямбды в цикле захватывают переменную, а не значение:**
> ```kotlin
> fun main() {
> val actions = mutableListOf<() -> Int>()
> var i = 0
> while (i < 3) { actions.add { i }; i++ }
> println(actions.map { it() }) // [3, 3, 3] — все видят финальное i
>
> val correct = (0 until 3).map { v -> { v } }
> println(correct.map { it() }) // [0, 1, 2] — параметр цикла свой у каждой
>
> val fixed = mutableListOf<() -> Int>()
> for (j in 0 until 3) fixed.add { j }
> println(fixed.map { it() }) // [0, 1, 2] — переменная for неизменяема
> }
> ```
>
> **Мемоизация течёт по памяти.** `mutableMapOf` в замыкании растёт без ограничений и держит все аргументы и результаты. Если функция живёт всё время работы приложения, это утечка; нужен кеш с ограничением размера.
>
> **Мемоизация не потокобезопасна.** `getOrPut` на обычном `MutableMap` при одновременном доступе может повредить структуру. Минимальная защита - `ConcurrentHashMap`, но и у него `getOrPut` не атомарен: используйте `computeIfAbsent`.
>
> **Лямбда не может ссылаться на себя:**
> ```kotlin
> fun main() {
> // val fact: (Int) -> Int = { if (it <= 1) 1 else it * fact(it - 1) } // ошибка
> fun fact(n: Int): Int = if (n <= 1) 1 else n * fact(n - 1) // локальная функция может
> println(fact(5)) // 120
> }
> ```
>
> **Больше двух-трёх функциональных параметров делают вызов нечитаемым.** `process(list, { }, { }, { })` ничего не говорит о смысле аргументов. Варианты: именованные аргументы, объект-конфигурация или интерфейс с несколькими методами.
>
> **Функциональные типы упаковывают примитивы.** `(Int) -> Int` в байт-коде - это `Function1<Integer, Integer>`. Специализаций вроде `IntUnaryOperator` в Kotlin нет, поэтому в численно-интенсивном коде `inline` важен не только из-за объекта лямбды, но и из-за упаковки.
Далее: [Inline-функции](Inline-функции.md)
@@ -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)