Kotlin Inline Functions, noinline and crossinline

Understand inlining as a calling contract, control non-local returns and choose noinline or crossinline for escaping callbacks.

Lesson 32 of 43Advanced10 min read

Inline functions can place their function body and supported lambda bodies into the caller's compiled code. This can remove some higher-order call overhead, but its most visible language effect is control flow: a lambda passed to an appropriate inline function can return from the enclosing function.

The generics lesson introduces inline with reified type parameters. Here the focus is the callback contract: non-local returns, labeled local returns, noinline and crossinline. You will write a small validation helper and learn why inline is not a promise that every function becomes faster. The examples use established standard-library and language features.

Review lambdas and function types first so the callback parameter types and labeled returns are familiar.

Inlining changes supported callback behavior

An ordinary function value is an object-like callable passed to another operation. Inlining can instead incorporate the body at the call site. That lets the compiler support operations that need caller context, including certain returns and reified type uses. It can also increase generated code size when a large body is repeated across many callers.

Use inline for a meaningful higher-order contract, not as a decoration on every helper. A tiny arithmetic function with no inlineable lambda or reified requirement usually offers little reason to add the modifier. Measure real performance problems and keep public contracts understandable.

Inline parameters cannot be used arbitrarily as stored values because their bodies are intended to be incorporated at the call site. If a callback must be retained in a field, returned or passed to a non-inline context, its parameter may need noinline. The compiler's restrictions describe that lifecycle difference rather than an inconvenience to bypass.

Local returns use labels

Return@forEach finishes the current lambda invocation and continues the surrounding operation. It does not return from the outer function. You can also give a lambda an explicit label when several calls have similar names. Labels should clarify which boundary is exited rather than require a reader to count nesting levels.

An anonymous function written with fun has its own ordinary return boundary. That can be useful when a callback needs several early exits that should stay local. The decision is about control-flow intent, not whether one syntax is more modern.

Do not confuse return@forEach with breaking the entire iteration. It behaves like finishing that callback invocation. If the requirement is stop processing all items, use a clear loop with break, a terminal search function or a supported non-local return from the enclosing function. State the intended exit in plain language first.

noinline retains an ordinary function value

Mark a lambda parameter noinline when it must behave as an ordinary callable value, such as a callback saved for later. Other lambda parameters of the same inline function may still be inlined. A noinline callback cannot use the same non-local return behavior as an inlineable lambda.

Capturing and storing a callback also extends the lifetime of whatever it captures. That ownership concern remains even when the helper's source is short. Do not retain user-specific objects unintentionally in an application-wide registry, and provide explicit removal when callback registration has a lifecycle.

The modifier is a precise contract choice rather than a performance ranking. A retained callback needs a real callable representation. Avoid forcing inline semantics onto an asynchronous or delayed operation whose callback will run after the caller's stack has returned.

crossinline forbids non-local returns

Crossinline applies to a lambda that is still eligible for inlining but cannot return from the enclosing caller. It is useful when the implementation invokes the lambda from another execution context, such as a nested object or Runnable. A non-local jump back into a finished or different call context would not be a valid control-flow promise.

Crossinline does not schedule work, create a thread or make captured values safe for concurrency. It restricts callback control flow. The implementation and caller still need an explicit lifetime and threading policy. A callback can have side effects regardless of whether a non-local return is forbidden.

The official inline-functions reference documents these modifiers and return rules. Public inline functions also have visibility restrictions because their implementation can be incorporated into consumer code. Keep access to implementation details compatible with the supported published API.

Kotlin Inline Functions, noinline and crossinline Examples

Each example shows the code, the output it produces and what each part does.

Example 1

A non-local return exits the enclosing function

ForEach is inline, so this return leaves firstNegative rather than merely finishing one iteration. That is useful here because finding the first match ends the search. An explicit for loop could be equally clear. Prefer the form that makes the exit obvious to the reader, especially when several lambdas are nested.

Kotlin
fun firstNegative(values: List<Int>): Int? {
    values.forEach { value ->
        if (value < 0) return value
    }
    return null
}

fun main() {
    println(firstNegative(listOf(2, -3, 4)))
    println(firstNegative(listOf(2, 3)))
}
Output
-3
null

Kotlin Inline Functions, noinline and crossinline in Real Projects

Worked exercise: apply a local validation callback

Create an inline helper that runs an action only when a supplied value is positive. In the caller, use a labeled return to reject one value locally without leaving the main function. The printed final marker must still appear, proving that the return boundary is the action rather than the program.

Kotlin
inline fun withPositive(value: Int, action: (Int) -> Unit) {
    if (value > 0) action(value)
}

fun main() {
    withPositive(3) action@ { value ->
        if (value == 3) return@action
        println("accepted:$value")
    }
    withPositive(4) { println("accepted:$it") }
    println("finished")
}
Output
accepted:4
finished

The explicit label makes the first callback's exit visible. Try using a plain return in a separate function and explain which enclosing boundary it exits. This helper is educational; a validation API that callers use broadly may be clearer when it returns a result instead of relying on callback control flow to communicate rejection.

Kotlin Inline Functions, noinline and crossinline FAQ

Is inline always a speed improvement?

No. It can reduce some lambda allocation and calling costs while increasing generated code size. Large repeated bodies or workloads dominated by database and network operations may gain little. Choose inline when its supported semantics or measured call overhead justify it. Avoid adding it everywhere based on a slogan. A readable function with explicit dependencies is usually more valuable than a speculative optimization that complicates visibility and callback restrictions.

What does return@forEach actually do?

It returns from the current forEach lambda invocation, so the surrounding traversal can continue with later items. It does not automatically break the loop or leave the enclosing function. Use a direct loop with break when the operation should stop entirely and that intent reads more clearly. Labels identify boundaries, so an explicit lambda label can help when nested operations share names. Trace the next statement that runs to verify the intended behavior.

How do noinline and crossinline differ?

Noinline keeps a parameter as an ordinary callable value that can be stored or passed where inlining is unavailable. Crossinline still allows inlining but forbids non-local returns because the implementation may invoke the callback through another context. Neither modifier supplies a scheduling or thread-safety policy. Choose based on how the function uses the callback, then document when it runs and whether it can escape the immediate call's lifetime.

Why can reified only appear in an inline function?

Inlining lets the compiler use the caller's concrete type information at the call site, which supports selected runtime operations on a reified type parameter. Ordinary erased generic functions cannot generally recover that information. Reified does not validate every nested generic argument automatically, and it is not a reason to inline unrelated functions. The existing generics lesson covers basic reified filtering; combine it with a clear callback and validation contract when designing a reusable API.

Kotlin Inline Functions, noinline and crossinline Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choicemedium

A lambda is invoked through a nested Runnable and must not return from its caller. Which modifier expresses that restriction?

Kotlin Inline Functions, noinline and crossinline Quiz

2 questions to check what you learned.

  1. What boundary does return@forEach exit?

    • The current lambda invocation
    • Every enclosing function automatically
    • The JVM process
  2. Which modifier suits a callback that must be stored as an ordinary function value?

    • crossinline alone as a storage mechanism
    • noinline
    • protected