Skip to main content

Inline functions

Short answer

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:

  1. main creates or provides a function value containing println("Hello").
  2. main calls doTwice and passes that function value as action.
  3. doTwice calls action() two times.
  4. Hello is 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.

Open in Kotlin Playground
Expected: 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.

Open in Kotlin Playground
Expected: 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.

Open in Kotlin Playground
Expected: -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.

Open in Kotlin Playground
Expected: 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:

  1. now can be copied into the call site.
  2. later must survive after runNowAndStore finishes.
  3. Therefore, later must remain a function value that can be stored.
  4. noinline tells Kotlin not to inline that parameter.

A noinline lambda cannot use a non-local return.

Open in Kotlin Playground
Expected: 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.

Open in Kotlin Playground
Expected: 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​

SituationUse inline?Reason
Small function with a frequently used lambdaConsider itMay remove lambda and call overhead
Reified type parameter requiredYesreified requires inline
Non-local return is part of the APIYesLambda body must be available at the caller
Large function bodyUsually noCopies too much code to every call site
Normal function without lambdasUsually noLittle or no benefit
Slow network/database operationInline is not the solutionI/O cost dominates
Inline does not mean asynchronous

inline does not create a thread, coroutine, background task, or parallel execution. It is primarily a compile-time code-generation feature.

Interview-ready answer

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.