Kotlin 知識(shí)梳理(11) - 內(nèi)聯(lián)函數(shù)

Kotlin 知識(shí)梳理系列文章

Kotlin 知識(shí)梳理(1) - Kotlin 基礎(chǔ)
Kotlin 知識(shí)梳理(2) - 函數(shù)的定義與調(diào)用
Kotlin 知識(shí)梳理(3) - 類民效、對(duì)象和接口
Kotlin 知識(shí)梳理(4) - 數(shù)據(jù)類勇凭、類委托 及 object 關(guān)鍵字
Kotlin 知識(shí)梳理(5) - lambda 表達(dá)式和成員引用
Kotlin 知識(shí)梳理(6) - Kotlin 的可空性
Kotlin 知識(shí)梳理(7) - Kotlin 的類型系統(tǒng)
Kotlin 知識(shí)梳理(8) - 運(yùn)算符重載及其他約定
Kotlin 知識(shí)梳理(9) - 委托屬性
Kotlin 知識(shí)梳理(10) - 高階函數(shù):Lambda 作為形參或返回值
Kotlin 知識(shí)梳理(11) - 內(nèi)聯(lián)函數(shù)
Kotlin 知識(shí)梳理(12) - 泛型類型參數(shù)


一哎榴、本文概要

本文是對(duì)<<Kotlin in Action>>的學(xué)習(xí)筆記,如果需要運(yùn)行相應(yīng)的代碼可以訪問在線環(huán)境 try.kotlinlang.org,這部分的思維導(dǎo)圖為:

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

當(dāng)我們使用lambda表達(dá)式時(shí),它會(huì)被正常地編譯成匿名類估蹄。這表示每調(diào)用一次lambda表達(dá)式,一個(gè)額外的類就會(huì)被創(chuàng)建沫换,并且如果lambda捕捉了某個(gè)變量臭蚁,那么每次調(diào)用的時(shí)候都會(huì)創(chuàng)建一個(gè)新的對(duì)象,這會(huì)帶來運(yùn)行時(shí)的額外開銷讯赏,導(dǎo)致使用lambda比使用一個(gè)直接執(zhí)行相同代碼的函數(shù)效率更低垮兑。

如果使用inline修飾符標(biāo)記一個(gè)函數(shù),在函數(shù)被調(diào)用的時(shí)候編譯器并不會(huì)生成函數(shù)調(diào)用的代碼漱挎,而是 使用函數(shù)實(shí)現(xiàn)的真實(shí)代碼替換每一次的函數(shù)調(diào)用系枪。

2.1 內(nèi)聯(lián)函數(shù)如何運(yùn)作

當(dāng)一個(gè)函數(shù)被聲明為inline時(shí),它的函數(shù)體是內(nèi)聯(lián)的磕谅,也就是說私爷,函數(shù)體會(huì)被直接替換到函數(shù)被調(diào)用地方,下面我們來看一個(gè)簡單的例子膊夹,下面是我們定義的一個(gè)內(nèi)聯(lián)的函數(shù):

inline fun inlineFunc(prefix : String, action : () -> Unit) {
    println("call before $prefix")
    action()
    println("call after $prefix")
}

我們用如下的方法來使用這個(gè)內(nèi)聯(lián)函數(shù):

fun main(args: Array<String>) {
    inlineFunc("inlineFunc") {
        println("HaHa")
    }
}

運(yùn)行結(jié)果為:

>> call before inlineFunc
>> HaHa
>> call after inlineFunc

最終它會(huì)被編譯成下面的字節(jié)碼:

fun main(args: Array<String>) {
    println("call before inlineFunc")
    println("HaHa")
    println("call after inlineFunc")
}

lambda表達(dá)式和inlineFunc的實(shí)現(xiàn)部分都被內(nèi)聯(lián)了衬浑,由lambda生成的字節(jié)碼成了函數(shù)調(diào)用者定義的一部分,而不是被包含在一個(gè)實(shí)現(xiàn)了函數(shù)接口的匿名類中放刨。

傳遞函數(shù)類型的變量作為參數(shù)

在調(diào)用內(nèi)聯(lián)函數(shù)的時(shí)候工秩,也可以傳遞函數(shù)類型的變量作為參數(shù),還是上面的例子进统,我們換一種調(diào)用方式:

fun main(args: Array<String>) {
    val call : () -> Unit = { println("HaHa") }
    inlineFunc("inlineFunc", call)
}

那么此時(shí)最終被編譯成的Java字節(jié)碼為:

fun main(args: Array<String>) {
    println("call before inlineFunc ")
    action()
    println("call after inlineFunc")
}

在這種情況助币,只有inlineFunc的實(shí)現(xiàn)部分被內(nèi)聯(lián)了,而lambda的代碼在內(nèi)聯(lián)函數(shù)被調(diào)用點(diǎn)是不可用的螟碎。

在兩個(gè)不同的位置使用同一個(gè)內(nèi)聯(lián)函數(shù)

如果在兩個(gè)不同的位置使用同一個(gè)內(nèi)聯(lián)函數(shù)眉菱,但是用的是不同的lambda,那么內(nèi)聯(lián)函數(shù)會(huì)在每一個(gè)被調(diào)用的位置分別內(nèi)聯(lián)掉分,內(nèi)聯(lián)函數(shù)的代碼會(huì)被拷貝到使用它的兩個(gè)不同位置倍谜,并把不同的lambda替換到其中迈螟。

2.2 內(nèi)聯(lián)函數(shù)的限制

鑒于內(nèi)聯(lián)的運(yùn)作方式叉抡,不是所有使用 lambda 的函數(shù)都可以被內(nèi)聯(lián)尔崔。當(dāng)函數(shù)被內(nèi)聯(lián)的時(shí)候,作為參數(shù)的lambda表達(dá)式的函數(shù)體會(huì)被 替換到最終生成的代碼中褥民。

這將限制函數(shù)體中的lambda參數(shù)的使用:

  • 如果lambda參數(shù) 被調(diào)用季春,這樣的代碼能被容易地內(nèi)聯(lián)。
  • 如果lambda參數(shù) 在某個(gè)地方被保存起來消返,以便以后繼續(xù)使用载弄,lambda表達(dá)式的代碼 將不能被內(nèi)聯(lián),因此必須要 有一個(gè)包含這些代碼的對(duì)象存在撵颊。

一般來說宇攻,參數(shù)如果 被直接調(diào)用或者作為參數(shù)傳遞 給另外一個(gè)inline函數(shù),它是可以被內(nèi)聯(lián)的倡勇,否則逞刷,編譯器會(huì) 禁止參數(shù)被內(nèi)聯(lián) 并給出錯(cuò)誤信息Illeagal usage of inline-parameter

例如妻熊,許多作用于序列的函數(shù)會(huì)返回一些類的實(shí)例夸浅,這些類代表對(duì)應(yīng)的序列操作并接收lambda作為構(gòu)造方法的參數(shù),以下是Sequence.map函數(shù)的定義:

fun <T, R> Sequence<T>.map(transform : (T) -> R) : Sequence<R> {
    return TransformingSequence(this, transform);
}

map函數(shù)沒有直接調(diào)用作為transform參數(shù)傳遞進(jìn)來的函數(shù)扔役。而是將這個(gè)函數(shù)傳遞給一個(gè)類的構(gòu)造方法帆喇,構(gòu)造方法將它保存在一個(gè)屬性當(dāng)中。為了支持這一點(diǎn)亿胸,作為transform參數(shù)傳遞的lambda需要 被編譯成標(biāo)準(zhǔn)的非內(nèi)聯(lián)表示法坯钦,即一個(gè)實(shí)現(xiàn)了函數(shù)接口的匿名類。

如果一個(gè)函數(shù)期望兩個(gè)或更多的lambda函數(shù)侈玄,可以選擇只內(nèi)聯(lián)其中一些參數(shù)婉刀,因?yàn)橐粋€(gè)lambda可能會(huì)包含很多代碼或者 以不允許內(nèi)聯(lián)的方式調(diào)用,接收這樣的非內(nèi)聯(lián)lambda的參數(shù)拗馒,可以用noinline修飾符來標(biāo)記它:

inline fun foo(inlined : () -> Unit, noinline noinlined : () -> Unit) {

}

注意路星,編譯器完全支持 內(nèi)聯(lián)跨模塊的函數(shù)或者第三方庫定義的函數(shù),也可以在 Java 中調(diào)用絕大部分內(nèi)聯(lián)函數(shù)诱桂。

2.3 內(nèi)聯(lián)集合操作

大部分標(biāo)準(zhǔn)庫中的集合函數(shù)都帶有lambda參數(shù)洋丐。例如filter,它被聲明為內(nèi)聯(lián)函數(shù)挥等,這意味著filter函數(shù)友绝,以及傳遞給它的lambda字節(jié)碼會(huì)被內(nèi)聯(lián)到filter被調(diào)用的地方,因此我們不用擔(dān)心性能問題肝劲。

假如我們像下面這樣迁客,連續(xù)調(diào)用filtermap兩個(gè)操作:

println(people.filter{ it.age > 30 }.map(Person :: name))

這個(gè)例子使用了一個(gè)lambda表達(dá)式和一個(gè)成員引用郭宝,filtermap函數(shù)都被聲明為inline函數(shù),所以不會(huì)額外產(chǎn)生類或者對(duì)象掷漱,但是上面的代碼會(huì)創(chuàng)建一個(gè)中間集合來保存列表過濾的結(jié)果粘室。

2.4 決定何時(shí)將函數(shù)聲明成內(nèi)聯(lián)

對(duì)于普通函數(shù)的調(diào)用,JVM已經(jīng)提供了強(qiáng)大的內(nèi)聯(lián)支持卜范。它會(huì)分析代碼的執(zhí)行衔统,并在任何通過內(nèi)聯(lián)能夠帶來好處的時(shí)候?qū)⒑瘮?shù)調(diào)用內(nèi)聯(lián)。

帶有lambda參數(shù)的函數(shù)內(nèi)聯(lián)能帶來好處:

  • 節(jié)約了函數(shù)調(diào)用的開銷海雪,節(jié)約了為lambda創(chuàng)建匿名類锦爵,以及創(chuàng)建lambda實(shí)例對(duì)象的開銷。
  • JVM目前并沒有聰明到總是能夠?qū)⒑瘮?shù)調(diào)用內(nèi)聯(lián)奥裸。
  • 內(nèi)聯(lián)使得我們可以使用一些不可能被普通lambda使用的特性险掀,例如 非局部返回

但是在使用inline關(guān)鍵字的時(shí)候湾宙,還是應(yīng)該注意代碼的長度樟氢,如果你要內(nèi)聯(lián)的函數(shù)很大,將它的字節(jié)碼拷貝到每一個(gè)調(diào)用點(diǎn)將會(huì)極大地增加字節(jié)碼的長度创倔。在這種情況下嗡害,你應(yīng)該將那些與lambda參數(shù)無關(guān)的代碼抽取到一個(gè)獨(dú)立的非內(nèi)聯(lián)函數(shù)中。

三畦攘、高階函數(shù)中的控制流

當(dāng)你使用lambda去替換像循環(huán)這樣的命令式代碼結(jié)構(gòu)時(shí)霸妹,很快就會(huì)遇到return表達(dá)式的問題,把一個(gè)return語句放在循環(huán)的中間是很簡單的事知押。但是如果將循環(huán)替換成一個(gè)類似filter的函數(shù)呢叹螟?

3.1 lambda 中的返回語句:從一個(gè)封閉的函數(shù)返回

下面,我們通過一個(gè)例子來演示台盯,在集合當(dāng)中尋找名為Alice的人罢绽,找到了就直接返回:

data class Person(val name: String, val age: Int)

val people = listOf(Person("Alice", 29), Person("Bob", 31))

fun lookForAlice(people: List<Person>) {
    people.forEach {
        if (it.name == "Alice") {
            println("Found!")
            return
        }
    }
    println("Alice is not found")
}

fun main(args: Array<String>) {
    lookForAlice(people)
}

運(yùn)行結(jié)果為:

>> Found !

如果在lambda中使用return關(guān)鍵字,它會(huì) 從調(diào)用 lambda 的函數(shù) 中返回静盅,并不只是 從 lambda 中返回良价,這樣的return語句叫做 非局部返回,因?yàn)樗鼜囊粋€(gè)比包含return的代碼塊更大的代碼塊中返回了蒿叠。

需要注意的是明垢,只有 以 lambda 作為參數(shù)的函數(shù)是內(nèi)聯(lián)函數(shù) 的時(shí)候才能從更外層的函數(shù)返回。在一個(gè)非內(nèi)聯(lián)的lambda中使用return表達(dá)式是不允許的市咽,一個(gè)非內(nèi)聯(lián)函數(shù)可以把它的lambda保存在變量中痊银,以便在函數(shù)返回以后可以繼續(xù)使用,這個(gè)時(shí)候lambda想要去影響函數(shù)的返回已經(jīng)太晚了施绎。

3.2 從 lambda 中返回:使用標(biāo)簽返回

也可以在lambda表達(dá)式中使用局部返回溯革,類似于for循環(huán)中的break表達(dá)式贞绳,它會(huì)終止lambda的執(zhí)行,并接著從調(diào)用lambda的代碼處執(zhí)行致稀。

要區(qū)分局部返回和非局部返回冈闭,要用到標(biāo)簽。想從一個(gè)lambda表達(dá)式處返回你可以標(biāo)記它豺裆,然后在return關(guān)鍵字后面引用這個(gè)標(biāo)簽拒秘。

data class Person(val name: String, val age: Int)

val people = listOf(Person("Alice", 29), Person("Bob", 31))

fun lookForAlice(people: List<Person>) {
    people.forEach label@{
        if (it.name == "Alice") return@label
    }
    println("Alice might be somewhere")
}

fun main(args: Array<String>) {
    lookForAlice(people)
}

運(yùn)行結(jié)果為:

>> Alice might be somewhere

另一種選擇是,使用lambda作為參數(shù)的函數(shù)的函數(shù)名可以作為標(biāo)簽臭猜,也就是上面的forEach,如果你顯示地指定了lambda表達(dá)式的標(biāo)簽押蚤,再使用函數(shù)名作為標(biāo)簽沒有任何效果蔑歌。

3.3 匿名函數(shù):默認(rèn)使用局部返回

匿名函數(shù)是一種不同的用于編寫傳遞給函數(shù)的代碼塊的方式,先來看一個(gè)示例:

data class Person(val name: String, val age: Int)

val people = listOf(Person("Alice", 29), Person("Bob", 31))

fun lookForAlice(people: List<Person>) {
    people.forEach(fun (person) {
        if (person.name == "Alice") return
        println("${person.name} is not Alice")
    })
}

fun main(args: Array<String>) {
    lookForAlice(people)
}

運(yùn)行結(jié)果為:

>> Bob is not Alice

匿名函數(shù)和普通函數(shù)有相同的指定返回值類型的規(guī)則揽碘,代碼塊匿名函數(shù) 需要顯示地指定返回類型,如果使用 表達(dá)式函數(shù)體,就可以省略返回類型深纲。

在匿名函數(shù)中划址,不帶return表達(dá)式會(huì)從匿名函數(shù)返回,而不是從包含匿名函數(shù)的函數(shù)返回掖桦,這條規(guī)則很簡單:return從最近的使用fun關(guān)鍵字聲明的函數(shù)返回本昏。

  • lambda表達(dá)式?jīng)]有使用fun關(guān)鍵字,所以lambda中的return從最外層的函數(shù)返回枪汪。
  • 匿名函數(shù)使用了fun涌穆,因此return表達(dá)式從匿名函數(shù)返回。

盡管匿名函數(shù)看起來和普通函數(shù)很相似雀久,但它其實(shí)是lambda表達(dá)式的另一種語法形式而已宿稀。關(guān)于lambda表達(dá)式如何實(shí)現(xiàn),以及在內(nèi)聯(lián)函數(shù)中如何被內(nèi)聯(lián)的討論同樣適用于匿名函數(shù)赖捌。


更多文章祝沸,歡迎訪問我的 Android 知識(shí)梳理系列:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市越庇,隨后出現(xiàn)的幾起案子罩锐,更是在濱河造成了極大的恐慌,老刑警劉巖悦荒,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件唯欣,死亡現(xiàn)場離奇詭異,居然都是意外死亡搬味,警方通過查閱死者的電腦和手機(jī)境氢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門蟀拷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人萍聊,你說我怎么就攤上這事问芬。” “怎么了寿桨?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵此衅,是天一觀的道長。 經(jīng)常有香客問我亭螟,道長挡鞍,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任预烙,我火速辦了婚禮墨微,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘扁掸。我一直安慰自己翘县,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布谴分。 她就那樣靜靜地躺著锈麸,像睡著了一般。 火紅的嫁衣襯著肌膚如雪牺蹄。 梳的紋絲不亂的頭發(fā)上忘伞,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音钞馁,去河邊找鬼虑省。 笑死,一個(gè)胖子當(dāng)著我的面吹牛僧凰,可吹牛的內(nèi)容都是我干的探颈。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼训措,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼伪节!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起绩鸣,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤怀大,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后呀闻,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體化借,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年捡多,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蓖康。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片铐炫。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖蒜焊,靈堂內(nèi)的尸體忽然破棺而出倒信,到底是詐尸還是另有隱情,我是刑警寧澤泳梆,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布鳖悠,位于F島的核電站,受9級(jí)特大地震影響优妙,放射性物質(zhì)發(fā)生泄漏乘综。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一鳞溉、第九天 我趴在偏房一處隱蔽的房頂上張望瘾带。 院中可真熱鬧,春花似錦熟菲、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至于颖,卻和暖如春呆贿,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背森渐。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來泰國打工做入, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人同衣。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓竟块,卻偏偏與公主長得像,于是被迫代替她去往敵國和親耐齐。 傳聞我的和親對(duì)象是個(gè)殘疾皇子浪秘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

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