Kotlin邊用邊學:Inline Functions的適用場景

weixin_34232744發表於2018-07-06

Key Takeaways(劃重點):

  • Collection自己提供的處理函式(forEach/map...)都支援inline,能用就別自己寫迴圈
    • 在嚴格執行了上條後,你基本上就不怎麼需要了解/寫inline functions了
  • 升級第三方庫後,務必重新編譯你的專案
  • 自己寫for/while迴圈,且迴圈的N值很大時,是使用inline function的一個切入點
  • 大函式不使用inline
  • 理解不了的時候看反編譯生成的Java有奇效
  • 看原始碼可以增進了解
4233074-c2cbbf3b0acb1156.jpg

基本介紹

Inline functions,中文大概就是內聯/內嵌函式,字面的意思就是把內部(偷偷的)把被呼叫函式的程式碼連線(Copy)過來,具體看下程式碼和反編譯的結果:

原始碼:

fun main(args: Array<String>) {
    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 <T> Iterable<T>.forEach(action: (T) -> Unit): Unit {
    for (element in this) action(element)
}

可以看到,自帶的Collection的函式都早已支援了inline了。所以,能使用自帶的Collection函式就別寫自己的迴圈了。

希望這篇博文能對你有所幫助,喜歡的話點個贊吧?!

更多Kotlin的實用技巧,請參考《Kotlin邊用邊學系列

Bonus

相關文章