Inline functions in Kotlin ๐
Dive into inline functions in Kotlin and understand crossinline and noinline keywords.
Introduction ๐
In this article, we will discuss inline functions in Kotlin. Inline functions can be tricky to grasp, especially how to use them effectively in your code. We will aim to explain things as clearly as we can.
For the beginning let's try to understand these two simple functions
fun testInline() {
print("Hello")
foo {
print("World")
}
}
fun foo(print: () -> Unit) {
print()
}
Here we have a function called testInline. Inside this function, we utilize a higher-order function called foo with a lambda function named print as an argument. Since this code is quite straightforward, I will skip the explanation and just offer the definition of a lambda function (or anonymous function) from Wikipedia:
In computer programming, an anonymous function (function literal, lambda abstraction, lambda function, lambda expression or block) is a function definition that is not bound to an identifier. Anonymous functions are often arguments being passed to higher-order functions or used for constructing the result of a higher-order function that needs to return a function.
- Wikipedia
Now what we really care about the above code is the compiled code.
So, first things first, in Android Studio, if you click Tools -> Kotlin -> Show Kotlin byteCode, a new window will open. If we press the Decompile button, we will likely see something like this:
public final void testInline() {
String var1 = "Hello";
System.out.print(var1);
((Scratch)this).foo((Function0)null.INSTANCE);
}
public final void foo(@NotNull Function0 print) {
Intrinsics.checkNotNullParameter(print, "print");
print.invoke();
}
This code is the equivalent java code that produced by the compiler. Now if we check this line:
((Scratch)this).foo((Function0)null.INSTANCE);
we can see that the same code in java in order to handle the higher-order function foo, the compiler generates a new object every time foo is called. Performance wise this is not a great option because it affects the memory by new object memory allocations.
Inline
In order to overcome this memory overhead Kotlin provides us with the inline keyword. So we can add this keyword in the beginning of foo function like this:
inline fun foo(print: () -> Unit) {
print()
}
With the help of inline keyword the compiler instead of generating new objects each time foo is called, it copy the actual code of foo.print() and replace the new Function call.
Let's provide an example code to understand it better:
public final void testInline() {
String var1 = "Hello";
System.out.print(var1);
Scratch this_$iv = (Scratch)this;
String var4 = "World";
System.out.print(var4);
}
public final void foo(@NotNull Function0 print) {
Intrinsics.checkNotNullParameter(print, "print");
print.invoke();
}
As we can see instead of the ((Scratch)this).foo((Function0)null.INSTANCE) line, compiler replace it with the actual code of print function
String var4 = "World";
System.out.print(var4);
When to use inline
Inline functions reduce memory overhead by reducing function calls, especially when using higher-order functions inside loops. However, this reduction comes with a trade-off. It's best to inline only small functions. Inlining a large function called from multiple points in the code can significantly increase the size of the generated code.
Crossinline
If we want to return from a higher-order function we must use return with a label like this: return@functionName. This is a local return because it exits only from the higher order function. Non local return is a bare return without any label and it exits the enclosing function instead of anonymous function.
If we try to call a non inline function from an inline function we are going to get a complain from compiler that we can't use inline function here because it may contains non local returns
In order to solve this issue, Kotlin provides us with crossinline keyword. We can use crossinline keyword to lambda parameter inside the inline foo function.
fun testInline() {
print("Hello")
foo {
print("World")
}
}
inline fun foo(crossinline print: () -> Unit) {
foo2 {
print()
}
}
fun foo2(printNoInline: () -> Unit) {
printNoInline()
}
In the above example we added a new "foo2" function which is not indicated as inline and we call it inside foo which is an inline function. In order to pass lambda print() inside foo we have to mark print as crossinline. Under the hood the compiler this time is initialise a new function (for foo2) inside foo to address the issue with non local returns because foo2 is no inline.
public final void foo(@NotNull final Function0 print) {
int $i$f$foo = 0;
Intrinsics.checkNotNullParameter(print, "print");
((Scratch)this).foo2((Function0)(new Function0() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke() {
this.invoke();
return Unit.INSTANCE;
}
public final void invoke() {
print.invoke();
}
}));
}
Noinline
If we have a function with multiple lambda parameters and we want to add inline keyword to this function then all the parameters will be indicated as inline.
inline fun foo(print: () -> Unit, print2: () -> Unit) {
print()
print2()
}
So in the above code print and print 2 are inlined lambda functions. In many cases we don't want to inline every parameter inside a function. For example let's assume that the print2 is a large function with many lines of code. In this case we can declare print2 as no inline like this
fun testInline() {
foo( { print("Hello") } ) {
print("World")
}
}
inline fun foo(print: () -> Unit, noinline print2: () -> Unit) {
print()
print2()
}
In this case the compiler will handle print as an inline parameter and print2 as a noinline parameter.
Let's check the generated code
public final void testInline() {
Scratch var1 = (Scratch)this;
Function0 print2$iv = (Function0)null.INSTANCE;
int $i$f$foo = false;
int var4 = false;
String var5 = "Hello";
System.out.print(var5);
print2$iv.invoke();
}
As we can see the compiler creates a new Function only for print2 lambda parameter
Function0 print2$iv = (Function0)null.INSTANCE;
print2$iv.invoke();
Instead the print1 is treated like an inline parameter
String var5 = "Hello";
System.out.print(var5);
Use cases in Android ๐๏ธ ๐๏ธ
Sometimes it's really useful to add inline in extension functions in order to reduce runtime overhead. Especially in conjunction with refied keyword.
I attach here some examples:
// this code give us the ability to directly use TAG in any object
// in our code and return the class name
inline val <reified T> T.TAG: String
get() = T::class.java.simpleName
// this generic extension function adds a generic object T in
// lru cache
inline fun <reified T : Any> T?.saveToCache(
applicationContext: BaseApplication,
objectId: String?
): Boolean =
this?.let { object ->
objectId?.let { id ->
applicationContext.lruCache.put(id, object)
true
} ?: false
} ?: false
// it's useful to add inline in a loop function to reduce the memory
// allocations
inline fun List<String>.loopList(name: (String) -> Unit) {
this.forEach {
name(it)
}
}
Conclusion ๐ฅ
So we have to keep in mind that it's a good idea to use inline:
if the outer function includes a lambda function as a parameter and it is relatively small โ
if we have a small lambda function inside a for loop โ
Inline with no functional (lambda) types โ
You can also check this video about inline functions ๐ฅ ๐ฌ
Send your feedback ๐
Feel free to share your feedback with us and let us know if this article has been helpful to you.
Thank you for taking the time to read. Happy coding! ๐๐ผ