Kotlin之高階函數

1、高階函數

1.1、高階函數的定義

高階函數的定義:如果一個函數接收另一個函數作為參數,或者返回值的類型是另一個函數,那么該函數稱為高階函數。你可能會有疑問,一個函數怎么能接收另一個函數作為參數呢?因為Kotlin中新增了函數類型,如果我們將這種函數類型添加到一個函數的參數聲明后者返回值聲明當中,那么該函數就成為高階函數。

1.2、函數類型的定義

函數類型的定義的基本規則如下:

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

這里example()函數就接收了一個函數類型的參數了,該函數就是高階函數了。函數類型的參數使用就和調用函數一樣,傳入相應的參數即可。

1.3、高階函數的用途

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

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

該函數前兩個參數沒什么好說的,第三個參數是函數類型的接收兩個Int變量并且返回值為Int類型,將前兩個Int類型的參數傳遞給第三個函數類型作為參數,高階函數中沒有其他邏輯,將具體的邏輯交由第三個函數類型的參數來完成。
那么第三個參數應該傳什么呢?我們可以在同文件下定義與其匹配的函數或者使用其他類中相匹配類型的函數作為參數,這里我們現在HighFuncFile文件下定義函數。

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

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

高階函數的調用

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

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

class HighFuncTest {

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

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

}

我們上面使用了::plusFunc來引用函數,那此時我們該怎么引用函數呢?

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

先創建對象,然后使用highFuncTest::plusFunc來引用HighFuncTest類中的函數作為參數傳遞給高階函數。
像這種每次調用高階函數都需要定義與其函數類型參數匹配的函數,使用起來確實很麻煩,為此Kotlin提供了其他方式調用高階函數,比如:Lambda表達式、匿名函數、成員引用等,Lambda表達式是最常用的高階函數調用方式。下面我們就來學習下如何使用Lambda表達式來調用高階函數,我們把上面的例子改成Lambda表達式的方法。

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")

可以發現使用Lambda表達式同樣可以完整的表達函數類型的參數和返回值,Lambda表達式的最后一行代碼的返回值作為函數的返回值返回。
下面對高階函數繼續探究,回顧一下apply標準函數的用法

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

apply標準函數會把調用對象傳遞到Lambda表達式中作為上下文,并且返回調用對象。下面我們就用高階函數來實現類似的功能。

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

這里給StringBuilder類定義了一個擴展函數,擴展函數接收一個函數類型的參數,并且返回值為StringBuilder。
注意:這里定義的函數類型的參數和我們前面學習的語法還是有區別的,在函數類型的前面加上了StringBuilder.,其實這才是完整的函數類型的定義規則,加上ClassName.表示在哪個類中定義函數類型。使用StringBuilder.表示在StringBuilder類中定義的函數類型,那么在傳入Lambda表達式時將會自動擁有StringBuilder的上下文。下面看下otherApply()的使用

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

可以看到和apply標準函數的使用完全一樣,只不過apply適用所有類的使用,而otherApply只局限于StringBuilder的使用,如果想實現apply的函數的這個功能,就需要借助Kotlin泛型才可以。

2、內聯函數

2.1、高階函數的實現原理

學習內聯函數前我們先來學習一下高級函數的實現原理。這里仍然使用剛才編寫的num1AndNum2()函數為例。

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

上面是Kotlin中高階函數的基本用法,我們知道Kotlin代碼最終會編譯成Java字節碼的,而Java中是沒有高階函數概念的,其實Kotlin編譯器最終會把Kotlin中高階函數的語法轉換成Java支持的語法結構,上述的Kotlin代碼大致被轉換成如下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;
            }
        });
    }

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

2.2、內聯函數的使用以及原理

內聯函數的使用比較簡單就是在定義的高階函數時加上inline關鍵字即可。

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

內聯函數的原理
內聯函數的原理也很簡單:Kotlin編譯器在編譯時把內聯函數內代碼自動替換到要調用的地方,這樣就解決了運行時的內存開銷。
下面看下替換的過程
步驟一、將Lambda表達式的代碼替換到函數類型參數調用的地方

image.png

步驟二、將內聯函數中全部代碼替換到函數調用的地方
image.png

最終替換后的代碼為

 val minusResult =20-30

正是如此內聯函數才能完全消除Lambda表達式運行時帶來的額外內存開銷。

3、noinline和crossinline

3.1、noinline

一個高階函數中接收兩個或更多函數類型的參數,如果高階函數被inline修飾了,那么所有函數類型的參數均會被內聯,如果想某個函數類型的參數不被內聯,可以用關鍵字noinline修飾。

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

可以看到testinline修飾,本來block1和block2這兩個函數類型參數所引用Lambda表達式均被內聯。由于我們在block2前加上了noinline關鍵字,那么只有block1這個函數類型參數所引用的Lambda表達式被內聯。
既然內聯函數能消除Lambda表達式運行時帶來的內存的額外開銷,那么為什么還提供了一個noinline來排除內聯呢?

  • 原因一:內聯函數類型的參數在編譯期間會進行代碼替換,所以內聯的函數類型的參數算不上真正的參數,非內聯的函數類型的參數可以作為真正的參數傳遞給任何函數。內聯函數類型的參數只能傳遞給另一個內聯函數。這也是它最大的局限性。
  • 原因二:內聯函數和非內聯函數有一個重要的區別:內聯函數所引用的Lambda表達式中可以使用return來進行函數的返回,而非內聯函數只能進行局部返回。
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")
        }

這里定義了一個非內聯的高階函數,在Lambda表達式中如果傳入的字符串為空,則直接返回,此時Lambda表達式中只能使用return@printString進行局部返回。打印結果如下:

main begin
printString begin
lambda begin
printString end
main end

可以看到lambda end并沒有輸出,因為輸入的字符串為空,則局部返回不再執行Lambda表達式中的函數,所以Log.e(tag, "lambda end")沒有執行。
下面我們聲明一個內聯函數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是內聯函數,我們可以在Lambda表達式中使用return進行返回,打印結果如下:

main begin
printString begin
lambda begin

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

3.2、crossinline

將高階函數聲明成內聯函數是一種良好的習慣,事實上絕大多數高階函數是可以被聲明成內聯函數的,但是也有例外的情況。觀察下面的代碼

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

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

image.png

首先我們在內聯函數runRunnable中創建一個runnable對象,并在Runnable的Lambda表達式中傳入的函數類型參數,而Lambda表達式在編譯的時候會被轉換成匿名類的實現方式,也就是說上面代碼是在匿名類中傳入了函數類型的參數。
而內聯函數所引用的Lambda表達式允許使用return進行函數的返回,但是由于我們是在匿名類中調用的函數類型參數,此時不能進行外層調用函數的返回,最多只能進行匿名類中的方法進行返回,因此就提示了上述錯誤。
也就是說:如果我們在高階函數中創建了Lambda或匿名類的實現,在這些實現中調用函數類型參數,此時再將高階函數聲明成內聯,肯定會報上面的錯誤。
那么如何在這種情況下使用內聯函數呢?這就需要關鍵字crossinline

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

經過前面的分析可知,上面錯誤的原因:內聯函數中允許使用return關鍵字和高階函數的匿名類的實現中不能使用return之間造成了沖突。而crossinline關鍵字用于保證在Lambda表達式中一定不使用return關鍵字,這樣沖突就不存在了。但是我們仍然可以使用return@runRunnable進行局部返回。總體來說,crossinline除了return用法不同外,仍然保留了內聯函數的所有特性。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,750評論 2 370