Kotlin 高階函數從未如此清晰(上)

前言

高階函數系列文章:

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 函數類型參數調用

形參和實參都有了,接著來看如何將兩者結合起來,總結來說有如下幾種方式:


image.png

接下來一一看看三者的實現方式。

函數引用

當我們定義了一個函數后,想將這個函數作為實參傳遞給另一個函數,可以通過:: + 函數名 的方式傳遞,官方說法叫做:函數引用。

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 為何方神圣?


image.png

實際上就是Kotlin 里為了兼容Java 調用定義了一堆接口,這些接口標明了入參和返回值,Java 調用時需要重寫invoke()方法即可,當在Kotlin里調用對應的函數參數時,將會調用到invoke()回到Java 代碼。
在Functions.kt里定義了23個接口:


image.png

基本上可以滿足大部分的參數需求。

小結
理解了以上內容,我相信大家對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 易懂易學系列

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容