Kotlin高階函數(shù)的理解與使用

1. 基礎(chǔ)定義

1.1 什么是高階函數(shù)

按照定義维咸,高階函數(shù)就是以另外一個(gè)函數(shù)作為參數(shù)或者返回值的函數(shù)
在Kotlin中惠爽,函數(shù)可以用lambda或者函數(shù)引用來(lái)表示癌蓖。
因此,任何以lambda或者函數(shù)引用作為參數(shù)的函數(shù)婚肆,或者返回值為lambda或者函數(shù)引用的函數(shù)租副,或者兩者都滿(mǎn)足的函數(shù)都是高階函數(shù)。

1.2 lambda的約定:

要熟悉Kotlin函數(shù)较性,首先得看懂代碼中的lambda表達(dá)式用僧,這里首先就得清楚一些約定,如:
當(dāng)函數(shù)中只有一個(gè)函數(shù)作為參數(shù)赞咙,并且使用了lambda表達(dá)式作為對(duì)應(yīng)的參數(shù)责循,那么可以省略函數(shù)的小括號(hào)()
函數(shù)的最后一個(gè)參數(shù)是函數(shù)類(lèi)型時(shí)攀操,可以使用lambda表達(dá)式將函數(shù)參數(shù)寫(xiě)在參數(shù)列表括號(hào)外面院仿。

例如:
str.sumBy( { it.toInt } )
可以省略成
str.sumBy{ it.toInt }

Anko的Context擴(kuò)展alert函數(shù),可以注意到positiveButton方法第一個(gè)參數(shù)是text速和,
第二個(gè)參數(shù)是監(jiān)聽(tīng)器lambda表達(dá)式歹垫,寫(xiě)在了參數(shù)列表圓括號(hào)外面。
alert("確定刪除嗎颠放?","Alert") {
    positiveButton("OK") { Log.i(TAG, "你點(diǎn)了確定按鈕")}
    negativeButton("Cancel") { Log.i(TAG, "你點(diǎn)了取消按鈕") }
}.build().show()

1.3 函數(shù)類(lèi)型變量與對(duì)應(yīng)的Java代碼

在Kotlin中县钥,變量的類(lèi)型可以是函數(shù)類(lèi)型,例如下面的代碼中sum變量的類(lèi)型是Int類(lèi)型慈迈,而predicate變量是函數(shù)類(lèi)型若贮,也就是說(shuō)這個(gè)變量代表一個(gè)函數(shù)

聲明一個(gè)名字為sum的Int類(lèi)型變量(這個(gè)sum變量的類(lèi)型是Int)
var sum:Int

聲明一個(gè)名字為predicate的函數(shù)類(lèi)型變量(這個(gè)predicate變量的類(lèi)型是函數(shù))
predicate是一個(gè)以Char為參數(shù)痒留,返回值為Boolean的函數(shù)谴麦。
var predicate: (Char) -> Boolean

聲明一個(gè)以predicate函數(shù)為參數(shù)的函數(shù)(高階函數(shù)),這個(gè)函數(shù)的返回類(lèi)型是String
fun filter(predicate: (Char) -> Boolean) :String

讓上面這個(gè)函數(shù)帶上接受者伸头,其實(shí)就是給String聲明了一個(gè)擴(kuò)展函數(shù)匾效。
帶上了接收者的函數(shù),函數(shù)內(nèi)部可以直接訪問(wèn)String的其他方法屬性恤磷,相當(dāng)于函數(shù)內(nèi)部的this就是String
fun String.filter(predicate: (char) -> Boolean) :String

Kotlin和Java代碼是可以混合調(diào)用的面哼,因此Kotlin的函數(shù)引用在Java是有一種對(duì)應(yīng)的形式野宜,那就是Function引用,Function1<P, R>代表只有一個(gè)參數(shù)類(lèi)型為P的返回值類(lèi)型為R的引用魔策。

2. 標(biāo)準(zhǔn)高階函數(shù)

2.1 標(biāo)準(zhǔn)高階函數(shù)的聲明

標(biāo)準(zhǔn)高階函數(shù)聲明在Standard.kt文件中匈子,其中有TODOrun闯袒、with虎敦、applyalso政敢、let其徙、takeIftakeUnless喷户、repeat函數(shù)唾那。
我們將功能類(lèi)似的函數(shù)放在一塊對(duì)比,如run & with褪尝、apply & also通贞、takeIf & takeUnlesslet & 擴(kuò)展函數(shù)版本run恼五。

2.2 run&with函數(shù)

/**
 * Calls the specified function [block] and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

/**
 * Calls the specified function [block] with `this` value as its receiver and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

/**
 * Calls the specified function [block] with the given [receiver] as its receiver and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return receiver.block()
}

run函數(shù)的版本有兩個(gè)版本昌罩,一個(gè)是普通版本的定義,一種是擴(kuò)展函數(shù)版本
從代碼定義可以看到灾馒,run函數(shù)接受一個(gè)函數(shù)引用作為參數(shù)(高階函數(shù))茎用,在內(nèi)部?jī)H僅只是調(diào)用了一下這個(gè)代碼塊并且返回block代碼塊的返回值。
可以發(fā)現(xiàn)withrun都是返回了block(是個(gè)函數(shù)引用)的返回值睬罗。
區(qū)別在哪:
區(qū)別在于有個(gè)run是擴(kuò)展函數(shù)轨功,如果在使用之前需要判空,那么擴(kuò)展函數(shù)版本的run函數(shù)的使用會(huì)比with函數(shù)優(yōu)雅容达,如:

// Yack!
with(webview.settings) {
      this?.javaScriptEnabled = true
      this?.databaseEnabled = true
}
// Nice.
webview.settings?.run {
    javaScriptEnabled = true
    databaseEnabled = true
}

可以看到擴(kuò)展函數(shù)版本的run函數(shù)在調(diào)用前可以先判斷webview.settings是否為空古涧,否則不進(jìn)入函數(shù)體調(diào)用。

2.3 apply&also

/**
 * Calls the specified function [block] with `this` value as its receiver and returns `this` value.
 */
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}

/**
 * Calls the specified function [block] with `this` value as its argument and returns `this` value.
 */
@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.also(block: (T) -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(this)
    return this
}

apply&also最后都會(huì)返回接收者自身T花盐,所以可以實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用細(xì)化代碼粒度羡滑,讓代碼更清晰,它們的區(qū)別是一個(gè)applyblock是用T(也就是apply的接收者本身)作為接收者算芯,因此在applyblock內(nèi)部可以訪問(wèn)到T這個(gè)this柒昏,alsoT是被當(dāng)做參數(shù)傳入block的,所以在alsoblock內(nèi)部需要用it(lambda的唯一參數(shù))代表這個(gè)also的接收者T熙揍。

使用上:
一般來(lái)說(shuō)职祷,lambda參數(shù)為it的函數(shù)比較適合用做讀值多的場(chǎng)合,也可以使用命名參數(shù)將it改成合適的名字提升代碼可讀性,將T傳入block(T)的場(chǎng)合比較適合用于寫(xiě)值有梆,因?yàn)槭×撕芏郥變量的重復(fù)聲明是尖。
【推薦】lambda表達(dá)式的block中,如果主要進(jìn)行對(duì)某個(gè)實(shí)例的寫(xiě)操作泥耀,則該實(shí)例聲明為Receiver饺汹;如果主要是讀操作,則該實(shí)例聲明為參數(shù)爆袍。
所以在寫(xiě)值操作多時(shí)使用apply代碼塊,在讀值操作多時(shí)使用also代碼塊作郭。

inline fun <T> T.apply(block: T.() -> Unit): T//對(duì)T進(jìn)行寫(xiě)操作陨囊,優(yōu)先使用apply

tvName.apply {
    text = "Jacky"
    textSize = 20f
}

inline fun <T> T.also(block: (T) -> Unit): T //對(duì)T進(jìn)行讀操作 優(yōu)先使用also

user.also {
    tvName.text = it.name
    tvAge.text = it.age
}

2.4 let函數(shù)

/**
 * Calls the specified function [block] with `this` value as its receiver and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

/**
 * Calls the specified function [block] with `this` value as its argument and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)

let這個(gè)函數(shù)和擴(kuò)展版本的run函數(shù)非常像,所以在上面的代碼我把它們放在一起對(duì)比夹攒,他們的區(qū)別在runblock參數(shù)是個(gè)帶run接收者T的函數(shù)引用蜘醋,而letblock參數(shù)是let的接收者T當(dāng)做參數(shù)傳給block,因此他們的調(diào)用區(qū)別是使用run時(shí)咏尝,block內(nèi)部的this是指向T的压语,而在letblock內(nèi)部需要使用it來(lái)指向Tletblock內(nèi)部的this指的是T外部的this编检,意思是類(lèi)似于你在Activity里面用let胎食,letblock里面的this就是這個(gè)Activity實(shí)例

run函數(shù)比較適合寫(xiě)值多的代碼塊允懂,let函數(shù)比較適合讀值多的代碼塊厕怜。

2.4.1 let與also的返回值區(qū)別

let返回的是block的返回值also返回的是接收者T自身蕾总,因此他們的鏈?zhǔn)秸{(diào)用有本質(zhì)區(qū)別粥航。
let能實(shí)現(xiàn)類(lèi)似RxJavamap的效果

val original = "abc"
// Evolve the value and send to the next chain
original.let {
    println("The original String is $it") // "abc"
    it.reversed() // evolve it as parameter to send to next let
}.let {
    println("The reverse String is $it") // "cba"
    it.length  // can be evolve to other type
}.let {
    println("The length of the String is $it") // 3
}
// Wrong
// Same value is sent in the chain (printed answer is wrong)
original.also {
    println("The original String is $it") // "abc"
    it.reversed() // even if we evolve it, it is useless
}.also {
    println("The reverse String is ${it}") // "abc"
    it.length  // even if we evolve it, it is useless
}.also {
    println("The length of the String is ${it}") // "abc"
}
// Corrected for also (i.e. manipulate as original string
// Same value is sent in the chain 
original.also {
    println("The original String is $it") // "abc"
}.also {
    println("The reverse String is ${it.reversed()}") // "cba"
}.also {
    println("The length of the String is ${it.length}") // 3
}

在上面看來(lái)T.also好像毫無(wú)意義,因?yàn)槲覀兛梢院苋菀椎貙⑺鼈兘M合成一個(gè)功能塊生百。但仔細(xì)想想递雀,它也有一些優(yōu)點(diǎn):

它可以在相同的對(duì)象上提供一個(gè)非常清晰的分離過(guò)程,即制作更小的功能部分蚀浆。
在使用之前缀程,它可以實(shí)現(xiàn)非常強(qiáng)大的自我操縱,實(shí)現(xiàn)鏈條建設(shè)者操作(builder 模式)市俊。

2.5 takeIf&takeUnless

/**
 * Returns `this` value if it satisfies the given [predicate] or `null`, if it doesn't.
 */
@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? {
    contract {
        callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
    }
    return if (predicate(this)) this else null
}

/**
 * Returns `this` value if it _does not_ satisfy the given [predicate] or `null`, if it does.
 */
@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.takeUnless(predicate: (T) -> Boolean): T? {
    contract {
        callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
    }
    return if (!predicate(this)) this else null
}

這兩個(gè)函數(shù)是用來(lái)做有條件判斷時(shí)使用的杠输,takeIf是在predicate條件返回true時(shí)返回接收者自身,否者返回null秕衙,takeUnless則剛好相反蠢甲,是在predicatefalse時(shí)返回接收者自身,否則返回null据忘。

2.6 repeat函數(shù)

/**
 * Executes the given function [action] specified number of [times].
 *
 * A zero-based index of current iteration is passed as a parameter to [action].
 *
 * @sample samples.misc.ControlFlow.repeat
 */
@kotlin.internal.InlineOnly
public inline fun repeat(times: Int, action: (Int) -> Unit) {
    contract { callsInPlace(action) }

    for (index in 0 until times) {
        action(index)
    }
}

這個(gè)函數(shù)就是把action代碼塊重復(fù)執(zhí)行times次鹦牛,action的參數(shù)就是當(dāng)前執(zhí)行的index(第幾次)搞糕。

2.7 This vs. it參數(shù)

如果你檢查T.run函數(shù)簽名,你會(huì)注意到T.run只是作為擴(kuò)展函數(shù)調(diào)用block: T.()曼追。因此窍仰,所有的范圍內(nèi),T可以被稱(chēng)為this礼殊。在編程中驹吮,this大部分時(shí)間可以省略。因此晶伦,在我們上面的例子中碟狞,我們可以在println聲明中使用$length,而不是${this.length}婚陪。我把這稱(chēng)為傳遞this參數(shù)族沃。
然而,對(duì)于T.let函數(shù)簽名泌参,你會(huì)注意到T.let把自己作為參數(shù)傳遞進(jìn)去脆淹,即block: (T)。因此沽一,這就像傳遞一個(gè)lambda參數(shù)盖溺。它可以在作用域范圍內(nèi)使用it作為引用。所以我把這稱(chēng)為傳遞it參數(shù)铣缠。
從上面看咐柜,它似乎T.run是更優(yōu)越,因?yàn)?code>T.let更隱含攘残,但是這是T.let函數(shù)有一些微妙的優(yōu)勢(shì)如下:

T.let相比外部類(lèi)函數(shù)/成員拙友,使用給定的變量函數(shù)/成員提供了更清晰的區(qū)分
this不能被省略的情況下,例如當(dāng)它作為函數(shù)的參數(shù)被傳遞時(shí)itthis更短歼郭,更清晰遗契。
T.let允許使用更好的變量命名,你可以轉(zhuǎn)換it為其他名稱(chēng)病曾。

stringVariable?.let {
      nonNullString ->
      println("The non null string is $nonNullString")
}

2.8 這幾個(gè)函數(shù)的選擇:

調(diào)用鏈中保持原類(lèi)型(T -> T) 調(diào)用鏈中轉(zhuǎn)換為其他類(lèi)型(T -> R) 調(diào)用鏈起始(考慮使用) 調(diào)用鏈中應(yīng)用條件語(yǔ)句
多寫(xiě)操作 T.apply { ... } T.run{ ... } with(T) { ... } T.takeIf/T.takeUnless
多讀操作 T.also { ... } T.let{ ... }
根據(jù)自己的需要選擇適合的標(biāo)準(zhǔn)高階函數(shù)

3. 自定義高階函數(shù)

3.1 debug環(huán)境才運(yùn)行的代碼

//聲明:
inline fun debug(code: () -> Unit){
    if (BuildConfig.DEBUG) {
        code() 
    }
}

//用法:
fun onCreate(savedInstanceState: Bundle?) {
    debug {
        showDebugTools();
    }
}

函數(shù)聲明為inline內(nèi)聯(lián)則會(huì)在編譯時(shí)將代碼復(fù)制粘貼到對(duì)應(yīng)調(diào)用的地方牍蜂,如果函數(shù)體很大很復(fù)雜,不建議使用內(nèi)聯(lián)泰涂,否則會(huì)使包體積增大鲫竞。

4. Anko相關(guān)介紹

4.1 Anko庫(kù)顯示一個(gè)標(biāo)準(zhǔn)的對(duì)話(huà)框

alert("確定刪除嗎?","Alert") {
    positiveButton("OK") { Log.i(TAG, "你點(diǎn)了確定按鈕")}
    negativeButton("Cancel") { Log.i(TAG, "你點(diǎn)了取消按鈕") }
}.build().show()

4.2 Anko庫(kù)包含的幾個(gè)部分逼蒙。

Anko consists of several parts:

  • Anko Commons: a lightweight library full of helpers for intents, dialogs, logging and so on;
  • Anko Layouts: a fast and type-safe way to write dynamic Android layouts;
  • Anko SQLite: a query DSL and parser collection for Android SQLite;
  • Anko Coroutines: utilities based on the [kotlinx.coroutines]

公共部分:intents从绘,dialogs, logging等高階函數(shù)。
布局部分:動(dòng)態(tài)擴(kuò)展函數(shù)快速添加layout,如4.1顯示的標(biāo)準(zhǔn)對(duì)話(huà)框僵井。
SQLite部分:查詢(xún)解析集合DSL
協(xié)程部分:協(xié)程相關(guān)工具陕截。

4.2.1 Anko Layouts (wiki)

Anko Layouts is a DSL for writing dynamic Android layouts. Here is a simple UI written with Anko DSL:

verticalLayout {
    val name = editText()
    button("Say Hello") {
        onClick { toast("Hello, ${name.text}!") }
    }
}

4.2.2 Anko SQLite (wiki)

Have you ever been tired of parsing SQLite query results using Android cursors? Anko SQLite provides lots of helpers to simplify working with SQLite databases.

For example, here is how you can fetch the list of users with a particular name:

fun getUsers(db: ManagedSQLiteOpenHelper): List<User> = db.use {
    db.select("Users")
            .whereSimple("family_name = ?", "John")
            .doExec()
            .parseList(UserParser)
}

詳細(xì)的看Anko主頁(yè)

5. 參考資源

Mastering Kotlin standard functions: run, with, let, also and apply
掌握Kotlin標(biāo)準(zhǔn)函數(shù):run, with, let, also and apply
Anko: https://github.com/Kotlin/anko

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市批什,隨后出現(xiàn)的幾起案子农曲,更是在濱河造成了極大的恐慌,老刑警劉巖驻债,帶你破解...
    沈念sama閱讀 206,482評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件乳规,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡合呐,警方通過(guò)查閱死者的電腦和手機(jī)暮的,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)合砂,“玉大人青扔,你說(shuō)我怎么就攤上這事源织◆嫖保” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,762評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵谈息,是天一觀的道長(zhǎng)缘屹。 經(jīng)常有香客問(wèn)我,道長(zhǎng)侠仇,這世上最難降的妖魔是什么轻姿? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,273評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮逻炊,結(jié)果婚禮上互亮,老公的妹妹穿的比我還像新娘。我一直安慰自己余素,他們只是感情好豹休,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評(píng)論 5 373
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著桨吊,像睡著了一般威根。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上视乐,一...
    開(kāi)封第一講書(shū)人閱讀 49,046評(píng)論 1 285
  • 那天洛搀,我揣著相機(jī)與錄音,去河邊找鬼佑淀。 笑死留美,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播独榴,決...
    沈念sama閱讀 38,351評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼僧叉,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了棺榔?” 一聲冷哼從身側(cè)響起瓶堕,我...
    開(kāi)封第一講書(shū)人閱讀 36,988評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎症歇,沒(méi)想到半個(gè)月后郎笆,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,476評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡忘晤,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評(píng)論 2 324
  • 正文 我和宋清朗相戀三年宛蚓,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片设塔。...
    茶點(diǎn)故事閱讀 38,064評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡凄吏,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出闰蛔,到底是詐尸還是另有隱情痕钢,我是刑警寧澤,帶...
    沈念sama閱讀 33,712評(píng)論 4 323
  • 正文 年R本政府宣布序六,位于F島的核電站任连,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏例诀。R本人自食惡果不足惜随抠,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望繁涂。 院中可真熱鬧拱她,春花似錦扔罪、人聲如沸秉沼。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,264評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至坏瘩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間丰包,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,486評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工邑彪, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留寄症,地道東北人宙彪。 一個(gè)月前我還...
    沈念sama閱讀 45,511評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像有巧,于是被迫代替她去往敵國(guó)和親释漆。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評(píng)論 2 345