Kotlin guide done. (20.10.26)
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user