Kotlin run let apply also with

Kotlin. Функции области видимости (Scope Functions)

В стандартной библиотеке Kotlin есть несколько вспомогательных функций, которые позволяют избавиться от громоздких конструкций, одновременно делая код более читабельным. Речь идёт о функциях области видимости — функции, выполняющие блок кода по отношению к конкретному объекту и при этом формирующие временную область видимости. Всего таких функций пять — let , run , with , apply , и also .

По сути все пять функций делают одно и тоже и часто могут быть взаимозаменяемыми. Тем не менее важно уловить тонкую нить различий, чтобы правильно применять их в своём коде. Предлагаю сначала рассмотреть отличительные особенности функций, а после подробнее остановится на их описании, а также в каком случае какую из функций использовать.

Отличительные особенности

Отличие функций друг от друга можно выразить двумя пунктами:

  • способ ссылки на объект, по отношению к которому функция была вызвана;
  • возвращаемое значение.

Способ ссылки на объект

Внутри каждой функции к субъекту вызова можно обратиться по краткой ссылке:

  • Как к лямбда-получателю при помощи ключевого слова this . При этом this можно опустить и обращаться к функциям и свойствам объекта напрямую. Но в тоже время это может понизить читаемость кода, так как станет сложнее различить внутреннее это свойство или внешнее. Поэтому используйте функции области видимости с ключевым словом this , когда в коде необходимо обращаться к функциям и свойствам объекта.
  • Как к лямбда-аргументу при помощи ключевого слова it . В данном случае использование it для обращения к объекту является обязательным в том случае, если не задано пользовательское имя. То есть it можно переименовать во что-то более понятное и читабельное. Следовательно, функции области видимости с ключевым словом it рекомендуется использовать, когда в коде необходимо обратиться к самому объекту, а не к его свойствам и функциям.
Читайте также:  Максимальная общая подпоследовательность python

Возвращаемое значение

Каждая из функций может вернуть одно из значений:

  • Субъект вызова (объект, по отношению к которому была вызвана функция). При помощи таких функций можно выстроить длинную цепочку вызовов относительного первоначального объекта.
  • Результат выполнения лямбды. Такие функции можно использовать для сохранения результата в переменную, либо использовать результат в дальнейшей цепочке вызовов. А можно вовсе игнорировать возможность возврата результата и применять их для создания временной области видимости.

Функции

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 .

scope-functions-cheet-sheet

Для тех, кто лучше воспринимает информацию в картинках и схемах (когда-нибудь перерисую покрасивше):

Вывод

Несмотря на то, что перечисленные функции предназначены для того, чтобы сделать код более кратким, следует избегать их чрезмерного использования: это может снизить читабельность кода и привести к ошибкам.

Избегайте вложенности функций и будьте осторожны при их объединении: можно легко запутаться в текущем значении объекта.

Источник

Оцените статью