1.函数类型,高阶函数,Lambda,它们分别是什么?
1.1 函数类型是什么?
顾名思义:函数类型,就是函数的类型。
// (Int, Int) ->Float
// ↑ ↑ ↑
fun add(a: Int, b: Int): Float { return (a+b).toFloat() }
将函数的 参数类型 和 返回值类型 抽象出来后,就得到了 函数类型。
(Int, Int) -> Float 就代表了参数类型是 两个 Int 返回值类型为 Float 的函数类型。
1.2 高阶函数是什么?
高阶函数是将函数用作参数或返回值的函数。
上面的话有点绕,直接看例子吧。如果将 Android 里点击事件的监听用 Kotlin 来实现,它就是一个典型的高阶函数。
// 函数作为参数的高阶函数
// ↓
fun setOnClickListener(l: (View) -> Unit) { ... }
1.3 Lambda 是什么?
Lambda 可以理解为函数的简写。
fun onClick(v: View): Unit { ... }
setOnClickListener(::onClick)
// 用 Lambda 表达式来替代函数引用
setOnClickListener({v: View -> ...})
看到这,如果你没有疑惑,那恭喜你,这说明你的悟性很高,或者说你基础很好;如果你感觉有点懵,那也很正常,请看后面详细的解释。
2. 为什么要引入 Lambda 和 高阶函数?
刚接触到高阶函数和 Lambda 的时候,我就一直有个疑问:为什么要引入 Lambda 和 高阶函数?这个问题,官方文档里没有解答,因此我只能自己去寻找。
2.1 Lambda 和 高阶函数解决了什么问题?
这个问题站在语言的设计者角度会更明了,让我们看个实际的例子,这是 Android 中的 View 定义,我省略了大部分代码:
// View.java
private OnClickListener mOnClickListener;
private OnContextClickListener mOnContextClickListener;
// 监听手指点击事件
public void setOnClickListener(OnClickListener l) {
mOnClickListener = l;
}
// 为传递这个点击事件,专门定义了一个接口
public interface OnClickListener {
void onClick(View v);
}
// 监听鼠标点击事件
public void setOnContextClickListener(OnContextClickListener l) {
getListenerInfo().mOnContextClickListener = l;
}
// 为传递这个鼠标点击事件,专门定义了一个接口
public interface OnContextClickListener {
boolean onContextClick(View v);
}
Android 中设置点击事件和鼠标点击事件,分别是这样写的:
// 设置手指点击事件
image.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
gotoPreview();
}
});
// 设置鼠标点击事件
image.setOnContextClickListener(new View.OnContextClickListener() {
@Override
public void onContextClick(View v) {
gotoPreview();
}
});
请问各位小伙伴有没有觉得这样的代码很啰嗦?
现在我们假装自己是语言设计者,让我们先看看上面的代码存在哪些问题:
定义方:每增加一个方法,就要新增一个接口:OnClickListener,OnContextClickListener 调用方:需要写一堆的匿名内部类,啰嗦,繁琐,毫无重点
仔细看上面的代码,开发者关心的其实只有一行代码:
gotoPreview();
如果将其中的核心逻辑抽出来,这样子才是最简明的:
image.setOnClickListener { gotoPreview() }
image.setOnContextClickListener { gotoPreview() }
Kotlin 语言的设计者是怎么做的?是这样:
1、用函数类型替代接口定义
与上面 View.java 的等价 Kotlin 代码如下:
//View.kt
var mOnClickListener: ((View) -> Unit)? = null
var mOnContextClickListener: ((View) -> Unit)? = null
fun setOnClickListener(l: (View) -> Unit) {
mOnClickListener = l;
}
fun setOnContextClickListener(l: (View) -> Unit) {
mOnContextClickListener = l;
}
2、用 Lambda 表达式作为函数参数
image.setOnClickListener{v ->
gotoPreview();
}
以上做法有以下的好处:
定义方:减少了两个接口类的定义 调用方:代码更加简明
细心的小伙伴可能已经发现了一个问题:Android 并没有提供 View.java 的 Kotlin 实现,为什么我们 Demo 里面可以用 Lambda 来简化事件监听?
// 在实际开发中,我们经常使用这种简化方式
setOnClickListener { gotoPreview() }
原因是这样的:由于 OnClickListener 符合 SAM 转换的要求,因此编译器自动帮我们做了一层转换,让我们可以用 Lambda 表达式来简化我们的函数调用。
那么,SAM 又是个什么鬼?
2.2 SAM 转换(Single Abstract Method Conversions)
SAM(Single Abstract Method),顾名思义,就是:只有一个抽象方法的类或者接口,但在 Kotlin 和 Java8 里,SAM 代表着:只有一个抽象方法的接口。符合 SAM 要求的接口,编译器就能进行 SAM 转换:让我们可以用 Lambda 表达式来简写接口类的参数。
注:Java8 中的 SAM 有明确的名称叫做:函数式接口(FunctionalInterface)。
FunctionalInterface 的限制如下,缺一不可:
必须是接口,抽象类不行 该接口有且仅有一个抽象的方法,抽象方法个数必须是1,默认实现的方法可以有多个。 也就是说,对于 View.java 来说,它虽然是 Java 代码,但 Kotlin 编译器知道它的参数 OnClickListener 符合 SAM 转换的条件,所以会自动做以下转换:
转换前:
public void setOnClickListener(OnClickListener l)
转换后:
fun setOnClickListener(l: (View) -> Unit)
// 实际上是这样:
fun setOnClickListener(l: ((View!) -> Unit)?)
((View!) -> Unit)?代表,这个参数可能为空。
2.3 Lambda 表达式引发的8种写法
当 Lambda 表达式作为函数参数的时候,有些情形下是可以简写的,这时候可以让我们的代码看起来更简洁。然而,大部分初学者对此也比较头疼,同样的代码,能有 8 种不同的写法,确实也挺懵的。
要理解 Lambda 表达式的简写逻辑,其实很简单,那就是:多写。
各位小伙伴可以跟着我接下来的流程来一起写一写:
2.3.1 第1种写法
这是原始代码,它的本质是用 object 关键字定义了一个匿名内部类:
image.setOnClickListener(object: View.OnClickListener {
override fun onClick(v: View?) {
gotoPreview(v)
}
})
2.3.2 第2种写法
如果我们删掉 object 关键字,它就是 Lambda 表达式了,因此它里面 override 的方法也要跟着删掉:
image.setOnClickListener(View.OnClickListener { view: View? ->
gotoPreview(view)
})
上面的 View.OnClickListener 被称为: SAM Constructor—— SAM 构造器,它是编译器为我们生成的。Kotlin 允许我们通过这种方式来定义 Lambda 表达式。
思考题: 这时候,View.OnClickListener {} 在语义上是 Lambda 表达式,但在语法层面还是匿名内部类。这句话对不对?
2.3.3 第3种写法
由于 Kotlin 的 Lambda 表达式是不需要 SAM Constructor的,所以它也可以被删掉。
image.setOnClickListener({ view: View? ->
gotoPreview(view)
})
2.3.4 第4种写法
由于 Kotlin 支持类型推导,所以 View? 可以被删掉:
image.setOnClickListener({ view ->
gotoPreview(view)
})
2.3.5 第5种写法
当 Kotlin Lambda 表达式只有一个参数的时候,它可以被写成 it。
image.setOnClickListener({ it ->
gotoPreview(it)
})
2.3.6 第6种写法
Kotlin Lambda 的 it 是可以被省略的:
image.setOnClickListener({
gotoPreview(it)
})
2.3.7 第7种写法
当 Kotlin Lambda 作为函数的最后一个参数时,Lambda 可以被挪到外面:
image.setOnClickListener() {
gotoPreview(it)
}
2.3.8 第8种写法
当 Kotlin 只有一个 Lambda 作为函数参数时,() 可以被省略:
image.setOnClickListener {
gotoPreview(it)
}
按照这个流程,在 IDE 里多写几遍,你自然就会理解了。一定要写,看文章是记不住的。
2.4 函数类型,高阶函数,Lambda表达式三者之间的关系
将函数的参数类型和返回值类型抽象出来后,就得到了函数类型。
(View) -> Unit 就代表了参数类型是 View ,返回值类型为 Unit 的函数类型。
如果一个函数的参数或者返回值的类型是函数类型,那这个函数就是高阶函数。很明显,我们刚刚就写了一个高阶函数,只是它比较简单而已。 Lambda 就是函数的一种简写
2.5 现在我们通过扩展来实现一个高阶函数
fun View.setOnClickListener(l: (View) -> Unit) {
l.invoke(this)
}
使用:
val view = View(this@MainActivity)
view.setOnClickListener {
startActivity()
}
假设有多个参数怎么办
fun View.setOnClickListeners(test:Int,l: (View) -> Boolean) {
l.invoke(this)
}
// 多个高阶函数
fun View.setOnClickListeners(test: Int, l: (View) -> Boolean,l2: (View) -> Boolean) {
this.let { l.invoke(this) }
}
还是一样的道理,将函数类型的函数单独放在花括号里:
// 写法1
val view = View(this@MainActivity)
view.setOnClickListeners(0, {
println("-----------$it")
return@setOnClickListeners true
})
// 写法2
val view = View(this@MainActivity)
view.setOnClickListeners(0) {
println("-----------$it")
return@setOnClickListeners true
}
// 多个高阶函数
val view = View(this@MainActivity)
view.setOnClickListeners(0, {
println("-----------$it")
return@setOnClickListeners true
}, {
return@setOnClickListeners false
})
2.6 回调里的printIn为什么会被执行
看图,大家知道kotlin中任何东西都是一个对象
那么图2中阴影部分的代码块其实就是一个对象,对象的引用其实就是图1中的 l1,那么要想图2的回调被执行,就需要图1中的l11去执行。即:
fun View.setOnClickListeners(l1: (View) -> Boolean, l2: View.() -> Boolean) {
l1(this)
// 或者
l1.invoke(this)
}
大家看到这里,会有疑问,为什么要传个this呢?是因为l1是函数类型参数,接收的是扩展对象参数,返回的是Boolean类型。而扩展对象参数View就是调用该扩展方法的调用者,这是在讲kotlin扩展函数中明确的。
也就是说view.setOnClickListeners中的view对象会传到扩展函数中的this,这个对象又作为函数类型参数的接收值回调到view.setOnClickListeners中去:
view.setOnClickListeners({ v ->
println("----111-------$v")
return@setOnClickListeners true
}
这个v就是回调回来的view对象,也可以这么写:
看到as的智能提示,这个对象用 it 表示,所以写 v 报错了,正确的是:
view.setOnClickListeners({
println("----111-------$it")
return@setOnClickListeners true
}
如果函数类型中接收参数是多个怎么办,比如:
这时候就不能简写lambda表达式了,会报错,也就没了 it
==所以:it 用于 函数类型中: 函数只有一个参数 。 it表示 参数对象,并且回调返回==
3. 带接收者(Receiver)的函数类型:A.(B,C) -> D
为什么要引出这个东西呢?
大家看上面的扩展函数中,l1后面还有个函数类型的参数,l2
发现 l1 和 l2 的区别了吗,函数类型申明方式不同
我们在定义高阶扩展函数的时候 ,某个参数是函数类型,假如想要把 扩展对象传递给 这个函数类型 。可以通过 两种方式 定义 函数类型
1、f: (T) -> Unit , 函数类型的参数为 扩展对象类型。 2、f: T.() -> Unit ,函数类型为带接收者的函数类型,接收者和扩展对象一致
先不考虑这两者的区别
要想 l2 执行,应该怎么做呢?直接看代码:
看出区别了吗
由上面可以知道: 1、{ }花括号里调用必须加(),()里调用直接调用对象引用,无需加()。 2、l1 是不带接受者的,也就是 l1 不被view对象持有,所以不能通过this调用 l1 ,l1要想执行,得自己去执行,而 l2是带接受者的,被view对象持有,而view对象又可以用this表示,即可以用this去调用 l2,也可以 l2 自己执行。
看到这大家可能疑问,那都自己执行自己不就行了吗,其实真正的区别不是体现在这里,而是体现在调用方的回调里:
我先把 l1的第二个参数去掉
所以:this 用于 带接受者的函数类型中,表示接受者,接收者和扩展对象一致,没有回调返回
虽然他们的对象地址都一样,但是描述的意义不一样。
如果将 l2 的返回值类型Boolean换成Unit,apply还可以简写:
如果接收者不和扩展对象一致,会怎么样呢
this就调用不了 l2
调用方的this就不再是调用方的对象
如果在此基础上,给 l2加个传入参数呢?
调用方:
这里怎么会同时有个 this 和 it呢,前面已经说过了,this代表了函数类型接受者,it
代表了唯一一个函数类型的传入参数,当然,也可以这么写:
结果显而易见: