Files
obsidian/Kotlin/Функциональный Kotlin/Inline-функции.md
T

323 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 <T> List<T>.forEachInline(action: (T) -> Unit) { for (item in this) action(item) }
fun findFirstNegative(numbers: List<Int>): 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 <T> List<T>.forEachSlow(action: (T) -> Unit) { for (item in this) action(item) }
fun firstNegative2(numbers: List<Int>): 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 <reified T> filterByType(items: List<Any>): List<T> = items.filterIsInstance<T>()
fun main() {
val mixed = listOf(1, "a", 2.0, 3)
println(filterByType<Int>(mixed)) // [1, 3]
println(filterByType<String>(mixed)) // [a]
}
```
Это основная причина объявлять `inline` функцию без лямбд вообще. Подробнее: [Reified и inline](../Дженерики/Reified%20и%20inline.md)
## Inline-свойства
`inline` применим и к аксессорам:
```kotlin
class Config2(val map: Map<String, String>) {
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<T>` параметром | `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 <T> Iterable<T>.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)