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
文件中,其中有TODO
、run
、with
、apply
、also
、let
、takeIf
、takeUnless
、repeat
函數。
我們將功能類似的函數放在一塊對比,如run & with
、apply & also
、takeIf & takeUnless
、let & 擴展函數版本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代碼塊的返回值。
可以發現with
和run
都是返回了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
,所以可以實現鏈式調用細化代碼粒度,讓代碼更清晰,它們的區別是一個apply
的block
是用T(也就是apply的接收者本身)
作為接收者,因此在apply
的block
內部可以訪問到T這個this
,also
的T
是被當做參數傳入block
的,所以在also
的block
內部需要用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函數
非常像,所以在上面的代碼我把它們放在一起對比,他們的區別在run
的block參數
是個帶run接收者T的函數引用
,而let
的block參數是
把let的接收者T
當做參數傳給block
,因此他們的調用區別是使用run
時,block
內部的this
是指向T
的,而在let
的block
內部需要使用it
來指向T
,let
的block
內部的this
指的是T
外部的this
,意思是類似于你在Activity
里面用let
,let
的block
里面的this
就是這個Activity實例
。
run
函數比較適合寫值多的代碼塊,let
函數比較適合讀值多的代碼塊。
2.4.1 let與also的返回值區別
let
返回的是block的返回值
,also
返回的是接收者T自身
,因此他們的鏈式調用有本質區別。
let
能實現類似RxJava
的map
的效果
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
時返回接收者自身,否者返回null
,takeUnless
則剛好相反,是在predicate
為false
時返回接收者自身,否則返回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
不能被省略的情況下,例如當它作為函數的參數被傳遞時it
比this
更短,更清晰。
在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