Kotlin之高階函數(shù)

1蒜胖、高階函數(shù)

1.1消别、高階函數(shù)的定義

高階函數(shù)的定義:如果一個函數(shù)接收另一個函數(shù)作為參數(shù),或者返回值的類型是另一個函數(shù)台谢,那么該函數(shù)稱為高階函數(shù)寻狂。你可能會有疑問,一個函數(shù)怎么能接收另一個函數(shù)作為參數(shù)呢朋沮?因為Kotlin中新增了函數(shù)類型蛇券,如果我們將這種函數(shù)類型添加到一個函數(shù)的參數(shù)聲明后者返回值聲明當(dāng)中,那么該函數(shù)就成為高階函數(shù)樊拓。

1.2纠亚、函數(shù)類型的定義

函數(shù)類型的定義的基本規(guī)則如下:

methodName:(Int,String)->Unit
  • 1、methodName是函數(shù)類型的名稱筋夏,名稱不限制蒂胞。
  • 2、(Int,String)代表函數(shù)接收的類型条篷,多個參數(shù)類型用逗號隔開骗随。
  • 3、->右邊表示函數(shù)的返回值赴叹,Unit類似于Java的void表示無返回值蚊锹。
    下面看下如何將這個函數(shù)類型添加到一個函數(shù)的參數(shù)聲明中:
fun example(block:(String,Int)->Unit){
    block("test",123)
}

這里example()函數(shù)就接收了一個函數(shù)類型的參數(shù)了,該函數(shù)就是高階函數(shù)了稚瘾。函數(shù)類型的參數(shù)使用就和調(diào)用函數(shù)一樣牡昆,傳入相應(yīng)的參數(shù)即可。

1.3摊欠、高階函數(shù)的用途

高階函數(shù)允許讓函數(shù)類型參數(shù)決定函數(shù)的執(zhí)行邏輯丢烘,即使同一個高階函數(shù),傳入的函數(shù)類型參數(shù)不同些椒,那么函數(shù)的執(zhí)行邏輯和返回結(jié)果可能也是完全不同的播瞳。下面我們舉例說明下:
這里準(zhǔn)備定義一個函數(shù)num1AndNum2()接收2個Int參數(shù)和一個函數(shù)類型的參數(shù),由函數(shù)類型的參數(shù)決定這兩個Int參數(shù)具體執(zhí)行的運(yùn)算免糕。
新建一個HighFuncFile文件赢乓,在其中定義高階函數(shù)

fun num1AndNum2(num1: Int, num2: Int, block: (Int, Int) -> Int): Int {
    return block(num1, num2)
}

該函數(shù)前兩個參數(shù)沒什么好說的忧侧,第三個參數(shù)是函數(shù)類型的接收兩個Int變量并且返回值為Int類型,將前兩個Int類型的參數(shù)傳遞給第三個函數(shù)類型作為參數(shù)牌芋,高階函數(shù)中沒有其他邏輯蚓炬,將具體的邏輯交由第三個函數(shù)類型的參數(shù)來完成。
那么第三個參數(shù)應(yīng)該傳什么呢躺屁?我們可以在同文件下定義與其匹配的函數(shù)或者使用其他類中相匹配類型的函數(shù)作為參數(shù)肯夏,這里我們現(xiàn)在HighFuncFile文件下定義函數(shù)。

fun plusFunc(num1: Int, num2: Int): Int {
    return num1 + num2
}

fun minusFunc(num1: Int, num2: Int): Int {
    return num1 - num2
}

高階函數(shù)的調(diào)用

num1AndNum2(20, 30, ::plusFunc)
num1AndNum2(20, 30, ::minusFunc)

可以看到第三個參數(shù)我們使用了::plusFunc這種寫法犀暑,這是一種函數(shù)引用的寫法驯击,表示將函數(shù)plusFunc()來作為參數(shù)傳遞給高階函數(shù)。如果這兩個函數(shù)是定義在某個類中耐亏,那么該怎么引用這個函數(shù)呢徊都?
在HighFuncTest.class中定義函數(shù)

class HighFuncTest {

    fun plusFunc(num1: Int, num2: Int): Int {
        return num1 + num2
    }

    fun minusFunc(num1: Int, num2: Int): Int {
        return num1 - num2
    }

}

我們上面使用了::plusFunc來引用函數(shù),那此時我們該怎么引用函數(shù)呢广辰?

val highFuncTest: HighFuncTest = HighFuncTest()
num1AndNum2(20, 30, highFuncTest::plusFunc)
num1AndNum2(20, 30, highFuncTest::minusFunc)

先創(chuàng)建對象碟贾,然后使用highFuncTest::plusFunc來引用HighFuncTest類中的函數(shù)作為參數(shù)傳遞給高階函數(shù)。
像這種每次調(diào)用高階函數(shù)都需要定義與其函數(shù)類型參數(shù)匹配的函數(shù)轨域,使用起來確實很麻煩,為此Kotlin提供了其他方式調(diào)用高階函數(shù)杀餐,比如:Lambda表達(dá)式干发、匿名函數(shù)、成員引用等史翘,Lambda表達(dá)式是最常用的高階函數(shù)調(diào)用方式枉长。下面我們就來學(xué)習(xí)下如何使用Lambda表達(dá)式來調(diào)用高階函數(shù),我們把上面的例子改成Lambda表達(dá)式的方法琼讽。

val plusResult = num1AndNum2(20, 30) { n1: Int, n2: Int -> n1 + n2 }
Log.e(tag, "$plusResult")

val minusResult = num1AndNum2(20, 30) { n1: Int, n2: Int -> n1 - n2 }
Log.e(tag, "$minusResult")

可以發(fā)現(xiàn)使用Lambda表達(dá)式同樣可以完整的表達(dá)函數(shù)類型的參數(shù)和返回值必峰,Lambda表達(dá)式的最后一行代碼的返回值作為函數(shù)的返回值返回。
下面對高階函數(shù)繼續(xù)探究钻蹬,回顧一下apply標(biāo)準(zhǔn)函數(shù)的用法

val stringBuilder = StringBuilder()
        val ss = stringBuilder.apply {
            append("hello")
            append("how are you")
        }
        Log.e(tag,ss.toString())

apply標(biāo)準(zhǔn)函數(shù)會把調(diào)用對象傳遞到Lambda表達(dá)式中作為上下文吼蚁,并且返回調(diào)用對象。下面我們就用高階函數(shù)來實現(xiàn)類似的功能问欠。

fun StringBuilder.otherApply(block: StringBuilder.() -> Unit): StringBuilder {
    block()
    return this
}

這里給StringBuilder類定義了一個擴(kuò)展函數(shù)肝匆,擴(kuò)展函數(shù)接收一個函數(shù)類型的參數(shù),并且返回值為StringBuilder顺献。
注意:這里定義的函數(shù)類型的參數(shù)和我們前面學(xué)習(xí)的語法還是有區(qū)別的旗国,在函數(shù)類型的前面加上了StringBuilder.,其實這才是完整的函數(shù)類型的定義規(guī)則注整,加上ClassName.表示在哪個類中定義函數(shù)類型能曾。使用StringBuilder.表示在StringBuilder類中定義的函數(shù)類型度硝,那么在傳入Lambda表達(dá)式時將會自動擁有StringBuilder的上下文。下面看下otherApply()的使用

val stringBuilder = StringBuilder()
            val result = stringBuilder.otherApply {
                append("hello")
                append("123")
            }
            Log.e(tag, result.toString())

可以看到和apply標(biāo)準(zhǔn)函數(shù)的使用完全一樣寿冕,只不過apply適用所有類的使用蕊程,而otherApply只局限于StringBuilder的使用,如果想實現(xiàn)apply的函數(shù)的這個功能蚂斤,就需要借助Kotlin泛型才可以存捺。

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

2.1曙蒸、高階函數(shù)的實現(xiàn)原理

學(xué)習(xí)內(nèi)聯(lián)函數(shù)前我們先來學(xué)習(xí)一下高級函數(shù)的實現(xiàn)原理捌治。這里仍然使用剛才編寫的num1AndNum2()函數(shù)為例。

fun num1AndNum2(num1: Int, num2: Int, block: (Int, Int) -> Int): Int {
    return block(num1, num2)
}
//調(diào)用
  val minusResult = num1AndNum2(20, 30) { n1: Int, n2: Int -> n1 - n2 }
            Log.e(tag, "$minusResult")

上面是Kotlin中高階函數(shù)的基本用法纽窟,我們知道Kotlin代碼最終會編譯成Java字節(jié)碼的肖油,而Java中是沒有高階函數(shù)概念的,其實Kotlin編譯器最終會把Kotlin中高階函數(shù)的語法轉(zhuǎn)換成Java支持的語法結(jié)構(gòu)臂港,上述的Kotlin代碼大致被轉(zhuǎn)換成如下Java代碼森枪。

public static int num1AndNum2(int num1, int num2, Function operation){
        return (int)operation.invoke(num1,num2);
    }

    public void test(){
        int minusResult=num1AndNum2(10, 20, new Function() {
            @Override
            public Integer invoke(Integer num1,Integer num2) {
                return num1+num2;
            }
        });
    }

考慮到可讀性,我們對代碼做了調(diào)整审孽,并不是嚴(yán)格對應(yīng)了Kotlin轉(zhuǎn)成Java的代碼县袱。這里第三個參數(shù)變成了Function接口,這是Kotlin的內(nèi)置接口佑力,里面有一個待實現(xiàn)的invoke()函數(shù)式散。而num1AndNum2()其實就是調(diào)用了Function接口的invoke()函數(shù),并把num1和num2參數(shù)傳了進(jìn)去打颤。
在調(diào)用num1AndNum2函數(shù)時暴拄,之前的Lambda表達(dá)式變成了Function接口的匿名類實現(xiàn),然后在invoke函數(shù)中實現(xiàn)了num1+num2的邏輯编饺。
這就是高階函數(shù)背后的原理乖篷,原來傳入的Lambda表達(dá)式在底層被匿名類所代替,這也就說明我們每調(diào)用一次Lambda表達(dá)式就會創(chuàng)建一個匿名類對象透且,當(dāng)然會帶來額外的內(nèi)存和性能開銷撕蔼。
而Kotlin中的內(nèi)聯(lián)函數(shù)就是為了解決這個問題的,它可以將使用Lambda表達(dá)式運(yùn)行時的開銷完全消除秽誊。

2.2罕邀、內(nèi)聯(lián)函數(shù)的使用以及原理

內(nèi)聯(lián)函數(shù)的使用比較簡單就是在定義的高階函數(shù)時加上inline關(guān)鍵字即可。

inline fun num1AndNum2(num1: Int, num2: Int, block: (Int, Int) -> Int): Int {
    return block(num1, num2)
}

內(nèi)聯(lián)函數(shù)的原理
內(nèi)聯(lián)函數(shù)的原理也很簡單:Kotlin編譯器在編譯時把內(nèi)聯(lián)函數(shù)內(nèi)代碼自動替換到要調(diào)用的地方养距,這樣就解決了運(yùn)行時的內(nèi)存開銷诉探。
下面看下替換的過程
步驟一、將Lambda表達(dá)式的代碼替換到函數(shù)類型參數(shù)調(diào)用的地方

image.png

步驟二棍厌、將內(nèi)聯(lián)函數(shù)中全部代碼替換到函數(shù)調(diào)用的地方
image.png

最終替換后的代碼為

 val minusResult =20-30

正是如此內(nèi)聯(lián)函數(shù)才能完全消除Lambda表達(dá)式運(yùn)行時帶來的額外內(nèi)存開銷肾胯。

3竖席、noinline和crossinline

3.1、noinline

一個高階函數(shù)中接收兩個或更多函數(shù)類型的參數(shù)敬肚,如果高階函數(shù)被inline修飾了毕荐,那么所有函數(shù)類型的參數(shù)均會被內(nèi)聯(lián),如果想某個函數(shù)類型的參數(shù)不被內(nèi)聯(lián)艳馒,可以用關(guān)鍵字noinline修飾憎亚。

inline fun test(block1: () -> Unit, noinline block2: () -> Unit) {
}

可以看到testinline修飾,本來block1和block2這兩個函數(shù)類型參數(shù)所引用Lambda表達(dá)式均被內(nèi)聯(lián)弄慰。由于我們在block2前加上了noinline關(guān)鍵字第美,那么只有block1這個函數(shù)類型參數(shù)所引用的Lambda表達(dá)式被內(nèi)聯(lián)。
既然內(nèi)聯(lián)函數(shù)能消除Lambda表達(dá)式運(yùn)行時帶來的內(nèi)存的額外開銷陆爽,那么為什么還提供了一個noinline來排除內(nèi)聯(lián)呢什往?

  • 原因一:內(nèi)聯(lián)函數(shù)類型的參數(shù)在編譯期間會進(jìn)行代碼替換,所以內(nèi)聯(lián)的函數(shù)類型的參數(shù)算不上真正的參數(shù)慌闭,非內(nèi)聯(lián)的函數(shù)類型的參數(shù)可以作為真正的參數(shù)傳遞給任何函數(shù)别威。內(nèi)聯(lián)函數(shù)類型的參數(shù)只能傳遞給另一個內(nèi)聯(lián)函數(shù)。這也是它最大的局限性驴剔。
  • 原因二:內(nèi)聯(lián)函數(shù)和非內(nèi)聯(lián)函數(shù)有一個重要的區(qū)別:內(nèi)聯(lián)函數(shù)所引用的Lambda表達(dá)式中可以使用return來進(jìn)行函數(shù)的返回省古,而非內(nèi)聯(lián)函數(shù)只能進(jìn)行局部返回。
fun printString(str: String, block: (String) -> Unit) {
    Log.e("LoginActivity", "printString begin")
    block(str)
    Log.e("LoginActivity", "printString end")
}

fun main(){
            Log.e(tag, "mainbegin")
            printString("") {
                Log.e(tag, "lambda begin")
                if (it.isEmpty()) return@printString
                Log.e(tag, "lambda end")
            }
            Log.e(tag, "mainend")
        }

這里定義了一個非內(nèi)聯(lián)的高階函數(shù)丧失,在Lambda表達(dá)式中如果傳入的字符串為空豺妓,則直接返回,此時Lambda表達(dá)式中只能使用return@printString進(jìn)行局部返回利花。打印結(jié)果如下:

main begin
printString begin
lambda begin
printString end
main end

可以看到lambda end并沒有輸出,因為輸入的字符串為空载佳,則局部返回不再執(zhí)行Lambda表達(dá)式中的函數(shù)炒事,所以Log.e(tag, "lambda end")沒有執(zhí)行。
下面我們聲明一個內(nèi)聯(lián)函數(shù)printStr

inline fun printStr(str: String, block: (String) -> Unit) {
    Log.e("LoginActivity", "printString begin")
    block(str)
    Log.e("LoginActivity", "printString end")
}

fun main(){
            Log.e(tag, "main begin")
            printStr("") {
                Log.e(tag, "lambda begin")
                if (it.isEmpty()) return
                Log.e(tag, "lambda end")
            }
            Log.e(tag, "main end")
        }

由于printStr是內(nèi)聯(lián)函數(shù)蔫慧,我們可以在Lambda表達(dá)式中使用return進(jìn)行返回挠乳,打印結(jié)果如下:

main begin
printString begin
lambda begin

在傳入的字符串為空時,返回出最外層的函數(shù)姑躲,所以lambda end和printString end和click end將不會被輸出睡扬。

3.2、crossinline

將高階函數(shù)聲明成內(nèi)聯(lián)函數(shù)是一種良好的習(xí)慣黍析,事實上絕大多數(shù)高階函數(shù)是可以被聲明成內(nèi)聯(lián)函數(shù)的卖怜,但是也有例外的情況。觀察下面的代碼

inline fun runRunnable(block:()->Unit){
    val runnable= Runnable {
        block()
    }
    runnable.run()
}

這段代碼如果沒有加上inline關(guān)鍵字是完全可以正常工作的阐枣,但是加上inline之后就會報如下錯誤:

image.png

首先我們在內(nèi)聯(lián)函數(shù)runRunnable中創(chuàng)建一個runnable對象马靠,并在Runnable的Lambda表達(dá)式中傳入的函數(shù)類型參數(shù)奄抽,而Lambda表達(dá)式在編譯的時候會被轉(zhuǎn)換成匿名類的實現(xiàn)方式,也就是說上面代碼是在匿名類中傳入了函數(shù)類型的參數(shù)甩鳄。
而內(nèi)聯(lián)函數(shù)所引用的Lambda表達(dá)式允許使用return進(jìn)行函數(shù)的返回逞度,但是由于我們是在匿名類中調(diào)用的函數(shù)類型參數(shù),此時不能進(jìn)行外層調(diào)用函數(shù)的返回妙啃,最多只能進(jìn)行匿名類中的方法進(jìn)行返回档泽,因此就提示了上述錯誤。
也就是說:如果我們在高階函數(shù)中創(chuàng)建了Lambda或匿名類的實現(xiàn)揖赴,在這些實現(xiàn)中調(diào)用函數(shù)類型參數(shù)馆匿,此時再將高階函數(shù)聲明成內(nèi)聯(lián),肯定會報上面的錯誤储笑。
那么如何在這種情況下使用內(nèi)聯(lián)函數(shù)呢甜熔?這就需要關(guān)鍵字crossinline

inline fun runRunnable(crossinline block:()->Unit){
    val runnable= Runnable {
        block()
    }
    runnable.run()
}

經(jīng)過前面的分析可知,上面錯誤的原因:內(nèi)聯(lián)函數(shù)中允許使用return關(guān)鍵字和高階函數(shù)的匿名類的實現(xiàn)中不能使用return之間造成了沖突突倍。而crossinline關(guān)鍵字用于保證在Lambda表達(dá)式中一定不使用return關(guān)鍵字腔稀,這樣沖突就不存在了。但是我們?nèi)匀豢梢允褂?code>return@runRunnable進(jìn)行局部返回羽历。總體來說焊虏,crossinline除了return用法不同外,仍然保留了內(nèi)聯(lián)函數(shù)的所有特性秕磷。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末诵闭,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子澎嚣,更是在濱河造成了極大的恐慌疏尿,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件易桃,死亡現(xiàn)場離奇詭異褥琐,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)晤郑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進(jìn)店門敌呈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人造寝,你說我怎么就攤上這事磕洪。” “怎么了诫龙?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵析显,是天一觀的道長。 經(jīng)常有香客問我签赃,道長叫榕,這世上最難降的妖魔是什么浑侥? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮晰绎,結(jié)果婚禮上寓落,老公的妹妹穿的比我還像新娘。我一直安慰自己荞下,他們只是感情好伶选,可當(dāng)我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著尖昏,像睡著了一般仰税。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上抽诉,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天陨簇,我揣著相機(jī)與錄音,去河邊找鬼迹淌。 笑死河绽,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的唉窃。 我是一名探鬼主播耙饰,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼纹份,長吁一口氣:“原來是場噩夢啊……” “哼苟跪!你這毒婦竟也來了蔓涧?” 一聲冷哼從身側(cè)響起件已,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤元暴,失蹤者是張志新(化名)和其女友劉穎篷扩,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體昨寞,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡厦滤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了掏导。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片享怀。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖趟咆,靈堂內(nèi)的尸體忽然破棺而出添瓷,到底是詐尸還是另有隱情,我是刑警寧澤鳞贷,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站搀愧,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏咱筛。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一溉愁、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧饲趋,春花似錦、人聲如沸篙贸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至扒披,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間圃泡,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工价说, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人鳖目。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓缤弦,卻偏偏與公主長得像领迈,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子狸捅,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,611評論 2 353