Что такое 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.