Sean Eli
Sean Eli

Reputation: 53

why Kotlin inline function params is must not be null

enter image description here

inline fun <T, R> isNullObject(value: T?, notNullBlock: (T) -> R, isNullBlock: (() -> Unit)? = null) {

if (value != null) {
    notNullBlock(value)
} else {
    if(isNullBlock != null){
        isNullBlock()
    }
}

}

I tried to write some higher-order functions to facilitate development, but it is error

Upvotes: 5

Views: 1796

Answers (2)

Jenea Vranceanu
Jenea Vranceanu

Reputation: 4694

There is a great post explaining how inline works from Android Developer Advocate Florina Muntenescu. Following all of the explanations it should be clear why nullable lambdas are not allowed.

In short:

Because of the inline keyword, the compiler copies the content of the inline function to the call site, avoiding creating a new Function object.

That is the performance benefit inline keyword gives us. But in order to do that compiler must be sure that you always pass in a lambda argument whether it is empty or not. When you try to make the lambda argument nullable compiler will not be able to copy the content of a null lambda to the call site. Similarly, you cannot execute compare operations like != null or use ? to unwrap the optional lambda that should be inlined because when compiled there will be no lamda/function objects. More explanation below.

Example (long explanation)

In my examples your function is updated and takes empty lambda as default arguments for isNullBlock:

inline fun <T, R> isNullObject(value: T?, notNullBlock: (T) -> R, isNullBlock: (() -> Unit) = {}) {
    if (value != null) {
        notNullBlock(value)
    } else {
        isNullBlock()
    }
}

Here is usage of not inlined version of your isNullObject function decompiled to Java.

Kotlin code

class Test {
    init {
        isNullObject(null as? Int,
            {
                println("notNullBlock called")
                it
            },
            { println("isNullBlock called") })
        isNullObject(0, 
            {
                println("notNullBlock called")
                it
            },
            { println("isNullBlock called") })
    }
}

Decompiled Java code

public final class Test {
   public Test() {
      TestKt.isNullObject((Integer)null, (Function1)null.INSTANCE, (Function0)null.INSTANCE);
      TestKt.isNullObject(0, (Function1)null.INSTANCE, (Function0)null.INSTANCE);
   }
}

As you can see nothing too unusual happens (though, it is hard to understand what null.INSTANCE is). Your isNullObject function called with three arguments passed as defined in Kotlin.

Here is how your inlined function will decompile using the same Kotlin code.


public final class Test {
   public Test() {
      Object value$iv = (Integer)null;
      int $i$f$isNullObject = false;
      int var3 = false;
      String var4 = "isNullBlock called";
      boolean var5 = false;
      System.out.println(var4);
      int value$iv = false;
      $i$f$isNullObject = false;
      int var8 = false;
      String var9 = "notNullBlock called";
      boolean var6 = false;
      System.out.println(var9);
   }
}

For the first function call, we immediately get if (value != null) statement resolved as false and notNullBlock passed in is not even ended up in the final code. At runtime, there will be no need to check each time if the value is null or not. Because the isNullObject is inlined with its lambdas there is no Function objects generated for lambda arguments. It means there is nothing to check for nullability. Also, this is the reason why you cannot hold a reference to the lambda/function arguments of the inlined function.

Object value$iv = (Integer)null;
int $i$f$isNullObject = false;
int var3 = false;
String var4 = "isNullBlock called";
boolean var5 = false;
System.out.println(var4);

But inlining works only if compiler is able to get values of given arguments at compile time. If instead of isNullObject(null as? Int, ...) and isNullObject(0, ...) the first argument was a function call - inlining would give no benefit!

When compiler cannot resolve if statement

A function added - getValue(). Returns optional Int. The compiler does not know the result of getValue() call ahead of time as it can be calculated only at runtime. Thus inlining does only one thing - copies full content of the isNullObject into Test class constructor and does it twice, for each function call. There is a benefit still - we get rid of 4 Functions instances created at runtime to hold the content of each lambda argument.

Kotlin

class Test {
    init {
        isNullObject(getValue(),
            {
                println("notNullBlock called")
                it
            },
            { println("isNullBlock called") })
        isNullObject(getValue(),
            {
                println("notNullBlock called")
                it
            },
            { println("isNullBlock called") })
    }

    fun getValue(): Int? {
        if (System.currentTimeMillis() % 2 == 0L) {
            return 0
        } else {
            return null
        }
    }
}

Decompiled Java

public Test() {
      Object value$iv = this.getValue();
      int $i$f$isNullObject = false;
      int it;
      boolean var4;
      String var5;
      boolean var6;
      boolean var7;
      String var8;
      boolean var9;
      if (value$iv != null) {
         it = ((Number)value$iv).intValue();
         var4 = false;
         var5 = "notNullBlock called";
         var6 = false;
         System.out.println(var5);
      } else {
         var7 = false;
         var8 = "isNullBlock called";
         var9 = false;
         System.out.println(var8);
      }

      value$iv = this.getValue();
      $i$f$isNullObject = false;
      if (value$iv != null) {
         it = ((Number)value$iv).intValue();
         var4 = false;
         var5 = "notNullBlock called";
         var6 = false;
         System.out.println(var5);
      } else {
         var7 = false;
         var8 = "isNullBlock called";
         var9 = false;
         System.out.println(var8);
      }

   }

Upvotes: 4

Sergio
Sergio

Reputation: 30655

I think it is related to how inline functions and lambdas passed to it are inlined. The inline modifier affects both the function itself and the lambdas passed to it: all of those will be inlined into the call site. It seems Kotlin doesn't allow to use nullable lambdas.

If you want some default value for isNullBlock parameter you can use empty braces isNullBlock: () -> Unit = {}:

inline fun <T, R> isNullObject(value: T?, notNullBlock: (T) -> R, isNullBlock: () -> Unit = {}) {

    if (value != null) {
        notNullBlock(value)
    } else {
        isNullBlock()
    }
}

Upvotes: 6

Related Questions