--- 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.run(block: T.() -> R): R = block() // inline fun T.apply(block: T.() -> Unit): T { block(); return this } // inline fun with(receiver: T, block: T.() -> R): R = receiver.block() // inline fun T.let(block: (T) -> R): R = block(this) // inline fun 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("") } fun build(block: HtmlBuilder.() -> Unit): String { block() return sb.toString() } } fun main() { val html = HtmlBuilder().build { "Заголовок".tag("h1") "Текст".tag("p") } println(html) //

Заголовок

Текст

} ``` Внутри `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() fun row(block: Row.() -> Unit) { rows.add(Row().apply(block).render()) } fun render() = rows.joinToString("\n") } class Row { private val cells = mutableListOf() 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() fun row(block: Row2.() -> Unit) { rows.add(Row2().apply(block).render()) } fun render() = rows.joinToString("\n") } @TableDsl class Row2 { private val cells = mutableListOf() 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() 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 = errors } fun validate(block: Validator.() -> Unit): List = 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() > 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)