323 lines
19 KiB
Markdown
323 lines
19 KiB
Markdown
---
|
||
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)
|