Функции области видимости
Стандартная библиотека Kotlin содержит несколько функций, единственной целью которых является выполнение блока кода в контексте объекта. Когда вы вызываете такую функцию на объекте с предоставленным лямбда-выражением, она формирует временную область видимости. В этой области видимости вы можете получить доступ к объекту без его имени. Такие функции называются функциями области видимости. Их пять: let, run, with, apply и also.
В основе этих функций лежит одно действие: выполнение блока кода на объекте. Разница заключается в том, как этот объект становится доступным внутри блока и в том, какой результат возвращает всё выражение.
Вот типичный пример использования функции области видимости:
data class Person(var name: String, var age: Int, var city: String) {
fun moveTo(newCity: String) { city = newCity }
fun incrementAge() { age++ }
}
fun main() {
//sampleStart
Person("Alice", 20, "Amsterdam").let {
println(it)
it.moveTo("London")
it.incrementAge()
println(it)
}
//sampleEnd
}
Если написать то же самое без let, вам придётся вводить новую переменную и повторять её имя всякий раз, когда вы её используете.
data class Person(var name: String, var age: Int, var city: String) {
fun moveTo(newCity: String) { city = newCity }
fun incrementAge() { age++ }
}
fun main() {
//sampleStart
val alice = Person("Alice", 20, "Amsterdam")
println(alice)
alice.moveTo("London")
alice.incrementAge()
println(alice)
//sampleEnd
}
Функции области видимости не вводят никаких новых технических возможностей, но они могут сделать ваш код более лаконичным и читаемым.
Из-за большого сходства функций области видимости выбор нужной функции для вашего случая может быть сложным. Выбор в основном зависит от ваших намерений и согласованности использования в вашем проекте. Ниже приведены подробные описания различий между функциями области видимости и их соглашений.
Выбор функции
Чтобы помочь вам выбрать подходящую функцию области видимости для ваших целей, мы предоставляем эту таблицу, в которой обобщены ключевые различия между ними.
Функция |
Ссылка на объект |
Значение возврата |
Является ли функцией расширения |
|---|---|---|---|
|
Результат лямбды |
Да |
|
|
Результат лямбды |
Да |
|
- |
Результат лямбды |
Нет: вызывается без объекта контекста |
|
|
Результат лямбды |
Нет: принимает объект контекста в качестве аргумента. |
|
|
Объект контекста |
Да |
|
|
Объект контекста |
Да |
Подробная информация об этих функциях приведена в соответствующих разделах ниже.
Вот краткое руководство по выбору функций области видимости в зависимости от предполагаемого назначения:
Выполнение лямбды на не-null объектах:
letВвод выражения в качестве переменной в локальной области видимости:
letКонфигурация объекта:
applyКонфигурация объекта и вычисление результата:
runВыполнение операторов, где требуется выражение: не функция расширения
runДополнительные эффекты:
alsoГруппирование вызовов функций на объекте:
with
Случаи использования различных функций области видимости перекрываются, поэтому вы можете выбрать, какие функции использовать, в зависимости от конкретных соглашений, используемых в вашем проекте или команде.
Хотя функции области видимости могут сделать ваш код более лаконичным, избегайте их чрезмерного использования: это может затруднить чтение кода и привести к ошибкам. Мы также рекомендуем избегать вложенных функций области видимости и быть осторожными при их объединении, так как легко запутаться в текущем объекте контекста и значении this или it.
Отличия
Поскольку функции области видимости похожи по своей природе, важно понимать их различия. Два основных отличия каждой функции области видимости:
Способы обращения к объекту контекста.
Их возвращаемое значение.
Объект контекста: this или it
Внутри лямбды, переданной функции области видимости, объект контекста доступен с помощью короткой ссылки вместо его фактического имени. Каждая функция области видимости использует один из двух способов ссылки на объект контекста: как получатель лямбды (this) или как аргумент лямбды (it). Оба предоставляют одинаковые возможности, поэтому мы описываем преимущества и недостатки каждого из них для различных случаев использования и даём рекомендации по их использованию.
fun main() {
val str = "Hello"
// this
str.run {
println("The string's length: $length")
//println("The string's length: ${this.length}") // does the same
}
// it
str.let {
println("The string's length is ${it.length}")
}
}
this
run, with, и apply ссылаются на объект контекста как на получатель лямбды - с помощью ключевого слова this. Таким образом, в их лямбдах объект доступен так же, как и в обычных функциях класса.
В большинстве случаев вы можете опустить this при обращении к членам объекта получателя, что сократит код. С другой стороны, если this опущено, может быть трудно отличить члены получателя от внешних объектов или функций. Поэтому рекомендуется использовать объект контекста в качестве получателя (this) для лямбд, которые в основном работают с членами объекта, вызывая его функции или присваивая значения свойствам.
data class Person(var name: String, var age: Int = 0, var city: String = "")
fun main() {
//sampleStart
val adam = Person("Adam").apply {
age = 20 // same as this.age = 20
city = "London"
}
println(adam)
//sampleEnd
}
it
В свою очередь, let и also ссылаются на объект контекста как на аргумент лямбды. Если имя аргумента не указано, к объекту можно обратиться по неявным имени it. it короче, чем this, и выражения с it обычно легче читаются.
Однако при вызове функций или свойств объекта у вас нет неявного доступа к объекту, как у this. Поэтому использование объекта контекста через it предпочтительнее, когда объект в основном используется в качестве аргумента в вызовах функций. it также лучше, если вы используете несколько переменных в блоке кода.
import kotlin.random.Random
fun writeToLog(message: String) {
println("INFO: $message")
}
fun main() {
//sampleStart
fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")
}
}
val i = getRandomInt()
println(i)
//sampleEnd
}
В примере ниже показано обращение к объекту контекста как к аргументу лямбды с указанным именем аргумента: value.
import kotlin.random.Random
fun writeToLog(message: String) {
println("INFO: $message")
}
fun main() {
//sampleStart
fun getRandomInt(): Int {
return Random.nextInt(100).also { value ->
writeToLog("getRandomInt() generated value $value")
}
}
val i = getRandomInt()
println(i)
//sampleEnd
}
Возвращаемое значение
Функции области видимости отличаются по возвращаемому значению:
applyиalsoвозвращают объект контекста.let,run, иwithвозвращают результат лямбды.
Вы должны тщательно продумать, какое возвращаемое значение вам нужно, исходя из того, что вы хотите сделать дальше в своём коде. Это поможет вам выбрать лучшую функцию области видимости.
Объект контекста
Возвращаемое значение apply и also — это сам объект контекста. Таким образом, они могут быть включены в цепочки вызовов в качестве побочных шагов: вы можете продолжить цепочку вызовов функций на том же объекте один за другим.
fun main() {
//sampleStart
val numberList = mutableListOf<Double>()
numberList.also { println("Populating the list") }
.apply {
add(2.71)
add(3.14)
add(1.0)
}
.also { println("Sorting the list") }
.sort()
//sampleEnd
println(numberList)
}
Их также можно использовать в операторах возврата функций, возвращающих объект контекста.
import kotlin.random.Random
fun writeToLog(message: String) {
println("INFO: $message")
}
fun main() {
//sampleStart
fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")
}
}
val i = getRandomInt()
//sampleEnd
}
Результат лямбды
let, run, и with возвращают результат лямбды. Таким образом, вы можете использовать их для присвоения результата переменной, объединения операций над результатом и так далее.
fun main() {
//sampleStart
val numbers = mutableListOf("one", "two", "three")
val countEndsWithE = numbers.run {
add("four")
add("five")
count { it.endsWith("e") }
}
println("There are $countEndsWithE elements that end with e.")
//sampleEnd
}
Кроме того, вы можете проигнорировать возвращаемое значение и использовать функцию области видимости для создания временной области видимости для локальных переменных.
fun main() {
//sampleStart
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
val firstItem = first()
val lastItem = last()
println("First item: $firstItem, last item: $lastItem")
}
//sampleEnd
}
Функции
Чтобы помочь вам выбрать подходящую функцию области действия для вашего случая использования, мы подробно описываем их и даём рекомендации по использованию. Технически, функции области действия во многих случаях взаимозаменяемы, поэтому примеры показывают соглашения об их использовании.
let
Объект контекста доступен в качестве аргумента (
it).Значение возврата — результат лямбды.
let можно использовать для вызова одной или нескольких функций по результатам цепочки вызовов. Например, следующий код выводит результаты двух операций над коллекцией:
fun main() {
//sampleStart
val numbers = mutableListOf("one", "two", "three", "four", "five")
val resultList = numbers.map { it.length }.filter { it > 3 }
println(resultList)
//sampleEnd
}
С помощью let, вы можете переписать приведенный выше пример так, чтобы результат операций над списком не присваивался переменной:
fun main() {
//sampleStart
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let {
println(it)
// and more function calls if needed
}
//sampleEnd
}
Если блок кода, переданный в let, содержит единственную функцию с it в качестве аргумента, вы можете использовать ссылку на метод (::) вместо аргумента лямбды:
fun main() {
//sampleStart
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let(::println)
//sampleEnd
}
let часто используется для выполнения блока кода, содержащего значения, отличные от null. Чтобы выполнить действия над объектом, отличным от null, используйте оператор безопасного вызова ?. и вызовите let с действиями в своей лямбде.
fun processNonNullString(str: String) {}
fun main() {
//sampleStart
val str: String? = "Hello"
//processNonNullString(str) // compilation error: str can be null
val length = str?.let {
println("let() called on $it")
processNonNullString(it) // OK: 'it' is not null inside '?.let { }'
it.length
}
//sampleEnd
}
Вы также можете использовать let для введения локальных переменных с ограниченной областью видимости, чтобы сделать ваш код более читабельным. Для определения новой переменной для объекта контекста укажите её имя в качестве аргумента лямбды, чтобы использовать его вместо значения по умолчанию it.
fun main() {
//sampleStart
val numbers = listOf("one", "two", "three", "four")
val modifiedFirstItem = numbers.first().let { firstItem ->
println("The first item of the list is '$firstItem'")
if (firstItem.length >= 5) firstItem else "!" + firstItem + "!"
}.uppercase()
println("First item after modifications: '$modifiedFirstItem'")
//sampleEnd
}
with
Объект контекста доступен в качестве получателя (
this).Значение возврата — результат лямбды.
Так как with не является функцией расширения: объект контекста передаётся в качестве аргумента, но внутри лямбды он доступен как получатель (this).
Рекомендуется использовать with для вызова функций объекта контекста, когда вам не нужно использовать возвращаемое значение. В коде with можно прочитать как «с этим объектом, выполните следующее.»
fun main() {
//sampleStart
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
println("'with' is called with argument $this")
println("It contains $size elements")
}
//sampleEnd
}
Вы также можете использовать with для введения вспомогательного объекта, свойства или функции которого используются для вычисления значения.
fun main() {
//sampleStart
val numbers = mutableListOf("one", "two", "three")
val firstAndLast = with(numbers) {
"The first element is ${first()}," +
" the last element is ${last()}"
}
println(firstAndLast)
//sampleEnd
}
run
Объект контекста доступен в качестве получателя (
this).Значение возврата — результат лямбды.
run делает то же, что и with, но реализовано как функция расширения. Таким образом, как и let, вы можете вызвать её для объекта контекста с помощью обозначения точки.
run полезно, когда ваша лямбда одновременно инициализирует объекты и вычисляет возвращаемое значение.
class MultiportService(var url: String, var port: Int) {
fun prepareRequest(): String = "Default request"
fun query(request: String): String = "Result for query '$request'"
}
fun main() {
//sampleStart
val service = MultiportService("https://example.kotlinlang.org", 80)
val result = service.run {
port = 8080
query(prepareRequest() + " to port $port")
}
// the same code written with let() function:
val letResult = service.let {
it.port = 8080
it.query(it.prepareRequest() + " to port ${it.port}")
}
//sampleEnd
println(result)
println(letResult)
}
Вы также можете вызвать run как функцию без расширения. Вариант функции без расширения run не имеет объекта контекста, но всё равно возвращает результат лямбды. Функция без расширения run позволяет выполнить блок нескольких инструкций, где требуется выражение.
fun main() {
//sampleStart
val hexNumberRegex = run {
val digits = "0-9"
val hexDigits = "A-Fa-f"
val sign = "+-"
Regex("[$sign]?[$digits$hexDigits]+")
}
for (match in hexNumberRegex.findAll("+123 -FFFF !%*& 88 XYZ")) {
println(match.value)
}
//sampleEnd
}
apply
Объект контекста доступен в качестве получателя (
this).Значение возврата — сам объект.
Так как apply возвращает сам объект контекста, мы рекомендуем использовать его для блоков кода, которые не возвращают значение и в основном работают с членами объекта-получателя. Наиболее распространённый случай использования apply — конфигурация объектов. Такие вызовы можно прочитать как «примените следующие присваивания к объекту.»
data class Person(var name: String, var age: Int = 0, var city: String = "")
fun main() {
//sampleStart
val adam = Person("Adam").apply {
age = 32
city = "London"
}
println(adam)
//sampleEnd
}
Ещё один случай использования apply — включение apply в несколько цепочек вызовов для более сложной обработки.
also
Объект контекста доступен в качестве аргумента (
it).Значение возврата — сам объект.
also полезна для выполнения некоторых действий, принимающих объект контекста в качестве аргумента. Используйте also для действий, которым нужна ссылка на объект, а не его свойства и функции, или когда вы не хотите затенять ссылку this из внешней области видимости.
Когда вы видите also в коде, можно прочитать его как «и также выполните следующее с объектом.»
fun main() {
//sampleStart
val numbers = mutableListOf("one", "two", "three")
numbers
.also { println("The list elements before adding new one: $it") }
.add("four")
//sampleEnd
}
takeIf и takeUnless
Помимо функций области действия, стандартная библиотека содержит функции takeIf и takeUnless. Эти функции позволяют встраивать проверки состояния объекта в цепочки вызовов.
При вызове над объектом вместе с предикатом, takeIf возвращает этот объект, если он удовлетворяет данному предикату. В противном случае он возвращает null. Таким образом, takeIf — это функция фильтрации для одного объекта.
takeUnless имеет обратную логику takeIf. При вызове над объектом вместе с предикатом, takeUnless возвращает null если он удовлетворяет данному предикату. В противном случае он возвращает объект.
При использовании takeIf или takeUnless, объект доступен в качестве аргумента лямбды (it).
import kotlin.random.*
fun main() {
//sampleStart
val number = Random.nextInt(100)
val evenOrNull = number.takeIf { it % 2 == 0 }
val oddOrNull = number.takeUnless { it % 2 == 0 }
println("even: $evenOrNull, odd: $oddOrNull")
//sampleEnd
}
fun main() {
//sampleStart
val str = "Hello"
val caps = str.takeIf { it.isNotEmpty() }?.uppercase()
//val caps = str.takeIf { it.isNotEmpty() }.uppercase() //compilation error
println(caps)
//sampleEnd
}
takeIf и takeUnless особенно полезны в сочетании с функциями области действия. Например, вы можете объединить takeIf и takeUnless с let для выполнения блока кода над объектами, которые соответствуют заданному предикату. Для этого вызовите takeIf на объекте, а затем вызовите let с безопасным вызовом (?). Для объектов, которые не соответствуют предикату, takeIf возвращает null, и let не вызывается.
fun main() {
//sampleStart
fun displaySubstringPosition(input: String, sub: String) {
input.indexOf(sub).takeIf { it >= 0 }?.let {
println("The substring $sub is found in $input.")
println("Its start position is $it.")
}
}
displaySubstringPosition("010000011", "11")
displaySubstringPosition("010000011", "12")
//sampleEnd
}
Для сравнения, ниже приведён пример того, как ту же функцию можно написать без использования takeIf или функций области действия:
fun main() {
//sampleStart
fun displaySubstringPosition(input: String, sub: String) {
val index = input.indexOf(sub)
if (index >= 0) {
println("The substring $sub is found in $input.")
println("Its start position is $index.")
}
}
displaySubstringPosition("010000011", "11")
displaySubstringPosition("010000011", "12")
//sampleEnd
}
© 2010–2023 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/scope-functions.html