Kotlin中的高階函數

博客地址sguotao.top/Kotlin-2018…html

在Kotlin中,高階函數是指將一個函數做爲另外一個函數的參數或者返回值。若是用f(x)、g(x)用來表示兩個函數,那麼高階函數能夠表示爲f(g(x))。Kotlin爲開發者提供了豐富的高階函數,好比Standard.kt中的let、with、apply等,_Collectioins.kt中的forEach等。爲了可以自如的使用這些高階函數,咱們有必要去了解這些高階函數的使用方法。app

函數類型

在介紹常見高階函數的使用以前,有必要先了解函數類型,這對咱們理解高階函數頗有幫助。Kotlin 使用相似 (Int) -> String 的一系列函數類型來處理函數的聲明,這些類型具備與函數簽名相對應的特殊表示法,即它們的參數和返回值:less

  • 全部函數類型都有一個圓括號括起來的參數類型列表以及一個返回類型:(A, B) -> C 表示接受類型分別爲 A 與 B 兩個參數並返回一個 C類型值的函數類型。參數類型列表能夠爲空,如 () -> A ,返回值爲空,如(A, B) -> Unit;
  • 函數類型能夠有一個額外的接收者類型,它在表示法中的點以前指定,如類型 A.(B) -> C 表示能夠在 A 的接收者對象上,調用一個以 B 類型做爲參數,並返回一個 C 類型值的函數。
  • 還有一種比較特殊的函數類型,掛起函數,它的表示法中有一個 suspend 修飾符 ,例如 suspend () -> Unit 或者 suspend A.(B) -> C 。

經常使用高階函數

Kotlin提供了不少高階函數,這裏根據這些高階函數所在文件的位置,分別進行介紹,先來看一下經常使用的高階函數,這些高階函數在Standard.kt文件中。函數

1.TODO

先來看一下TODO的源碼:學習

/** * Always throws [NotImplementedError] stating that operation is not implemented. */

@kotlin.internal.InlineOnly
public inline fun TODO(): Nothing = throw NotImplementedError()

/** * Always throws [NotImplementedError] stating that operation is not implemented. * * @param reason a string explaining why the implementation is missing. */
@kotlin.internal.InlineOnly
public inline fun TODO(reason: String): Nothing = throw NotImplementedError("An operation is not implemented: $reason")
複製代碼

TODO函數有兩個重載函數,都會拋出一個NotImplementedError的異常。在Java中,有時會爲了保持業務邏輯的連貫性,對未實現的邏輯添加TODO標識,這些標識不進行處理,也不會致使程序的異常,可是在Kotlin中使用TODO時,就須要針對這些標識進行處理,不然當代碼邏輯運行到這些標識處時,就會出現程序的崩潰。ui

2.run

先給出run函數的源碼:this

/** * 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()
}
複製代碼

這兩個run函數都接收一個lambda表達式,執行傳入的lambda表達式,而且返回lambda表達式的執行結果。區別是T.run()是做爲泛型T的一個擴展函數,因此在傳入的lambda表達式中可使用this關鍵字來訪問這個泛型T中的成員變量和成員方法。spa

好比,對一個EditText控件,進行一些設置時:ssr

//email 是一個EditText控件
email.run { 
            this.setText("請輸入郵箱地址")
            setTextColor(context.getColor(R.color.abc_btn_colored_text_material))
}
複製代碼

3.with

先看一下with函數的源碼:code

/** * 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()
}
複製代碼

with函數有兩個參數,一個類型爲泛型T類型的receiver,和一個lambda表達式,這個表達式會做爲receiver的擴展函數來執行,而且返回lambda表達式的執行結果。

with函數與T.run函數只是寫法上的不一樣,好比上面的示例能夠用with函數:

with(email, {
            setText("請輸入郵箱地址")
            setTextColor(context.getColor(R.color.abc_btn_colored_text_material))
        })
        
        //能夠進一步簡化爲
        with(email) {
            setText("請輸入郵箱地址")
            setTextColor(context.getColor(R.color.abc_btn_colored_text_material))
        }
複製代碼

4.apply

看一下apply函數的源碼:

/** * 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
}
複製代碼

apply函數做爲泛型T的擴展函數,接收一個lambda表達式,表達式的receiver是泛型T,沒有返回值,apply函數返回泛型T對象自己。能夠看到T.run()函數也是接收lambda表達式,可是返回值是lambda表達式的執行結果,這是與apply函數最大的區別。

仍是上面的示例,能夠用apply函數:

email.apply { 
            setText("請輸入郵箱地址")
        }.apply {
            setTextColor(context.getColor(R.color.abc_btn_colored_text_material))
        }.apply { 
            setOnClickListener { 
                TODO()
            }
        }
複製代碼

5.also

看一下also函數的源碼:

/** * 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函數相似,也是做爲泛型T的擴展函數,接收一個lambda表達式,lambda表達式沒有返回值。also函數也返回泛型T對象自己,不一樣的是also函數接收的lambda表達式須要接收一個參數T,因此在lambda表達式內部,可使用it,而apply中只能使用this。

關於this和it的區別,總結一下:

  1. 若是泛型T,做爲lambda表達式的參數,形如:(T) -> Unit,此時在lambda表示內部使用it;
  2. 若是泛型T,做爲lambda表達式的接收者,形如:T.() -> Unit,此時在lambda表達式內部使用this;
  3. 不論this,仍是it,都表明T對象,區別是it可使用其它的名稱代替。

仍是上面的示例,若是用also函數:

email.also { 
            it.setText("請輸入郵箱地址")
        }.also { 
            //可使用其它名稱
            editView -> editView.setTextColor(applicationContext.getColor(R.color.abc_btn_colored_text_material))
        }.also { 
            it.setOnClickListener { 
                //TODO
            }
        }
複製代碼

6.let

看一下let函數的源碼:

/** * 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函數做爲泛型T的擴展函數,接收一個lambda表達式,lambda表達式須要接收一個參數T,存在返回值。lambda表達式的返回值就是let函數的返回值。因爲lambda表達式接受參數T,因此也能夠在其內部使用it。

let應用最多的場景是用來判空,若是上面示例中的EditText是自定義的可空View,那麼使用let就很是方便:

var email: EditText? = null
        TODO()
        email?.let { 
            email.setText("請輸入郵箱地址")
            email.setTextColor(getColor(R.color.abc_btn_colored_text_material))
        }
複製代碼

7.takeIf

看一下takeIf函數的源碼:

/** * 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
}
複製代碼

takeIf函數做爲泛型T的擴展函數,接受一個lambda表達式,lambda表達式接收一個參數T,返回Boolean類型,takeIf函數根據接收的lambda表達式的返回值,決定函數的返回值,若是lambda表達式返回true,函數返回T對象自己,若是lambda表達式返回false,函數返回null。

仍是上面的示例,假設用戶沒有輸入郵箱地址,進行信息提示:

email.takeIf { 
            email.text.isEmpty()
        }?.setText("郵箱地址不能爲空")
複製代碼

8.takeUnless

給出takeUnless函數的源碼:

/** * 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
}
複製代碼

takeUnless函數與takeIf函數相似,惟一的區別是邏輯相反,takeUnless函數根據lambda表達式的返回值決定函數的返回值,若是lambda表達式返回true,函數返回null,若是lambda表達式返回false,函數返回T對象自己。

仍是上面的示例,若是用takeUnless實現,就須要調整一下邏輯:

email.takeUnless {
            email.text.isNotEmpty()  //與takeIf的區別
        }?.setText("郵箱地址不能爲空")
複製代碼

9.repeat

給出repeat函數的源碼:

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

    for (index in 0 until times) {
        action(index)
    }
}
複製代碼

repeat函數接收兩個參數,一個Int型參數times表示重複次數,一個lambda表達式,lambda表達式接收一個Int型參數,無返回值。repeat函數就是將咱們傳入的lambda表達式執行times次。

repeat(3) {
        println("執行第${it + 1}次")
    }
    
    //運行結果
執行第1次
執行第2次
執行第3複製代碼

因爲repeat函數接收的lambda表達式,須要一個Int型參數,所以在表達式內部使用it,其實it就是for循環的索引,從0開始。

總結

最後對這些高階函數作一下總結,TODO對比Java中的TODO,須要實現業務邏輯,不能聽任不理,不然會出現異常,致使崩潰。takeIf、takeUnless這一對都是根據接收lambda表達式的返回值,決定函數的最終返回值是對象自己,仍是null,區別是takeIf,若是lambda表達式返回true,返回對象自己,不然返回null;takeUnless與takeIf的邏輯正好相反,若是lambda表達式返回true,返回null,不然返回對象自己。repeat函數,見名知意,將接收的lambda表達式重複執行指定次。

run、with、apply、also、let這幾個函數區別不是很明顯,有時候使用其中一個函數實現的邏輯,徹底也能夠用另一個函數實現,具體使用哪個,根據我的習慣。須要注意的是:

  1. 對做爲擴展函數的高階函數,使用前須要判斷接收的對象是否爲空,好比T.run,apply,also,let在使用前須要進行空檢查;
  2. 對於返回對象自己的函數,好比apply,also能夠造成鏈式調用;
  3. 對於在函數內部可以使用it的函數,it能夠用意思更加清晰的變量代替,好比T.run,also,let

對這幾個函數的區別作一個對比:

函數名稱 是否做爲擴展函數 是否返回對象自己 在函數內部使用this/ it
run no no -
T.run yes no it
with no no this
apply yes yes this
also yes yes it
let yes no it

學習資料

  1. Kotlin Bootcamp for Programmers
  2. Kotlin Koans
相關文章
相關標籤/搜索