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