文章目錄
-
- 高階函數
- 内聯函數
-
- 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,包治百病的性能良藥?