440 lines
21 KiB
Markdown
440 lines
21 KiB
Markdown
---
|
||
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)
|