Kotlin 高阶函数从未如此清晰(上)

2,837 阅读7分钟

前言

高阶函数系列文章:

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 易学易懂系列