Files
obsidian/Kotlin/Функциональный Kotlin/Scope-функции.md
T

442 lines
20 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: 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)