天天看點

kotlin内聯函數inline、noinline、crossinline

文章目錄

    • 高階函數
    • 内聯函數
      • inline
      • noinline
      • crossinline
    • 總結
    • 參考

高階函數

首先來看下kotlin裡的高階函數定義:如果一個函數接收另一個函數作為參數,或傳回類型是一個函數類型,那麼該函數被稱為是高階函數。

比如下面的代碼:

private fun highFuc(name: String, block: (String) -> Unit) {
    block(name)
}
           

其中

highFuc

是函數名,函數中傳入了2個參數,第一個參數為

String

類型,第二個參數即是函數類型,->左邊的部分用來聲明該函數接收什麼參數的,多個參數之間用逗号隔開,如果沒有參數直接使用()表示就可以了;->右邊表示該函數的傳回值是什麼類型,如果沒有傳回值直接使用

Unit

即可。

内聯函數

内聯函數,顧名思義,就是在編譯時将作為函數參數的函數體直接映射到函數調用處,直接用一個例子來說明:

fun requestInfo() {
    getStr()
}

fun getStr() {
    println("inline")
}
           

很簡單,getStr()中列印了一個字元串,然後requestInfo()中調用了getStr()函數,将上述代碼轉換成java代碼之後:

public final void requestInfo() {
   this.getStr();
}

public final void getStr() {
   String var1 = "inline";
   System.out.println(var1);
}
           

繼續,在getStr()的前面加上inline聲明,如下:

fun requestInfo() {
    getStr()
}

//普通函數中并不推薦加inline關鍵字
inline fun getStr() {
    println("inline")
}
           

轉換成java之後:

public final void requestInfo() {
   String var3 = "inline";
   System.out.println(var3);
}
           

可以看到轉換成java之後的代碼有明顯的差別:加上inline之後,getStr()中的函數内容直接“複制粘貼”到requestInfo()中,即内聯到函數調用處了。

inline

通過上面的例子,inline的作用就很明顯了,就是在編譯時直接将函數内容直接複制粘貼到調用處。

我們知道函數調用最終是通過JVM操作數棧的棧幀完成的,每一個方法從調用開始至執行完成的過程,都對應着一個棧幀在虛拟機棧裡從入棧到出棧的過程,使用了inline關鍵字理論上可以減少一個棧幀層級。

那麼是不是所有的函數前面都适合加上inline關鍵字了呢?答案是否定的,其實JVM本身在編譯時,就支援函數内聯,并不是kotlin中特有的,那麼kotlin中什麼樣的函數才需要使用inline關鍵字呢?答:高階函數!

隻有高階函數中才需要inline去做内聯優化,普通函數并不需要,如果在普通函數強行加上inline,編輯器會立刻提醒:

Expected performance impact from inlining is insignificant. Inlining works best for functions with parameters of functional types
           

意思是 内聯對普通函數性能優化的預期影響是微不足道的。内聯最适合帶有函數類型參數的函數

為什麼高階函數要使用inline

inline優化了什麼問題呢?因為我們使用的Lambda表示式在編譯轉換後被換成了匿名類的實作方式。

fun requestInfo() {
    highFuc("inline") { str ->
        println(str)
    }
}

fun highFuc(name: String, block: (String) -> Unit) {
    block(name)
}
           

轉換成java之後:

public final void requestInfo() {
   this.highFuc("inline", (Function1)null.INSTANCE);
}

private final void highFuc(String name, Function1 block) {
   block.invoke(name);
}

public interface Function1<in P1, out R> : Function<R> {
    public operator fun invoke(p1: P1): R
}
           

是以函數參數最終會轉換成interface,并通過建立一個匿名執行個體來實作。這樣就會造成額外的記憶體開銷。為了解決這個問題,kotlin引入inline内聯功能,将Lambda表達式帶來的性能開銷消除。還是上面的例子,這次我們對高階函數添加inline關鍵字:

fun requestInfo() {
    highFuc("inline") { str ->
        println(str)
    }
}

//注意:這裡添加了inline關鍵字
inline fun highFuc(name: String, block: (String) -> Unit) {
    block(name)
}
           

轉換成java之後:

public final void requestInfo() {
   String name$iv = "inline";
   System.out.println(name$iv);
}
           

noinline

當函數被inline标記時,使用noinline可以使函數參數不被内聯。

fun requestInfo() {
    highFuc({
        println("noinline")
    }, {
        println("inline")
    })
}

//highFuc被inline修飾,而函數參數block0()使用了noinline修飾
inline fun highFuc(noinline block0: () -> Unit, block1: () -> Unit) {
    block0()
    block1()
}
           

轉換成java之後:

public final void requestInfo() {
   Function0 block0$iv = (Function0)null.INSTANCE;
   block0$iv.invoke();
   
   String var5 = "inline";
   System.out.println(var5);
}
           

結果也很明顯,block0()函數沒有被内聯,而block()函數被内聯,這就是noinline的作用。

如果想在非内聯函數Lambda中直接return怎麼辦?比如我想這麼寫:

fun requestInfo() {
    highFuc {
        return //錯誤,不允許在非内聯函數中直接return
    }
}

fun highFuc(block: () -> Unit) {
    println("before")
    block()
    println("after")
}
           

對不起,不允許!會直接在

return

的地方報**‘return’ is not allowed here**錯誤。 但是可以寫成

[email protected]

,即:

fun requestInfo() {
    highFuc {
        [email protected] //正确,局部傳回
    }
}

fun highFuc(block: () -> Unit) {
    println("before")
    block()
    println("after")
}
           

其中

return

是全局傳回,會影響Lambda之後的執行流程;而

[email protected]

是局部傳回,不會影響Lambda之後的執行流程。如果我就想全局傳回,那麼可以通過inline來進行聲明:

fun requestInfo() {
    highFuc {
        return 
    }
}

inline fun highFuc(block: () -> Unit) {
    println("before")
    block()
    println("after")
}
           

因為

highFuc

通過

inline

聲明為内聯函數,是以調用方可以直接使用

return

進行全局傳回,執行

requestInfo()

的結果:

before
           

可以看到

Lambda

之後的

after

并沒有被執行,因為是全局傳回,當然可以改成

[email protected]

局部傳回,這樣就可以都執行了。

結論:内聯函數所引用的Lambda表達式中是可以使用return關鍵字來進行函數傳回的,而非内聯函數隻能進行局部傳回。

現在有一種場景,我既想使用

inline

優化高階函數,同時又不想調用方打斷我的執行流程(因為inline是支援全局return的),貌似沖突了,這時候怎麼辦呢,這時候就需要

crossinline

了。

crossinline

允許inline内聯函數裡的函數類型參數可以被間接調用,但是不能在Lambda表達式中使用全局return傳回。

fun requestInfo() {
    highFuc {
        return //錯誤,雖然是inline内聯函數,但Lambda中使用crossinline修飾,是以不允許全局傳回了
    }
}

inline fun highFuc(crossinline block: () -> Unit) {
    println("before")
    block()
    println("after")
}
           

crossinline

關鍵字就像一個契約,它用于保證内聯函數的

Lambda

表達式中一定不會使用return全局傳回,這樣就不會沖突了。當然

[email protected]

局部傳回還是可以的。

總結

  • inline:編譯時直接将函數内容直接複制粘貼到調用處。
  • noinline:當函數被inline标記時,使用noinline可以使函數參數不被内聯。
  • crossinline: 允許内聯函數裡的函數類型參數可以被間接調用,但是不能在Lambda表達式中使用全局return傳回

參考

【1】高階函數與 lambda 表達式

【2】https://juejin.cn/post/6869954460634841101

【3】重學 Kotlin —— inline,包治百病的性能良藥?