前言
高階函數系列文章:
Kotlin 高階函數從未如此清晰(上)
Kotlin 高階函數從未如此清晰(中)
Kotlin 高階函數從未如此清晰(下) let/also/with/run/apply/repeat 一看就會
上一篇羅列過Kotlin的屬性與函數的基本知識,算是入門篇本。本篇將繼續對函數的一些高級用法進行深入分析。
通過本篇,你將了解到:
1、什么是函數類型?
2、Kotlin 函數類型形參聲明/實參定義
3、Kotlin 函數類型參數調用
4、匿名函數與Lambda
5、Kotlin 函數作為返回值
6、Java 如何調用Kotlin 函數?
1、什么是函數類型?
Java 如何傳遞方法?
有個場景:
輸入學生的姓名、年齡,返回該學生的考試分數。
通常我們會將它封裝為一個方法,而方法需要放在類或者接口里,最終調用時是通過類/接口的實例化對象調用該方法,如下:
private void testStudent(HandleStudent handleStudent) {
float score = 0;
if (handleStudent != null) {
score = handleStudent.getScore("fish", 18);
}
System.out.println("score:" + score);
}
//接口
interface HandleStudent {
//傳入學生的姓名、年齡,返回學生的分數
float getScore(String name, int age);
}
實際上我們只需要調用getScore(xx)方法,為了實現這個目的,需要將它放到類/接口封裝,最后生成實例對象調用,多了好幾個步驟。
有沒有更簡單的方式呢?比如直接傳遞方法本身?
答案是:沒有。
因為在Java 的世界里,類/接口 是一等公民,方法必須依賴于它們存在。
Kotlin 函數類型
Java 不支持方法作為方法的參數,而Kotlin 卻支持函數作為函數的參數/返回值。
因為在Kotlin 的世界里,函數是一等公民,可以脫離類/接口而存在。
如果你接觸過C++等語言,相信你對函數參數不會太陌生,C++里有函數指針,指向的是一個函數的指針,通過該指針就可以調用其指向的函數。
還是以獲取學生分數為例:
fun upFun1(name: String, age: Int): Float {
return 88f
}
我們只關注該函數的輸入參數與返回值,并不關心該函數的名字,而函數的輸入參數與返回值就決定了該函數的類型。
upFun1 的函數類型為:
(String, Int)->Float
輸入參數類型為:String 和 Int,多個參數之間用","隔開,所有參數使用()括起來
返回值類型為:Float,返回值與輸入參數之間使用"->"連接。
如此一來就可以表示一個函數的類型。
2、Kotlin 函數類型形參聲明/實參定義
形參聲明
在上一篇文章里,我們有提到過:Kotlin里的引用類型包括函數這種引用類型,既然是引用,那么當然可以作為參數傳遞了,來看看如何聲明一個使用了函數作為形參的函數。
//testUpFun1 接收的參數為函數類型:(String, Int)->Float
fun testUpFun1(getScore : (String, Int)->Float) {
}
傳入的形參不使用的話沒啥意義,對于函數類型,通常是調用該函數,如下:
//testUpFun1 接收的參數為函數類型:(String, Int)->String
fun testUpFun1(getScore : (String, Int)->Float) {
var score = getScore("fish", 18)
println("student score:$score")
}
實參定義
形參有了,當調用testUpFun1(xx)時需要傳入實參,也就是傳入函數的定義:
fun upFun1(name: String, age: Int): Float {
return 88f
}
定義了upFun1函數,該函數類型為:(String, Int)->Float,符合作為testUpFun1 形參的條件。
3、Kotlin 函數類型參數調用
形參和實參都有了,接著來看如何將兩者結合起來,總結來說有如下幾種方式:
接下來一一看看三者的實現方式。
函數引用
當我們定義了一個函數后,想將這個函數作為實參傳遞給另一個函數,可以通過:: + 函數名 的方式傳遞,官方說法叫做:函數引用。
fun upFun1(name: String, age: Int): Float {
return 88f
}
//testUpFun1 接收的參數為函數類型:(String, Int)->String
fun testUpFun1(getScore : (String, Int)->Float) {
var score = getScore("fish", 18)
println("student score:$score")
}
fun main(args: Array<String>) {
//通過函數引用調用
testUpFun1(::upFun1)
}
如上,testUpFun1 函數需要傳入一個函數類型的參數,通過"::"引用函數名即可。
變量(函數類型)
普通函數定義
既然函數引用可以當做參數傳遞,那么它當然可以賦值給變量,如下:
fun upFun1(name: String, age: Int): Float {
return 88f
}
//賦值
//var varFun:(String, Int)->Float = ::upFun1
//類型推斷,可以不用寫變量類型
var varFun = ::upFun1
fun main(args: Array<String>) {
testUpFun1(varFun)
}
此時,我們只需要把varFun 作為實參傳遞即可。
需要注意的是:Kotlin 會對變量進行類型推斷,因此我們可以省略變量類型
匿名函數
當然了,若是想要在聲明變量的同時將函數定義了,這也是可以的:
//匿名函數
//var varFun1:(String, Int)->Float = fun (name: String, age: Int):Float {
// return 88f
//}
//類型推斷
var varFun1 = fun (name: String, age: Int):Float {
return 88f
}
fun main(args: Array<String>) {
testUpFun1(varFun1)
}
可以看出,我們聲明的函數沒有函數名,只有一個"fun"聲明。
此時,varFun1 表示的是一個匿名函數。
同樣的因為類型自動推導,可以不用寫變量類型。
Lambda 表達式
在Java 里,有時候我們會將匿名內部類轉為Lambda形式,而Kotlin 對于Lambda的使用更廣泛了,上面的匿名函數我們可以用Lambda表示。
//Lambda 表達式
//var lambda1:(String, Int)->Float = {
// name:String,age:Int->
// 88f
//}
//類型推導
var lambda1 = { name: String, age: Int ->
88f
}
fun main(args: Array<String>) {
testUpFun1(lambda1)
}
可以看出,變量作為實參傳遞,對比普通函數定義、匿名函數、Lambda 表達式三者寫法,發現Lambda最簡潔,簡潔有時候也意味著難以理解。
Lambda 格式以及一些風騷寫法,我們放在下節分析
直接傳入函數體
不管是函數引用還是變量作為實參,都需要先將函數定義好,有時候函數只在一個地方使用,無需再單獨定義出來,此時可以選擇直接將函數體當做實參傳遞。
匿名函數
在調用函數的時候,直接傳入匿名函數作實參:
//testUpFun1 接收的參數為函數類型:(String, Int)->String
fun testUpFun1(getScore: (String, Int) -> Float) {
var score = getScore("fish", 18)
println("student score:$score")
}
fun main(args: Array<String>) {
//傳入匿名函數
testUpFun1(fun(name: String, age: Int): Float {
return 88f
})
}
Lambda
老樣子,一般匿名函數都可以用Lambda 替代:
fun main(args: Array<String>) {
//Lambda 表示
testUpFun1({ name: String, age: Int ->
88f
}
)
}
此時,編譯器會提示你可以再優化一下寫法:
fun main(args: Array<String>) {
//傳入匿名函數
testUpFun1 { name: String, age: Int ->
88f
}
}
我們知道函數的調用需要用"()"括起來,此時"()"都沒了,越來越簡潔了。
4、匿名函數與Lambda
上面簡單展示了匿名函數和Lambda的使用,只是一些基本寫法,尤其是Lambda還有一些風騷寫法,接著來分析。
匿名函數
顧名思義,函數是有函數名的,如果省略了函數名那么就稱之為匿名函數。
//定義匿名函數
var anoymous1: (String, Int) -> Float = fun(name: String, age: Int): Float {
println("name:$name age:$age")
return 88f
}
//自動推導,消除變量類型
var anoymous2 = fun(name: String, age: Int): Float {
println("name:$name age:$age")
return 88f
}
//調用
fun main(args: Array<String>) {
//傳入匿名函數
testUpFun1(fun(name: String, age: Int): Float {
println("name:$name age:$age")
return 88f
})
testUpFun1(anoymous1)
testUpFun1(anoymous2)
}
需要注意的是:
匿名函數返回值表示的是該匿名函數本身的返回值。
Lambda
匿名函數還是不夠簡潔,此時Lambda出現了,分三步闡述。
Lambda 基本結構
var varLambda2 = { name: String, int: Int ->
println()
test()
"jj"
}
以此為例,Lambda 有如下約定:
1、大括號"{}" 包裹內容。
2、使用"->"連接參數與實現體。
3、"->"左邊表示參數列表,參數間使用","分割。
4、"->"右邊表示實現體,多個語句分行表示。
5、如果沒有參數列表,那么"->"可以省略
6、Lambda 無需"return"關鍵字,最后一行默認表示返回值(如例子中"jj"表示Lambda返回了String類型
變量接收Lambda
//完整寫法
var varLambda1:(String, Int)->Float = { name: String, age: Int ->
println("student name:$name age:$age")
88f
}
因為自動推導類型,因此可以省略變量類型:
//省略類型
var varLambda1 = { name: String, age: Int ->
println("student name:$name age:$age")
88f
}
當然,非得要類型的話,還可以這么寫:
//Lambda里省略了參數類型,因為"="之前已經聲明了
var varLambda1: (String, Int) -> Float = { name, age ->
println("student name:$name age:$age")
88f
}
函數調用傳入Lambda
接下來通過不同的case由淺入深演示Lambda各種風騷寫法。
第一種Case:
將之前的testUpFun1 改造一下,新增一個參數,如下:
//testUpFun1 接收的參數為函數類型:(String, Int)->String
fun testUpFun1(getScore: (String, Int) -> Float) {
var score = getScore("fish", 18)
println("student score:$score")
}
//改造后
fun testUpFun2(getScore: (String, Int) -> Float, needDelay:Boolean) {
if (needDelay)
println("delay...")
var score = getScore("fish", 18)
println("student score:$score")
}
testUpFun2 函數有兩個參數,一個是函數類型,另一個是Boolean。
接著來看看如何調用testUpFun2 函數,我們以直接傳入函數體為例:
fun main2(args : Array<String>) {
testUpFun2({ name: String, age: Int ->
println("name:$name age:$age")
88f
}, true
)
}
第二種Case:
我們再變一下testUpFun2 參數,函數類型和Boolean交換位置:
fun testUpFun3( needDelay: Boolean, getScore: (String, Int) -> Float) {
if (needDelay)
println("delay...")
var score = getScore("fish", 18)
println("student score:$score")
}
調用如下:
fun main3(args: Array<String>) {
testUpFun3(true, { name: String, age: Int ->
println("name:$name age:$age")
88f
}
)
}
此時,編譯器會提示你可以將"{}"整體提取出來放在"()"括號后,如下:
fun main3(args: Array<String>) {
testUpFun3(true
) { name: String, age: Int ->
println("name:$name age:$age")
88f
}
}
這是Lambda的一個約定:
如果Lambda 作為函數的最后一個參數,那么Lambda可以提取到"()"外展示。
第三種Case:
再對testUpFun3 參數做調整,只保留一個函數類型的參數:
//定義
fun testUpFun4(getScore: (String, Int) -> Float) {
var score = getScore("fish", 18)
println("student score:$score")
}
//調用
fun main4(args: Array<String>) {
testUpFun4(
) { name: String, age: Int ->
println("name:$name age:$age")
88f
}
}
同樣的編譯器會提示可以將"()"省略,如下:
fun main4(args: Array<String>) {
//省略"()"
testUpFun4 { name: String, age: Int ->
println("name:$name age:$age")
88f
}
}
這是Lambda的一個約定:
如果Lambda 作為函數的唯一參數,那么調用函數"()"可以省略。
第四種Case:
這次不修改testUpFunX,我們修改Lambda表達式的入參,改為:
//單參數
fun testUpFun5(getScore: (String) -> Float) {
var score = getScore("fish")
println("student score:$score")
}
調用如下:
fun main5(args: Array<String>) {
////省略"()"
testUpFun5 { name: String ->
println("name:$name")
88f
}
}
此時,可以寫成如下方式:
fun main5(args: Array<String>) {
////省略"()"
testUpFun5 {
//用it 替代了Lambda 的name
println("name:$it")
88f
}
}
這是Lambda的一個約定:
如果Lambda 入參只有一個,那么可以省略"->"以及入參列表,并在實現體里用it 指代這個唯一的參數。
第五種Case:
Lambda 有1個入參可以用"it"指代,Lambda 沒有入參呢?
//無參數
fun testUpFun6(getScore: () -> Float) {
var score = getScore()
println("student score:$score")
}
fun main6(args: Array<String>) {
////省略"()"
testUpFun6 {
println("name")
88f
}
}
可以看出,此時只需要"{}"括起來即可。
這是Lambda的一個約定:
如果Lambda 沒有入參,那么可以省略"->"以及入參列表。
注:此時在Lambda里不能使用"it",因為它根本沒入參。
以上就是Kotlin Lambda 常用的一些變換規則。
5、Kotlin 函數作為返回值
定義函數:
fun testUpFun7(getScore: (String) -> Unit): (Boolean, Int) -> String {
//調用函數
var score = getScore("fish")
println("student score:$score")
//返回函數,Lambda表示
return { need: Boolean, age: Int ->
println("need:$need age:$age")
"fish"
}
}
調用:
fun main7(args: Array<String>) {
////省略"()"
var testReturn = testUpFun7 {
println("name:$it")
}
//調用
testReturn(true, 5)
}
只要掌握了高階函數的傳參,返回值也不在話下,此處就不展開細說了。
6、Java 如何調用Kotlin 函數?
以上都是Kotlin 調用 Kotlin,來看Java 如何調用Kotlin的高階函數。
還是以如下函數為例:
fun testUpFun3(needDelay: Boolean, getScore: (String, Int) -> Float) {
if (needDelay)
println("delay...")
var score = getScore("fish", 18)
println("student score:$score")
}
在Java里調用:
private void testKotlin() {
UpFunKt.testUpFun3(true, new Function2<String, Integer, Float>() {
@Override
public Float invoke(String s, Integer integer) {
return null;
}
});
}
可以看出testUpFun3里的函數類型參數轉化為了Function2 的實例,Function2 為何方神圣?
實際上就是Kotlin 里為了兼容Java 調用定義了一堆接口,這些接口標明了入參和返回值,Java 調用時需要重寫invoke()方法即可,當在Kotlin里調用對應的函數參數時,將會調用到invoke()回到Java 代碼。
在Functions.kt里定義了23個接口:
基本上可以滿足大部分的參數需求。
小結
理解了以上內容,我相信大家對Lambda各種寫法都不會再陌生,如果你還是有疑惑,可能是我沒闡述明白,歡迎留言討論。
下篇將會繼續分析泛型函數、擴展函數、內聯函數、常用的高階函數如let/run/apply 等,進而自然過渡到協程的分析,那時再看協程就事半功倍了。
本文基于Kotlin 1.5.3,文中Demo請點擊
您若喜歡,請點贊、關注,您的鼓勵是我前進的動力
持續更新中,和我一起步步為營系統、深入學習Android/Kotlin
1、Android各種Context的前世今生
2、Android DecorView 必知必會
3、Window/WindowManager 不可不知之事
4、View Measure/Layout/Draw 真明白了
5、Android事件分發全套服務
6、Android invalidate/postInvalidate/requestLayout 徹底厘清
7、Android Window 如何確定大小/onMeasure()多次執行原因
8、Android事件驅動Handler-Message-Looper解析
9、Android 鍵盤一招搞定
10、Android 各種坐標徹底明了
11、Android Activity/Window/View 的background
12、Android Activity創建到View的顯示過
13、Android IPC 系列
14、Android 存儲系列
15、Java 并發系列不再疑惑
16、Java 線程池系列
17、Android Jetpack 前置基礎系列
18、Android Jetpack 易懂易學系列