Kotlin guide done. (20.10.26)

This commit is contained in:
2026-10-20 12:54:25 +03:00
parent da3ad9dd1a
commit 6853ada348
81 changed files with 30112 additions and 0 deletions
@@ -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)