Avatar ·

Когда использовать inline-функции в Kotlin и как они влияют на производительность?

Всем привет! Я недавно начал изучать Kotlin и наткнулся на такую штуку, как inline-функции. В документации написано, что они помогают избежать накладных расходов на создание лямбда-объектов, но я не совсем понимаю, когда их реально нужно применять. Например, у меня есть простая функция высшего порядка: ```kotlin fun measureTime(action: () -> Unit) { val start = System.currentTimeMillis() action() val end = System.currentTimeMillis() println("Time: ${end - start} ms") } ``` Я вызываю её так: ```kotlin measureTime { Thread.sleep(100) println("Hello") } ``` Всё работает, но я прочитал, что без `inline` каждый раз создаётся новый объект для лямбды. Это действительно проблема? Когда это становится критичным? Ещё я видел примеры с `reified` (вещественные типы) — якобы их можно использовать только с inline. Это правда? И как это связано с производительностью? Можете объяснить на пальцах: в каких случаях стоит писать `inline`, а когда лучше не надо? Боюсь, что начну пихать `inline` везде, где вижу лямбду, и это только ухудшит код. Заранее спасибо за помощь! <<>>

Когда использовать inline-функции: полный разбор

Отличный вопрос! Давайте разберёмся по порядку. inline — это не магическая кнопка "сделай быстро", а инструмент с чёткой областью применения.

Как работает inline?

Обычная функция высшего порядка:

fun measureTime(action: () -> Unit) { ... }

На уровне JVM создаётся объект-функция (лямбда). При каждом вызове — новый объект. inline говорит компилятору: "вставь тело функции прямо в место вызова, не создавая объект".

Без inline:

// Создаётся анонимный класс Function0
val lambda = object : Function0 { 
    override fun invoke() { 
        Thread.sleep(100)
        println("Hello")
    }
}
measureTime(lambda)

С inline:

// Копируется тело функции
val start = System.currentTimeMillis()
Thread.sleep(100)
println("Hello")
val end = System.currentTimeMillis()
println("Time: ${end - start} ms")

Когда использовать inline — чёткие критерии

  • Функции с лямбда-параметрами — основное применение. Если ваша функция принимает другую функцию как параметр (например, measureTime), и эта лямбда вызывается внутри — ставьте inline.
  • Маленькие функции, вызываемые в циклах — если функция вызывается тысячи раз, накладные расходы на создание объектов становятся заметными.
  • Reified (вещественные) типы — да, вы правы. Только inline позволяет использовать reified, потому что на уровне JVM generic-типы стираются, а inline-функция "встраивает" реальный тип в место вызова.

Пример с reified:

inline fun <reified T> isType(value: Any): Boolean {
    return value is T  // Работает только с inline + reified
}

Когда НЕ стоит использовать inline

  • Большие функции — встраивание увеличивает размер байт-кода (code bloat). Если функция содержит 100+ строк, её копирование в каждое место вызова — плохая идея.
  • Функции без лямбда-параметров — обычные функции не выигрывают от inline. Компилятор сам может оптимизировать их без вашего вмешательства.
  • Прямые рекурсивные вызовы — inline не работает с рекурсией.

Практический пример: когда inline реально помогает

Представьте, что вы пишете DSL или библиотеку с часто вызываемыми функциями:

inline fun buildString(builderAction: StringBuilder.() -> Unit): String {
    val sb = StringBuilder()
    sb.builderAction()
    return sb.toString()
}

// Использование
val result = buildString {
    append("Hello, ")
    append("World!")
}
println(result) // "Hello, World!"

Без inline каждый вызов buildString создавал бы новый объект лямбды. С inline — никаких лишних объектов.

Итоговые рекомендации

  1. Используйте inline для функций высшего порядка с лямбдами, особенно если они вызываются часто.
  2. Используйте inline + reified, когда нужно сохранить тип в runtime.
  3. Не используйте inline для больших функций или функций без лямбда-параметров.
  4. Помните: компилятор Kotlin/JVM сам делает много оптимизаций. inline — это когда вы хотите контролировать процесс вручную.

Надеюсь, теперь стало понятнее! Если сомневаетесь — начните с inline только для функций с лямбдами, и вы не ошибётесь.

<<>>

Дополнение: производительность и профилирование

Коллега дал отличный ответ, добавлю важный нюанс.

Не стоит думать, что inline всегда делает код быстрее. Современная JVM (HotSpot) имеет JIT-компилятор, который сам умеет "встраивать" (inline) маленькие функции на лету, независимо от того, поставили вы модификатор или нет. Разница часто незаметна в обычных приложениях.

Где inline реально даёт прирост:

  • Android-разработка — там нет JIT (только AOT), поэтому лишние объекты — это реальные затраты памяти и GC.
  • Библиотеки/фреймворки — если вашу функцию будут вызывать миллионы раз (как let, apply, with в стандартной библиотеке).
  • Hot path в критическом коде — например, в игровом движке или обработке данных в реальном времени.

Совет: не оптимизируйте заранее. Пишите сначала читаемый код, потом профилируйте. Если профилировщик (например, Android Studio Profiler) показывает, что создание лямбд — узкое место — вот тогда ставьте inline.

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