Kotlin 內(nèi)聯(lián)函數(shù)

一办铡、內(nèi)聯(lián)函數(shù)原理

使用高階函數(shù)為開發(fā)帶來了便利脐雪,但同時也產(chǎn)生了一些性能上的損失付翁,官方是這樣描述這個問題:

使用高階函數(shù)會帶來一些運(yùn)行時的效率損失:每一個函數(shù)都是一個對象尖阔,并且會捕獲一個閉包贮缅。 即那些在函數(shù)體內(nèi)會訪問到的變量。 內(nèi)存分配(對于函數(shù)對象和類)和虛擬調(diào)用會引入運(yùn)行時間開銷诺祸,但是通過內(nèi)聯(lián)化 Lambda 表達(dá)式可以消除這類的開銷携悯。

為了解決這個問題,可以使用內(nèi)聯(lián)函數(shù)筷笨,用inline修飾的函數(shù)就是內(nèi)聯(lián)函數(shù)憔鬼,inline修飾符影響函數(shù)本身和傳給它的 Lambda 表達(dá)式,所有這些都將內(nèi)聯(lián)到調(diào)用處胃夏,即編譯器會把調(diào)用這個函數(shù)的地方用這個函數(shù)的方法體進(jìn)行替換轴或,而不是創(chuàng)建一個函數(shù)對象并生成一個調(diào)用。

接下來用代碼驗證這個說法仰禀,先定義一個普通的高階函數(shù)照雁,然后調(diào)用兩次:

fun calculate(a: Int, b: Int, cal: (Int, Int) -> String) {
    println(cal(a, b))
}
fun main(args: Array<String>) {
    calculate(3, 7) { a, b ->
        "$a + $b = ${a + b}"
    }

    calculate(3, 7) { a, b ->
        "$a * $b = ${a * b}"
    }
}
// 輸出
3 + 7 = 10
3 * 7 = 21

這樣其實是看不出什么問題的,Kotlin 文件編譯后會生成對應(yīng)的 class 文件答恶,所以我們將 class 文件反編譯成 Java 文件后再看饺蚊。如果使用Android Studio或者IntelliJ IDEA,可以按照如下方式查看 Kotlin 文件對應(yīng)反編譯后的 Java 文件:

  1. 打開目標(biāo) Kotlin 文件
  2. 查看 Kotlin 文件字節(jié)碼:Tools –> Kotlin –> Show Kotlin ByteCode
  3. 在 kotlin 文件字節(jié)碼頁面中點擊左上角的 decompile 按鈕悬嗓,就會生成對應(yīng)的 Java 文件

我們來看上邊代碼對應(yīng)的 Java 代碼:

1

雖然不是正常的 Java 代碼污呼,但不妨礙我們分析流程,可以看出包竹,編譯器創(chuàng)建了兩個 Lambda 的實例燕酷,并進(jìn)行了兩次calculate函數(shù)調(diào)用籍凝。

那如果將calculate聲明為內(nèi)聯(lián)函數(shù)呢:

inline fun calculate(a: Int, b: Int, cal: (Int, Int) -> String) {
    println(cal(a, b))
}

我們再看最終的 Java 文件:


2

即編譯器會把調(diào)用這個函數(shù)的地方用這個函數(shù)的方法體進(jìn)行替換,這樣驗證了之前的說法苗缩。

需要注意的是饵蒂, 內(nèi)聯(lián)函數(shù)提高代碼性能的同時也會導(dǎo)致代碼量的增加,所以應(yīng)避免內(nèi)聯(lián)函數(shù)過大酱讶。

二退盯、禁用內(nèi)聯(lián)(noinline)

如果一個內(nèi)聯(lián)函數(shù)可以接收多個 Lambda 表達(dá)式作為參數(shù),默認(rèn)這些 Lambda 表達(dá)式都會被內(nèi)內(nèi)聯(lián)到調(diào)用處浴麻,如果需要某個 Lambda 表達(dá)式不被內(nèi)聯(lián)得问,可以使用noinline修飾對應(yīng)的函數(shù)參數(shù):

inline fun calculate(a: Int, b: Int, noinline title: () -> Unit, cal: (Int, Int) -> String) {
    title()
    println(cal(a, b))
}
fun main(args: Array<String>) {
    calculate(3, 7, { println("開始計算") }) { a, b ->
        "$a * $b = ${a * b}"
    }
}
// 輸出
開始計算
3 * 7 = 21

title對應(yīng)的 Lambda 確實沒有被內(nèi)聯(lián),看圖:

3

一個內(nèi)聯(lián)函數(shù)沒有可內(nèi)聯(lián)的函數(shù)參數(shù)并且沒有具體化的類型參數(shù)软免,編譯器會有警告,因為這樣并不能帶來什么好處焚挠,如果你不愿去掉內(nèi)聯(lián)修飾膏萧,可以使用@Suppress("NOTHING_TO_INLINE") 注解關(guān)閉這個警告。

三蝌衔、非局部返回

我們知道默認(rèn)情況下榛泛,在高階函數(shù)中,要顯式的退出(返回)一個 Lambda 表達(dá)式噩斟,需要使用 return@標(biāo)簽的語法曹锨,不能使用裸return,但這樣也不能使高階函數(shù)和包含高階函數(shù)的函數(shù)退出剃允。例如:

fun message(block: () -> Unit) {
    block()
    println("-----")
}

fun test() {
    message {
        println("Hello")
        return@message
    }
    println("World")
}
fun main(args: Array<String>) {
    test()
}
// 輸出
Hello
-----
World

但如果把 Lambda 表達(dá)式作為參數(shù)傳遞給一個內(nèi)聯(lián)函數(shù)沛简,就可以在 Lambda 表達(dá)式中正常的使用return語句了,并且會使該內(nèi)聯(lián)函數(shù)和包含該內(nèi)聯(lián)函數(shù)的函數(shù)退出(返回)斥废,這種操作就是非局部返回椒楣。例如:

inline fun message(block: () -> Unit) {
    block()
    println("-----")
}

fun test() {
    message {
        println("Hello")
        return
    }
    println("World")
}
fun main(args: Array<String>) {
    test()
}
// 輸出
Hello

注意,由于非局部返回的原因牡肉,這里只輸出了Hello捧灰。

在使用了非局部返回后,Lambda 表達(dá)式中return的返回值受調(diào)用該內(nèi)聯(lián)函數(shù)的函數(shù)的返回值類型影響统锤。例如:

fun test(): Boolean {
    message {
        println("Hello")
        return false
    }
    println("World")
    return true
}

四毛俏、禁用非局部返回(crossinline)

從前邊已經(jīng)知道,通過內(nèi)聯(lián)函數(shù)可以使 Lambda表達(dá)式實現(xiàn)非局部返回饲窿,但是煌寇,如果一個內(nèi)聯(lián)函數(shù)的函數(shù)類型參數(shù)被crossinline修飾,則對應(yīng)傳入的 Lambda表達(dá)式將不能非局部返回了免绿,只能局部返回了唧席。還是用之前的例子修改:

inline fun message(crossinline block: () -> Unit) {
    block()
    println("-----")
}

fun test() {
    message {
        println("Hello")
        return@message
    }
    println("World")
}
fun main(args: Array<String>) {
    test()
}
// 輸出
Hello
-----
World

通過crossinline可以禁用掉非局部返回,但有什么意義呢?這其實是有實際的場景需求的淌哟,看個例子:

interface Calculator {
    fun calculate(a: Int, b: Int): Int
}

inline fun test(block: (Int, Int) -> Int) {
    val c = object : Calculator {
        override fun calculate(a: Int, b: Int): Int = block(a, b)
    }
    c.calculate(3, 7)
}

首先定義一個Calculator計算接口迹卢,然后在內(nèi)聯(lián)函數(shù)test中創(chuàng)建Calculator的一個對象表達(dá)式,重寫calculate方法時徒仓,我們讓calculate的函數(shù)體是test函數(shù)的block參數(shù)腐碱,當(dāng)block是 Lambda表達(dá)式時,由于非局部返回的原因掉弛,導(dǎo)致calculate函數(shù)的返回值不是預(yù)期的症见,進(jìn)而發(fā)生異常,為了避免這種情況的發(fā)生殃饿,所以就有必要使用crossinline來禁用非局部返回谋作,來保證calculate的返回值類型是安全的。

上邊的代碼會有這樣一個錯誤提示:

Can't inline 'block' here: it may contain non-local returns. Add 'crossinline' modifier to parameter declaration 'block'

使用crossinline后就正常了:

inline fun test(crossinline block: (Int, Int) -> Int) {
    val c = object : Calculator {
        override fun calculate(a: Int, b: Int): Int = block(a, b)
    }
    c.calculate(3, 7)
}

五乎芳、具體化的類型參數(shù)(reified)

對于一個泛型函數(shù)遵蚜,如果需要訪問泛型參數(shù)的類型,但由于泛型類型被擦除的原因奈惑,可能無法直接訪問吭净,但通過反射還是可以做到的,例如:

fun <T> test(param: Any, clazz: Class<T>) {
    if (clazz.isInstance(param)) {
        println("參數(shù)類型匹配")
    } else {
        println("參數(shù)類型不匹配")
    }
}
fun main(args: Array<String>) {
    test("Hello World", String::class.java)
    test(666, String::class.java)
}
// 輸出
參數(shù)類型匹配
參數(shù)類型不匹配

功能雖然實現(xiàn)了肴甸,但是不夠優(yōu)雅寂殉,Kotlin 中有更好的辦法來實現(xiàn)這樣的功能。

在內(nèi)聯(lián)函數(shù)中支持具體化的參數(shù)類型原在,即用reified來修飾需要具體化的參數(shù)類型友扰,這樣我們用reified來修飾泛型的參數(shù)類型,以達(dá)到我們的目的:

inline fun <reified T> test(param: Any) {
    if (param is T) {
        println("參數(shù)類型匹配")
    } else {
        println("參數(shù)類型不匹配")
    }
}

調(diào)用的過程也變得簡單了:

fun main(args: Array<String>) {
    test<String>("Hello World")
    test<String>(666)
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末晤斩,一起剝皮案震驚了整個濱河市焕檬,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌澳泵,老刑警劉巖实愚,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異兔辅,居然都是意外死亡腊敲,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門维苔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來碰辅,“玉大人,你說我怎么就攤上這事介时∶槐觯” “怎么了凌彬?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長循衰。 經(jīng)常有香客問我铲敛,道長,這世上最難降的妖魔是什么会钝? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任伐蒋,我火速辦了婚禮,結(jié)果婚禮上迁酸,老公的妹妹穿的比我還像新娘先鱼。我一直安慰自己,他們只是感情好奸鬓,可當(dāng)我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布焙畔。 她就那樣靜靜地躺著,像睡著了一般全蝶。 火紅的嫁衣襯著肌膚如雪闹蒜。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天抑淫,我揣著相機(jī)與錄音,去河邊找鬼姥闪。 笑死始苇,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的筐喳。 我是一名探鬼主播催式,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼避归!你這毒婦竟也來了荣月?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤梳毙,失蹤者是張志新(化名)和其女友劉穎哺窄,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體账锹,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡萌业,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了奸柬。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片生年。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖廓奕,靈堂內(nèi)的尸體忽然破棺而出抱婉,到底是詐尸還是另有隱情档叔,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布蒸绩,位于F島的核電站衙四,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏侵贵。R本人自食惡果不足惜届搁,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望窍育。 院中可真熱鬧卡睦,春花似錦、人聲如沸漱抓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽乞娄。三九已至瞬逊,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間仪或,已是汗流浹背确镊。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留范删,地道東北人蕾域。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像到旦,于是被迫代替她去往敵國和親旨巷。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容