Kotlin之高階函數(shù)

1、高階函數(shù)

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

高階函數(shù)的定義:如果一個(gè)函數(shù)接收另一個(gè)函數(shù)作為參數(shù),或者返回值的類型是另一個(gè)函數(shù),那么該函數(shù)稱為高階函數(shù)。你可能會(huì)有疑問,一個(gè)函數(shù)怎么能接收另一個(gè)函數(shù)作為參數(shù)呢?因?yàn)镵otlin中新增了函數(shù)類型,如果我們將這種函數(shù)類型添加到一個(gè)函數(shù)的參數(shù)聲明后者返回值聲明當(dāng)中,那么該函數(shù)就成為高階函數(shù)。

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

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

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

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

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

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

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

該函數(shù)前兩個(gè)參數(shù)沒什么好說的,第三個(gè)參數(shù)是函數(shù)類型的接收兩個(gè)Int變量并且返回值為Int類型,將前兩個(gè)Int類型的參數(shù)傳遞給第三個(gè)函數(shù)類型作為參數(shù),高階函數(shù)中沒有其他邏輯,將具體的邏輯交由第三個(gè)函數(shù)類型的參數(shù)來完成。
那么第三個(gè)參數(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)

可以看到第三個(gè)參數(shù)我們使用了::plusFunc這種寫法,這是一種函數(shù)引用的寫法,表示將函數(shù)plusFunc()來作為參數(shù)傳遞給高階函數(shù)。如果這兩個(gè)函數(shù)是定義在某個(gè)類中,那么該怎么引用這個(gè)函數(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í)我們?cè)撛趺匆煤瘮?shù)呢?

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

先創(chuàng)建對(duì)象,然后使用highFuncTest::plusFunc來引用HighFuncTest類中的函數(shù)作為參數(shù)傳遞給高階函數(shù)。
像這種每次調(diào)用高階函數(shù)都需要定義與其函數(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ù)的返回值返回。
下面對(duì)高階函數(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ù)會(huì)把調(diào)用對(duì)象傳遞到Lambda表達(dá)式中作為上下文,并且返回調(diào)用對(duì)象。下面我們就用高階函數(shù)來實(shí)現(xiàn)類似的功能。

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

這里給StringBuilder類定義了一個(gè)擴(kuò)展函數(shù),擴(kuò)展函數(shù)接收一個(gè)函數(shù)類型的參數(shù),并且返回值為StringBuilder。
注意:這里定義的函數(shù)類型的參數(shù)和我們前面學(xué)習(xí)的語法還是有區(qū)別的,在函數(shù)類型的前面加上了StringBuilder.,其實(shí)這才是完整的函數(shù)類型的定義規(guī)則,加上ClassName.表示在哪個(gè)類中定義函數(shù)類型。使用StringBuilder.表示在StringBuilder類中定義的函數(shù)類型,那么在傳入Lambda表達(dá)式時(shí)將會(huì)自動(dòng)擁有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的使用,如果想實(shí)現(xiàn)apply的函數(shù)的這個(gè)功能,就需要借助Kotlin泛型才可以。

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

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

學(xué)習(xí)內(nèi)聯(lián)函數(shù)前我們先來學(xué)習(xí)一下高級(jí)函數(shù)的實(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代碼最終會(huì)編譯成Java字節(jié)碼的,而Java中是沒有高階函數(shù)概念的,其實(shí)Kotlin編譯器最終會(huì)把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;
            }
        });
    }

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

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

內(nèi)聯(lián)函數(shù)的使用比較簡單就是在定義的高階函數(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編譯器在編譯時(shí)把內(nèi)聯(lián)函數(shù)內(nèi)代碼自動(dòng)替換到要調(diào)用的地方,這樣就解決了運(yùn)行時(shí)的內(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)行時(shí)帶來的額外內(nèi)存開銷。

3、noinline和crossinline

3.1、noinline

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

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

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

  • 原因一:內(nèi)聯(lián)函數(shù)類型的參數(shù)在編譯期間會(huì)進(jìn)行代碼替換,所以內(nèi)聯(lián)的函數(shù)類型的參數(shù)算不上真正的參數(shù),非內(nèi)聯(lián)的函數(shù)類型的參數(shù)可以作為真正的參數(shù)傳遞給任何函數(shù)。內(nèi)聯(lián)函數(shù)類型的參數(shù)只能傳遞給另一個(gè)內(nèi)聯(lián)函數(shù)。這也是它最大的局限性。
  • 原因二:內(nèi)聯(lián)函數(shù)和非內(nèi)聯(lián)函數(shù)有一個(gè)重要的區(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")
        }

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

main begin
printString begin
lambda begin
printString end
main end

可以看到lambda end并沒有輸出,因?yàn)檩斎氲淖址疄榭眨瑒t局部返回不再執(zhí)行Lambda表達(dá)式中的函數(shù),所以Log.e(tag, "lambda end")沒有執(zhí)行。
下面我們聲明一個(gè)內(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í),返回出最外層的函數(shù),所以lambda end和printString end和click end將不會(huì)被輸出。

3.2、crossinline

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

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

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

image.png

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

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

經(jīng)過前面的分析可知,上面錯(cuò)誤的原因:內(nèi)聯(lián)函數(shù)中允許使用return關(guān)鍵字和高階函數(shù)的匿名類的實(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)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容