Understanding Inline Functions in Kotlin: A Beginner’s Guide

Table of Contents

Kotlin is well known for its concise syntax, expressive features, and seamless support for functional programming. One of its powerful yet often misunderstood features is inline functions. If you’ve ever worked with higher-order functions in Kotlin, you’ve probably encountered situations where performance becomes a concern due to lambda expressions. This is where inline functions in Kotlin come to the rescue!

In this guide, we’ll break down everything you need to know about inline functions, why they exist, how they work and improve performance, and when (or when not) to use them. Whether you’re a beginner or an experienced developer looking for a refresher, this post will help you grasp inline functions in Kotlin with clear explanations and practical examples.

What Are Inline Functions in Kotlin?

Before diving into the details, let’s start with a simple definition.

In Kotlin, an inline function is a function whose body is copied (or “inlined”) at every place it’s called during compilation. This eliminates the overhead of function calls, making the code more efficient, especially when working with lambda functions.

Here’s a basic example of an inline function:

Kotlin
inline fun greet(name: String) {
    println("Hello, $name!")
}

fun main() {
    greet("amol") // Output: Hello, amol!
}

In this case, the function greet() is marked as inline, which means that when greet("amol") is called, the compiler replaces it with:

Kotlin
println("Hello, amol!")

This eliminates the function call and directly places the function body in the main() function, improving efficiency.

Why Were Inline Functions Introduced in Kotlin?

In Kotlin, inline functions can help remove the overhead associated with lambdas and improve performance. When you use a lambda expression, it is typically compiled into an anonymous class. This means that each time you use a lambda, an additional class is created. Moreover, if the lambda captures variables, a new object is created for each invocation. As a result, using Lambdas can introduce runtime overhead and make the implementation less efficient compared to directly executing the code.

To mitigate this performance impact, Kotlin provides the inline modifier for functions. When you mark a function with inline, the compiler replaces every call to that function with the actual code implementation, instead of generating a function call. This way, the overhead of creating additional classes and objects is avoided.

Let’s see a simple example to illustrate this:

Kotlin
inline fun multiply(a: Int, b: Int): Int {
    return a * b
}

fun main() {
    val result = multiply(2, 3)
    println(result)
}

In this example, the multiply function is marked as inline. When you call multiply(2, 3), the compiler replaces the function call with the actual code of the multiply function:

Kotlin
fun main() {
    val result = 2 * 3  // only for illustrating purposes, later we will see how it actually works 
    println(result)
}

This allows the code to execute the multiplication directly without the overhead of a function call.

Let’s see one more example to illustrate this:

Kotlin
inline fun performOperation(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
    return operation(a, b)
}

fun main() {
    val result = performOperation(5, 3) { x, y -> x + y }
    println(result)
}

In this example, the performOperation function is marked as inline. It takes two integers, a and b, and a lambda expression representing an operation to be performed on a and b. When performOperation is called, instead of generating a function call, the compiler directly replaces the code inside the function with the code from the lambda expression.

So, in the main function, the call to performOperation(5, 3) will be replaced with the actual code 5 + 3. This eliminates the overhead of creating an anonymous class and improves performance.

BTW, How inlining works actually?

When you declare a function as inline in Kotlin, its body is substituted directly into the places where the function is called, instead of being invoked as a separate function. This substitution process is known as inlining.

Let’s take a look at an example to understand it more:

Kotlin
inline fun <T> synchronized(lock: Lock, action: () -> T): T {
    lock.lock()
    try {
        return action()
    } finally {
        lock.unlock()
    }
}

In this example, the synchronized function is declared as inline. It takes a Lock object and a lambda action as parameters. The function locks the Lock object, executes the provided action lambda, and then releases the lock.

When you use the synchronized function, the code generated for every call to it is similar to a synchronized statement in Java.

Here’s an example of usage:

Kotlin
fun foo(l: Lock) {
    println("Before sync")
    synchronized(l) {
        println("Action")
    }
    println("After sync")
}

The equivalent code, which will be compiled to the same bytecode, is:

Kotlin
fun foo(l: Lock) {
    println("Before sync")
    l.lock()
    try {
        println("Action")
    } finally {
        l.unlock()
    }
    println("After sync")
}
The compiled version of the foo function

In this case, the lambda expression passed to synchronized is substituted directly into the code of the calling function. The bytecode generated from the lambda becomes part of the definition of the calling function and is not wrapped in an anonymous class implementing a function interface.

Not inlined Case (passing lambda as a parameter)

It’s worth noting that if you call an inline function and pass a parameter of a function type from a variable, rather than a lambda directly, the body of the inline function is not inlined.

Here’s an example:

Kotlin
class LockOwner(val lock: Lock) {
    fun runUnderLock(body: () -> Unit) {
        synchronized(lock, body)    // A variable of a function type is passed as an argument, not a lambda.
    }
}

In this case, the lambda’s code is not available at the site where the inline function is called, so it cannot be inlined. The body of the runUnderLock function is not inlined because there’s no lambda at the invocation. Only the body of the synchronized function is inlined; the lambda is called as usual. The runUnderLock function will be compiled to bytecode similar to the following function:

Kotlin
class LockOwner(val lock: Lock) {
    fun __runUnderLock__(body: () -> Unit) {  // This function is similar to the bytecode the real runUnderLock is compiled to
        lock.lock()
        try {
            body()    // The body isn’t inlined, because there’s no lambda at the invocation.
        } finally {
            lock.unlock()
        }
    }
}

Here, the body of the runUnderLock function cannot be inlined because the lambda is passed as a parameter from a variable (body) rather than directly providing a lambda expression.

Suppose when you pass a lambda as a parameter directly, like this:

Kotlin
lockOwner.runUnderLock {
    // code block A
}

The body of the inline function runUnderLock can be inlined, as the compiler knows the exact code to replace at the call site.

However, when you pass a lambda from a variable, like this:

Kotlin
val myLambda = {
    // code block A
}
lockOwner.runUnderLock(myLambda)

The body of the inline function cannot be inlined because the compiler doesn’t have access to the code inside the lambda (myLambda) at the call site. It would require the compiler to know the contents of the lambda in order to inline it.

In such cases, the function call behaves like a regular function call, and the body of the function is not copied to the call site. Instead, the lambda is passed as an argument to the function and executed within the function’s context.

So, suppose even though the runUnderLock function is marked as inline, the body of the function won’t be inlined because the lambda is passed as a parameter from a variable.

What about multiple inlining?

If you have two uses of an inline function in different locations with different lambdas, each call site will be inlined independently. The code of the inline function will be copied to both locations where you use it, with different lambdas substituted into it.

If you have multiple calls to the inline function with different lambdas, like this:

Kotlin
lockOwner.runUnderLock {
    // code block A
}

lockOwner.runUnderLock {
    // code block B
}

Each call site will be inlined independently. The code of the inline function will be copied to both locations where you use it, with different lambdas substituted into it. This allows the compiler to inline the code at each call site separately.

Restrictions on inline functions

When a function is declared as inline in Kotlin, the body of the lambda expression passed as an argument is substituted directly into the resulting code. However, this substitution imposes certain restrictions on how the corresponding parameter can be used in the function body.

If the parameter is called directly within the function body, the code can be easily inlined. But if the parameter is stored for later use, the code of the lambda expression cannot be inlined because there must be an object that contains this code.

In general, the parameter can be inlined if it’s called directly or passed as an argument to another inline function. If it’s used in a way that prevents inlining, such as storing it for later use, the compiler will prohibit the inlining and show an error message stating “Illegal usage of inline-parameter.”

Let’s consider an example with the Sequence.map function:

Kotlin
fun <T, R> Sequence<T>.map(transform: (T) -> R): Sequence<R> {
    return TransformingSequence(this, transform)
}

The map function doesn’t call the transform function directly. Instead, it passes the transform function as a constructor parameter to a class (TransformingSequence) that stores it in a property. To support this, the lambda passed as the transform argument needs to be compiled into the standard non-inline representation, which is an anonymous class implementing a function interface.

Why Use Inline Functions in Kotlin?

Inline functions are particularly useful in scenarios where:

  1. Performance Optimization: Reduces function call overhead, making execution faster.
  2. Higher-Order Functions: When passing lambda functions, inline functions prevent unnecessary object creation.
  3. Avoiding Anonymous Object Allocations: Reduces memory allocations by avoiding extra objects for lambdas.

When Not to Use Inline Functions

Although inline functions in Kotlin are beneficial, overusing them can lead to code bloat (increased code size). Here are some situations where you should avoid using inline functions:

  1. Large Function Bodies: If the function body is large, inlining it multiple times will increase the APK size.
  2. Non-Performance Critical Code: For functions that don’t need optimization, inlining may be unnecessary.
  3. Recursion: Recursive inline functions are not allowed in Kotlin since they would cause infinite expansion.

Performance Benefits of Inline Functions

The key benefits of inline functions in Kotlin include:

  • Reduces function call overhead – No need for extra method calls.
  • Eliminates unnecessary object creation – No wrapper objects for lambda functions.
  • Improves performance for high-order functions – Especially beneficial when passing multiple lambdas.

This is just a part..! Get the full insights here: [Main Article URL]

Conclusion

Inline functions in Kotlin are a powerful tool for improving performance, especially when working with higher-order functions and lambda expressions. By eliminating function call overhead and avoiding unnecessary object creation, they help make Kotlin code more efficient.

However, inline functions should be used carefully. Overuse can lead to increased bytecode size, making your application slower rather than faster. The key is to strike a balance—use them where they add value but avoid excessive inlining.

Skill Up: Software & AI Updates!

Receive our latest insights and updates directly to your inbox

Related Posts

error: Content is protected !!