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

语言: CN / TW / HK

前言

上一篇罗列过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](http://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/61e8f79ca1b34d6388fbe36a94ef5ac8~tplv-k3u1fbpfcp-zoom-1.image)

接下来一一看看三者的实现方式。

函数引用

当我们定义了一个函数后,想将这个函数作为实参传递给另一个函数,可以通过:: + 函数名 的方式传递,官方说法叫做:函数引用。
``` 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) { //通过函数引用调用 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) { 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) { //传入匿名函数 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) { //传入匿名函数 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) { 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) { ////省略"()" 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](http://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/efeb763e6b314dce8935224875675e1f~tplv-k3u1fbpfcp-zoom-1.image)

实际上就是Kotlin 里为了兼容Java 调用定义了一堆接口,这些接口标明了入参和返回值,Java 调用时需要重写invoke()方法即可,当在Kotlin里调用对应的函数参数时,将会调用到invoke()回到Java 代码。
在Functions.kt里定义了23个接口:

![image.png](http://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/294b164fdefd40bf841200e5587249bb~tplv-k3u1fbpfcp-zoom-1.image)

基本上可以满足大部分的参数需求。

小结 理解了以上内容,我相信大家对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 易学易懂系列