Kotlin边用边学:Inline Functions的适用场景

Key Takeaways(划重点):

  • Collection自己提供的处理函数(forEach/map...)都支持inline,能用就别自己写循环
    • 在严格执行了上条后,你基本上就不怎么需要了解/写inline functions了
  • 升级第三方库后,务必重新编译你的项目
  • 自己写for/while循环,且循环的N值很大时,是使用inline function的一个切入点
  • 大函数不使用inline
  • 理解不了的时候看反编译生成的Java有奇效
  • 看源代码可以增进了解
Kotlin边用边学:Inline Functions的适用场景_第1张图片

基本介绍

Inline functions,中文大概就是内联/内嵌函数,字面的意思就是把内部(偷偷的)把被调用函数的代码连接(Copy)过来,具体看下代码和反编译的结果:

源代码:

fun main(args: Array) {
    val localGreeting = "Hello from main"

    Demo().withPublicField { println(localGreeting) }
}

class Demo() {
    val title = "title in demo"

    fun withPublicField(otherFun: () -> Unit) {
        println("Call from withPublicField, title: $title")
        otherFun()
    }
}

查看反编译的Java:(IntelliJ IDEA/Android Studio Tools -> Kotlin -> Show Kotlin Bytecode,别忘了点击Decompile按钮)

public final class MainKt {
   public static final void main(@NotNull String[] args) {
      final String localGreeting = "Hello";
      (new Demo()).withPublicField((Function0)(new Function0() {
         ...
         public final void invoke() {
            String var1 = localGreeting;
            System.out.println(var1);
         }
      }));
   }
}

public final class Demo {
   private final String title = "title_private";

   @NotNull
   public final String getTitle() { return this.title; }

   public final void withPublicField(@NotNull Function0 otherFun) {
      String var2 = "Call from withPublicField, title: " + this_$iv.getTitle();
      System.out.println(var2);
      otherFun.invoke();
   }
}

由于Java的function不是first-class member,所以其高阶函数(higher-order function)的使用,实际上存在着封装function成对应的wrapper class的实例,并对可见范围内(closure)的变量存在引用/访问。这实际上是以一定的资源损耗换来的便利性。对于我们上述的代码:

  • 第4行(Function0)(new Function0() {...}即是function转化成wrapper class实例

  • 第7行 String var1 = localGreeting;即是对可见范围变量的引用

在平常使用中,这个损耗是微小可忽略的,但如果是被一个N次(N值很大)循环中调用,那积少成多的损耗就是一个值得重视的问题了。

对于这个问题,Kotlin的处理方式是直接在编译期将原来的higher-order function的执行/实现代码,copy到调用处。具体看代码(唯一的改动点:原函数前添加了inline关键字):

    inline fun withPublicField(otherFun: () -> Unit) {
        ...
    }

然后查看生成的Java二进制:

    public final class MainKt {
       public static final void main(@NotNull String[] args) {
          String localGreeting = "Hello";
          Demo this_$iv = new Demo();
          String var3 = "Call from withPublicField, title: " + this_$iv.getTitle();
          System.out.println(var3);
          System.out.println(localGreeting);
       }
    }

注意对比两次生成的java代码:原先的L4~L10,现在的L4~L7。可以看到,新的Java代码很简单粗暴的把withPublicField的实现代码copy进入了main函数(自然移除了对withPublicField的调用)。

好处

  • 不需要额外创建wrapper class instance
  • 不需要维护可见范围内的变量引用/调用

代价肯定有的,不然就所有的方法都设成inline不就完了:-)

使用注意点

任何inline function的修改,必须重新编译后才生效

这个从之前反编译的代码可以推断出,毕竟调用inline function的代码已经被抹除,替换成了inline function的实现代码。重新编译才能再次重复这个替换过程。这个对于调用第三方库的inline函数尤其需要注意。

private变量

之前我们看到植入的代码中有这么一行代码(L5):

String var3 = "Call from withPublicField, publicTitle: " + this_$iv.getPublicTitle();

这里,getPublicTitle()如果是个private会怎样,相信大家可以猜测到。具体可以用代码验证:

class Demo() {
    private val title = "title in demo"
    ...
}

修改成private可见性后,代码编译出错(this_$iv.getPublicTitle()在main函数是不可见了)

当然inline函数的写法,除了这个变量的可见性还有其他,譬如:Non-local returns,但只要理解了其编译时的copy行为后,很多就水落石出了。(好的IDE其实还是给了很多有用的提示的,所以尽管写,让IDE去操这个心吧)

源代码查看/验证

Iterator.forEach

public inline fun  Iterable.forEach(action: (T) -> Unit): Unit {
    for (element in this) action(element)
}

可以看到,自带的Collection的函数都早已支持了inline了。所以,能使用自带的Collection函数就别写自己的循环了。

希望这篇博文能对你有所帮助,喜欢的话点个赞吧!

更多Kotlin的实用技巧,请参考《Kotlin边用边学系列》

Bonus

  • 一个轻巧的反编译GUI:bytecode-viewer
  • 关于inline functions带来的性能提升,可以看这篇文章:Effective Kotlin: Consider inline modifier for higher-order functions

你可能感兴趣的:(Kotlin边用边学:Inline Functions的适用场景)