Kotlin高階函數的理解與使用

1. 基礎定義

1.1 什么是高階函數

按照定義,高階函數就是以另外一個函數作為參數或者返回值的函數
在Kotlin中,函數可以用lambda或者函數引用來表示。
因此,任何以lambda或者函數引用作為參數的函數,或者返回值為lambda或者函數引用的函數,或者兩者都滿足的函數都是高階函數。

1.2 lambda的約定:

要熟悉Kotlin函數,首先得看懂代碼中的lambda表達式,這里首先就得清楚一些約定,如:
當函數中只有一個函數作為參數,并且使用了lambda表達式作為對應的參數,那么可以省略函數的小括號()
函數的最后一個參數是函數類型時,可以使用lambda表達式將函數參數寫在參數列表括號外面。

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

Anko的Context擴展alert函數,可以注意到positiveButton方法第一個參數是text,
第二個參數是監聽器lambda表達式,寫在了參數列表圓括號外面。
alert("確定刪除嗎?","Alert") {
    positiveButton("OK") { Log.i(TAG, "你點了確定按鈕")}
    negativeButton("Cancel") { Log.i(TAG, "你點了取消按鈕") }
}.build().show()

1.3 函數類型變量與對應的Java代碼

在Kotlin中,變量的類型可以是函數類型,例如下面的代碼中sum變量的類型是Int類型,而predicate變量是函數類型,也就是說這個變量代表一個函數

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

聲明一個名字為predicate的函數類型變量(這個predicate變量的類型是函數)
predicate是一個以Char為參數,返回值為Boolean的函數。
var predicate: (Char) -> Boolean

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

讓上面這個函數帶上接受者,其實就是給String聲明了一個擴展函數。
帶上了接收者的函數,函數內部可以直接訪問String的其他方法屬性,相當于函數內部的this就是String
fun String.filter(predicate: (char) -> Boolean) :String

Kotlin和Java代碼是可以混合調用的,因此Kotlin的函數引用在Java是有一種對應的形式,那就是Function引用,Function1<P, R>代表只有一個參數類型為P的返回值類型為R的引用。

2. 標準高階函數

2.1 標準高階函數的聲明

標準高階函數聲明在Standard.kt文件中,其中有TODOrunwithapplyalsolettakeIftakeUnlessrepeat函數。
我們將功能類似的函數放在一塊對比,如run & withapply & alsotakeIf & takeUnlesslet & 擴展函數版本run

2.2 run&with函數

/**
 * 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函數的版本有兩個版本,一個是普通版本的定義,一種是擴展函數版本
從代碼定義可以看到,run函數接受一個函數引用作為參數(高階函數),在內部僅僅只是調用了一下這個代碼塊并且返回block代碼塊的返回值。
可以發現withrun都是返回了block(是個函數引用)的返回值。
區別在哪:
區別在于有個run是擴展函數,如果在使用之前需要判空,那么擴展函數版本的run函數的使用會比with函數優雅,如:

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

可以看到擴展函數版本的run函數在調用前可以先判斷webview.settings是否為空,否則不進入函數體調用。

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最后都會返回接收者自身T,所以可以實現鏈式調用細化代碼粒度,讓代碼更清晰,它們的區別是一個applyblock是用T(也就是apply的接收者本身)作為接收者,因此在applyblock內部可以訪問到T這個thisalsoT是被當做參數傳入block的,所以在alsoblock內部需要用it(lambda的唯一參數)代表這個also的接收者T

使用上:
一般來說,lambda參數為it的函數比較適合用做讀值多的場合,也可以使用命名參數將it改成合適的名字提升代碼可讀性,將T傳入block(T)的場合比較適合用于寫值,因為省了很多T變量的重復聲明。
【推薦】lambda表達式的block中,如果主要進行對某個實例的寫操作,則該實例聲明為Receiver;如果主要是讀操作,則該實例聲明為參數
所以在寫值操作多時使用apply代碼塊,在讀值操作多時使用also代碼塊。

inline fun <T> T.apply(block: T.() -> Unit): T//對T進行寫操作,優先使用apply

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

inline fun <T> T.also(block: (T) -> Unit): T //對T進行讀操作 優先使用also

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

2.4 let函數

/**
 * 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這個函數和擴展版本的run函數非常像,所以在上面的代碼我把它們放在一起對比,他們的區別在runblock參數是個帶run接收者T的函數引用,而letblock參數是let的接收者T當做參數傳給block,因此他們的調用區別是使用run時,block內部的this是指向T的,而在letblock內部需要使用it來指向Tletblock內部的this指的是T外部的this,意思是類似于你在Activity里面用letletblock里面的this就是這個Activity實例

run函數比較適合寫值多的代碼塊,let函數比較適合讀值多的代碼塊。

2.4.1 let與also的返回值區別

let返回的是block的返回值also返回的是接收者T自身,因此他們的鏈式調用有本質區別。
let能實現類似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
}

在上面看來T.also好像毫無意義,因為我們可以很容易地將它們組合成一個功能塊。但仔細想想,它也有一些優點:

它可以在相同的對象上提供一個非常清晰的分離過程,即制作更小的功能部分
在使用之前,它可以實現非常強大的自我操縱,實現鏈條建設者操作(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
}

這兩個函數是用來做有條件判斷時使用的,takeIf是在predicate條件返回true時返回接收者自身,否者返回nulltakeUnless則剛好相反,是在predicatefalse時返回接收者自身,否則返回null

2.6 repeat函數

/**
 * 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)
    }
}

這個函數就是把action代碼塊重復執行times次,action的參數就是當前執行的index(第幾次)。

2.7 This vs. it參數

如果你檢查T.run函數簽名,你會注意到T.run只是作為擴展函數調用block: T.()。因此,所有的范圍內,T可以被稱為this。在編程中,this大部分時間可以省略。因此,在我們上面的例子中,我們可以在println聲明中使用$length,而不是${this.length}。我把這稱為傳遞this參數。
然而,對于T.let函數簽名,你會注意到T.let把自己作為參數傳遞進去,即block: (T)。因此,這就像傳遞一個lambda參數。它可以在作用域范圍內使用it作為引用。所以我把這稱為傳遞it參數。
從上面看,它似乎T.run是更優越,因為T.let更隱含,但是這是T.let函數有一些微妙的優勢如下:

T.let相比外部類函數/成員,使用給定的變量函數/成員提供了更清晰的區分
this不能被省略的情況下,例如當它作為函數的參數被傳遞時itthis更短,更清晰。
T.let允許使用更好的變量命名,你可以轉換it為其他名稱。

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

2.8 這幾個函數的選擇:

調用鏈中保持原類型(T -> T) 調用鏈中轉換為其他類型(T -> R) 調用鏈起始(考慮使用) 調用鏈中應用條件語句
多寫操作 T.apply { ... } T.run{ ... } with(T) { ... } T.takeIf/T.takeUnless
多讀操作 T.also { ... } T.let{ ... }
根據自己的需要選擇適合的標準高階函數

3. 自定義高階函數

3.1 debug環境才運行的代碼

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

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

函數聲明為inline內聯則會在編譯時將代碼復制粘貼到對應調用的地方,如果函數體很大很復雜,不建議使用內聯,否則會使包體積增大。

4. Anko相關介紹

4.1 Anko庫顯示一個標準的對話框

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

4.2 Anko庫包含的幾個部分。

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等高階函數。
布局部分:動態擴展函數快速添加layout,如4.1顯示的標準對話框。
SQLite部分:查詢解析集合DSL
協程部分:協程相關工具。

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

詳細的看Anko主頁

5. 參考資源

Mastering Kotlin standard functions: run, with, let, also and apply
掌握Kotlin標準函數:run, with, let, also and apply
Anko: https://github.com/Kotlin/anko

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。