Site icon JoinAppStudio

Inline Functions In Kotlin Explained | by Michal Ankiersztajn | Feb, 2024

Inlining functions causes the compiler to produce code at call sites instead of producing the function itself. It’s beneficial when you’re using lambdas in your code. It might be a little hard to understand at the moment; look at the example below:

Let’s look at the example to understand what happens under the hood:

fun load(onEnd: () -> Unit) {
// Load something here
onEnd()
}

// Use
fun main() {
load {
print("Done")
}
}

You might not know this, but doing so creates an object from this lambda. It’s the same as if you’ve had this written:

interface SingleAction {
operator fun invoke()
}

fun load(onEnd: SingleAction) {
// Load something here
onEnd()
}

// Use
fun main() {
load(
object : SingleAction {
override fun invoke() {
print("Done")
}
}
)
}

The effect will be the same because passing lambdas comes with a penalty of having to create an object.

It might look okay, but what if you call this function in a loop or multiple places in your codebase? Each call will create a new object, and it might affect your performance.

What if we inlined our function?

inline fun load(onEnd: () -> Unit) {
// Load something here
onEnd()
}

// Use
fun main() {
load {
print("Done")
}
}

Using the function is the same, but here’s what the compiler will produce:

fun main() {
// Load something here
print("Done")
}

The entire body function is moved to a place where it’s used. The compiler will produce code that doesn’t even have a load function in it. We aren’t creating any objects because we’re not passing any lambdas.

This way, we can use lambdas to their full potential without having performance issues. But there is a catch…

Inlining functions causes your code to grow. If it’s in more than 1 place, you must repeat the same code so it naturally grows. However, if done right, the payoff in performance will be amazing, especially at megamorphic call sites.

Another problem is that inlined lambdas can only be called inside inlined functions and passed as inlined arguments. This means a code like this, it’ll throw a compile error:

inline fun load(onHalfLoaded: () -> Unit, onEnd: () -> Unit) {
fetch(onHalfLoaded /* This causes compilation error */)
onEnd()
}

fun fetch(onHalfLoaded: () -> Unit) {
// Load
onHalfLoaded()
// Load
}

We can fix it by adding noinline keyword to the passed lambda in load function:

inline fun load(noinline onHalfLoaded: () -> Unit, onEnd: () -> Unit) {
fetch(onHalfLoaded /* This causes compilation error */)
onEnd()
}

This way, we still benefit from inlining onEnd and are still able to use fetch . Another way to fix this issue is to inline fetch instead:

inline fun fetch(onHalfLoaded: () -> Unit) {
// Load
onHalfLoaded()
// Load
}

Normally, we can return only locally in the function’s scope, but because our functions are inlined, we can return outside. For example:

inline fun load(onEnd: () -> Unit) {
// Load
onEnd()
}

fun main() {
load {
return // The main ends here
}
// Loaded is NOTprinted
print("Loaded")
}

To return in a scope of a function, we can do so with Kotlin Scope annotation:

fun main() {
load {
return@load
}
// Loaded IS printed
print("Loaded")
}

Sometimes, you’ll need to access the type of the passed parameter:

fun <T> checkType(data: Any) = data is T // Compilation error

But you can’t because the T type is erased, but what if we inlined it? At a call site, the type isn’t erased! This way, we can create a generic function and still know the type. Here’s the transformed function:

inline fun <reified T> checkType(data: Any) = data is T // Now it works

It allows You to create very generic functions.

Source link

Exit mobile version