Chapter 22: Inline Functions, Reified Types & Non-Local Returns
Inline Functions, Reified Types & Non-Local Returns — this is one of the most powerful, magical, and interview-favorite advanced features in Kotlin! ☕✨
These concepts are what make Kotlin feel like a super-language — they give you zero-cost abstractions, runtime type information, and beautiful control flow that Java can only dream of.
We’re going to go super slowly, like we’re sitting together in a quiet Mumbai café — I’ll explain every single concept with real-life analogies, complete runnable examples, step-by-step breakdowns, tables, common mistakes with fixes, and tons of practical code you can copy-paste and run right now.
Let’s dive in!
1. Inline Functions – Zero-Cost Abstractions
Inline functions tell the Kotlin compiler: “Instead of calling this function, just copy-paste its body at every call site.”
Result:
- No function call overhead (faster execution)
- No extra object allocation (better memory)
- Compiler can optimize aggressively (inlining + reification)
Syntax: Just add inline before fun
Example 1 – Basic inline function
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
inline fun sayHello(name: String) { println("नमस्ते $name!") println("How are you today?") } fun main() { sayHello("Webliance") sayHello("Amit") } |
What actually happens (compiler inlines it):
|
0 1 2 3 4 5 6 7 8 9 10 11 |
fun main() { println("नमस्ते Webliance!") println("How are you today?") println("नमस्ते Amit!") println("How are you today?") } |
No function call — just the code copied in!
When to use inline?
- Functions with lambdas (especially higher-order functions)
- Very small utility functions
- Performance-critical code
Important: inline is not always a win — for large functions, it can make your code bigger (code bloat).
2. Reified Type Parameters – Knowing the Type at Runtime
Normally in generics, type parameters are erased at runtime (T becomes Any?).
reified lets you keep the type information at runtime — only possible in inline functions.
Syntax: inline fun <reified T> …
Example 1 – Classic use case: isInstance
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
inline fun <reified T> isInstanceOf(obj: Any?): Boolean { return obj is T } fun main() { println(isInstanceOf<String>("Hello")) // true println(isInstanceOf<Int>("Hello")) // false println(isInstanceOf<List<String>>(listOf("a", "b"))) // true } |
Without reified → impossible:
|
0 1 2 3 4 5 6 7 8 |
fun <T> isInstanceOf(obj: Any?): Boolean { return obj is T // ERROR! Cannot check T at runtime } |
Real-world use cases (you’ll see these everywhere):
- Generic JSON parsing
- Repository: inline fun <reified T> findAll(): List<T>
- Type checks in DSLs
Example 2 – Generic repository pattern
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
data class User(val id: Int, val name: String) class Repository { private val data = listOf( User(1, "Webliance"), User(2, "Amit"), User(3, "Priya") ) inline fun <reified T> findAll(): List<T> { return data.filterIsInstance<T>() } } fun main() { val repo = Repository() val users: List<User> = repo.findAll() // Smart cast – List<User> println(users) // [User(1, Webliance), User(2, Amit), User(3, Priya)] val strings: List<String> = repo.findAll() // Empty list – safe } |
3. noinline & crossinline – Controlling Inlining of Lambdas
When you mark a function inline, all lambdas passed to it are also inlined by default. But sometimes you don’t want a lambda to be inlined (e.g., store it for later use).
A. noinline – Prevent inlining of a lambda
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
inline fun doSomething( action: () -> Unit, noinline callback: () -> Unit // Don't inline this one! ) { action() // Inlined // Store callback for later (e.g., in a list) callbacks.add(callback) } val callbacks = mutableListOf<() -> Unit>() fun main() { doSomething( action = { println("This is inlined!") }, callback = { println("This is stored, not inlined!") } ) // Later... callbacks.forEach { it() } } |
Why noinline? You can store the lambda in a variable, pass it to another function, etc.
B. crossinline – Allow inlining but prevent non-local return
crossinline is used when you pass the lambda to another inline function — it allows inlining but forbids non-local returns.
Example:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
inline fun runTwice(crossinline action: () -> Unit) { action() action() } fun main() { runTwice { println("Hello!") // return // ERROR! Non-local return forbidden because of crossinline } } |
Why crossinline? It prevents the lambda from returning from the outer function (non-local return) — which would break inlining.
4. Non-Local Returns – The Most Powerful (and Dangerous) Feature
In Kotlin, lambdas can return from the outer function (non-local return) — but only if the lambda is inlined!
Example – Non-local return
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
fun processList(list: List<Int>) { list.forEach { num -> if (num > 5) { println("Found big number: $num") return // Returns from processList() – not just from lambda! } println("Processing $num") } println("All done") // Never reached if big number found } fun main() { processList(listOf(1, 2, 8, 4)) } |
Output:
|
0 1 2 3 4 5 6 7 8 |
Processing 1 Processing 2 Found big number: 8 |
Non-local return works because forEach is inline — the compiler inlines the lambda body.
When non-local return is forbidden:
- Lambda passed to non-inline function
- Lambda marked noinline or crossinline
Example – Forbidden non-local return
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
fun processWithCallback(callback: () -> Unit) { callback() } fun main() { processWithCallback { println("Inside lambda") // return // ERROR! Non-local return not allowed } } |
How to do local return (return only from lambda):
|
0 1 2 3 4 5 6 7 8 9 10 11 |
list.forEach { num -> if (num > 5) { println("Found: $num") return@forEach // Local return – continue loop } } |
Quick Recap Table (Your Cheat Sheet)
| Feature | Syntax / Example | Key Benefit |
|---|---|---|
| inline | inline fun <T> doSomething(action: () -> T) | Zero-cost, reified types, non-local returns |
| reified | inline fun <reified T> isInstance(obj: Any) | Know type at runtime (no erasure) |
| noinline | inline fun foo(noinline callback: () -> Unit) | Store lambda without inlining |
| crossinline | inline fun bar(crossinline action: () -> Unit) | Allow inlining but forbid non-local return |
| Non-local return | return inside inlined lambda | Return from outer function |
| Local return | return@label or return@forEach | Return only from lambda |
Common Newbie Mistakes & Fixes
| Mistake | Problem | Fix |
|---|---|---|
| Using reified without inline | Compile error | reified only works in inline functions |
| Non-local return in non-inline lambda | Compile error | Use return@label or mark function inline |
| Forgetting crossinline | Non-local return allowed in cross-inline context | Add crossinline when passing to another inline function |
| Overusing inline on large functions | Code bloat (larger APK/binary) | Only inline small functions with lambdas |
| Using !! inside inlined lambda | Still crashes if null | Use safe calls or Elvis instead |
Homework for You (Let’s Make It Fun!)
- Basic Create inline function inline fun <reified T> printType(value: Any) → prints type name and value.
- Medium Create inline function inline fun <reified T> List<*>.findFirstOfType(): T? → returns first element of type T or null.
- Advanced Create inline function inline fun <reified T> runSafely(action: () -> T) → catches exceptions and returns null if fails.
- Fun Create inline fun <reified T> measureTime(action: () -> T) → prints time taken and returns result.
- Challenge Fix this code so it compiles and does non-local return:
Kotlin0123456789fun process(items: List<Int>) {items.forEach { if (it > 5) return }println("All small")}
You’ve just mastered Kotlin’s inline & reified magic — now you can write zero-cost, type-safe, and super-performant code!
