Inline-функции в Kotlin: когда они реально нужны, а когда вредят

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Inline-функции в Kotlin: магия оптимизации или ловушка для новичка?

Если вы когда-нибудь писали на Kotlin и заглядывали в стандартную библиотеку, то наверняка видели загадочное слово inline перед объявлением функций. Возможно, вы даже добавляли его в свой код, следуя примеру «бывалых» коллег. Но действительно ли вы понимаете, когда inline-функция приносит пользу, а когда — только раздувает байткод и замедляет компиляцию?

Этот вопрос — один из самых популярных на Stack Overflow (тысячи просмотров!), и неспроста. Мы не будем копировать сухие мануалы. Вместо этого разберем три реальных сценария, где inline-функции становятся вашим лучшим другом, и один — где от них лучше отказаться. Поехали!

Что такое inline-функция за 30 секунд (освежим память)

Когда вы помечаете функцию ключевым словом inline, компилятор не создает для нее отдельный объект в памяти. Вместо этого он буквально «вклеивает» тело функции в каждое место, где она вызывается. Представьте, что вы копируете кусок кода и вставляете его прямо в точку вызова — вот что происходит под капотом.

Без inline:

fun measureTime(action: () -> Unit) {
    val start = System.currentTimeMillis()
    action()
    println("Time: ${System.currentTimeMillis() - start}ms")
}

fun main() {
    measureTime { /* ваш код */ }
}

С inline:

inline fun measureTime(action: () -> Unit) {
    val start = System.currentTimeMillis()
    action()
    println("Time: ${System.currentTimeMillis() - start}ms")
}

fun main() {
    measureTime { /* ваш код */ }
}

Разница незаметна глазу, но компилятор создаст совершенно разный байткод. В первом случае будет создан анонимный объект-лямбда, во втором — код просто скопируется. Теперь разберемся, когда это реально важно.

Сценарий №1: Higher-order функции с лямбдами (самый частый случай)

Если ваша функция принимает другую функцию как параметр (так называемая higher-order function), каждый вызов лямбды создает анонимный класс. На горячем участке кода, вызываемом тысячи раз в секунду, это может привести к:

  • Лишней аллокации объектов в куче
  • Нагрузке на сборщик мусора
  • Потере производительности до 10-20%

Когда inline спасает:

  • Вы часто вызываете одну и ту же функцию с лямбдами в цикле
  • Ваша функция — часть библиотеки, и вы не знаете, как её будут использовать
  • Лямбда захватывает много переменных (inline уменьшает накладные расходы)
// Плохо без inline (если вызывается часто)
fun repeatAction(times: Int, action: () -> Unit) {
    for (i in 0 until times) {
        action()
    }
}

// Хорошо с inline
inline fun repeatAction(times: Int, action: () -> Unit) {
    for (i in 0 until times) {
        action()
    }
}

Но есть нюанс: если ваша функция вызывается редко (раз в минуту) или лямбда большая, выгода от inline стремится к нулю. Не делайте inline «на всякий случай».

Сценарий №2: Reified generics — единственный способ работать с типами в рантайме

В Java и Kotlin дженерики стираются во время компиляции. Вы не можете написать if (value is T) или T::class.java внутри обычной функции. Но с inline и reified это становится возможным!

Пример из реальной жизни — создание экземпляра по типу:

// Это НЕ скомпилируется
fun <T> createInstance(): T {
    return T::class.java.newInstance() // Ошибка!
}

// А это — работает!
inline fun <reified T> createInstance(): T {
    return T::class.java.newInstance()
}

Без inline вы не сможете использовать reified. Это единственный случай, когда inline обязателен, а не просто оптимизация. Типичные применения:

  • Фабрики объектов по типу
  • Сериализация/десериализация (Gson, kotlinx.serialization)
  • Фильтрация списка по типу (filterIsInstance<String>())

Сценарий №3: Non-local returns — выход из внешней функции

Обычная лямбда не может сделать return из внешней функции. Только inline-лямбда позволяет это. Пример:

fun findFirst(list: List<Int>, predicate: (Int) -> Boolean): Int? {
    for (item in list) {
        if (predicate(item)) return item // Ошибка! return не разрешен
    }
    return null
}

// Исправляем с inline
inline fun findFirst(list: List<Int>, predicate: (Int) -> Boolean): Int? {
    for (item in list) {
        if (predicate(item)) return item // Теперь работает!
    }
    return null
}

Этот паттерн активно используется в стандартной библиотеке Kotlin: forEach, with, apply и другие — все они inline. Если вы пишете собственный аналог, добавляйте inline для поддержки return.

Когда inline ВРЕДИТ: три красных флага

Теперь хорошие новости: в большинстве случаев компилятор Kotlin умнее нас. Но есть ситуации, где inline только ухудшает код:

1. Большие функции (более 10-15 строк)

Каждый вызов inline-функции копирует её тело. Если функция большая и вызывается в 100 местах, размер байткода вырастает в 100 раз. Это может вызвать:

  • Раздувание APK/DEX файлов
  • Проблемы с кэшированием JIT
  • Замедление компиляции

Правило: inline для маленьких функций (до 5 строк) — отлично. Для больших — используйте обычные функции.

2. Функции с множественными лямбда-параметрами

Если у вас три-четыре лямбды, компилятор вставит все их тела. Это редко оправдано. Используйте noinline для тех лямбд, которые не должны быть встроены:

inline fun process(
    onSuccess: () -> Unit,
    noinline onError: (Exception) -> Unit
) {
    try {
        onSuccess()
    } catch (e: Exception) {
        onError(e) // Эта лямбда не будет встроена
    }
}

3. Публичные inline-функции в библиотеках

Если вы пишете библиотеку, inline в публичном API может вызвать проблемы бинарной совместимости. Любое изменение тела функции потребует перекомпиляции всех клиентов. Для приватных/internal функций — без проблем.

Сравнение подходов: таблица решений

Чтобы не гадать

Блоги

Рекомендаций по книгам