Kotlin guide done. (20.10.26)
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
---
|
||||
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)
|
||||
Reference in New Issue
Block a user