В стандартной библиотеке Kotlin есть несколько вспомогательных функций, которые позволяют избавиться от громоздких конструкций, одновременно делая код более читабельным. Речь идёт о функциях области видимости - функции, выполняющие блок кода по отношению к конкретному объекту и при этом формирующие временную область видимости. Всего таких функций пять - let
, run
, with
, apply
, и also
.
По сути все пять функций делают одно и тоже и часто могут быть взаимозаменяемыми. Тем не менее важно уловить тонкую нить различий, чтобы правильно применять их в своём коде. Предлагаю сначала рассмотреть отличительные особенности функций, а после подробнее остановится на их описании, а также в каком случае какую из функций использовать.
Отличительные особенности
Отличие функций друг от друга можно выразить двумя пунктами:
- способ ссылки на объект, по отношению к которому функция была вызвана;
- возвращаемое значение.
Способ ссылки на объект
Внутри каждой функции к субъекту вызова можно обратиться по краткой ссылке:
- Как к лямбда-получателю при помощи ключевого слова
this
. При этомthis
можно опустить и обращаться к функциям и свойствам объекта напрямую. Но в тоже время это может понизить читаемость кода, так как станет сложнее различить внутреннее это свойство или внешнее. Поэтому используйте функции области видимости с ключевым словомthis
, когда в коде необходимо обращаться к функциям и свойствам объекта. - Как к лямбда-аргументу при помощи ключевого слова
it
. В данном случае использованиеit
для обращения к объекту является обязательным в том случае, если не задано пользовательское имя. То естьit
можно переименовать во что-то более понятное и читабельное. Следовательно, функции области видимости с ключевым словомit
рекомендуется использовать, когда в коде необходимо обратиться к самому объекту, а не к его свойствам и функциям.
Возвращаемое значение
Каждая из функций может вернуть одно из значений:
- Субъект вызова (объект, по отношению к которому была вызвана функция). При помощи таких функций можно выстроить длинную цепочку вызовов относительного первоначального объекта.
- Результат выполнения лямбды. Такие функции можно использовать для сохранения результата в переменную, либо использовать результат в дальнейшей цепочке вызовов. А можно вовсе игнорировать возможность возврата результата и применять их для создания временной области видимости.
Функции
let
Способ ссылки на объект - it
.
Возвращаемое значение - результат выполнения лямбды (последняя строчка в блоке с кодом).
В библиотеке функция let
выглядит следующим образом:
1
inline fun <T, R> T.let(block: (T) -> R): R
Варианты использования функции:
Выполнение каких-либо операций с результатом цепочки вызовов.
Например, есть код, в котором выполняется цепочка вызовов (map, filter). Результат всего этого записывается в отдельную переменную, после чего она выводится на печать.
1
2
3
val numbers = mutableListOf("one", "two", "three", "four", "five")
val resultList = numbers.map { it.length }.filter { it > 3 }
println(resultList)
С помощью функции let
можно избавиться от создания промежуточной переменной:
1
2
3
4
5
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let {
println(it)
// тут могут быть еще функции
}
Выполнение операций только для non-null значений.
Это самый популярный способ применения
let
. Достигается при совместном использовании функцииlet
и оператора безопасного вызова (?.
).
1
2
3
4
5
6
7
8
9
10
val str: String? = null
// compilation error: значением переменной может быть null
println(str.length)
// ОК: 'it' не может быть null внутри конструкции '?.let { }'
val length = str?.let {
println("Длина строки = ${it.length}")
it.length
}
Таким образом, если значение переменной str
будет null, то функция let
просто не будет выполняться.
Пример можно усложнить, добавив элвис-оператор и цикл forEach
.
1
2
3
4
5
listOf(0, 1, 2, null, 4, null, 6, 7).forEach{
it?.let{
println("Значение элемента = $it")
} ?: println("Значение элемента = null")
}
Изменение имени аргумента лямбды.
Внутри функции мы можем обращаться к объекту при помощи ключевого слова
it
. Чтобы код стал более читабельным, можно определить новую переменную и использовать ее вместоit
.
1
2
3
4
5
val numbers = listOf("one", "two", "three", "four")
val modifiedFirstItem = numbers.first().let { firstItem ->
println("Первый элемент в списке: '$firstItem'")
}.toUpperCase()
println("Первый элемент списка после изменений: '$modifiedFirstItem'")
Как правило, это становится полезным, когда в коде есть несколько вложенных друг в друга функций let
. А так как все они будут использовать ключевое слово it
, то и вам, и компилятору будет сложно в этом во всём разобраться.
run
Способ ссылки на объект - this
. Кроме того может быть вызвана без объекта
Возвращаемое значение - результат выполнения лямбды (последняя строчка в блоке с кодом).
В библиотеке функция run
выглядит следующим образом:
1
2
3
4
5
// Для вызова по отношению к объекту
inline fun <T, R> T.run(block: T.() -> R): R
// Без объекта
inline fun <R> run(block: () -> R): R
Варианты использования:
По отношению к объекту.
run
удобно использовать, когда одновременно нужно инициализировать объект, с помощью него вычислить какое-либо значение и вернуть результат.
1
2
3
4
5
6
7
8
9
10
11
class Person(var name: String, var age: Int)
...
fun main() {
val newAge = Person().run {
println("Старый возраст - $age")
age += 1
}
println("Новый возраст - $newAge")
}
Без объекта.
Многие пишут, что данный вариант на практике встречается редко. Связано это с тем, что функция вызывается в нужном месте и просто выполняет по порядку весь указанный в блоке код. Тем не менее введена она была неспроста и я нашла один из вариантов ее применения.
1
2
3
4
5
6
val info = run {
val name = "Adam"
val age = 30
val address = "Some address"
"Name - $name, age - $age, address - $address"
}
Для инициализации одной переменной иногда нам требуется создать n-ое количество других временных переменных. Чтобы не засорять этими временными переменными код, можно объявить их в области видимости, создаваемой функцией run
.
with
Способ ссылки на объект - this
. Не является функцией-расширением, так как все остальные функции вызываются по отношению к объекту, а функции with
этот объект должен быть явно передан в качестве аргумента.
Возвращаемое значение - результат выполнения лямбды (последняя строчка в блоке с кодом).
В библиотеке функции with
выглядит следующим образом:
1
inline fun <T, R> with(receiver: T, block: T.() -> R): R
Варианты использования:
- Вызов функций без возврата какого-либо значения. Такой код можно прочитать так: с этим объектом нужно сделать следующее.
1
2
3
4
5
6
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
println("Функция 'with' была вызвана с аргументом $this")
println("Список содержит $size элементов")
}
- Ввод временных переменных или функций, которые требуются для инициализации объекта.
1
2
3
4
5
6
7
8
val numbers = mutableListOf("one", "two", "three")
val firstAndLast = with(numbers) {
"Первый элемент списка - ${first()}," +
" последний элемент списка - ${last()}"
}
println(firstAndLast)
Как можно заметить, функция with
делает тоже самое, что и run
. Единственное отличие - ее неудобно использовать при проверке значения на null.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// with - необходимо проверять на null все свойства объекта
val person: Person? = null
with(person) {
this?.name = "Adam"
this?.contactNumber = "1234"
this?.address = "address"
this?.displayInfo()
}
// run - проверяет на null сам объект
val person: Person? = null
person?.run {
name = "Adam"
contactNumber = "1234"
address = "address"
displayInfo()
}
apply
Способ ссылки на объект - this
.
Возвращаемое значение - сам объект (субъект вызова).
В библиотеке функция apply
выглядит следующим образом:
1
inline fun T.apply(block: T.() -> Unit): T
Предназначение apply
- инициализация и настройка объекта. Функция позволяет без повтора имени объекта вызывать его функции, изменять свойства и как результат возвращает объект со всеми указанными настройками.
1
2
3
4
5
6
7
8
9
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)
}
also
Способ ссылки на объект - it
.
Возвращаемое значение - сам объект (субъект вызова).
В библиотеке функция also
выглядит следующим образом:
1
inline fun <T> T.also(block: (T) -> Unit): T
Когда вы видите в коде also
, то это можно прочитать как “а также с объектом нужно сделать следующее.” Ведь благодаря тому, что also
возвращаем сам объект, можно выстроить длинную цепочку вызовов, где каждый вызов добавит новый эффект.
1
2
3
4
5
6
7
8
val name = Person().also {
println("Текущее имя: ${it.name}")
it.name = "ModifiedName"
}.run {
"Имя после изменений: $name"
}
println(name)
Шпаргалка по выбору функции
Функция | Ссылка на объект | Возвращаемое значение | Является функцией-расширением |
---|---|---|---|
let | it | Результат лямбды | Да |
run | this | Результат лямбды | Да |
run | - | Результат лямбды | Нет: может быть вызвана без объекта |
with | this | Результат лямбды | Нет: принимает объект в качестве аргумента |
apply | this | Контекстный объект | Да |
also | it | Контекстный объект | Да |
Если таблицы недостаточно:
- Выполнить блок кода для значения, отличного от null -
let
. - Использовать результат выполнения цепочки вызовов -
let
. - Инициализация и настройка объекта -
apply
. - Настройка объекта и получение результата выполнения операций -
run
. - Выполнение операций, для которых требуется временная область видимости -
run
без субъекта вызова. - Добавление объекту дополнительных значений -
also
. - Группировка всех функций, вызываемых для объекта:
with
.
Для тех, кто лучше воспринимает информацию в картинках и схемах (когда-нибудь перерисую покрасивше):
Вывод
Несмотря на то, что перечисленные функции предназначены для того, чтобы сделать код более кратким, следует избегать их чрезмерного использования: это может снизить читабельность кода и привести к ошибкам.
Избегайте вложенности функций и будьте осторожны при их объединении: можно легко запутаться в текущем значении объекта.
Полезные ссылки
Scope Functions - официальная документация.
Функции области видимости - перевод документации на русский язык.
Разбор функций с примерами.