Функции области видимости
Стандартная библиотека Kotlin содержит несколько функций, единственной целью которых является выполнение блока кода в контексте объекта. Эти функции формируют временную область видимости для объекта, к которому были применены, и вызывают код, указанный в переданном лямбда-выражении. В этой области видимости можно получить доступ к объекту без явного к нему обращения по имени. Такие функции называются функциями области видимости (англ. scope functions). Всего их пять: 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() {
Person("Alice", 20, "Amsterdam").let {
println(it)
it.moveTo("London")
it.incrementAge()
println(it)
}
}
Если вы захотите написать то же самое без функции let
, то вам придется объявить новую переменную и обращаться к ней всякий раз, когда она используется.
data class Person(var name: String, var age: Int, var city: String) {
fun moveTo(newCity: String) { city = newCity }
fun incrementAge() { age++ }
}
fun main() {
val alice = Person("Alice", 20, "Amsterdam")
println(alice)
alice.moveTo("London")
alice.incrementAge()
println(alice)
}
Функции области видимости не предоставляют никаких новых технических возможностей, но они могут сделать ваш код более кратким и читабельным.
Из-за того, что все функции области видимости имеют схожий характер, выбрать правильную для вашего конкретного случая может быть затруднительно. В основном выбор зависит от ваших намерений и от структуры вашего проекта. Ниже приведено подробное описание того, чем функции области видимости отличаются между собой, а также соглашение об их использовании.
Отличительные особенности
Поскольку функции области видимости очень похожи друг на друга, важно понимать чем они различаются. Между ними есть два основных различия: * Способ ссылки на контекстный объект * Возвращаемое значение.
Контекстный объект: this
или it
Внутри лямбда-выражения, которое передается функции области видимости, объект контекста доступен по краткой ссылке, а не по его фактическому имени. Каждая функция области видимости использует один из двух способов доступа к объекту контекста: как лямбда-получатель (this
) или как лямбда-аргумент (it
). Оба предоставляют одинаковые возможности, поэтому опишем плюсы и минусы каждого для разных случаев и дадим рекомендации по их использованию.
fun main() {
val str = "Hello"
// this
str.run {
println("Длина строки: $length")
//println("Длина строки: ${this.length}") // делает то же самое
}
// it
str.let {
println("Длина строки: ${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() {
val adam = Person("Adam").apply {
age = 20 // то же самое, что и this.age = 20 или adam.age = 20
city = "London"
}
println(adam)
}
it
В свою очередь, let
и also
передают контекстный объект как аргумент в лямбду. Если имя аргумента не указано, то к объекту обращаются неявным способом при помощи ключевого слова it
. it
короче this
и выражения с it
более читабельны. Однако при вызове функций или свойств объекта у вас не будет доступа к такому неявному объекту как this
. Следовательно, использовать контекстный объект it
лучше, когда объект в основном используется в качестве аргумента для вызова функций. it
также лучше, если вы используете несколько переменных в блоке кода.
import kotlin.random.Random
fun writeToLog(message: String) {
println("INFO: $message")
}
fun main() {
fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("Метод getRandomInt() сгенерировал значение $it")
}
}
val i = getRandomInt()
}
Кроме того, когда вы передаете объект в качестве аргумента, вы можете указать пользовательское имя для этого объекта внутри области видимости.
import kotlin.random.Random
fun writeToLog(message: String) {
println("INFO: $message")
}
fun main() {
fun getRandomInt(): Int {
return Random.nextInt(100).also { value ->
writeToLog("Метод getRandomInt() сгенерировал значение $value")
}
}
val i = getRandomInt()
}
Возвращаемое значение
Функции области видимости также отличаются по значению, которое они возвращают:
* apply
и also
возвращают объект контекста.
* let
, run
и with
возвращают результат лямбды.
Это отличие позволит выбрать правильную функцию в зависимости от того, что вы будете делать дальше в своем коде.
Контекстный объект
Функции apply
и also
возвращают объект контекста. Следовательно, с их помощью можно вызвать длинную цепочку функций относительно оригинального контекстного объекта. Такая цепочка функций известна как side steps.
fun main() {
val numberList = mutableListOf<Double>()
numberList.also { println("Заполнение списка") }
.apply {
add(2.71)
add(3.14)
add(1.0)
}
.also { println("Сортировка списка") }
.sort()
println(numberList)
}
Они также могут быть использованы совместно с ключевым словом return
.
import kotlin.random.Random
fun writeToLog(message: String) {
println("Информация: $message")
}
fun main() {
fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("Метод getRandomInt() сгенерировал значение $it")
}
}
val i = getRandomInt()
}
Результат лямбды
let
, run
и with
возвращают результат лямбды. Таким образом, вы можете использовать их при присваивании переменной результата вычислений, либо использовать результат для последующего вызова цепочки операций и тд.
fun main() {
val numbers = mutableListOf("one", "two", "three")
val countEndsWithE = numbers.run {
add("four")
add("five")
count { it.endsWith("e") }
}
println("Элементы в $countEndsWithE, которые заканчиваются на e.")
}
Кроме того, вы можете игнорировать возвращаемое значение и использовать функцию для создания временной области видимости для переменной.
fun main() {
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
val firstItem = first()
val lastItem = last()
println("Первый элемент: $firstItem, последний элемент: $lastItem")
}
}
Функции
Чтобы помочь вам выбрать правильную функцию области видимости, ниже будет представлено их подробное описание и рекомендации по использованию. Во многих случаях они взаимозаменяемы, поэтому в примерах будет отражен общий стиль использования, а также соглашение по их применению.
let
Контекстный объект доступен в качестве аргумента (it
). Возвращаемое значение - результат выполнения лямбды.
Если значение переменной вычислялось при помощи цепочки операций, то let
позволяет использовать полученный результат для вызова одной или нескольких функций в блоке кода. Например, в следующем коде выполняется цепочка из двух операций, результат записывается в отдельную переменную, после чего она выводится на печать.
fun main() {
val numbers = mutableListOf("one", "two", "three", "four", "five")
val resultList = numbers.map { it.length }.filter { it > 3 }
println(resultList)
}
С функцией let
этот код может быть переписан следующим образом:
fun main() {
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let {
println(it)
// при необходимости можно вызвать больше функций
}
}
Если блок кода содержит одну функцию, где it
является аргументом, то лямбда-выражение может быть заменено ссылкой на метод (::
):
fun main() {
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let(::println)
}
let
часто используется для выполнения блока кода только с non-null значениями. Чтобы выполнить действия с non-null объектом, используйте оператор безопасного вызова ?.
совместно с функцией let
.
fun processNonNullString(str: String) {}
fun main() {
val str: String? = "Hello"
//processNonNullString(str) // compilation error: str может быть null
val length = str?.let {
println("Вызов функции let() для $it")
processNonNullString(it) // OK: 'it' не может быть null внутри конструкции '?.let { }'
it.length
}
}
Еще один вариант использования let
- это введение локальных переменных с ограниченной областью видимости для улучшения читабельности кода. Чтобы определить новую переменную для контекстного объекта, укажите ее имя в качестве аргумента лямбды, чтобы ее можно было использовать вместо ключевого слова it
.
fun main() {
val numbers = listOf("one", "two", "three", "four")
val modifiedFirstItem = numbers.first().let { firstItem ->
println("Первый элемент в списке: '$firstItem'")
if (firstItem.length >= 5) firstItem else "!" + firstItem + "!"
}.toUpperCase()
println("Первый элемент списка после изменений: '$modifiedFirstItem'")
}
with
Не является функцией-расширением. Контекстный объект передается в качестве аргумента, а внутри лямбда-выражения он доступен как получатель (this
). Возвращаемое значение - результат выполнения лямбды.
Функцию with
рекомендуется использовать для вызова функций контекстного объекта без предоставления результата лямбды. В коде with
может читаться как” с этим объектом, сделайте следующее. “
fun main() {
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
println("'with' вызывает с аргументом $this")
println("Список содержит $size элементов")
}
}
Другой вариант использования with
- введение вспомогательного объекта, свойства или функции которые будут использоваться для вычисления значения.
fun main() {
val numbers = mutableListOf("one", "two", "three")
val firstAndLast = with(numbers) {
"Первый элемент списка - ${first()}," +
" последний элемент списка - ${last()}"
}
println(firstAndLast)
}
run
Контекстный объект доступен в качестве получателя (this
). Возвращаемое значение - результат выполнения лямбды.
run
делает то же самое, что и with
, но вызывается как let
- как функция расширения контекстного объекта.
run
удобен, когда лямбда содержит и инициализацию объекта, и вычисление возвращаемого значения.
class MultiportService(var url: String, var port: Int) {
fun prepareRequest(): String = "Запрос по умолчанию"
fun query(request: String): String = "Результат запроса: '$request'"
}
fun main() {
val service = MultiportService("https://example.kotlinlang.org", 80)
val result = service.run {
port = 8080
query(prepareRequest() + " порт - $port")
}
// аналогичный код с использованием функции let():
val letResult = service.let {
it.port = 8080
it.query(it.prepareRequest() + " порт - ${it.port}")
}
println(result)
println(letResult)
}
Помимо вызова run
для объекта-получателя, вы можете использовать его как функцию без расширения. В этом случае run
позволяет выполнить блок из нескольких операторов там, где это требуется.
fun main() {
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)
}
}
apply
Контекстный объект доступен в качестве получателя (this
). Возвращаемое значение - контекстный объект.
Используйте apply
для такого блока кода, который не возвращает значение и в основном работает с членами объекта-получателя. Типичный способ использования функции apply
- настройка объекта-получателя. Это всеравно что мы скажем “примени перечисленные настройки к объекту.”
data class Person(var name: String, var age: Int = 0, var city: String = "")
fun main() {
val adam = Person("Adam").apply {
age = 32
city = "London"
}
println(adam)
}
Так как возвращаемое значение - это сам объект, то можно с легкостью включить apply
в цепочки вызовов для более сложной обработки.
also
Контекстный объект доступен в качестве аргумента (it
). Возвращаемое значение - контекстный объект.
also
хорош для выполнения таких действий, которые принимают контекстный объект в качестве аргумента. То есть, эту функции следует использовать, когда требуется ссылка именно на объект, а не на его свойства и функции. Либо, когда вы хотите, чтобы была доступна ссылка на this
из внешней области видимости.
Когда вы видите в коде also
, то это можно прочитать как “а также с объектом нужно сделать следующее.”
fun main() {
val numbers = mutableListOf("one", "two", "three")
numbers
.also { println("Элементы списка перед добавлением нового: $it") }
.add("four")
}
Выбор функции
В таблице ниже приведены ключевые различия между функциями области видимости, что должно помочь вам сделать правильный выбор в пользу той или иной функции.
Функция | Обращение к объекту | Возвращаемое значение | Является функцией-расширением |
---|---|---|---|
let |
it |
Результат лямбды | Да |
run |
this |
Результат лямбды | Да |
run |
- | Результат лямбды | Нет: может быть вызвана без контекстного объекта |
with |
this |
Результат лямбды | Нет: принимает контекстный объект в качестве аргумента. |
apply |
this |
Контекстный объект | Да |
also |
it |
Контекстный объект | Да |
Краткое руководство по выбору функции области видимости в зависимости от предполагаемого назначения:
- Выполнение лямбды для non-null объектов:
let
- Представление переменной в виде выражения со своей локальной областью видимости:
let
- Настройка объекта:
apply
- Настройка объекта и вычисление результата:
run
- Выполнение операций, для которых требуется выражение:
run
без расширения - Применение дополнительных значений:
also
- Группировка всех функций, вызываемых для объекта:
with
Некоторые функции области видимости являются взаимозаменяемыми, поэтому вы можете выбирать функции исходя из соглашений, принятых в вашем проекте или команде.
Несмотря на то, что функции области видимости предназначены для того, чтобы сделать код более кратким, избегайте их чрезмерного использования: это может снизить читабельность кода и привести к ошибкам. Избегайте вложенности функций и будьте осторожны при их объединении: можно легко запутаться в текущем значении контекстного объекта и в значениях this
или it
.
takeIf
и takeUnless
Помимо функций области видимости, стандартная библиотека содержит функции takeIf
и takeUnless
. Эти функции позволяют встроить проверку состояния объекта в цепочке вызовов.
При вызове takeIf
для объекта с предикатом этот объект будет возвращен, если он соответствует предикату. В противном случае возвращается null
. В свою очередь, takeUnless
возвращает объект, если он не соответствует предикату, и null
, если соответствует. Объект доступен как лямбда-аргумент (it
).
import kotlin.random.*
fun main() {
val number = Random.nextInt(100)
val evenOrNull = number.takeIf { it % 2 == 0 }
val oddOrNull = number.takeUnless { it % 2 == 0 }
println("четный: $evenOrNull, нечетный: $oddOrNull")
}
При добавлении в цепочку вызовов других функций после takeIf
и takeUnless
, не забудьте выполнить проверку на null или используйте оператор безопасного вызова (?.
), потому что их возвращаемое значение имеет тип nullable.
fun main() {
val str = "Hello"
val caps = str.takeIf { it.isNotEmpty() }?.toUpperCase()
//val caps = str.takeIf { it.isNotEmpty() }.toUpperCase() //compilation error
println(caps)
}
takeIf
и takeUnless
особенно полезны при совместном использовании с функциями области видимости. Хорошим примером является объединение их в цепочку с let
для выполнения блока кода для объектов, которые соответствуют заданному предикату. Для этого вызовите takeIf
для объекта, а затем вызовите let
с оператором безопасного вызова (?
). Для объектов, которые не соответствуют предикату, takeIf
возвращает null
, а let
не вызывается.
fun main() {
fun displaySubstringPosition(input: String, sub: String) {
input.indexOf(sub).takeIf { it >= 0 }?.let {
println("Подстрока $sub находится в $input.")
println("Начинается с индекса $it.")
}
}
displaySubstringPosition("010000011", "11")
displaySubstringPosition("010000011", "12")
}
Тот же самый код, но без использования функций из стандартной библиотеки, выглядит следующим образом:
fun main() {
fun displaySubstringPosition(input: String, sub: String) {
val index = input.indexOf(sub)
if (index >= 0) {
println("Подстрок $sub находится в $input.")
println("Начинается с индекса $index.")
}
}
displaySubstringPosition("010000011", "11")
displaySubstringPosition("010000011", "12")
}