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

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

В стандартной библиотеке 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)

Шпаргалка по выбору функции

ФункцияСсылка на объектВозвращаемое значениеЯвляется функцией-расширением
letitРезультат лямбдыДа
runthisРезультат лямбдыДа
run-Результат лямбдыНет: может быть вызвана без объекта
withthisРезультат лямбдыНет: принимает объект в качестве аргумента
applythisКонтекстный объектДа
alsoitКонтекстный объектДа

Если таблицы недостаточно:

  • Выполнить блок кода для значения, отличного от null - let.
  • Использовать результат выполнения цепочки вызовов - let.
  • Инициализация и настройка объекта - apply.
  • Настройка объекта и получение результата выполнения операций - run.
  • Выполнение операций, для которых требуется временная область видимости - run без субъекта вызова.
  • Добавление объекту дополнительных значений - also.
  • Группировка всех функций, вызываемых для объекта: with.

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


Вывод

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

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


Полезные ссылки

Scope Functions - официальная документация.
Функции области видимости - перевод документации на русский язык.
Разбор функций с примерами.

This post is licensed under CC BY 4.0 by the author.

Kotlin. Null-безопасность. Операторы "?.", "!!.", "?:"

Kotlin. Классы данных (Data classes)