--- title: Inline-функции description: Подстановка тела на этапе компиляции, noinline и crossinline, нелокальный return и цена inline tags: - kotlin - functional - performance --- `inline` говорит компилятору: не вызывай эту функцию, а подставь её тело в место вызова. Для обычных функций это почти бесполезно (JIT справляется сам), а для функций с лямбдами - убирает объект лямбды. ```kotlin inline fun measure(block: () -> Unit): Long { val start = System.nanoTime() block() return System.nanoTime() - start } fun main() { val time = measure { (1..1000).sum() } println(time > 0) // true } ``` После компиляции в `main` не остаётся ни вызова `measure`, ни объекта `Function0` - там просто `val start = ...; (1..1000).sum(); ...`. ## Что именно экономится Без `inline` на каждый вызов создаётся объект, реализующий `Function0`: ```kotlin fun notInlined(block: () -> Int): Int = block() inline fun inlined(block: () -> Int): Int = block() fun main() { var captured = 0 repeat(3) { notInlined { captured + 1 } } // лямбда захватывает captured -> новый объект каждый раз repeat(3) { inlined { captured + 1 } } // объектов нет вообще println(notInlined { 42 }) // 42 println(inlined { 42 }) // 42 } ``` Лямбда **без захвата** компилируется в синглтон, поэтому объект создаётся один раз - тут `inline` экономит только вызов. Лямбда **с захватом** создаёт объект на каждое вычисление, и вот здесь разница существенна. Поэтому все операции над коллекциями в stdlib объявлены `inline`: ```kotlin fun main() { val sum = listOf(1, 2, 3).filter { it > 1 }.sumOf { it * 2 } println(sum) // 10 } ``` В байт-коде это два обычных цикла `for` с телами лямбд внутри. Ни одного объекта лямбды и ни одного виртуального вызова - именно поэтому короткие цепочки в Kotlin обычно быстрее Java-стримов. ## Нелокальный return Второе следствие подстановки: из inline-лямбды можно выйти из внешней функции. ```kotlin inline fun List.forEachInline(action: (T) -> Unit) { for (item in this) action(item) } fun findFirstNegative(numbers: List): Int? { numbers.forEachInline { if (it < 0) return it } // выход из findFirstNegative return null } fun main() { println(findFirstNegative(listOf(1, 2, -3, 4))) // -3 println(findFirstNegative(listOf(1, 2))) // null } ``` Это работает, потому что `return` оказывается физически внутри `findFirstNegative`. В не-inline функции такой `return` запрещён: ```kotlin fun List.forEachSlow(action: (T) -> Unit) { for (item in this) action(item) } fun firstNegative2(numbers: List): Int? { numbers.forEachSlow { if (it < 0) return@forEachSlow } // только локальный выход return null } fun main() { println(firstNegative2(listOf(-1))) } // null ``` Именно поэтому `forEach` из stdlib ведёт себя как `continue`, а не как `break`: ```kotlin fun main() { listOf(1, 2, 3).forEach { if (it == 2) return@forEach // пропустить элемент print("$it ") } println() // 1 3 } ``` ## noinline Если лямбду нужно сохранить или передать дальше, подставить её нельзя - у подставленного тела нет объекта: ```kotlin inline fun register( name: String, noinline handler: (String) -> Unit, validator: (String) -> Boolean, ): (String) -> Unit { println("регистрирую $name, валиден: ${validator(name)}") return handler // возвращаем объект — нужен noinline } fun main() { val h = register("событие", handler = { println("обработано: $it") }, validator = { it.isNotEmpty() }) h("данные") // регистрирую событие, валиден: true // обработано: данные } ``` Без `noinline` компилятор откажется: «Illegal usage of inline-parameter». Правило: лямбду, которая только вызывается внутри функции, можно инлайнить; лямбду, которую сохраняют в поле, возвращают или передают в не-inline функцию - нельзя. ## crossinline Промежуточный случай: лямбда вызывается не напрямую, а внутри другой лямбды или объекта. ```kotlin inline fun runLater(crossinline block: () -> Unit): Runnable = Runnable { block() } fun main() { val task = runLater { println("выполнено позже") } task.run() // выполнено позже } ``` `crossinline` означает «инлайнить можно, но нелокальный `return` запрещён». Логика такая: тело лямбды подставляется внутрь `Runnable`, а оттуда выйти из вызывающей функции физически невозможно - она уже завершилась. ```kotlin inline fun repeatTwice(crossinline block: () -> Unit) { val wrapped = { block(); block() } wrapped() } fun demo() { repeatTwice { println("раз") } // repeatTwice { return } // ошибка: нелокальный return запрещён } fun main() { demo() } // раз \n раз ``` | Модификатор | Лямбда подставляется | Нелокальный `return` | Можно сохранить/передать | |---|---|---|---| | (по умолчанию) | да | да | нет | | `crossinline` | да | нет | только внутрь другой лямбды | | `noinline` | нет | нет | да | ## reified Третье следствие подстановки: в месте вызова известен фактический тип, поэтому параметр типа можно «материализовать». ```kotlin inline fun filterByType(items: List): List = items.filterIsInstance() fun main() { val mixed = listOf(1, "a", 2.0, 3) println(filterByType(mixed)) // [1, 3] println(filterByType(mixed)) // [a] } ``` Это основная причина объявлять `inline` функцию без лямбд вообще. Подробнее: [Reified и inline](../Дженерики/Reified%20и%20inline.md) ## Inline-свойства `inline` применим и к аксессорам: ```kotlin class Config2(val map: Map) { inline val isEmpty: Boolean get() = map.isEmpty() val host: String inline get() = map["host"] ?: "localhost" } fun main() { val c = Config2(mapOf("host" to "example.com")) println("${c.isEmpty} ${c.host}") // false example.com } ``` Обратите внимание: `map` объявлен `val` без `private`. Для `private val` сниппет не скомпилируется - на inline-аксессоры действует то же правило, что на inline-функции: подставленное тело окажется в чужом классе, где приватный член недоступен. Выгода минимальна - JIT инлайнит такие геттеры и сам. Реально `inline`-свойства нужны редко: когда геттер должен использовать `reified` или когда свойство - часть публичного API, которое не хочется оставлять виртуальным. ## Когда inline оправдан | Случай | Нужен ли `inline` | |---|---| | функция принимает лямбду, которая вызывается сразу | да | | нужен `reified` | да, обязательно | | нужен нелокальный `return` из лямбды | да, обязательно | | функция без лямбд и без `reified` | нет, JIT справится | | тело большое (десятки строк) | нет, байт-код раздуется | | функция рекурсивная | нельзя | | публичное API библиотеки | осторожно - см. подводные камни | ## Что происходит с байт-кодом ```kotlin inline fun twice(block: () -> Unit) { block(); block() } fun main() { var count = 0 twice { count++ } twice { count += 10 } println(count) // 22 } ``` Тело `twice` скопировано в `main` дважды, и каждая копия содержит своё тело лямбды - итого четыре инкремента в коде. Для короткой функции это дешевле вызова; для функции на сто строк, вызываемой из пятидесяти мест, это пять тысяч строк байт-кода. Компилятор предупреждает, когда `inline` бесполезен: ```kotlin inline fun uselessInline(x: Int): Int = x * 2 // warning: expected performance impact is insignificant fun main() { println(uselessInline(2)) } // 4 ``` Такое предупреждение - сигнал убрать `inline`, если не нужен `reified`. > [!info] Отличия от Java > | Java | Kotlin | > |---|---| > | нет inline на уровне языка | `inline fun` | > | JIT инлайнит в рантайме | компилятор инлайнит в байт-код | > | лямбда всегда объект | при `inline` объекта нет | > | `return` из лямбды невозможен | нелокальный `return` при `inline` | > | `Class` параметром | `reified T` | > | `final` подсказывает JIT | `inline` - директива компилятору | > > **Java инлайнит в рантайме, Kotlin - при компиляции.** HotSpot инлайнит горячие методы агрессивнее, чем любой компилятор: он знает фактические типы, статистику ветвлений и может деоптимизировать решение. Поэтому `inline` в Kotlin - не про скорость исполнения тела, а про три вещи, недоступные JIT: отсутствие объекта лямбды, нелокальный `return` и `reified`. > > **Escape analysis частично закрывает ту же проблему в Java.** Если JIT доказывает, что объект лямбды не покидает метод, он может не выделять его в куче (scalar replacement). Но это оптимизация «на удачу»: она применяется не всегда и ломается при полиморфном вызове. `inline` в Kotlin гарантирован и виден в байт-коде. > > **Нелокального `return` в Java нет вообще.** Из тела `forEach` нельзя выйти из внешнего метода - приходится либо возвращаться к обычному `for`, либо бросать исключение, либо использовать `findFirst` на стриме. Kotlin позволяет писать абстракции обхода, которые ведут себя как встроенный цикл: > ```kotlin > inline fun Iterable.firstMatching(p: (T) -> Boolean): T? { > for (item in this) if (p(item)) return item > return null > } > > fun main() { println(listOf(1, 2, 3).firstMatching { it > 1 }) } // 2 > ``` > > **Цена - совместимость библиотек.** В Java изменение тела метода не требует перекомпиляции клиентов. В Kotlin тело inline-функции физически скопировано в байт-код клиента, поэтому исправление в библиотеке не подействует до пересборки вызывающего кода. Для публичного API это серьёзный аргумент, и stdlib помечает такие функции `@PublishedApi`, когда они обращаются к internal-коду. > > **Inline-функцию нельзя вызвать из Java** с сохранением смысла: Java-компилятор не умеет подставлять тело, он вызовет сгенерированный метод как обычный. `reified` при этом не работает. > > Сравнить: [JIT в Java](../../Программирование%20на%20Java/JVM/JIT.md), [Байт-код](../../Программирование%20на%20Java/JVM/Байт-код.md) > [!warning] Подводные камни > **Inline раздувает байт-код.** Тело копируется в каждое место вызова. Для больших функций это рост размера класса, давление на кеш инструкций процессора и более медленная загрузка классов. `inline` оправдан для коротких функций - в stdlib почти все inline-функции занимают несколько строк. > > **Изменение тела inline-функции не действует без перекомпиляции клиентов:** > ```kotlin > inline fun version() = "v1" // если выпустить v2, клиенты со старой сборкой увидят v1 > > fun main() { println(version()) } // v1 > ``` > Это ломает обычные ожидания от семантического версионирования библиотек. > > **Inline-функция не видит `private` членов своего класса:** > ```kotlin > class Service3 { > private val secret = "скрыто" > // inline fun leak() = secret // ошибка: public-inline не может обращаться к private > fun ok() = secret > internal inline fun fine() = ok() // а так можно > } > > fun main() { println(Service3().fine()) } // скрыто > ``` > Причина: тело окажется в чужом классе, где `private` недоступен. Обход - `@PublishedApi internal` на члене. > > **Рекурсивную функцию нельзя сделать inline:** > ```kotlin > // inline fun fact(n: Int): Int = if (n <= 1) 1 else n * fact(n - 1) // ошибка > fun fact(n: Int): Int = if (n <= 1) 1 else n * fact(n - 1) > > fun main() { println(fact(5)) } // 120 > ``` > Подстановка была бы бесконечной. > > **Лямбду из inline-параметра нельзя сохранить:** > ```kotlin > inline fun broken(block: () -> Unit) { > // val saved = block // ошибка: Illegal usage of inline-parameter > block() > } > > inline fun fixed(noinline block: () -> Unit): () -> Unit = block > > fun main() { > broken { println("ок") } > fixed { println("сохранено") }() > } > ``` > > **`crossinline` запрещает нелокальный `return`, и ошибка возникает у пользователя функции**, а не у автора. Если лямбда оборачивается в `Runnable`, объект или другую лямбду - `crossinline` обязателен, и вызывающий код теряет возможность выйти из своей функции. > > **Нелокальный `return` из inline-лямбды пропускает код после вызова:** > ```kotlin > inline fun withLogging(block: () -> Unit) { > println("до") > block() > println("после") // не выполнится при return из лямбды > } > > fun run1() { > withLogging { return } > } > > fun main() { run1(); println("конец") } // до \n конец > ``` > Строка «после» потерялась. Если в функции есть освобождение ресурсов, его нужно ставить в `finally` - иначе нелокальный `return` его обойдёт: > ```kotlin > inline fun safeLogging(block: () -> Unit) { > println("до") > try { block() } finally { println("после") } > } > > fun run2() { safeLogging { return } } > > fun main() { run2() } // до \n после > ``` > Это важнее, чем кажется: `use`, `withLock` и транзакционные обёртки обязаны использовать `finally` именно по этой причине. > > **`inline` на функции без лямбд вызывает предупреждение** и почти не влияет на производительность. Единственные оправдания - `reified` и необходимость подставить тело в клиентский код. > > **Внутри inline-лямбды нельзя использовать `suspend`-вызовы, если не помечено специально.** Для функций вроде `measure` это выглядит рабочим, но обёртка над `suspend`-кодом требует либо `suspend inline`, либо `crossinline` и явного учёта контекста. Подробнее: [Suspend-функции](../Корутины/Suspend-функции.md) > > **Инлайн не убирает упаковку примитивов в функциональных типах автоматически** - но при полной подстановке упаковки не остаётся, потому что лямбды как объекта нет. А вот с `noinline` параметром `(Int) -> Int` упаковка сохраняется. > > **Профилируйте, прежде чем добавлять `inline` вручную.** Накладные расходы лямбды измеримы только в горячем коде с миллионами вызовов. В обычном бизнес-коде преждевременный `inline` даёт лишь раздутый байт-код и проблемы совместимости. Далее: [Функции-расширения](Функции-расширения.md)