KotlinMiddleTechnical

Что такое inline functions в Kotlin и когда их стоит использовать?

inline вставляет тело функции и лямбд прямо в место вызова, устраняя создание объектов-функций. Применяется для HOF в горячих путях и для reified-параметров, позволяющих обойти JVM type erasure.

Inline functions в Kotlin

Модификатор inline указывает компилятору вставить тело функции (и тела переданных лямбд) непосредственно в место вызова вместо создания объектов-функций и вызова через виртуальную диспетчеризацию. Это устраняет накладные расходы, связанные с лямбдами как объектами.

Зачем нужен inline

Каждая лямбда в JVM — это экземпляр анонимного класса. При частых вызовах (например, в цикле) это создаёт давление на GC. С inline компилятор копирует байткод функции и лямбды прямо в вызывающий код:

// Без inline: создаётся объект Function1 на каждый вызов
fun repeat(times: Int, action: (Int) -> Unit) {
    for (i in 0 until times) action(i)
}

// С inline: лямбда встраивается в вызывающий код
inline fun repeatInlined(times: Int, action: (Int) -> Unit) {
    for (i in 0 until times) action(i)
}

fun main() {
    // Этот вызов компилируется так, будто вы написали for-цикл напрямую
    repeatInlined(1_000_000) { i ->
        println(i)
    }
}

Non-local returns

Одно из важных свойств inline-функций: лямбды, переданные в них, могут использовать return для выхода из внешней функции (non-local return). В обычных функциях это запрещено.

inline fun findFirst(list: List<Int>, predicate: (Int) -> Boolean): Int? {
    for (item in list) {
        if (predicate(item)) return item // возврат из findFirst
    }
    return null
}

fun searchInList(): Int? {
    val numbers = listOf(1, 5, 3, 8, 2)
    // return выходит из searchInList целиком (non-local return)
    return findFirst(numbers) { it > 4 }
}

println(searchInList()) // 5

noinline и crossinline

Если функция inline, но конкретный лямбда-параметр не нужно встраивать (например, его передают дальше как объект), используется noinline. crossinline запрещает non-local return для конкретной лямбды, оставляя её встраиваемой.

inline fun process(
    data: String,
    noinline onComplete: () -> Unit, // не встраивается, можно сохранить в переменную
    crossinline transform: (String) -> String // встраивается, но без non-local return
) {
    val result = transform(data)
    scheduleCallback(onComplete) // onComplete передаётся как объект
    println(result)
}

fun scheduleCallback(cb: () -> Unit) { cb() }

Reified type parameters

Только inline-функции могут иметь параметры типа с reified — это позволяет использовать тип как реальный объект во время выполнения, обходя JVM type erasure:

inline fun <reified T> List<*>.filterByType(): List<T> {
    return filterIsInstance<T>()
}

inline fun <reified T> parseJson(json: String): T {
    // можно использовать T::class, T::class.java
    return jacksonObjectMapper().readValue(json, T::class.java)
}

fun main() {
    val mixed = listOf(1, "hello", 2.0, "world", 3)
    val strings = mixed.filterByType<String>()
    println(strings) // [hello, world]
}

Когда применять inline

  • Функции высшего порядка, вызываемые часто (в циклах, в методах коллекций).
  • Когда нужен reified тип-параметр.
  • Утилитарные функции-обёртки (try/catch wrappers, логирование времени и т.д.).
  • DSL-строители с lambda with receiver.
// Пример: измерение времени выполнения
inline fun <T> measureMs(block: () -> T): Pair<T, Long> {
    val start = System.currentTimeMillis()
    val result = block()
    val elapsed = System.currentTimeMillis() - start
    return result to elapsed
}

val (result, time) = measureMs {
    (1..1_000_000).sum()
}
println("Результат: $result, время: ${time}мс")

Подводные камни

  • Раздувание байткода (code bloat). Каждый вызов inline-функции копирует её тело. Если функция большая и вызывается в 100 местах, итоговый .class значительно увеличится.
  • Нельзя хранить noinline-параметры внутри inline-функций. Встроенная лямбда не существует как объект — попытка сохранить её в поле класса не скомпилируется без noinline.
  • Non-local return может быть неожиданным. Коллеги, использующие вашу inline-функцию, могут не ожидать, что return в лямбде выйдет из их функции.
  • inline не всегда даёт прирост. Для крупных функций или функций с малым числом вызовов встраивание не ускоряет код. JIT JVM сам оптимизирует частые вызовы.
  • Рекурсивные inline-функции запрещены. Компилятор не позволит inline-функции вызывать саму себя — это привело бы к бесконечному раскрытию кода.
  • reified не работает с обычными функциями. Попытка использовать reified без inline вызывает ошибку компиляции.
  • Видимость приватных членов. Встроенный код может обращаться к private-членам вызывающего класса, что нарушает инкапсуляцию — компилятор предупреждает об этом.

Common mistakes

  • Объяснять «inline functions» только как синтаксис и не описывать поведение runtime/compiler.
  • Игнорировать важный риск: Чрезмерный inline раздувает bytecode и ухудшает читаемость stack traces.
  • Давать пример без edge case: отмены, null, recomposition, platform boundary или ошибки.

What the interviewer is testing

  • Формулирует суть темы «inline functions» своими словами и связывает ее с кодом.
  • Называет механизм: Особенно полезно для маленьких higher-order helpers, reified generics и DSL; noinline/crossinline ограничивают поведение lambda.
  • Видит production-последствие: Чрезмерный inline раздувает bytecode и ухудшает читаемость stack traces.

Sources

Related topics