Files
obsidian/Kotlin/Функциональный Kotlin/Receivers и context parameters.md
T

440 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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)