Функции области видимости
Стандартная библиотека 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
}
Функции области видимости не предоставляют никаких новых технических возможностей, но они могут сделать ваш код более лаконичным и читабельным.
Из-за схожего характера функций области видимости выбор подходящей для вашего случая может быть немного сложным. Выбор в основном зависит от ваших намерений и согласованности использования в вашем проекте. Ниже мы предоставим подробное описание различий между функциями области видимости и правилами их использования.
Отличия
Поскольку функции области видимости все довольно похожи по своей природе, важно понять различия между ними. Есть два основных различия между каждой функцией области видимости:
- Способ ссылки на контекстный объект
- Значение возврата.
Контекстный объект: this или it
Внутри лямбды функции области видимости контекстный объект доступен с помощью короткой ссылки вместо его фактического имени. Каждая функция области видимости использует один из двух способов доступа к контекстному объекту: как лямбда-получатель (this) или как аргумент лямбды (it). Оба предоставляют одинаковые возможности, поэтому мы опишем преимущества и недостатки каждого для разных случаев и дадим рекомендации по их использованию.
fun main() {
val str = "Hello"
// this
str.run {
println("The receiver string length: $length")
//println("The receiver string length: ${this.length}") // does the same
}
// it
str.let {
println("The receiver 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 or adam.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()
//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()
//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
}
Если блок кода содержит одну функцию с it в качестве аргумента, вы можете использовать ссылку на метод (::) вместо лямбды:
fun main() {
//sampleStart
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let(::println)
//sampleEnd
}
let часто используется для выполнения блока кода только с ненулевыми значениями. Чтобы выполнить действия над ненулевым объектом, используйте оператор безопасного вызова ?. на нём и вызовите 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 + "!"
}.toUpperCase()
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("+1234 -FFFF not-a-number")) {
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
}
Выбор функции
Чтобы помочь вам выбрать подходящую функцию области действия для вашей цели, мы предоставляем таблицу ключевых отличий между ними.
| Функция | Ссылка на объект | Значение возврата | Функция расширения |
|---|---|---|---|
let | it | Результат лямбда-выражения | Да |
run | this | Результат лямбда-выражения | Да |
run | - | Результат лямбда-выражения | Нет: вызывается без объекта контекста |
with | this | Результат лямбда-выражения | Нет: принимает объект контекста в качестве аргумента. |
apply | this | Объект контекста | Да |
also | it | Объект контекста | Да |
Вот краткое руководство по выбору функций области видимости в зависимости от предполагаемого назначения:
- Выполнение лямбда-выражения на непустых объектах:
let - Введение выражения как переменной в локальной области видимости:
let - Настройка объекта:
apply - Настройка объекта и вычисление результата:
run - Выполнение операторов, где требуется выражение: не функция расширения
run - Дополнительные эффекты:
also - Группировка вызовов функций на объекте:
with
Случаи использования разных функций перекрываются, поэтому вы можете выбирать функции на основе конкретных соглашений, используемых в вашем проекте или команде.
Хотя функции области видимости позволяют сделать код более лаконичным, избегайте их чрезмерного использования: это может снизить читаемость вашего кода и привести к ошибкам. Избегайте вложенных функций области видимости и будьте осторожны при их объединении: легко запутаться в текущем объекте контекста и значении this или it.
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 или безопасный вызов (?. ), так как их значение возврата может быть null.
fun main() {
//sampleStart
val str = "Hello"
val caps = str.takeIf { it.isNotEmpty() }?.toUpperCase()
//val caps = str.takeIf { it.isNotEmpty() }.toUpperCase() //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–2020 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/reference/scope-functions.html