Kotlin 高阶函数从未如此清晰(上)
前言
上一篇罗列过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
变量(函数类型)
普通函数定义
既然函数引用可以当做参数传递,那么它当然可以赋值给变量,如下:
```
fun upFun1(name: String, age: Int): Float {
return 88f
}
//赋值
//var varFun:(String, Int)->Float = ::upFun1
//类型推断,可以不用写变量类型
var varFun = ::upFun1
fun main(args: Array
需要注意的是: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
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
匿名函数返回值表示的是该匿名函数本身的返回值。
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
第二种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
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 易学易懂系列
- 来吧!接受Kotlin 协程--线程池的7个灵魂拷问
- Kotlin Flow啊,你将流向何方?
- Kotlin 协程 Select:看我如何多路复用
- Kotlin 协程调度切换线程是时候解开真相了
- 讲真,Kotlin 协程的挂起没那么神秘(原理篇)
- 讲真,Kotlin 协程的挂起没那么神秘(故事篇)
- 少年,你可知 Kotlin 协程最初的样子?
- 一个小故事讲明白进程、线程、Kotlin 协程到底啥关系?
- Kotlin 高阶函数从未如此清晰(中)
- Kotlin 高阶函数从未如此清晰(上)
- Android 容易遗漏的刷新小细节
- Jetpack ViewModel 抽丝剥茧
- Jetpack LiveData 是时候了解一下了
- Jetpack Lifecycle 该怎么看?还肝否?
- Android Activity 与View 的互动思考
- 数字签名/数字证书/对称/非对称加密/CA 等概念明晰
- Android IPC 之AIDL应用(下)
- Android IPC 之Messenger 原理及应用
- Android clipToPadding 使用与疑难点解析
- Android invalidate/postInvalidate/requestLayout 彻底厘清