Inline functions
inline is mainly useful for small functions that receive lambdas. It lets the compiler place the function's code and lambda code directly where the function is called. This can avoid some lambda objects and indirect calls. It also enables non-local returns and reified type parameters.
Do not begin with optimization. First understand what changes at the call site.
Step 1: a normal higher-order function
fun doTwice(action: () -> Unit) {
action()
action()
}
fun main() {
doTwice {
println("Hello")
}
}
Execution is straightforward:
maincreates or provides a function value containingprintln("Hello").maincallsdoTwiceand passes that function value asaction.doTwicecallsaction()two times.Hellois printed twice.
Conceptually, the call involves two levels:
main → doTwice → action
On the JVM, a lambda may require a function object and an indirect invoke call. The compiler can optimize some lambdas, so do not assume an allocation occurs every time. This is the problem inline is designed to help with for small higher-order functions.
Hello is printed twice.Step 2: mark the function inline
Only one word changes:
inline fun doTwice(action: () -> Unit) {
action()
action()
}
The call remains the same:
doTwice {
println("Hello")
}
At compile time, Kotlin can copy the bodies into the call site. A useful mental model is that the caller becomes similar to this:
fun main() {
println("Hello")
println("Hello")
}
The last snippet is conceptual generated code, not code you need to write. The exact bytecode is decided by the compiler.
The important change is:
Without inline: main → doTwice → action
With inline: action code is placed inside main's call site
The output is identical. inline changes how code can be compiled, not what this example means.
Hello from inline is printed twice.What can inline save?
For a small higher-order function, inlining may remove:
- A separate lambda/function object
- The indirect call through
action.invoke() - The call to the small wrapper function itself
These savings matter most when a small higher-order function is called frequently. They may not matter in ordinary UI code, network calls, database work, or other operations whose real cost is much larger.
What does inline cost?
The copied code appears at every call site.
inline fun largeOperation(block: () -> Unit) {
// Imagine 100 lines here
block()
}
If it is called from 50 places, substantial bytecode may be copied into those places. Larger bytecode can increase app size and affect instruction-cache behavior.
✕ Do not add inline automatically
inline fun add(a: Int, b: Int): Int = a + b
There is no lambda, non-local return, or reified type here. A normal function is already simple, and Kotlin may warn that inlining has insignificant benefit.
✓ Use it for a small higher-order utility when it provides a reason
inline fun runIf(condition: Boolean, action: () -> Unit) {
if (condition) action()
}
Non-local return: leaving the caller
Inlining makes another behavior possible. An eligible lambda can return from the function that called the inline function.
inline fun forEachValue(values: List<Int>, action: (Int) -> Unit) {
for (value in values) action(value)
}
fun findFirstNegative(values: List<Int>): Int? {
forEachValue(values) { value ->
if (value < 0) return value
}
return null
}
The return value exits findFirstNegative, not only the lambda.
Why? After conceptual inlining, the code is similar to:
fun findFirstNegative(values: List<Int>): Int? {
for (value in values) {
if (value < 0) return value
}
return null
}
Without the inline relationship, a bare return cannot jump out through a normal function object.
-7, followed by null.Labeled return: leave only the current lambda call
listOf(1, -2, 3).forEach { value ->
if (value < 0) return@forEach
println(value)
}
return@forEach skips the negative value and continues with the next item. It does not return from the surrounding function.
1, 3, and Finished.noinline: keep one lambda as a real value
An inline function may receive several lambdas. Sometimes one must be stored or passed elsewhere.
val pendingActions = mutableListOf<() -> Unit>()
inline fun runNowAndStore(
now: () -> Unit,
noinline later: () -> Unit,
) {
now()
pendingActions += later
}
Step by step:
nowcan be copied into the call site.latermust survive afterrunNowAndStorefinishes.- Therefore,
latermust remain a function value that can be stored. noinlinetells Kotlin not to inline that parameter.
A noinline lambda cannot use a non-local return.
Now, Stored actions = 1, and Later.crossinline: inline, but forbid returning from the caller
Sometimes the inline function places the lambda inside another object or execution step:
inline fun asRunnable(crossinline action: () -> Unit): Runnable =
Runnable { action() }
The returned Runnable may execute after asRunnable and its original caller have finished. A lambda inside it cannot safely use return to jump back into a caller that is no longer running. crossinline keeps the lambda eligible for inlining but forbids that non-local return.
Running later through Runnable.How reified connects to inline
At a call such as isType<String>(value), the compiler knows the real type is String. Because an inline function is copied into that call site, Kotlin can place a real String check there. This enables a reified type parameter:
inline fun <reified T> isType(value: Any): Boolean = value is T
The next page explains reified types fully. For now, remember: reification needs the call-site type information made available by inlining.
Practical decision table
| Situation | Use inline? | Reason |
|---|---|---|
| Small function with a frequently used lambda | Consider it | May remove lambda and call overhead |
| Reified type parameter required | Yes | reified requires inline |
| Non-local return is part of the API | Yes | Lambda body must be available at the caller |
| Large function body | Usually no | Copies too much code to every call site |
| Normal function without lambdas | Usually no | Little or no benefit |
| Slow network/database operation | Inline is not the solution | I/O cost dominates |
inline does not create a thread, coroutine, background task, or parallel execution. It is primarily a compile-time code-generation feature.
inline lets the Kotlin compiler copy a small higher-order function and eligible lambda bodies into each call site. This can avoid some lambda objects and indirect calls, enables non-local returns, and is required for reified type parameters. It does not make work asynchronous or automatically faster. The trade-off is duplicated bytecode, so I use it only when there is a clear reason.
Check your understanding
Does inline change the output of doTwice?
No. Both versions print twice. It changes how the compiler can generate the call, not the intended result.
Why can a noinline lambda be stored?
It remains a normal function value instead of being copied away at the call site.
Why does crossinline reject a bare return?
The lambda may run inside another object or later execution step, where returning to its original caller would be impossible.