目录
07 | 高阶函数:为什么说函数是一等公民
- 原文
- StandardKt:分析其中的 with、let、also、takeIf、repeat、apply 等操作符
- CollectionsKt:分析其中的 map、flatMap、fold、groupBy 等操作符
对 C/Java 开发者来首,高阶函数是一个全新的概念,很难从经典的 C/Java 里找到同等的概念迁移过来。
高阶函数
高阶函数是 Kotlin 函数式编程
的基石,是理解协程
源码的基础,对学习 Jetpack Compose
也大有帮助,同时也是各种开源框架
的关键元素。在特定业务场景下,我们也可以用它来实现自己的 DSL
(Domain Specific Language)。
- 高阶函数:High-order functions,是指使用函数作为
参数或返回值
的函数 - 函数类型:Function Type,是对函数的
参数类型
和返回值类型
的抽象
函数类型
在 Kotlin 的世界里,函数是一等公民。类、变量有类型,一等公民函数当然也有类型。
// 函数类型 (Int, Int) -> Float
// ↑ ↑ ↑
fun add(a: Int, b: Int): Float { return (a+b).toFloat() }
上面的代码中,(Int, Int) -> Float
就是 add
函数的类型,它代表了参数类型是两个 Int、返回值类型为 Float 的函数类型。
变量有引用、赋值的概念,一等公民函数当然也有。
val function: (Int, Int) -> Float = ::add // 将函数赋值给变量
上面的代码中,::add
就代表函数 add
的引用。
SAM 与 Lambda
- SAM:Single Abstract Method,代表只有一个抽象方法的接口
- Lambda:可以理解为是
函数的简写
。符合一定条件的函数,就可以使用 Lambda 表达式来简写
符合 SAM 要求的接口,就能被编译器进行 SAM 转换,也就可以使用 Lambda 表达式来简写。
注意,Java 8 中的 SAM 叫做函数式接口(FunctionalInterface),限制条件如下:
- 必须是接口,抽象类不行
- 该接口有且仅有一个抽象的方法(有默认实现的方法不考虑)
public void setOnClickListener(OnClickListener l) // 方法声明
fun mOnClick(v: View): Unit = println(v.toString()) // 自定义的一个函数
view.setOnClickListener(::mOnClick) // 引用 mOnClick 函数,当做函数的参数
// 接口 OnClickListener 符合 SAM 转换的条件,所以可以用 Lambda 表达式替代函数引用
view.setOnClickListener { v: View -> println(v.toString()) }
这里有一个问题:Android 并没有提供 View.java
的 Kotlin 实现,为什么我们可以用 Lambda 来简化事件监听呢?
因为,虽然 View.java
是 Java 代码,但 Kotlin 编译器知道它的参数 OnClickListener 符合 SAM 转换的条件,所以会自动做以下转换。
public void setOnClickListener(OnClickListener l) // 转换前的 Java 代码
fun setOnClickListener(l: ((View!) -> Unit)?) // 转换后的 Kotlin 代码
高阶函数实现原理
Kotlin 引入的高阶函数
,在编译后,其实被转换成了 匿名内部类。
class View {
fun setOnClickListener(onlick: (View) -> Unit) = onlick(this) // 调用传入的方法
}
fun main() {
fun mOnClick(v: View): Unit = println(v.toString()) // 定义一个方法
View().setOnClickListener(::mOnClick) // 引用一个方法
}
反编译成 Java 后的代码如下:
public final class View {
public final void setOnClickListener(Function1 onlick) { // 函数类型被转换成了一个接口
onlick.invoke(this); // 调用接口实例的方法
}
}
// 和之前遇到过的情况一样,反编译成 Java 后代码语法错误
public static final void main() {
<undefinedtype> $fun$mOnClick$1 = null.INSTANCE; // 创建了某个匿名内部类的实例
(new View()).setOnClickListener((Function1)null.INSTANCE); // 传入的也是匿名内部类的实例
}
// 我们只能从字节码中寻找蛛丝马迹,和之前遇到过的情况一样,这里也是定义了一些匿名内部类
// final class MainKt$main$1 extends Lambda implements Function1
// final class MainKt$main$2 extends FunctionReferenceImpl implements Function1
// A function that takes 1 argument,预先定义的一系列接口,有 x 个参数就用 Functionx
public interface Function1<in P1, out R> : Function<R> {
public operator fun invoke(p1: P1): R // Invokes the function with the specified argument
}
// Represents a value of a functional type,代表函数类型,接口未定义任何方法
public interface Function<out R> // R represent return type of the function
Kotlin 弄了个这么高端的高阶函数,最终还是以匿名内部类
的形式在运行,那它的性能如何?
Kotlin 高阶函数的性能,在某些情况下可以达到匿名内部类的 100 倍!具体原理后面再详细探讨。
带接收者的函数类型
使用 apply 函数
使用 Kotlin 的标准函数 apply
可以简化代码。
data class User(var name: String, var text: String)
if (user != null) {
println(user.name + user.blog) // 显示调用 user
image.setOnClickListener { gotoPreview(user) }
}
// 使用 apply 简化上面的代码
user?.apply {
println(this.name + blog) // 可以省略 user 的调用
image.setOnClickListener { gotoPreview(this) } // 用 this 代替
}
- apply 肯定是个函数,只是
()
被省略了,完整格式应该是:user?.apply() {...}
- apply 的参数中肯定有一个
Lambda
表达式,其中参数名是this
,其代表了 user
自定义 apply 函数
现在,我们尝试自己实现这个 apply 方法:
// 形参不允许被命名为 this (Kotlin 的语言设计者可以),因此我这里用的是 self
fun User.apply2(self: User, block: (self: User) -> Unit): User{
block(self) // 调用这个函数
return this // 返回值
}
user?.apply2(self = user) { self: User -> // 选用传入 self
println(self.name + self.blog) // 必须通过 self 访问成员
image.setOnClickListener { gotoPreview(this) }
}
可以看到,和 Kotlin 的标准函数 apply
相比,我们反推实现的 apply2
用起来比较繁琐。
简化 apply 函数
使用 带接收者的函数类型,可以简化上面 apply 函数的定义:
// fun User.apply2(self: User, block: (self: User) -> Unit): User{}
// User.apply2(block: (self: User) -> Unit): User
fun User.apply3(block: User.() -> Unit): User{ // 【(self: User)】【User.()】
block() // 不用再传this
return this
}
user?.apply3 {
println(this.name + blog) // 可以省略 user 的调用
image.setOnClickListener { gotoPreview(this) } // 用 this 代替
}
定义为成员方法
上面的 apply3 方法,看起来就像是在 User 里,增加了一个成员方法一样。
data class User(var name: String, var blog: String) {
fun apply4() {
println(this.name + blog) // 成员方法可以通过 this 访问成员变量
image.setOnClickListener { gotoPreview(this) }
}
}
user?.apply4()
总结
从外表上看,带接收者的函数类型,就 等价于成员方法。但从本质上讲,它仍是通过编译器注入 this 来实现的。
带接收者的函数类型
也算是一种扩展函数
,从语法层面讲,他们都相当于成员方法
。
Lambda 表达式引发的 8 种写法
① object 匿名内部类
这是原始代码,它的本质是用 object 关键字定义了一个匿名内部类
:
image.setOnClickListener(object: View.OnClickListener { // 匿名内部类
override fun onClick(v: View?) {
gotoPreview(v)
}
})
② Lambda 表达式
在这种情况下,object 关键字可以被省略。这时候它在语法层面就不再是匿名内部类了,它其实已经变成 Lambda
表达式了,因此它里面 override
的方法也要跟着删掉:
image.setOnClickListener(View.OnClickListener { v: View? -> // Lambda 表达式
gotoPreview(v)
})
上面的
View.OnClickListener
被称为SAM Constructor
,它是编译器为我们生成的。
③ 省略 SAM 构造器
由于 Kotlin 的 Lambda 表达式是不需要 SAM Constructor
的,所以它也可以被删掉:
image.setOnClickListener({ v: View? -> // 省略 SAM Constructor
gotoPreview(v)
})
④ 自动类型推导
由于 Kotlin 支持类型推导
,所以 View 可以被删掉:
image.setOnClickListener({ v -> // 省略类型 View
gotoPreview(v)
})
⑤ 单一参数写成 it
当 Kotlin Lambda 表达式只有一个参数的时候,它可以被写成 it:
image.setOnClickListener({ it -> // 参数写成 it
gotoPreview(it)
})
⑥ 省略 Lambda 的 it
Kotlin Lambda 的 it
是可以被省略的:
image.setOnClickListener({ // 省略 it
gotoPreview(it)
})
⑦ 移动 Lambda 位置
当 Kotlin Lambda 作为函数的 最后一个参数 时,Lambda 可以被挪到外面:
image.setOnClickListener() { // 移动 Lambda 位置
gotoPreview(it)
}
⑧ 省略小括号
当 Kotlin 只有一个 Lambda 作为函数参数时,()
可以被省略:
image.setOnClickListener { // 省略小括号
gotoPreview(it)
}
演进过程
这 8 种写法的演进过程:
小结
- 为什么引入高阶函数:为了简化
- 高阶函数是什么:函数作为参数 or 返回值
- 函数类型是什么:函数的类型
- 函数引用是什么:类比变量的引用
- Lambda 是什么:可以简单理解为函数的简写
- 带接收者的函数类型是什么:可以简单理解为
成员函数的类型
2016-12-05