В стандартной библиотеке 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 - официальная документация.
Функции области видимости - перевод документации на русский язык.
Разбор функций с примерами.