Avatar ·

Что такое «ресивер» (receiver) в Kotlin и зачем он нужен?

Привет, коллеги! Я только начал изучать Kotlin после Java и наткнулся на термин «receiver» (ресивер/приёмник). Читаю документацию и вижу такие вещи, как `apply`, `let`, `run`, `with` — и везде пишут про ресиверы. Но я не могу до конца понять, что это такое и как это работает. Вот простой пример. Я пишу такой код: ```kotlin data class Person(var name: String = "", var age: Int = 0) fun main() { val person = Person().apply { name = "Alice" // Здесь я обращаюсь к name напрямую, без person.name age = 30 } println(person) } ``` Я понимаю, что внутри блока `apply` я могу писать `name = "Alice"`, а не `person.name = "Alice"`. Но как это работает? Что значит «функция с ресивером»? Это какое-то магическое замыкание? Или это похоже на то, как работает `this` в Java? Мне кажется, я упускаю какую-то фундаментальную концепцию. Можете объяснить на пальцах, что такое receiver в Kotlin, чем он отличается от обычного аргумента функции, и где это может пригодиться, кроме как в `apply`/`run`? Буду благодарен за простые примеры и аналогии. <<>>

Коротко: ресивер — это неявный объект, к которому можно обращаться внутри блока

В Kotlin ресивер (receiver) — это объект, в контексте которого выполняется лямбда или функция. Проще говоря, внутри такого блока this ссылается на этот объект, и можно вызывать его методы и свойства напрямую, без префикса объект..

Твой пример с apply — идеальная иллюстрация. Давай разберём его по косточкам.

val person = Person().apply {
    name = "Alice"   // это то же самое, что this.name = "Alice"
    age = 30         // это то же самое, что this.age = 30
}

Функция apply принимает лямбду с ресивером типа Person. Это значит, что внутри фигурных скобок this автоматически становится экземпляром класса Person, который только что создали. Поэтому мы можем писать name вместо this.name.

Как объявить свою функцию с ресивером?

Ты можешь сам создавать такие функции. Есть два способа:

1. Функция-расширение (extension function)

fun Person.sayHello() {
    println("Привет, меня зовут $name") // name — это свойство ресивера Person
}

Здесь Person — ресивер. Когда ты вызываешь person.sayHello(), внутри функции this равно person.

2. Лямбда с ресивером (lambda with receiver)

fun buildPerson(block: Person.() -> Unit): Person {
    val p = Person()
    p.block()   // здесь мы передаём ресивер p в лямбду
    return p
}

fun main() {
    val person = buildPerson {
        name = "Bob"   // ресивер — Person
        age = 25
    }
}

Тип Person.() -> Unit означает: «лямбда, которая принимает ресивер типа Person и ничего не возвращает». Внутри такой лямбды this — это Person.

Чем ресивер отличается от обычного аргумента?

  • Обычный аргумент: ты явно передаёшь объект в функцию, и внутри обращаешься к нему по имени параметра.
  • Ресивер: объект становится контекстом (this), и все обращения к его членам происходят неявно.

Сравни:

// Обычный аргумент
fun configure(p: Person) {
    p.name = "Charlie"
    p.age = 40
}

// Ресивер
fun Person.configure() {
    name = "Charlie"
    age = 40
}

В первом случае нужно писать p. каждый раз, во втором — нет. Это удобно, когда много раз обращаешься к одному объекту.

Где ещё используется ресивер?

Кроме apply, run, with и also, ресиверы широко применяются в:

  • DSL (предметно-ориентированные языки) — например, построение HTML или SQL запросов. Каждый вложенный блок имеет свой ресивер.
  • Строители (builders) — как в твоём примере с Person.
  • Тестирование — библиотеки вроде MockK используют ресиверы для настройки моков.
  • Kotlin Coroutines — внутри корутинного билдера launch ресивером является CoroutineScope.

Аналогия из жизни

Представь, что ты заходишь в комнату. Внутри комнаты ты можешь пользоваться всем, что в ней находится, не говоря «в этой комнате возьми стул», а просто «возьми стул». Комната — это ресивер. Выходишь из комнаты — и снова нужно уточнять, где что лежит.

Надеюсь, теперь стало понятнее! Ресивер — это мощный инструмент для создания читаемого и лаконичного кода.

<<>>

Дополнение: ресивер — это не магия, а синтаксический сахар

Коллега выше отлично объяснил суть. Я добавлю немного технической подоплёки, чтобы снять ореол «магии».

На самом деле, под капотом лямбда с ресивером — это обычная лямбда, которая принимает один дополнительный параметр. Компилятор просто делает для нас синтаксический сахар.

Вот как это выглядит в байткоде (упрощённо):

// Твой код
person.apply { name = "Alice" }

// Примерно так это преобразуется компилятором
person.apply(object : Function1<Person, Unit> {
    override fun invoke(p: Person) {
        p.name = "Alice"
    }
})

То есть ресивер — это просто первый (и часто неявный) аргумент лямбды. Но Kotlin позволяет обращаться к его членам без префикса this., что делает код чище.

Практический совет: Если ты видишь тип вида T.() -> R — это и есть лямбда с ресивером типа T. А если видишь (T) -> R — это обычная лямбда, принимающая T как аргумент. Разница только в способе обращения внутри блока.

Зная это, ты можешь легко создавать свои DSL-конструкции:

class HtmlBuilder {
    fun text(value: String) { /* ... */ }
}

fun html(block: HtmlBuilder.() -> Unit): HtmlBuilder {
    val builder = HtmlBuilder()
    builder.block()
    return builder
}

fun main() {
    html {
        text("Hello!") // ресивер — HtmlBuilder
    }
}

Так что ресивер — это просто удобный способ «расширить» контекст внутри блока. Никакой магии, только синтаксический сахар и хороший дизайн языка.