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