本篇文章主要介紹以下幾個(gè)知識(shí)點(diǎn):
- 高階函數(shù)
- 內(nèi)聯(lián)函數(shù)
- noinline 與 crossinline
- 高階函數(shù)的應(yīng)用
內(nèi)容參考自第一行代碼第3版
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ù)的里覆,但也有特例,如下:
上面代碼若沒有加上 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()
。
本篇文章就介紹到這丸凭。