--- title: Scope-функции description: let, run, apply, also, with - различия, выбор и типичные ошибки tags: - kotlin - functional --- Пять функций stdlib, которые выполняют блок кода в контексте объекта. Делают они почти одно и то же, различаясь двумя вещами: как доступен объект (`this` или `it`) и что возвращается (объект или результат блока). | Функция | Объект внутри | Возвращает | Расширение | |---|---|---|---| | `let` | `it` | результат блока | да | | `run` | `this` | результат блока | да | | `run` (без получателя) | - | результат блока | нет | | `apply` | `this` | сам объект | да | | `also` | `it` | сам объект | да | | `with` | `this` | результат блока | нет (принимает аргумент) | ```kotlin data class Person3(var name: String, var age: Int) fun main() { val p = Person3("Иван", 30) val letResult = p.let { "${it.name} ${it.age}" } // it, результат блока val runResult = p.run { "$name $age" } // this, результат блока val applyResult = p.apply { age = 31 } // this, сам объект val alsoResult = p.also { println("создан ${it.name}") } // it, сам объект val withResult = with(p) { "$name $age" } // this, результат блока println(letResult) // Иван 30 println(runResult) // Иван 30 println(applyResult) // Person3(name=Иван, age=31) println(alsoResult === p) // true println(withResult) // Иван 31 } ``` ```mermaid graph TD A["Нужна scope-функция"] --> B{"Что возвращать?"} B -->|"сам объект"| C{"Как обращаться?"} B -->|"результат блока"| D{"Как обращаться?"} C -->|"this - настройка полей"| E["apply"] C -->|"it - побочный эффект"| F["also"] D -->|"it - преобразование"| G["let"] D -->|"this - много обращений"| H["run"] D -->|"объект уже есть, не цепочка"| I["with"] ``` ## let Основное применение - работа с nullable и преобразование значения: ```kotlin fun main() { val name: String? = "Иван" val length = name?.let { it.length } ?: 0 println(length) // 4 name?.let { println("имя: $it") } // имя: Иван val empty: String? = null empty?.let { println("не выполнится") } println("после") // после } ``` `?.let { }` - идиоматичная замена `if (x != null)`, когда внутри нужно значение. Подробнее: [Операторы null-безопасности](../Синтаксис%20и%20типы/Null-безопасность/Операторы%20null-безопасности.md) Второе применение - ограничить область видимости промежуточного значения: ```kotlin fun main() { val result = listOf(1, 2, 3, 4) .filter { it % 2 == 0 } .let { evens -> "чётных: ${evens.size}, сумма: ${evens.sum()}" } println(result) // чётных: 2, сумма: 6 } ``` Переменная `evens` не утекает наружу. Явное имя параметра вместо `it` здесь делает код понятнее. ## run То же, что `let`, но объект доступен как `this` - удобно, когда обращений к нему много: ```kotlin data class Config3(val host: String, val port: Int, val tls: Boolean) fun main() { val url = Config3("example.com", 8443, true).run { val scheme = if (tls) "https" else "http" "$scheme://$host:$port" } println(url) // https://example.com:8443 } ``` `run` без получателя - просто блок, возвращающий значение: ```kotlin fun main() { val value = run { val a = 2 val b = 3 a * b } println(value) // 6 } ``` Это способ вычислить `val` несколькими строками, не заводя функцию. Полезно в инициализации свойств. ## apply Возвращает сам объект - значит, предназначен для настройки: ```kotlin class Request2 { var url: String = "" var method: String = "GET" val headers = mutableMapOf() override fun toString() = "$method $url $headers" } fun main() { val request = Request2().apply { url = "https://example.com" method = "POST" headers["Content-Type"] = "application/json" } println(request) // POST https://example.com {Content-Type=application/json} } ``` Это замена builder-паттерна для классов с изменяемыми свойствами. Подробнее: [Паттерны проектирования](Паттерны%20проектирования.md) Частый случай - настройка при возврате: ```kotlin fun buildList2(): List = mutableListOf().apply { add(1) add(2) addAll(listOf(3, 4)) } fun main() { println(buildList2()) // [1, 2, 3, 4] } ``` Хотя для коллекций в stdlib уже есть `buildList`, который чище. ## also Возвращает объект, доступ через `it` - для побочных эффектов, не меняющих объект: ```kotlin fun main() { val numbers = listOf(3, 1, 2) .also { println("исходный: $it") } .sorted() .also { println("отсортированный: $it") } .filter { it > 1 } .also { println("отфильтрованный: $it") } println(numbers) // исходный: [3, 1, 2] // отсортированный: [1, 2, 3] // отфильтрованный: [2, 3] // [2, 3] } ``` `also` вставляется в цепочку, ничего в ней не меняя - идеально для логирования и отладки. Второе применение - валидация и регистрация при создании: ```kotlin class Resource(val name: String) { fun close() = println("закрыт $name") } fun main() { val registry = mutableListOf() val r = Resource("файл").also { registry.add(it) } println("${r.name}, в реестре: ${registry.size}") // файл, в реестре: 1 } ``` ## with Единственная не-расширение: объект передаётся аргументом. ```kotlin fun main() { val sb = StringBuilder() with(sb) { append("Привет") append(", ") append("мир") append("!") } println(sb) // Привет, мир! } ``` `with(x) { }` читается как «с объектом x сделай» и применяется, когда объект уже есть в переменной и цепочка не нужна. Результат - значение блока: ```kotlin data class Point2(val x: Int, val y: Int) fun main() { val distance = with(Point2(3, 4)) { kotlin.math.sqrt((x * x + y * y).toDouble()) } println(distance) // 5.0 } ``` ## Как выбирать Практические правила, покрывающие почти все случаи: | Задача | Функция | |---|---| | выполнить код, если значение не `null` | `?.let { }` | | преобразовать значение в другое | `let` | | настроить свойства нового объекта | `apply` | | залогировать значение в середине цепочки | `also` | | вычислить значение из нескольких полей объекта | `run` | | сгруппировать вызовы на существующем объекте | `with` | | вычислить `val` из нескольких строк | `run { }` без получателя | Выбор между `it` и `this` - вопрос читаемости: `this` короче при многих обращениях к полям, `it` яснее, когда объект используется один-два раза или когда внутри есть другой `this`. ## Вложенность и затенение ```kotlin data class Inner(val value: Int) data class Outer(val inner: Inner, val name: String) fun main() { val o = Outer(Inner(42), "внешний") // плохо: два this, непонятно какой val bad = o.run { inner.run { "$value" } } println(bad) // 42 // хорошо: явные имена val good = o.let { outer -> outer.inner.let { inner -> "${outer.name}: ${inner.value}" } } println(good) // внешний: 42 } ``` Вложенные scope-функции - главный источник нечитаемого кода на Kotlin. Правило: вложенность больше одного уровня - заводите явные имена параметров или обычные переменные. ## Scope-функции и null ```kotlin fun main() { val maybe: String? = "текст" println(maybe?.let { it.length }) // 5 — блок выполнится, если не null println(maybe?.run { length }) // 5 — то же println(maybe.let { it?.length }) // 5 — блок выполнится всегда, it nullable val nothing: String? = null println(nothing?.let { it.length }) // null println(nothing.let { it?.length }) // null — но блок выполнился! } ``` Разница в месте `?.`: `x?.let { }` не вызывает блок при `null`, `x.let { }` вызывает всегда с nullable `it`. Это часто путают. ## takeIf и takeUnless Родственные функции - фильтр для одного значения: ```kotlin fun main() { val number = 42 println(number.takeIf { it > 0 }) // 42 println(number.takeIf { it > 100 }) // null println(number.takeUnless { it > 0 }) // null val name = " " println(name.takeIf { it.isNotBlank() } ?: "(по умолчанию)") // (по умолчанию) val config = mapOf("port" to "8080") val port = config["port"]?.takeIf { it.isNotBlank() }?.toIntOrNull() ?: 80 println(port) // 8080 } ``` `takeIf` превращает условие в nullable-значение, которое дальше обрабатывается через `?:` и `?.`. Это заменяет `if`-цепочки в конвейерах преобразования. > [!info] Отличия от Java > | Java | Kotlin | > |---|---| > | `if (x != null) { ... }` | `x?.let { ... }` | > | `Optional.map` | `let` | > | `Optional.ifPresent` | `?.let` или `?.also` | > | builder с `return this` | `apply` | > | промежуточная переменная | `let`/`run` | > | `Objects.requireNonNullElse(x, y)` | `x ?: y` | > | нет аналога | `also` в середине цепочки | > | нет аналога | `with` | > > **Scope-функции заменяют `Optional` и промежуточные переменные.** В Java цепочка преобразований с проверками на null выглядит так: > ```java > String result = Optional.ofNullable(user) > .map(User::getAddress) > .map(Address::getCity) > .orElse("неизвестно"); > ``` > В Kotlin `?.` и `?:` делают то же без обёртки, а `let` подключается, когда нужен не просто вызов метода: > ```kotlin > data class Address(val city: String) > data class User11(val address: Address?) > > fun main() { > val user: User11? = User11(Address("Москва")) > val result = user?.address?.city?.let { it.uppercase() } ?: "неизвестно" > println(result) // МОСКВА > } > ``` > Принципиальная разница: `Optional` - объект-обёртка со своей стоимостью и своим API, а `?.` - синтаксис над обычной ссылкой. Поэтому `Optional` в Kotlin не используют вообще. > > **`apply` заменяет builder-паттерн для изменяемых объектов.** В Java чтобы настраивать объект цепочкой, класс должен возвращать `this` из каждого сеттера - это решение автора класса. `apply` работает с любым классом, включая чужой: > ```kotlin > fun main() { > val props = java.util.Properties().apply { > setProperty("host", "localhost") > setProperty("port", "8080") > } > println(props.getProperty("host")) // localhost > } > ``` > `Properties` из JDK не имеет fluent-API, но с `apply` ведёт себя как будто имеет. > > **`also` в цепочке аналога в Java не имеет.** `Stream.peek` похож, но работает только внутри стрима и его использование не рекомендуется. `also` применим к любому значению. > > **Это не магия, а обычные inline-расширения.** `let` объявлена как `inline fun T.let(block: (T) -> R): R = block(this)` - одна строка. Поэтому накладных расходов нет: после компиляции остаётся только тело блока. Подробнее: [Inline-функции](Inline-функции.md) > > Сравнить: [Функции в Java](../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Функции.md), [Builder](../../Программирование%20на%20Java/Паттерны%20проектирования/Builder.md) > [!warning] Подводные камни > **`apply` возвращает объект, даже если блок что-то вычислил:** > ```kotlin > fun main() { > val wrong = StringBuilder().apply { append("текст"); length } > println(wrong) // текст — не 5! > val right = StringBuilder().run { append("текст"); length } > println(right) // 5 > } > ``` > Перепутать `apply` и `run` легко, а ошибка типов возникает далеко от места. > > **`let` на nullable без `?.` выполняется всегда:** > ```kotlin > fun main() { > val x: String? = null > x.let { println("выполнилось, it = $it") } // выполнилось, it = null > x?.let { println("не выполнится") } > } > ``` > > **Вложенные `this` и `it` путаются:** > ```kotlin > data class Address2(var city: String) > data class User12(var name: String, val address: Address2) > > fun main() { > val u = User12("Иван", Address2("Москва")) > u.apply { > name = "Пётр" > address.apply { > city = "Казань" > // name = "x" // name внешнего this — компилируется, но запутанно > } > } > println(u) // User12(name=Пётр, address=Address2(city=Казань)) > } > ``` > Внутренний блок видит и свой `this`, и внешний. Компилятор разрешает обращаться к членам обоих, и ошибку заметить трудно. > > **`also` и `apply` не защищают от изменения объекта.** Оба возвращают **тот же** объект, а не копию: > ```kotlin > data class Counter3(var value: Int) > > fun main() { > val c = Counter3(1) > val same = c.also { it.value = 99 } > println("${c.value} ${same === c}") // 99 true > } > ``` > > **`?.let` на цепочке проверяет только последнее звено:** > ```kotlin > data class Inner2(val value: Int?) > > fun main() { > val i: Inner2? = Inner2(null) > println(i?.let { it.value?.plus(1) }) // null > println(i?.value?.plus(1)) // null — короче и яснее > } > ``` > Когда достаточно `?.`, `let` только добавляет шум. Используйте `let`, если внутри нужно больше одного действия. > > **Цепочка из scope-функций читается хуже обычного кода:** > ```kotlin > fun main() { > val bad = "42".let { it.toInt() }.let { it * 2 }.also { println(it) }.let { "$it" } > println(bad) // 84 \n 84 > > val good = ("42".toInt() * 2).toString() > println(good) // 84 > } > ``` > Scope-функции - инструмент, а не стиль. Если без них короче - пишите без них. > > **`run` без получателя путается с `run` на объекте.** `x.run { }` и `run { }` - разные функции с одним именем; при чтении диффа разница в одну точку. > > **Нелокальный `return` внутри scope-функции выходит из внешней функции:** > ```kotlin > fun process5(value: String?): String { > value?.let { > if (it.isBlank()) return "пусто" // выход из process5! > } > return "обработано: $value" > } > > fun main() { > println(process5(" ")) // пусто > println(process5("текст")) // обработано: текст > } > ``` > Это работает благодаря `inline`, но для читателя неочевидно: `return` внутри лямбды выглядит как возврат из лямбды. > > **`takeIf` возвращает nullable - и это заразно:** > ```kotlin > fun main() { > val port: Int? = 8080.takeIf { it > 1024 } > // println(port + 1) // ошибка: Int? не складывается > println(port?.plus(1)) // 8081 > } > ``` > Каждый `takeIf` добавляет в тип `?`, который нужно где-то разрешить. > > **`apply` на `val`-свойствах бесполезен:** > ```kotlin > data class Immutable(val a: Int, val b: Int) > > fun main() { > val x = Immutable(1, 2).apply { > // a = 10 // ошибка: val > } > println(x) // Immutable(a=1, b=2) > println(Immutable(1, 2).copy(a = 10)) // Immutable(a=10, b=2) > } > ``` > Для неизменяемых классов нужен `copy`, а не `apply`. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md) Далее: [Инфиксные и операторные функции](Инфиксные%20и%20операторные%20функции.md)