为什么 Kotlin 内联函数参数不能为空

why Kotlin inline function params is must not be null

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

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

}

我试着写了一些高阶函数方便开发,但是报错

我认为这与 inline functions 和传递给它的 lambda 的内联方式有关。 inline 修饰符影响函数本身和传递给它的 lambda:所有这些都将内联到调用站点。似乎 Kotlin 不允许使用 nullable lambdas.

如果您想要 isNullBlock 参数的一些默认值,您可以使用空括号 isNullBlock: () -> Unit = {}:

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

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

great post 解释了 inline 的工作原理 Android Developer Advocate Florina Muntenescu。在所有解释之后,应该清楚为什么不允许为空的 lambda。

简而言之:

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

这就是内联关键字给我们带来的性能优势。但是为了做到这一点,编译器必须确保您始终传入一个 lambda 参数,无论它是否为空。当您尝试使 lambda 参数可为空时,编译器将无法将 null lambda 的内容复制到调用站点。同样,您不能执行 != null 之类的比较操作或使用 ? 来解包应该内联的可选 lambda 因为编译时不会有 lamda/function 对象 .下面有更多解释。

示例(详细解释)

在我的示例中,您的函数已更新并采用空 lambda 作为 isNullBlock:

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

这里是 非内联 版本的 isNullObject 函数反编译为 Java.

的用法

Kotlin 代码

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

反编译Java代码

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);
   }
}

如您所见,没有发生任何异常情况(尽管很难理解 null.INSTANCE 是什么)。您的 isNullObject 函数使用 Kotlin 中定义的三个参数调用。

以下是您的 inlined 函数将如何使用相同的 Kotlin 代码进行反编译。


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);
   }
}

对于第一个函数调用,我们立即将 if (value != null) 语句解析为 false 并且传入的 notNullBlock 甚至没有在最终代码中结束。在运行时,不需要每次都检查该值是否为 null。因为 isNullObject 与其 lambda 内联,所以没有为 lambda 参数生成 Function 对象。这意味着无需检查可空性。此外,这就是为什么 你不能引用内联函数 lambda/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);

但是只有当编译器能够在编译时获取给定参数的值时,内联才有效。如果第一个参数不是 isNullObject(null as? Int, ...)isNullObject(0, ...) 而是函数调用 - 内联不会带来任何好处!

当编译器无法解析 if 语句时

添加了一个功能 - getValue()。 Returns 可选 Int。编译器无法提前知道 getValue() 调用的结果,因为它只能在运行时计算。因此,内联只做一件事——将 isNullObject 的全部内容复制到 Test class 构造函数中,并为每个函数调用执行两次。还有一个好处 - 我们摆脱了在运行时创建的 4 Functions 个实例来保存每个 lambda 参数的内容。

科特林

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
        }
    }
}

反编译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);
      }

   }