You are designing a public inline higher-order API where the lambda is invoked from a background callback. How do you decide between crossinline and noinline, and what are the downstream consequences for callers and codegen?
answer
- Stored as value? -> noinline; only invoked? -> crossinline
- Both kill non-local return for callers — document it
- Inline bloats every call site (crossinline too)
- Inline leaks impl into consumers (ABI)
- If not hot-path, maybe skip inline entirely
basics
~20 sIf you only need to call the lambda (even from a callback), use crossinline so it stays inlined with no allocation. If you must store or pass the lambda onward as a value, use noinline. Both stop callers from using non-local returns, which changes how they write control flow.
solid answer
~50 sThe deciding question is whether the lambda must become a first-class value. If the background callback only invokes the lambda and never needs to outlive the call as a stored reference, crossinline is right: the body stays inlined, no Function object is allocated, and the inline function keeps its performance rationale. If you must retain the lambda — put it in a registry, return it, hand it to a non-inline scheduler that stores it — noinline is mandatory, accepting one allocation. Downstream, both modifiers strip non-local returns from callers, so consumers must use return@fn; document this. For a public API, consider whether forcing inlining is worth it at all: inlining a large body bloats every call site and leaks implementation into binaries (an ABI concern). Sometimes the cleaner contract is a non-inline function taking an ordinary lambda, reserving inline+crossinline for hot paths where allocation truly matters.
code
kotlin · 11 lines// crossinline: invoked from a background task, never stored
inline fun runOnPool(crossinline block: () -> Unit) {
pool.execute(Runnable { block() })
}
// noinline: must be retained for later use
inline fun subscribe(noinline onEvent: (Event) -> Unit): Subscription {
val sub = Subscription(onEvent)
registry += sub
return sub
}go deeper
Can say: store it -> noinline, just call it -> crossinline.
Explains the allocation difference and that both block non-local returns for consumers.
Weighs the trade-off per parameter and flags the documentation impact of losing non-local return.
Reasons holistically about call-site bloat, ABI/implementation leakage, and whether inline is even justified for the public contract, not just which modifier to use.
## Frame the decision around lambda lifetime The single most important question: **does the lambda need to exist as a value beyond the moment of invocation?** - **No — it is only invoked** (even if from a background `Runnable`, executor task, or nested lambda): use `crossinline`. The lambda body is inlined, no `kotlin.Function` object is allocated on the simple path, and you preserve the entire reason to use `inline`. - **Yes — it must be stored, returned, or passed to a non-inline API that keeps it**: use `noinline`. The lambda becomes a real `Function` object you can hold; you accept one allocation. ```kotlin // Only invoked from a background task -> crossinline inline fun runOnPool(crossinline block: () -> Unit) { pool.execute(Runnable { block() }) } // Must be retained for later cancellation -> noinline inline fun subscribe(noinline onEvent: (Event) -> Unit): Subscription { val s = Subscription(onEvent) // stored registry += s return s } ``` ## Consequences for callers Both modifiers **remove non-local returns**. A consumer can no longer write a bare `return` to exit their enclosing function from inside your lambda — only `return@yourFn`. This is a real ergonomic shift versus a plain inline lambda (think of how `forEach` allows non-local return). For a public API, **document it**, because it changes how idiomatic control-flow-style usage feels. ## Codegen and binary consequences - **Inlining bloats call sites.** Every call site gets a full copy of the inline body. A large inline function multiplied across many call sites increases bytecode/binary size and can hurt instruction-cache behavior. `crossinline` does not change this — it still inlines. - **noinline reintroduces an allocation** for that one parameter but keeps the rest of the function inlined. - **ABI/implementation leakage.** Because inline bodies are copied into consumers, they bake your implementation into compiled callers. Changing the body requires recompiling consumers to take effect — a stability concern for published libraries. Internal symbols referenced from a public inline function also have visibility implications. ## When to step back from inline entirely If the lambda is only invoked from a background callback and is not on a hot allocation-sensitive path, a **plain non-inline function taking an ordinary lambda** is often the cleaner public contract: no call-site bloat, no ABI leakage, and you can store the lambda freely. Reserve `inline` (with `crossinline`/`noinline` as needed) for genuinely hot code where eliminating per-call lambda allocation measurably matters. ## Decision checklist 1. Must the lambda be stored/returned as a value? -> `noinline`. 2. Only invoked, even indirectly, and inlining benefit is real? -> `crossinline`. 3. Body large or it's a stable public ABI? -> consider dropping `inline` altogether. 4. Either way, document the loss of non-local return. ## Keywords `inline`, `crossinline`, `noinline`, `kotlin.Function`, non-local `return`/`return@label`, SAM (`Runnable`), call-site inlining, ABI/binary size.
- Why might you avoid inline altogether for a public callback API?Inlining bloats every call site and bakes your implementation into compiled consumers (an ABI concern), so for non-hot paths a plain function with an ordinary lambda is a cleaner, more stable contract.
- Does crossinline reduce the call-site bloat of inlining?No. crossinline still inlines the body; it only removes non-local returns. The bloat is the same as a plain inline lambda.
saying these in an interview costs you the question
- Picking crossinline when the lambda must be stored (won't compile / wrong)
- Ignoring that both modifiers remove non-local returns for callers
- Claiming inline always improves performance regardless of body size
- Overlooking ABI/implementation-leakage of public inline functions
- Reaching for inline on cold paths just out of habit