20 KiB
title, description, tags
| title | description | tags | ||
|---|---|---|---|---|
| Scope-функции | let, run, apply, also, with - различия, выбор и типичные ошибки |
|
Пять функций stdlib, которые выполняют блок кода в контексте объекта. Делают они почти одно и то же, различаясь двумя вещами: как доступен объект (this или it) и что возвращается (объект или результат блока).
| Функция | Объект внутри | Возвращает | Расширение |
|---|---|---|---|
let |
it |
результат блока | да |
run |
this |
результат блока | да |
run (без получателя) |
- | результат блока | нет |
apply |
this |
сам объект | да |
also |
it |
сам объект | да |
with |
this |
результат блока | нет (принимает аргумент) |
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
}
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 и преобразование значения:
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-безопасности
Второе применение - ограничить область видимости промежуточного значения:
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 - удобно, когда обращений к нему много:
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 без получателя - просто блок, возвращающий значение:
fun main() {
val value = run {
val a = 2
val b = 3
a * b
}
println(value) // 6
}
Это способ вычислить val несколькими строками, не заводя функцию. Полезно в инициализации свойств.
apply
Возвращает сам объект - значит, предназначен для настройки:
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-паттерна для классов с изменяемыми свойствами. Подробнее: Паттерны проектирования
Частый случай - настройка при возврате:
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 - для побочных эффектов, не меняющих объект:
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 вставляется в цепочку, ничего в ней не меняя - идеально для логирования и отладки.
Второе применение - валидация и регистрация при создании:
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
Единственная не-расширение: объект передаётся аргументом.
fun main() {
val sb = StringBuilder()
with(sb) {
append("Привет")
append(", ")
append("мир")
append("!")
}
println(sb) // Привет, мир!
}
with(x) { } читается как «с объектом x сделай» и применяется, когда объект уже есть в переменной и цепочка не нужна. Результат - значение блока:
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.
Вложенность и затенение
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
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
Родственные функции - фильтр для одного значения:
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.mapletOptional.ifPresent?.letили?.alsobuilder с return thisapplyпромежуточная переменная let/runObjects.requireNonNullElse(x, y)x ?: yнет аналога alsoв середине цепочкинет аналога withScope-функции заменяют
Optionalи промежуточные переменные. В Java цепочка преобразований с проверками на null выглядит так:String result = Optional.ofNullable(user) .map(User::getAddress) .map(Address::getCity) .orElse("неизвестно");В Kotlin
?.и?:делают то же без обёртки, аletподключается, когда нужен не просто вызов метода: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работает с любым классом, включая чужой: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-функцииСравнить: Функции в Java, Builder
[!warning] Подводные камни
applyвозвращает объект, даже если блок что-то вычислил:fun main() { val wrong = StringBuilder().apply { append("текст"); length } println(wrong) // текст — не 5! val right = StringBuilder().run { append("текст"); length } println(right) // 5 }Перепутать
applyиrunлегко, а ошибка типов возникает далеко от места.
letна nullable без?.выполняется всегда:fun main() { val x: String? = null x.let { println("выполнилось, it = $it") } // выполнилось, it = null x?.let { println("не выполнится") } }Вложенные
thisиitпутаются: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не защищают от изменения объекта. Оба возвращают тот же объект, а не копию: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на цепочке проверяет только последнее звено: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-функций читается хуже обычного кода:
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-функции выходит из внешней функции:fun process5(value: String?): String { value?.let { if (it.isBlank()) return "пусто" // выход из process5! } return "обработано: $value" } fun main() { println(process5(" ")) // пусто println(process5("текст")) // обработано: текст }Это работает благодаря
inline, но для читателя неочевидно:returnвнутри лямбды выглядит как возврат из лямбды.
takeIfвозвращает nullable - и это заразно:fun main() { val port: Int? = 8080.takeIf { it > 1024 } // println(port + 1) // ошибка: Int? не складывается println(port?.plus(1)) // 8081 }Каждый
takeIfдобавляет в тип?, который нужно где-то разрешить.
applyнаval-свойствах бесполезен: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
Далее: Инфиксные и операторные функции