Kotlin學(xué)習(xí) 5 -- 高階函數(shù)

本篇文章主要介紹以下幾個(gè)知識(shí)點(diǎn):

SUMMER DAY (圖片來源于網(wǎng)絡(luò))

1. 定義高階函數(shù)

前面學(xué)習(xí)了如map, filter, run, apply 等函數(shù)擎厢,它們有一個(gè)共同的特點(diǎn):傳入一個(gè) Lambda 表達(dá)式作為參數(shù)。

這種接收 Lambda 參數(shù)的函數(shù)可稱為具有函數(shù)式編程風(fēng)格的 API敬锐,若要自定義函數(shù)式 API显拳,則需要借助高階函數(shù)來實(shí)現(xiàn)证鸥。

高階函數(shù):接收另一個(gè)函數(shù)(函數(shù)類型)作為參數(shù)镜盯,或返回值的類型是另一個(gè)函數(shù)吗浩。

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

// -> 左邊部分是用來聲明該函數(shù)接收什么參數(shù)腹忽,多個(gè)參數(shù)可用逗號(hào)隔開来累,若不接收參數(shù)則寫空括號(hào)即可
// -> 右邊部分是用來聲明該函數(shù)的返回值是什么類型,若沒有返回值就使用 Unit (類似Java中的void)
(String, Int) -> Unit

把上述函數(shù)類型添加到某個(gè)函數(shù)的參數(shù)聲明或者返回值聲明上窘奏,這個(gè)函數(shù)就是一個(gè)高階函數(shù)了:

// example() 函數(shù)接收了一個(gè)函數(shù)類型的參數(shù)嘹锁,這邊 example() 函數(shù)就是一個(gè)高階函數(shù)
fun example(func: (String, Int) -> Unit) {
    func("hello", 123)
}

高階函數(shù)允許讓函數(shù)類型的參數(shù)來決定函數(shù)的執(zhí)行邏輯。同一個(gè)高階函數(shù)着裹,傳入不同的函數(shù)類型领猾,其執(zhí)行邏輯和最終返回結(jié)果就可能不同。

下面舉個(gè)栗子,定義一個(gè) num1AndNum2() 的高階函數(shù):

// 這邊第三個(gè)參數(shù)是一個(gè)接收兩個(gè)整型參數(shù)并且返回值也是整型的函數(shù)類型參數(shù)
fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
    val result = operation(num1, num2)
    return result
}

接著定義與上面函數(shù)類型(第三個(gè)參數(shù))相匹配的函數(shù)如下:

// 兩數(shù)相加并返回
fun plus(num1: Int, num2: Int): Int {
    return num1 + num2
}

// 兩數(shù)相減并返回
fun minus(num1: Int, num2: Int): Int {
    return num1 - num2
}

定義好上述函數(shù)后瘤运,就可以調(diào)用 num1AndNum2() 函數(shù)了:

fun main() {
    val num1 = 100
    val num2 = 200
    val result1 = num1AndNum2(num1, num2, ::plus)
    val result2 = num1AndNum2(num1, num2, ::minus)
    println("result1 is $result1")
    println("result2 is $result2")
}

除了用上面的方式來調(diào)用高階函數(shù)窍霞,Kotlin 也支持用 Lambda 表達(dá)式、匿名函數(shù)拯坟、成員引用等來調(diào)用高階函數(shù)但金,如使用 Lambda 表達(dá)式來實(shí)現(xiàn)上述代碼:

fun main() {
    val num1 = 100
    val num2 = 200
    val result1 = num1AndNum2(num1, num2) { n1, n2 ->
        n1 + n2
    }
    val result2 = num1AndNum2(num1, num2) { n1, n2 ->
        n1 - n2
    }
    println("result1 is $result1")
    println("result2 is $result2")
}

這樣用 Lambda 表達(dá)式來調(diào)用就無需定義 plus()minus() 函數(shù)了。


回顧之前的 apply() 函數(shù)的例子:

fun main() {
    val list = listOf("apple", "orange", "pear")

    // apply 函數(shù)返回的是 StringBuilder 對(duì)象
    val applyResult = StringBuilder().apply {
        append("開始吃水果:\n")
        for (fruit in list) {
            append(fruit).append("\n")
        }
        append("吃完全部水果!")
    }

    println(applyResult.toString())
}

若要用高階函數(shù)實(shí)現(xiàn)類似的功能郁季,可以給StringBuilder 類定義一個(gè) build 擴(kuò)展函數(shù)冷溃,這個(gè)擴(kuò)展函數(shù)接收一個(gè)函數(shù)類型參數(shù)并且返回值類型也是 StringBuilder,如下:

// 在函數(shù)類型的前面加上 ClassName. 表示這個(gè)函數(shù)類型是定義在哪個(gè)類中的
// 這邊在函數(shù)類型的前面加上了一個(gè) StringBuilder. 
// 這樣調(diào)用 build 函數(shù)時(shí)傳入的 Lambda 表達(dá)式就會(huì)自動(dòng)擁有 StringBuilder 的上下文
fun StringBuilder.build(block: StringBuilder.() -> Unit): StringBuilder {
    return this
}

這樣就可以用定義的 build 擴(kuò)展函數(shù)來實(shí)現(xiàn)上面 apply() 函數(shù)的例子了:

fun main() {
    val list = listOf("apple", "orange", "pear")
    
    val result = StringBuilder().build {
        append("開始吃水果:\n")
        for (fruit in list) {
            append(fruit).append("\n")
        }
        append("吃完全部水果!")
    }

    println(result.toString())
}

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

首先來簡(jiǎn)單分析下高階函數(shù)的實(shí)現(xiàn)原理梦裂,比如上面的 num1AndNum2() 函數(shù):

fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
    val result = operation(num1, num2)
    return result
}

fun main() {
    val num1 = 100
    val num2 = 200
    val result = num1AndNum2(num1, num2) { n1, n2 ->
        n1 + n2
    }
}

由于 Kotlin 的代碼最終還是要編譯成 Java 字節(jié)碼的似枕,但 Java 中并無高階函數(shù)的概念,從而編譯器會(huì)將這些高階函數(shù)的語法轉(zhuǎn)換成 Java 支持的語法結(jié)構(gòu)年柠,上面代碼被轉(zhuǎn)換成 Java 代碼大致如下:

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

public static void main() {
    int num1 = 100;
    int num2 = 200;
    int result = num1AndNum2(num1, num2, new Fuction() {
        @Override
        public Integer invoke(Integer n1, Integer n2) {
            return n1 + n2;    
        }
    });
}

原來一直使用的 Lambda 表達(dá)式在底層被轉(zhuǎn)化成了匿名類的實(shí)現(xiàn)方式凿歼,這樣每調(diào)用一次 Lambda 表達(dá)式,就會(huì)創(chuàng)建一個(gè)新的匿名內(nèi)部類冗恨,從而造成額外的內(nèi)存和性能開銷答憔。

為了解決這個(gè)問題,Kotlin 提供了內(nèi)聯(lián)函數(shù)的功能掀抹,它可以把使用 Lambda 表達(dá)式帶來的運(yùn)行時(shí)開銷完全消除虐拓。

使用內(nèi)聯(lián)函數(shù)只需在定義高階函數(shù)時(shí)加上 inline 關(guān)鍵字即可:

inline fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
    val result = operation(num1, num2)
    return result
}

內(nèi)聯(lián)函數(shù)的工作原理:Kotlin 編譯器會(huì)將內(nèi)聯(lián)函數(shù)中的代碼在編譯時(shí)自動(dòng)替換到調(diào)用它的地方,這樣也就不存在運(yùn)行時(shí)的開銷了傲武。

3. noinline 與 crossinline

當(dāng)一個(gè)高階函數(shù)中接收了兩個(gè)或多個(gè)函數(shù)類型的參數(shù)蓉驹,若給函數(shù)加上 inline 關(guān)鍵字,那么編譯器會(huì)自動(dòng)將所有引用的 Lambda 表達(dá)式全部進(jìn)行內(nèi)聯(lián)揪利。

若此時(shí)只想內(nèi)聯(lián)其中的一個(gè) Lambda 表達(dá)式态兴,就可以使用 noinline 關(guān)鍵字了:

// block2 參數(shù)前加了 noinline 關(guān)鍵字后,只會(huì)對(duì) block1 參數(shù)所引用的 Lambda 表達(dá)式進(jìn)行內(nèi)聯(lián)
inline fun inlineTest(block1: () -> Unit, noinline block2: () -> Unit) { }

為什么 Kotlin 還要提供一個(gè) noinline 關(guān)鍵字來排除內(nèi)聯(lián)功能土童?

內(nèi)聯(lián)的函數(shù)類型參數(shù)在編譯時(shí)會(huì)被進(jìn)行替換诗茎,因此它沒有真正的參數(shù)屬性。

非內(nèi)聯(lián)的函數(shù)類型參數(shù)可自由傳遞給其他任何函數(shù)献汗,它是一個(gè)真實(shí)的參數(shù)敢订。

而內(nèi)聯(lián)的函數(shù)類型參數(shù)值允許傳遞給另外一個(gè)內(nèi)聯(lián)函數(shù),這也是它的局限性罢吃。

另外楚午,內(nèi)聯(lián)函數(shù)所引用的 Lambda 表達(dá)式可使用 return 關(guān)鍵字來進(jìn)行函數(shù)返回,而非內(nèi)聯(lián)函數(shù)只能進(jìn)行局部返回尿招。舉個(gè)栗子矾柜,如分別定義非內(nèi)聯(lián)和內(nèi)聯(lián)高階函數(shù)如下:

fun noinlineTest(str: String, block: (String) -> Unit) {
    block(str)
}

inline fun inlineTest(str: String, block: (String) -> Unit) {
    block(str)
}

fun main() {
    val str = ""

    noinlineTest(str) {
        // 非內(nèi)聯(lián)函數(shù)中引用的 Lambda 表達(dá)式是不允許直接使用 return 關(guān)鍵字的
        // 這邊使用 return@noinlineTest 的寫法阱驾,表示進(jìn)行局部返回
        if (it.isEmpty()) return@noinlineTest
        println(it)
    }

    inlineTest(str) {
        // 內(nèi)聯(lián)函數(shù)中引用的 Lambda 表達(dá)式可以使用 return 關(guān)鍵字
        // 這邊的 return 代表的是返回外層的調(diào)用函數(shù),即 main() 函數(shù)
        if (it.isEmpty()) return
        println(it)
    }
}

將高階函數(shù)聲明成內(nèi)聯(lián)函數(shù)是一種良好的編程習(xí)慣怪蔑,大部分高階函數(shù)是可以直接聲明成內(nèi)聯(lián)函數(shù)的里覆,但也有特例,如下:


使用內(nèi)聯(lián)函數(shù)可能出現(xiàn)的錯(cuò)誤

上面代碼若沒有加上 inline 關(guān)鍵字時(shí)是不會(huì)報(bào)錯(cuò)缆瓣,加上后就會(huì)報(bào)上面的錯(cuò)誤喧枷。

上面代碼在 runRunnable() 函數(shù)中創(chuàng)建了一個(gè) Runnable 對(duì)象,在它的 Lambda 表達(dá)式中調(diào)用了傳入的函數(shù)類型參數(shù)弓坞。而 Lambda 表達(dá)式在編譯時(shí)會(huì)被轉(zhuǎn)換成匿名類的實(shí)現(xiàn)方式隧甚,也就是說上面代碼實(shí)際上是在匿名類中調(diào)用了傳入的參數(shù)類型參數(shù)。

內(nèi)聯(lián)函數(shù)的 Lambda 表達(dá)式中允許使用 return 關(guān)鍵字渡冻,和高階函數(shù)的匿名類實(shí)現(xiàn)中不允許使用 return 關(guān)鍵子之間造成了沖突戚扳,從而出現(xiàn)上述報(bào)錯(cuò)。

即在高階函數(shù)中創(chuàng)建了另外的 Lambda 或匿名類的實(shí)現(xiàn)族吻,并且在這些實(shí)現(xiàn)中調(diào)用函數(shù)類型參數(shù)帽借,此時(shí)聲明成內(nèi)聯(lián)函數(shù)就會(huì)報(bào)錯(cuò)。

當(dāng)然超歌,借助 crossinline 關(guān)鍵字可以解決上面的問題:

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

在函數(shù)類型參數(shù)的前面加上 crossinline 聲明宜雀,就可以編譯通過了。

crossinline 關(guān)鍵字是用于保證在內(nèi)聯(lián)函數(shù)的 Lambda 表達(dá)式中一定不會(huì)使用 return 關(guān)鍵字握础。

聲明了 crossinline 后,就無法在調(diào)用 runRunnable() 函數(shù)時(shí)的 Lambda 表達(dá)式中使用 return 進(jìn)行函數(shù)返回悴品,但仍然可用 return@runRunnable 的寫法進(jìn)行局部返回禀综。

小結(jié):除了在 return 關(guān)鍵字使用上有所區(qū)別,crossinline 保留了內(nèi)聯(lián)函數(shù)的所有其他特性苔严。

4. 高階函數(shù)的應(yīng)用

4.1 簡(jiǎn)化 SharedPreferences 的用法

高階函數(shù)非常適用與簡(jiǎn)化各種 API 的調(diào)用定枷,一些簡(jiǎn)化后在易用性和可讀性上都會(huì)有很大提升。

比如 SharedPreferences 原來的用法如下:

val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit()
editor.putString("name", "Wonderful")
editor.putInt("age", 18)
editor.apply()

接下來使用高階函數(shù)簡(jiǎn)化 SharedPreferences 的用法届氢,向 SharedPreferences 添加一個(gè)擴(kuò)展函數(shù)open 如下:

fun SharedPreferences.open(block: SharedPreferences.Editor.() -> Unit) {
    val editor = edit()
    editor.block()
    editor.apply()
}

這樣再使用 SharedPreferences 存儲(chǔ)數(shù)據(jù)是就更加方便了:

getSharedPreferences("data", Context.MODE_PRIVATE).open {
    putString("name", "Wonderful")
    putInt("age", 18)
}

當(dāng)然欠窒,Google 提供的 KTX 擴(kuò)展庫中已經(jīng)包含了上述的簡(jiǎn)化用法。

4.2 簡(jiǎn)化 ContentValues 的用法

ContentValues 主要用于結(jié)合 SQLiteDatabase 的 API 存儲(chǔ)和修改數(shù)據(jù)庫中的數(shù)據(jù)退子,用法如下:

val values = ContentValues()
values.put("name", "Wonderful")
values.put("age", 18)
values.put("height", 200.5)
db.insert("Person", null, values)

上面代碼可以用 apply 簡(jiǎn)化岖妄,但這里先用 Pair 對(duì)象(鍵值對(duì))來定義一個(gè) cvOf() 方法:

// 接收一個(gè) Pair 參數(shù),
// vararg 關(guān)鍵字允許傳入任意多的參數(shù)(即 Java 中的可變參數(shù)列表)
// Any 是 Kotlin 中所有類的共同基類(相當(dāng)于 Java 中的 Object)
fun cvOf(vararg pairs: Pair<String, Any?>): ContentValues {

}

繼續(xù)完善方法如下:

fun cvOf(vararg pairs: Pair<String, Any?>): ContentValues {
    val cv = ContentValues()
    for (pair in pairs) {
        val key = pair.first
        val value = pair.second
        when (value) {
            is Int -> cv.put(key, value)
            is Long -> cv.put(key, value)
            is Short -> cv.put(key, value)
            is Float -> cv.put(key, value)
            is Double -> cv.put(key, value)
            is Boolean -> cv.put(key, value)
            is String -> cv.put(key, value)
            is Byte -> cv.put(key, value)
            is ByteArray -> cv.put(key, value)
            null -> cv.putNull(key)
        }
    }
    return cv
}

當(dāng)然可以用 apply 進(jìn)一步優(yōu)化:

fun cvOf(vararg pairs: Pair<String, Any?>) = ContentValues().apply {
    for (pair in pairs) {
        val key = pair.first
        when (val value = pair.second) {
            is Int -> put(key, value)
            is Long -> put(key, value)
            is Short -> put(key, value)
            is Float -> put(key, value)
            is Double -> put(key, value)
            is Boolean -> put(key, value)
            is String -> put(key, value)
            is Byte -> put(key, value)
            is ByteArray -> put(key, value)
            null -> putNull(key)
        }
    }
}

這樣使用 ContentValues 時(shí)就更加簡(jiǎn)單了寂祥,比如開頭代碼可改為如下:

val values = cvOf("name" to "Wonderful", "age" to 18, "height" to 200.5)
db.insert("Person", null, values)

當(dāng)然荐虐,Google 提供的 KTX 擴(kuò)展庫中已經(jīng)包含了上述的簡(jiǎn)化用法:contentValuesOf()

本篇文章就介紹到這丸凭。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末福扬,一起剝皮案震驚了整個(gè)濱河市腕铸,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌铛碑,老刑警劉巖狠裹,帶你破解...
    沈念sama閱讀 211,561評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異汽烦,居然都是意外死亡涛菠,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門刹缝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來碗暗,“玉大人,你說我怎么就攤上這事梢夯⊙粤疲” “怎么了?”我有些...
    開封第一講書人閱讀 157,162評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵颂砸,是天一觀的道長(zhǎng)噪奄。 經(jīng)常有香客問我,道長(zhǎng)人乓,這世上最難降的妖魔是什么勤篮? 我笑而不...
    開封第一講書人閱讀 56,470評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮色罚,結(jié)果婚禮上碰缔,老公的妹妹穿的比我還像新娘。我一直安慰自己戳护,他們只是感情好金抡,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,550評(píng)論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著腌且,像睡著了一般梗肝。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上铺董,一...
    開封第一講書人閱讀 49,806評(píng)論 1 290
  • 那天巫击,我揣著相機(jī)與錄音,去河邊找鬼精续。 笑死坝锰,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的驻右。 我是一名探鬼主播什黑,決...
    沈念sama閱讀 38,951評(píng)論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼堪夭!你這毒婦竟也來了愕把?” 一聲冷哼從身側(cè)響起拣凹,我...
    開封第一講書人閱讀 37,712評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎恨豁,沒想到半個(gè)月后嚣镜,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,166評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡橘蜜,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,510評(píng)論 2 327
  • 正文 我和宋清朗相戀三年菊匿,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片计福。...
    茶點(diǎn)故事閱讀 38,643評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡跌捆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出象颖,到底是詐尸還是另有隱情佩厚,我是刑警寧澤,帶...
    沈念sama閱讀 34,306評(píng)論 4 330
  • 正文 年R本政府宣布说订,位于F島的核電站抄瓦,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏陶冷。R本人自食惡果不足惜钙姊,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,930評(píng)論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望埂伦。 院中可真熱鬧煞额,春花似錦、人聲如沸沾谜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽类早。三九已至,卻和暖如春嗜逻,著一層夾襖步出監(jiān)牢的瞬間涩僻,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工栈顷, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留逆日,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,351評(píng)論 2 360
  • 正文 我出身青樓萄凤,卻偏偏與公主長(zhǎng)得像室抽,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子靡努,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,509評(píng)論 2 348