Kotlin guide done. (20.10.26)

This commit is contained in:
2026-10-20 12:54:25 +03:00
parent da3ad9dd1a
commit 6853ada348
81 changed files with 30112 additions and 0 deletions
@@ -0,0 +1,441 @@
---
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<String, String>()
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<Int> = mutableListOf<Int>().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<Resource>()
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, R> 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)