1蒜胖、高階函數(shù)
1.1消别、高階函數(shù)的定義
高階函數(shù)的定義:如果一個函數(shù)接收另一個函數(shù)作為參數(shù),或者返回值的類型是另一個函數(shù)台谢,那么該函數(shù)稱為高階函數(shù)寻狂。你可能會有疑問,一個函數(shù)怎么能接收另一個函數(shù)作為參數(shù)呢朋沮?因為Kotlin中新增了
函數(shù)類型
蛇券,如果我們將這種函數(shù)類型添加到一個函數(shù)的參數(shù)聲明后者返回值聲明當(dāng)中,那么該函數(shù)就成為高階函數(shù)樊拓。
1.2纠亚、函數(shù)類型的定義
函數(shù)類型的定義的基本規(guī)則如下:
methodName:(Int,String)->Unit
- 1、methodName是函數(shù)類型的名稱筋夏,名稱不限制蒂胞。
- 2、(Int,String)代表函數(shù)接收的類型条篷,多個參數(shù)類型用逗號隔開骗随。
- 3、->右邊表示函數(shù)的返回值赴叹,Unit類似于Java的void表示無返回值蚊锹。
下面看下如何將這個函數(shù)類型添加到一個函數(shù)的參數(shù)聲明中:
fun example(block:(String,Int)->Unit){
block("test",123)
}
這里example()
函數(shù)就接收了一個函數(shù)類型的參數(shù)了,該函數(shù)就是高階函數(shù)了稚瘾。函數(shù)類型的參數(shù)使用就和調(diào)用函數(shù)一樣牡昆,傳入相應(yīng)的參數(shù)即可。
1.3摊欠、高階函數(shù)的用途
高階函數(shù)允許讓函數(shù)類型參數(shù)決定函數(shù)的執(zhí)行邏輯丢烘,即使同一個高階函數(shù),傳入的函數(shù)類型參數(shù)不同些椒,那么函數(shù)的執(zhí)行邏輯和返回結(jié)果可能也是完全不同的播瞳。下面我們舉例說明下:
這里準(zhǔn)備定義一個函數(shù)num1AndNum2()
接收2個Int參數(shù)和一個函數(shù)類型的參數(shù),由函數(shù)類型的參數(shù)決定這兩個Int參數(shù)具體執(zhí)行的運(yùn)算免糕。
新建一個HighFuncFile文件赢乓,在其中定義高階函數(shù)
fun num1AndNum2(num1: Int, num2: Int, block: (Int, Int) -> Int): Int {
return block(num1, num2)
}
該函數(shù)前兩個參數(shù)沒什么好說的忧侧,第三個參數(shù)是函數(shù)類型的接收兩個Int變量并且返回值為Int類型,將前兩個Int類型的參數(shù)傳遞給第三個函數(shù)類型作為參數(shù)牌芋,高階函數(shù)中沒有其他邏輯蚓炬,將具體的邏輯交由第三個函數(shù)類型的參數(shù)來完成。
那么第三個參數(shù)應(yīng)該傳什么呢躺屁?我們可以在同文件下定義與其匹配的函數(shù)或者使用其他類中相匹配類型的函數(shù)作為參數(shù)肯夏,這里我們現(xiàn)在HighFuncFile文件
下定義函數(shù)。
fun plusFunc(num1: Int, num2: Int): Int {
return num1 + num2
}
fun minusFunc(num1: Int, num2: Int): Int {
return num1 - num2
}
高階函數(shù)的調(diào)用
num1AndNum2(20, 30, ::plusFunc)
num1AndNum2(20, 30, ::minusFunc)
可以看到第三個參數(shù)我們使用了::plusFunc
這種寫法犀暑,這是一種函數(shù)引用的寫法驯击,表示將函數(shù)plusFunc()
來作為參數(shù)傳遞給高階函數(shù)。如果這兩個函數(shù)是定義在某個類中耐亏,那么該怎么引用這個函數(shù)呢徊都?
在HighFuncTest.class中定義函數(shù)
class HighFuncTest {
fun plusFunc(num1: Int, num2: Int): Int {
return num1 + num2
}
fun minusFunc(num1: Int, num2: Int): Int {
return num1 - num2
}
}
我們上面使用了::plusFunc
來引用函數(shù),那此時我們該怎么引用函數(shù)呢广辰?
val highFuncTest: HighFuncTest = HighFuncTest()
num1AndNum2(20, 30, highFuncTest::plusFunc)
num1AndNum2(20, 30, highFuncTest::minusFunc)
先創(chuàng)建對象碟贾,然后使用highFuncTest::plusFunc
來引用HighFuncTest類
中的函數(shù)作為參數(shù)傳遞給高階函數(shù)。
像這種每次調(diào)用高階函數(shù)都需要定義與其函數(shù)類型參數(shù)匹配的函數(shù)轨域,使用起來確實很麻煩,為此Kotlin提供了其他方式調(diào)用高階函數(shù)杀餐,比如:Lambda表達(dá)式干发、匿名函數(shù)、成員引用
等史翘,Lambda表達(dá)式是最常用的高階函數(shù)調(diào)用方式枉长。下面我們就來學(xué)習(xí)下如何使用Lambda表達(dá)式來調(diào)用高階函數(shù),我們把上面的例子改成Lambda表達(dá)式的方法琼讽。
val plusResult = num1AndNum2(20, 30) { n1: Int, n2: Int -> n1 + n2 }
Log.e(tag, "$plusResult")
val minusResult = num1AndNum2(20, 30) { n1: Int, n2: Int -> n1 - n2 }
Log.e(tag, "$minusResult")
可以發(fā)現(xiàn)使用Lambda表達(dá)式同樣可以完整的表達(dá)函數(shù)類型的參數(shù)和返回值必峰,Lambda表達(dá)式的最后一行代碼的返回值作為函數(shù)的返回值返回。
下面對高階函數(shù)繼續(xù)探究钻蹬,回顧一下apply
標(biāo)準(zhǔn)函數(shù)的用法
val stringBuilder = StringBuilder()
val ss = stringBuilder.apply {
append("hello")
append("how are you")
}
Log.e(tag,ss.toString())
apply
標(biāo)準(zhǔn)函數(shù)會把調(diào)用對象傳遞到Lambda表達(dá)式中作為上下文吼蚁,并且返回調(diào)用對象。下面我們就用高階函數(shù)來實現(xiàn)類似的功能问欠。
fun StringBuilder.otherApply(block: StringBuilder.() -> Unit): StringBuilder {
block()
return this
}
這里給StringBuilder類
定義了一個擴(kuò)展函數(shù)肝匆,擴(kuò)展函數(shù)接收一個函數(shù)類型的參數(shù),并且返回值為StringBuilder顺献。
注意:這里定義的函數(shù)類型的參數(shù)和我們前面學(xué)習(xí)的語法還是有區(qū)別的旗国,在函數(shù)類型的前面加上了StringBuilder.
,其實這才是完整的函數(shù)類型的定義規(guī)則注整,加上ClassName.
表示在哪個類中定義函數(shù)類型能曾。使用StringBuilder.
表示在StringBuilder類中定義的函數(shù)類型度硝,那么在傳入Lambda表達(dá)式時將會自動擁有StringBuilder的上下文。下面看下otherApply()
的使用
val stringBuilder = StringBuilder()
val result = stringBuilder.otherApply {
append("hello")
append("123")
}
Log.e(tag, result.toString())
可以看到和apply
標(biāo)準(zhǔn)函數(shù)的使用完全一樣寿冕,只不過apply
適用所有類的使用蕊程,而otherApply
只局限于StringBuilder的使用,如果想實現(xiàn)apply
的函數(shù)的這個功能蚂斤,就需要借助Kotlin泛型才可以存捺。
2、內(nèi)聯(lián)函數(shù)
2.1曙蒸、高階函數(shù)的實現(xiàn)原理
學(xué)習(xí)內(nèi)聯(lián)函數(shù)前我們先來學(xué)習(xí)一下高級函數(shù)的實現(xiàn)原理捌治。這里仍然使用剛才編寫的num1AndNum2()
函數(shù)為例。
fun num1AndNum2(num1: Int, num2: Int, block: (Int, Int) -> Int): Int {
return block(num1, num2)
}
//調(diào)用
val minusResult = num1AndNum2(20, 30) { n1: Int, n2: Int -> n1 - n2 }
Log.e(tag, "$minusResult")
上面是Kotlin中高階函數(shù)的基本用法纽窟,我們知道Kotlin代碼最終會編譯成Java字節(jié)碼的肖油,而Java中是沒有高階函數(shù)概念的,其實Kotlin編譯器最終會把Kotlin中高階函數(shù)的語法轉(zhuǎn)換成Java支持的語法結(jié)構(gòu)臂港,上述的Kotlin代碼大致被轉(zhuǎn)換成如下Java代碼森枪。
public static int num1AndNum2(int num1, int num2, Function operation){
return (int)operation.invoke(num1,num2);
}
public void test(){
int minusResult=num1AndNum2(10, 20, new Function() {
@Override
public Integer invoke(Integer num1,Integer num2) {
return num1+num2;
}
});
}
考慮到可讀性,我們對代碼做了調(diào)整审孽,并不是嚴(yán)格對應(yīng)了Kotlin轉(zhuǎn)成Java的代碼县袱。這里第三個參數(shù)變成了Function
接口,這是Kotlin的內(nèi)置接口佑力,里面有一個待實現(xiàn)的invoke()
函數(shù)式散。而num1AndNum2()
其實就是調(diào)用了Function接口的invoke()函數(shù),并把num1和num2參數(shù)傳了進(jìn)去打颤。
在調(diào)用num1AndNum2
函數(shù)時暴拄,之前的Lambda表達(dá)式變成了Function接口的匿名類實現(xiàn),然后在invoke函數(shù)中實現(xiàn)了num1+num2
的邏輯编饺。
這就是高階函數(shù)背后的原理乖篷,原來傳入的Lambda表達(dá)式在底層被匿名類所代替,這也就說明我們每調(diào)用一次Lambda表達(dá)式就會創(chuàng)建一個匿名類對象透且,當(dāng)然會帶來額外的內(nèi)存和性能開銷撕蔼。
而Kotlin中的內(nèi)聯(lián)函數(shù)
就是為了解決這個問題的,它可以將使用Lambda表達(dá)式運(yùn)行時的開銷完全消除秽誊。
2.2罕邀、內(nèi)聯(lián)函數(shù)的使用以及原理
內(nèi)聯(lián)函數(shù)的使用比較簡單就是在定義的高階函數(shù)時加上inline
關(guān)鍵字即可。
inline fun num1AndNum2(num1: Int, num2: Int, block: (Int, Int) -> Int): Int {
return block(num1, num2)
}
內(nèi)聯(lián)函數(shù)的原理
內(nèi)聯(lián)函數(shù)的原理也很簡單:Kotlin編譯器在編譯時把內(nèi)聯(lián)函數(shù)內(nèi)代碼自動替換到要調(diào)用的地方养距,這樣就解決了運(yùn)行時的內(nèi)存開銷诉探。
下面看下替換的過程
步驟一、將Lambda表達(dá)式的代碼替換到函數(shù)類型參數(shù)調(diào)用的地方
步驟二棍厌、將內(nèi)聯(lián)函數(shù)中全部代碼替換到函數(shù)調(diào)用的地方
最終替換后的代碼為
val minusResult =20-30
正是如此內(nèi)聯(lián)函數(shù)才能完全消除Lambda表達(dá)式運(yùn)行時帶來的額外內(nèi)存開銷肾胯。
3竖席、noinline和crossinline
3.1、noinline
一個高階函數(shù)中接收兩個或更多函數(shù)類型的參數(shù)敬肚,如果高階函數(shù)被inline
修飾了毕荐,那么所有函數(shù)類型的參數(shù)均會被內(nèi)聯(lián),如果想某個函數(shù)類型的參數(shù)不被內(nèi)聯(lián)艳馒,可以用關(guān)鍵字noinline
修飾憎亚。
inline fun test(block1: () -> Unit, noinline block2: () -> Unit) {
}
可以看到test
被inline
修飾,本來block1和block2
這兩個函數(shù)類型參數(shù)所引用Lambda表達(dá)式均被內(nèi)聯(lián)弄慰。由于我們在block2
前加上了noinline
關(guān)鍵字第美,那么只有block1
這個函數(shù)類型參數(shù)所引用的Lambda表達(dá)式被內(nèi)聯(lián)。
既然內(nèi)聯(lián)函數(shù)能消除Lambda表達(dá)式運(yùn)行時帶來的內(nèi)存的額外開銷陆爽,那么為什么還提供了一個noinline
來排除內(nèi)聯(lián)呢什往?
- 原因一:內(nèi)聯(lián)函數(shù)類型的參數(shù)在編譯期間會進(jìn)行代碼替換,所以內(nèi)聯(lián)的函數(shù)類型的參數(shù)算不上真正的參數(shù)慌闭,非內(nèi)聯(lián)的函數(shù)類型的參數(shù)可以作為真正的參數(shù)傳遞給任何函數(shù)别威。內(nèi)聯(lián)函數(shù)類型的參數(shù)只能傳遞給另一個內(nèi)聯(lián)函數(shù)。這也是它最大的局限性驴剔。
- 原因二:內(nèi)聯(lián)函數(shù)和非內(nèi)聯(lián)函數(shù)有一個重要的區(qū)別:內(nèi)聯(lián)函數(shù)所引用的Lambda表達(dá)式中可以使用return來進(jìn)行函數(shù)的返回省古,而非內(nèi)聯(lián)函數(shù)只能進(jìn)行局部返回。
fun printString(str: String, block: (String) -> Unit) {
Log.e("LoginActivity", "printString begin")
block(str)
Log.e("LoginActivity", "printString end")
}
fun main(){
Log.e(tag, "mainbegin")
printString("") {
Log.e(tag, "lambda begin")
if (it.isEmpty()) return@printString
Log.e(tag, "lambda end")
}
Log.e(tag, "mainend")
}
這里定義了一個非內(nèi)聯(lián)的高階函數(shù)丧失,在Lambda表達(dá)式中如果傳入的字符串為空豺妓,則直接返回,此時Lambda表達(dá)式中只能使用return@printString
進(jìn)行局部返回利花。打印結(jié)果如下:
main begin
printString begin
lambda begin
printString end
main end
可以看到lambda end
并沒有輸出,因為輸入的字符串為空载佳,則局部返回不再執(zhí)行Lambda表達(dá)式中的函數(shù)炒事,所以Log.e(tag, "lambda end")
沒有執(zhí)行。
下面我們聲明一個內(nèi)聯(lián)函數(shù)printStr
inline fun printStr(str: String, block: (String) -> Unit) {
Log.e("LoginActivity", "printString begin")
block(str)
Log.e("LoginActivity", "printString end")
}
fun main(){
Log.e(tag, "main begin")
printStr("") {
Log.e(tag, "lambda begin")
if (it.isEmpty()) return
Log.e(tag, "lambda end")
}
Log.e(tag, "main end")
}
由于printStr
是內(nèi)聯(lián)函數(shù)蔫慧,我們可以在Lambda表達(dá)式中使用return
進(jìn)行返回挠乳,打印結(jié)果如下:
main begin
printString begin
lambda begin
在傳入的字符串為空時,返回出最外層的函數(shù)姑躲,所以lambda end和printString end和click end
將不會被輸出睡扬。
3.2、crossinline
將高階函數(shù)聲明成內(nèi)聯(lián)函數(shù)是一種良好的習(xí)慣黍析,事實上絕大多數(shù)高階函數(shù)是可以被聲明成內(nèi)聯(lián)函數(shù)的卖怜,但是也有例外的情況。觀察下面的代碼
inline fun runRunnable(block:()->Unit){
val runnable= Runnable {
block()
}
runnable.run()
}
這段代碼如果沒有加上inline
關(guān)鍵字是完全可以正常工作的阐枣,但是加上inline之后就會報如下錯誤:
首先我們在內(nèi)聯(lián)函數(shù)
runRunnable
中創(chuàng)建一個runnable對象马靠,并在Runnable的Lambda表達(dá)式中傳入的函數(shù)類型參數(shù)奄抽,而Lambda表達(dá)式在編譯的時候會被轉(zhuǎn)換成匿名類的實現(xiàn)方式,也就是說上面代碼是在匿名類中傳入了函數(shù)類型的參數(shù)甩鳄。而內(nèi)聯(lián)函數(shù)所引用的Lambda表達(dá)式允許使用return進(jìn)行函數(shù)的返回逞度,但是由于我們是在匿名類中調(diào)用的函數(shù)類型參數(shù),此時不能進(jìn)行外層調(diào)用函數(shù)的返回妙啃,最多只能進(jìn)行匿名類中的方法進(jìn)行返回档泽,因此就提示了上述錯誤。
也就是說:如果我們在高階函數(shù)中創(chuàng)建了Lambda或匿名類的實現(xiàn)揖赴,在這些實現(xiàn)中調(diào)用函數(shù)類型參數(shù)馆匿,此時再將高階函數(shù)聲明成內(nèi)聯(lián),肯定會報上面的錯誤储笑。
那么如何在這種情況下使用內(nèi)聯(lián)函數(shù)呢甜熔?這就需要關(guān)鍵字
crossinline
inline fun runRunnable(crossinline block:()->Unit){
val runnable= Runnable {
block()
}
runnable.run()
}
經(jīng)過前面的分析可知,上面錯誤的原因:內(nèi)聯(lián)函數(shù)中允許使用return關(guān)鍵字和高階函數(shù)的匿名類的實現(xiàn)中不能使用return之間造成了沖突突倍。而crossinline關(guān)鍵字用于保證在Lambda表達(dá)式中一定不使用return關(guān)鍵字腔稀,這樣沖突就不存在了。但是我們?nèi)匀豢梢允褂?code>return@runRunnable進(jìn)行局部返回羽历。總體來說焊虏,crossinline
除了return用法不同外,仍然保留了內(nèi)聯(lián)函數(shù)的所有特性秕磷。