Inline functions and value classes

Kotlin · Expert · 14 min

These features explain the zero-cost abstractions of the standard library and let you write type-safe IDs cheaply.

Every lambda passed to a normal function becomes an object (and capturing lambdas allocate on each call), plus a virtual call. An inline function avoids this: the compiler copies the function body and the lambda's code into each call site.

inline fun <T> measure(block: () -> T): T { val t = System.nanoTime(); return block().also { println(System.nanoTime() - t) } }

Effects of inlining:

  • No lambda allocation or call overhead; this is why map, filter, let, use cost nothing extra.
  • Non-local returns: a bare return inside an inlined lambda returns from the enclosing function.
  • Reified type parameters become possible because the actual type is known at each call site.

Modifiers on lambda parameters: noinline keeps one lambda as an object (so you can store or pass it on); crossinline forbids non-local returns, required when the lambda is called from another context such as a nested object or another lambda. Inline only small functions that take lambdas; inlining big bodies bloats bytecode.

A value class wraps a single value with a distinct type, usually without allocation:

@JvmInline value class UserId(val raw: Long)

At runtime a UserId is mostly just a long, but the compiler won't let you pass an OrderId where a UserId is expected. It gets boxed when used as a nullable, generic or interface type. Names of functions taking value classes are mangled in bytecode, which matters for Java callers.

Example

@JvmInline value class UserId(val raw: Long) { init { require(raw > 0) } }
@JvmInline value class OrderId(val raw: Long)

fun loadUser(id: UserId) = "user #${id.raw}"

inline fun <T> timed(label: String, block: () -> T): T {
    val start = System.nanoTime()
    return block().also { println("$label took ${(System.nanoTime() - start) / 1_000_000} ms") }
}
inline fun runTwice(crossinline action: () -> Unit) {
    val r = Runnable { action() }                 // called from another context
    r.run(); r.run()
}
fun firstNegative(xs: List<Int>): Int? {
    xs.forEach { if (it < 0) return it }          // non-local return (forEach is inline)
    return null
}
fun main() {
    println(loadUser(UserId(42)))                 // user #42
    // loadUser(OrderId(42))                      // compile error: wrong type
    val sum = timed("sum") { (1..1000).sum() }    // sum took N ms
    println(sum)                                  // 500500
    runTwice { print("x") }; println()            // xx
    println(firstNegative(listOf(3, -1, -5)))     // -1
}

Remember

  • inline copies the function and lambda bodies to the call site: no allocations
  • Inlined lambdas allow non-local return and reified types
  • noinline keeps a lambda as an object; crossinline forbids non-local returns
  • Value classes give type safety for wrapped values, usually without allocation

Watch out for

  • Marking large functions without lambda parameters inline, bloating bytecode for no benefit.

Check yourself

  1. Why is crossinline needed when an inline function calls its lambda inside a Runnable?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: kotlinlang.org/docs/inline-functions.html