Kotlin guide done. (20.10.26)

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