高阶函数是Kotlin函数式编程的基石,各种开源框架的关键元素,掌握了高阶函数对一些框架的源代码更容易理解,对学习Jetpack Compose也变得得心应手。
了解高阶函数
可以先看View.java中点击事件的代码,分析下:
- 首先,为了设置点击事件的监听,代码里特地定义了一个 OnClickListener 接口;
- 接着,为了设置鼠标点击事件的监听,又专门定义了一个 OnContextClickListener 接口。
这里如果借助高阶函数,可以一个接口都不定义。
//View.java
//成员变量
private OnClickListenermOnClickListener;
private OnContextClickListenermOnContextClickListener;
//监听手指点击事件
public void setOnClickListener(OnClickListenerl){
mOnClickListener=l;
}
//为传递这个点击事件,专门定义了一个接口
public interface OnClickListener{
voidon Click(Viewv);
}
//监听鼠标点击事件
public void setOnContextClickListener(OnContextClickListenerl){
getListenerInfo().mOnContextClickListener=l;
}
//为传递这个鼠标点击事件,专门定义了一个接口
public interface OnContextClickListener{
booleanonContextClick(Viewv);
}
看上段代码的Kotlin等价代码:
//View.kt
// (View)->Unit就是「函数类型」
// ↑ ↑
var mOnClickListener:((View)->Unit)? = null
var mOnContextClickListener:((View)->Unit)? = null
//高阶函数
fun setOnClickListener(l:(View)->Unit){
mOnClickListener=l;
}
//高阶函数
fun setOnContextClickListener(l:(View)->Unit){
mOnContextClickListener=l;
}
通过对比,我们可以发现Kotlin引入高阶函数,实际分为两个部分:
- 用函数类型替代接口定义;
- 用 Lambda 表达式作为函数参数。
使用高阶函数好处
也带来了几个好处:一个是针对定义方,代码中减少了两个接口类的定义;另一个是对于调用方来说,代码也会更加简洁。就大大减少了代码量,提高了代码可读性,并通过减少类的数量,提高了代码的性能。
高阶函数的相关概念
函数类型
函数类型(Function Type)就是函数的类型
在Kotlin中,函数是一等公民(first class),这意味着函数可以被存储在变量或者数据结构中,它是有类型的。Kotlin使用函数类型来描述一个函数的具体类型。一个完整语法的函数类型如下:
//(x:Int, y:Int) -> Int 这个就是 sum 函数的类型
// ↑ ↑ ↑
fun sum(a: Int, b: Int):Int{
return (a+b)
}
(Int, Int) ->Int 就代表了参数类型是两个 Int,返回值类型为 Int 的函数类型。
函数引用
函数的引用,普通的变量也有引用的概念,我们可以将一个变量赋值给另一个变量。而这一点,在函数上也是同样适用的,函数也有引用,并且也可以赋值给变量。如:作为参数传递给高阶函数。
//函数赋值给变量 函数引用
// ↑ ↑
val function: (Int, Int) ->Int = ::sum
高阶函数
定义:高阶函数是将函数用作参数或返回值的函数。
其实Android里的点击事件监听用Kotlin来实现的话,它就是一个高阶函数。
// 函数作为参数的高阶函数
// ↓
fun setOnClickListener(l: (View) -> Unit) { ... }
// 返回值是函数类型的高阶函数
// ↓
fun higherFunction(): (Int, Int) -> Int { ... }
综上可以理解一个函数的参数或是返回值,它们当中有一个是函数的情况下,这个函数就是高阶函数。
Lambda
上面讲到函数的引用这种方法可以给函数类型赋值,也可以通过Lambda表达式对一个函数类型的变量进行赋值(大多数情况都是使用Lambda表达式来调用高阶函数)如下所示:
val function: (Int, Int) ->Int = {num1: Int, num2: Int -> num1 + num2}
Lambda 表达式语法结构:{参数名1: 参数类型, 参数名2: 参数类型 -> 函数体} 函数体中可以编写任意行代码,最后一行代码会自动作为 Lambda 表达式的返回值
高阶函数的调用示例
这里用forEach函数来举例,forEach函数也是一个高阶函数。源码如下:
public inline fun IntArray.forEach(action: (Int) -> Unit): Unit {
for (element in this) action(element)
intArray.forEach(?) //此处? 是个函数类型的参数,函数类型是 (Int) -> Unit
//函数类型的参数是可以定义相同的函数类型的变量传给forEach,如下:
val action: (Int) -> Unit = ??
fun main() {
intArray.forEach(action)
}
上述我们讲到了函数赋值可以使用函数引用也可以使用 Lambda 表达式,使用函数引用代码如下:
val action: (Int) -> Unit = ::printValue
fun main() {
intArray.forEach(action)
}
fun printValue(value: Int): Unit {
println(value)
}
函数引用的方式调用高阶函数太过麻烦,还需要特地写一个函数来调用高阶函数。所以我们绝对多数情况都是使用Lambda 表达式来调用高阶函数的,且Lambda 表达式有很多简洁的写法。
使用Lambda 表达式来改写上述代码:
val action: (Int) -> Unit = {value: Int -> println(value)}
fun main() {
intArray.forEach(action)
}
1、Kotlin 有类型推到机制,所以 Int 可以去掉
val action: (Int) -> Unit = {value -> println(value)}
2、Lambda 表达式如果只有一个参数,可以直接用 it 来代替,并且不需要声明参数名
val action: (Int) -> Unit = {println(it)}
//将简化后的代码代入,现在上述的代码就变成如下这样
fun main() {
intArray.forEach({println(it)})
}
3、当Lambda 参数是函数的最后一个参数时,可以将 Lambda 表达式移到函数括号的外面。
fun main() {
intArray.forEach(){
println(it)
}
}
4、Lambda 表达式是函数的唯一一个参数的话,还可以将函数的括号省略
fun main() {
intArray.forEach{
println(it)
}
}
带接收者的函数类型
这里拿 Kotlin 的标准函数 apply函数和also函数来分析:
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}
public inline fun <T> T.also(block: (T) -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block(this)
return this
}
仔细⽐较这两个扩展函数会发现它们⾮常相似:它们都接收⼀个 block 函数并返回 this 值。
唯⼀的差别就在于apply的 block 函数类型为:T.() -> Unit,⽽also的 block 函数类型为:(T) -> Unit)
T.() -> Unit和(T) -> Unit) 区别
看下图apply和also使用可以得出:
在also方法中接收到的是it,而在apply方法中接收到的是this
apply以this值作为接收者调用指定的函数[block],并返回this值。
also以this值作为参数调用指定的函数[block],并返回this值。
-
T.()->Unit 的函数体中可以直接使用T代表的对象,即用this代表对象
(T) -> Unit 将T表示的对象作为实参通过函数参数传递进来,供函数体使用
()->Unit与T表示的对象没有直接联系,只能通过外部T实例的变量来访问对象
总结
- 将函数的参数类型和返回值类型抽象出来后,我们就得到了函数类型。比如(View) ->Unit 就代表了参数类型是 View,返回值类型为 Unit 的函数类型。
- 如果一个函数的“参数”或者“返回值”的类型是函数类型,那这个函数就是高阶函数。
- Lambda 就是函数的一种简写,但存在运行时带来的额外内存开销,可以使用inline关键字来修饰函数,使其变为内联函数,可提升性能。
Kotlin官方的源代码Standard.Kt可以去分析其中的 with、let、also、takeIf、repeat、apply,来进一步加深对高阶函数的理解。还有Collections.Kt,可以去分析其中的 map、flatMap、fold、groupBy 等操作符,从而对高阶函数的应用场景有一个更具体的认知。
关键字 inline「内联函数」
Kotlin 中新增了「内联函数」,内联函数起初是在 C++ 里面的。当一个函数被内联 inline 标注后,在调用它的地方,会把这个函数方法体中的所以代码移动到调用的地方,而不是通过方法间压栈进栈的方式。
内联函数可以消除Lambda表达式运行时带来的额外内存开销