Функции области видимости
Стандартная библиотека 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
}
Функции области видимости не вводят никаких новых технических возможностей, но они могут сделать ваш код более лаконичным и читабельным.
Из-за схожего характера функций области видимости выбор подходящей для вашего случая может быть немного сложным. Выбор в основном зависит от ваших намерений и согласованности использования в вашем проекте. Ниже мы предоставим подробные описания различий между функциями области видимости и соглашениями по их использованию.
Выбор функции
Чтобы помочь вам выбрать подходящую функцию области видимости для вашей цели, мы предоставляем таблицу ключевых различий между ними.
Функция |
Ссылка на объект |
Возвращаемое значение |
Является ли расширяющей функцией |
|---|---|---|---|
|
|
Результат лямбды |
Да |
|
|
Результат лямбды |
Да |
|
- |
Результат лямбды |
Нет: вызывается без объекта контекста |
|
|
Результат лямбды |
Нет: принимает объект контекста в качестве аргумента. |
|
|
Объект контекста |
Да |
|
|
Объект контекста |
Да |
Подробная информация о различиях приведена в отдельных разделах ниже.
Вот краткое руководство по выбору функций области видимости в зависимости от предполагаемой цели:
Выполнение лямбды на непустых объектах:
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
}
Кроме того, когда вы передаете объект контекста как аргумент, вы можете указать пользовательское имя для объекта контекста внутри области видимости.
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)
}
Они также могут использоваться в операторах return функций, возвращающих объект контекста.
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
}
Если блок кода содержит единственную функцию с 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 для вызова функций на объекте контекста без предоставления результата лямбды. В коде 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 позволяет выполнить блок нескольких операторов, где требуется выражение.
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 в цепочки вызовов для более сложной обработки.
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 возвращает объект, если он не соответствует предикату, и null, если соответствует. Объект доступен в качестве аргумента лямбды (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
}
При объединении других функций после takeIf и takeUnless, не забывайте выполнять проверку на null или безопасный вызов (?.), потому что их возвращаемое значение может быть nullable.
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 особенно полезны в сочетании с функциями области действия. Хороший случай — объединение их с 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
}
Вот как та же функция выглядит без функций стандартной библиотеки:
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