Урок по треку Kotlin 7 мин

Лямбды, inline и reified за 7 минут

Функциональные типы, замыкания, inline, noinline, crossinline, non-local return и reified.

Функции высшего порядка в 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?
  • Чем лямбда с получателем отличается от обычной?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.