Функции высшего порядка в Kotlin повсюду: map, let, launch, remember. Middle и senior спросят, во что лямбда компилируется, зачем inline, что такое non-local return и почему reified работает только в inline-функциях.
Функциональные типы и лямбды
val sum: (Int, Int) -> Int = { a, b -> a + b }
val greet: () -> Unit = { println("hi") }
val onClick: ((String) -> Unit)? = null // nullable функциональный тип
val parse: String.() -> Int = { this.length } // тип с получателем
fun retry(times: Int, block: () -> Unit) { repeat(times) { block() } }
retry(3) { println("попытка") } // trailing lambda
Лямбда с одним параметром получает имя it. Последнее выражение в лямбде это её результат, return внутри обычной лямбды писать нельзя (только return@метка). Ссылки на функции: ::println, String::length, user::save.
Тип с получателем T.() -> R лежит в основе DSL и apply: внутри лямбды this указывает на объект.
Во что компилируется лямбда
Без inline лямбда становится объектом, реализующим функциональный интерфейс (Function0, Function1 и т.д.; с Kotlin 2.0 по умолчанию через invokedynamic, как в Java). Если лямбда захватывает переменные, при каждом вызове создаётся новый объект. Захваченная var оборачивается в Ref-объект, чтобы лямбда могла её менять:
var counter = 0
listOf(1, 2, 3).forEach { counter += it } // ok, в отличие от Java
В Java захватывать можно только effectively final переменные, в Kotlin любые.
inline
inline fun measure(block: () -> Unit): Long {
val start = System.nanoTime()
block()
return System.nanoTime() - start
}
Компилятор копирует тело inline-функции и тело лямбды прямо в место вызова. Объект лямбды не создаётся, вызова через интерфейс нет. Поэтому map, filter, let, apply, forEach в stdlib помечены inline.
Цена: код раздувается в каждом месте вызова. Инлайнить стоит небольшие функции, которые принимают лямбды. Большую функцию без лямбд-параметров инлайнить бессмысленно, компилятор даже предупредит.
Non-local return
В лямбду, переданную в inline-функцию, можно написать return, и он выйдет из внешней функции:
fun findAdmin(users: List<User>): User? {
users.forEach { if (it.isAdmin) return it } // выход из findAdmin
return null
}
Чтобы выйти только из лямбды, используется метка: return@forEach (аналог continue).
noinline и crossinline
noinlineзапрещает инлайнить конкретный параметр-лямбду. Нужен, если лямбду надо сохранить в поле или передать в не-inline функцию.crossinlineразрешает инлайн, но запрещает non-local return. Нужен, когда лямбда вызывается из другого контекста: из вложенной лямбды, объекта, другого потока.
inline fun runOnUi(crossinline block: () -> Unit) {
handler.post { block() } // без crossinline не скомпилируется
}
reified
Из-за стирания типов на JVM внутри обычной дженерик-функции T неизвестен в рантайме: x is T и T::class не компилируются. В inline-функции тип можно пометить reified: при подстановке в место вызова компилятор знает конкретный тип.
inline fun <reified T> Any.isOf(): Boolean = this is T
inline fun <reified T : Activity> Context.start() =
startActivity(Intent(this, T::class.java))
context.start<SettingsActivity>()
val users = json.decodeFromString<List<User>>(text) // kotlinx.serialization
Reified-функцию нельзя вызвать из Java: ей нужен инлайн.
SAM-конверсия и fun interface
Для Java-интерфейсов с одним методом лямбду можно передать напрямую: button.setOnClickListener { }. Для своих Kotlin-интерфейсов это работает, если объявить их как fun interface.
Частые ошибки
- Помечать
inlineвсё подряд ради «скорости». - Путать
returnиreturn@labelвforEach. - Не понимать, почему
T::classне компилируется безreified. - Хранить лямбды, захватывающие
Activity, в долгоживущих объектах: утечка памяти.
Что спросят на собесе
- Что делает
inlineи когда его не стоит использовать? - Что такое non-local return и как выйти только из лямбды?
- Зачем
noinlineиcrossinline? - Что такое
reifiedи почему он работает только сinline? - Чем лямбда с получателем отличается от обычной?