1、高階函數
1.1、高階函數的定義
高階函數的定義:如果一個函數接收另一個函數作為參數,或者返回值的類型是另一個函數,那么該函數稱為高階函數。你可能會有疑問,一個函數怎么能接收另一個函數作為參數呢?因為Kotlin中新增了
函數類型
,如果我們將這種函數類型添加到一個函數的參數聲明后者返回值聲明當中,那么該函數就成為高階函數。
1.2、函數類型的定義
函數類型的定義的基本規則如下:
methodName:(Int,String)->Unit
- 1、methodName是函數類型的名稱,名稱不限制。
- 2、(Int,String)代表函數接收的類型,多個參數類型用逗號隔開。
- 3、->右邊表示函數的返回值,Unit類似于Java的void表示無返回值。
下面看下如何將這個函數類型添加到一個函數的參數聲明中:
fun example(block:(String,Int)->Unit){
block("test",123)
}
這里example()
函數就接收了一個函數類型的參數了,該函數就是高階函數了。函數類型的參數使用就和調用函數一樣,傳入相應的參數即可。
1.3、高階函數的用途
高階函數允許讓函數類型參數決定函數的執行邏輯,即使同一個高階函數,傳入的函數類型參數不同,那么函數的執行邏輯和返回結果可能也是完全不同的。下面我們舉例說明下:
這里準備定義一個函數num1AndNum2()
接收2個Int參數和一個函數類型的參數,由函數類型的參數決定這兩個Int參數具體執行的運算。
新建一個HighFuncFile文件,在其中定義高階函數
fun num1AndNum2(num1: Int, num2: Int, block: (Int, Int) -> Int): Int {
return block(num1, num2)
}
該函數前兩個參數沒什么好說的,第三個參數是函數類型的接收兩個Int變量并且返回值為Int類型,將前兩個Int類型的參數傳遞給第三個函數類型作為參數,高階函數中沒有其他邏輯,將具體的邏輯交由第三個函數類型的參數來完成。
那么第三個參數應該傳什么呢?我們可以在同文件下定義與其匹配的函數或者使用其他類中相匹配類型的函數作為參數,這里我們現在HighFuncFile文件
下定義函數。
fun plusFunc(num1: Int, num2: Int): Int {
return num1 + num2
}
fun minusFunc(num1: Int, num2: Int): Int {
return num1 - num2
}
高階函數的調用
num1AndNum2(20, 30, ::plusFunc)
num1AndNum2(20, 30, ::minusFunc)
可以看到第三個參數我們使用了::plusFunc
這種寫法,這是一種函數引用的寫法,表示將函數plusFunc()
來作為參數傳遞給高階函數。如果這兩個函數是定義在某個類中,那么該怎么引用這個函數呢?
在HighFuncTest.class中定義函數
class HighFuncTest {
fun plusFunc(num1: Int, num2: Int): Int {
return num1 + num2
}
fun minusFunc(num1: Int, num2: Int): Int {
return num1 - num2
}
}
我們上面使用了::plusFunc
來引用函數,那此時我們該怎么引用函數呢?
val highFuncTest: HighFuncTest = HighFuncTest()
num1AndNum2(20, 30, highFuncTest::plusFunc)
num1AndNum2(20, 30, highFuncTest::minusFunc)
先創建對象,然后使用highFuncTest::plusFunc
來引用HighFuncTest類
中的函數作為參數傳遞給高階函數。
像這種每次調用高階函數都需要定義與其函數類型參數匹配的函數,使用起來確實很麻煩,為此Kotlin提供了其他方式調用高階函數,比如:Lambda表達式、匿名函數、成員引用
等,Lambda表達式是最常用的高階函數調用方式。下面我們就來學習下如何使用Lambda表達式來調用高階函數,我們把上面的例子改成Lambda表達式的方法。
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")
可以發現使用Lambda表達式同樣可以完整的表達函數類型的參數和返回值,Lambda表達式的最后一行代碼的返回值作為函數的返回值返回。
下面對高階函數繼續探究,回顧一下apply
標準函數的用法
val stringBuilder = StringBuilder()
val ss = stringBuilder.apply {
append("hello")
append("how are you")
}
Log.e(tag,ss.toString())
apply
標準函數會把調用對象傳遞到Lambda表達式中作為上下文,并且返回調用對象。下面我們就用高階函數來實現類似的功能。
fun StringBuilder.otherApply(block: StringBuilder.() -> Unit): StringBuilder {
block()
return this
}
這里給StringBuilder類
定義了一個擴展函數,擴展函數接收一個函數類型的參數,并且返回值為StringBuilder。
注意:這里定義的函數類型的參數和我們前面學習的語法還是有區別的,在函數類型的前面加上了StringBuilder.
,其實這才是完整的函數類型的定義規則,加上ClassName.
表示在哪個類中定義函數類型。使用StringBuilder.
表示在StringBuilder類中定義的函數類型,那么在傳入Lambda表達式時將會自動擁有StringBuilder的上下文。下面看下otherApply()
的使用
val stringBuilder = StringBuilder()
val result = stringBuilder.otherApply {
append("hello")
append("123")
}
Log.e(tag, result.toString())
可以看到和apply
標準函數的使用完全一樣,只不過apply
適用所有類的使用,而otherApply
只局限于StringBuilder的使用,如果想實現apply
的函數的這個功能,就需要借助Kotlin泛型才可以。
2、內聯函數
2.1、高階函數的實現原理
學習內聯函數前我們先來學習一下高級函數的實現原理。這里仍然使用剛才編寫的num1AndNum2()
函數為例。
fun num1AndNum2(num1: Int, num2: Int, block: (Int, Int) -> Int): Int {
return block(num1, num2)
}
//調用
val minusResult = num1AndNum2(20, 30) { n1: Int, n2: Int -> n1 - n2 }
Log.e(tag, "$minusResult")
上面是Kotlin中高階函數的基本用法,我們知道Kotlin代碼最終會編譯成Java字節碼的,而Java中是沒有高階函數概念的,其實Kotlin編譯器最終會把Kotlin中高階函數的語法轉換成Java支持的語法結構,上述的Kotlin代碼大致被轉換成如下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;
}
});
}
考慮到可讀性,我們對代碼做了調整,并不是嚴格對應了Kotlin轉成Java的代碼。這里第三個參數變成了Function
接口,這是Kotlin的內置接口,里面有一個待實現的invoke()
函數。而num1AndNum2()
其實就是調用了Function接口的invoke()函數,并把num1和num2參數傳了進去。
在調用num1AndNum2
函數時,之前的Lambda表達式變成了Function接口的匿名類實現,然后在invoke函數中實現了num1+num2
的邏輯。
這就是高階函數背后的原理,原來傳入的Lambda表達式在底層被匿名類所代替,這也就說明我們每調用一次Lambda表達式就會創建一個匿名類對象,當然會帶來額外的內存和性能開銷。
而Kotlin中的內聯函數
就是為了解決這個問題的,它可以將使用Lambda表達式運行時的開銷完全消除。
2.2、內聯函數的使用以及原理
內聯函數的使用比較簡單就是在定義的高階函數時加上inline
關鍵字即可。
inline fun num1AndNum2(num1: Int, num2: Int, block: (Int, Int) -> Int): Int {
return block(num1, num2)
}
內聯函數的原理
內聯函數的原理也很簡單:Kotlin編譯器在編譯時把內聯函數內代碼自動替換到要調用的地方,這樣就解決了運行時的內存開銷。
下面看下替換的過程
步驟一、將Lambda表達式的代碼替換到函數類型參數調用的地方
步驟二、將內聯函數中全部代碼替換到函數調用的地方
最終替換后的代碼為
val minusResult =20-30
正是如此內聯函數才能完全消除Lambda表達式運行時帶來的額外內存開銷。
3、noinline和crossinline
3.1、noinline
一個高階函數中接收兩個或更多函數類型的參數,如果高階函數被inline
修飾了,那么所有函數類型的參數均會被內聯,如果想某個函數類型的參數不被內聯,可以用關鍵字noinline
修飾。
inline fun test(block1: () -> Unit, noinline block2: () -> Unit) {
}
可以看到test
被inline
修飾,本來block1和block2
這兩個函數類型參數所引用Lambda表達式均被內聯。由于我們在block2
前加上了noinline
關鍵字,那么只有block1
這個函數類型參數所引用的Lambda表達式被內聯。
既然內聯函數能消除Lambda表達式運行時帶來的內存的額外開銷,那么為什么還提供了一個noinline
來排除內聯呢?
- 原因一:內聯函數類型的參數在編譯期間會進行代碼替換,所以內聯的函數類型的參數算不上真正的參數,非內聯的函數類型的參數可以作為真正的參數傳遞給任何函數。內聯函數類型的參數只能傳遞給另一個內聯函數。這也是它最大的局限性。
- 原因二:內聯函數和非內聯函數有一個重要的區別:內聯函數所引用的Lambda表達式中可以使用return來進行函數的返回,而非內聯函數只能進行局部返回。
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")
}
這里定義了一個非內聯的高階函數,在Lambda表達式中如果傳入的字符串為空,則直接返回,此時Lambda表達式中只能使用return@printString
進行局部返回。打印結果如下:
main begin
printString begin
lambda begin
printString end
main end
可以看到lambda end
并沒有輸出,因為輸入的字符串為空,則局部返回不再執行Lambda表達式中的函數,所以Log.e(tag, "lambda end")
沒有執行。
下面我們聲明一個內聯函數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
是內聯函數,我們可以在Lambda表達式中使用return
進行返回,打印結果如下:
main begin
printString begin
lambda begin
在傳入的字符串為空時,返回出最外層的函數,所以lambda end和printString end和click end
將不會被輸出。
3.2、crossinline
將高階函數聲明成內聯函數是一種良好的習慣,事實上絕大多數高階函數是可以被聲明成內聯函數的,但是也有例外的情況。觀察下面的代碼
inline fun runRunnable(block:()->Unit){
val runnable= Runnable {
block()
}
runnable.run()
}
這段代碼如果沒有加上inline
關鍵字是完全可以正常工作的,但是加上inline之后就會報如下錯誤:
首先我們在內聯函數
runRunnable
中創建一個runnable對象,并在Runnable的Lambda表達式中傳入的函數類型參數,而Lambda表達式在編譯的時候會被轉換成匿名類的實現方式,也就是說上面代碼是在匿名類中傳入了函數類型的參數。而內聯函數所引用的Lambda表達式允許使用return進行函數的返回,但是由于我們是在匿名類中調用的函數類型參數,此時不能進行外層調用函數的返回,最多只能進行匿名類中的方法進行返回,因此就提示了上述錯誤。
也就是說:如果我們在高階函數中創建了Lambda或匿名類的實現,在這些實現中調用函數類型參數,此時再將高階函數聲明成內聯,肯定會報上面的錯誤。
那么如何在這種情況下使用內聯函數呢?這就需要關鍵字
crossinline
inline fun runRunnable(crossinline block:()->Unit){
val runnable= Runnable {
block()
}
runnable.run()
}
經過前面的分析可知,上面錯誤的原因:內聯函數中允許使用return關鍵字和高階函數的匿名類的實現中不能使用return之間造成了沖突。而crossinline關鍵字用于保證在Lambda表達式中一定不使用return關鍵字,這樣沖突就不存在了。但是我們仍然可以使用return@runRunnable
進行局部返回。總體來說,crossinline
除了return用法不同外,仍然保留了內聯函數的所有特性。