高阶函数、lambda表达式

278 阅读9分钟

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为什么会被执行

image.png

image.png 看图,大家知道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对象,也可以这么写:

image.png 看到as的智能提示,这个对象用 it 表示,所以写 v 报错了,正确的是:

view.setOnClickListeners({
      println("----111-------$it")
      return@setOnClickListeners true
}

如果函数类型中接收参数是多个怎么办,比如:

image.png 这时候就不能简写lambda表达式了,会报错,也就没了 it

image.png ==所以:it 用于 函数类型中: 函数只有一个参数 。 it表示 参数对象,并且回调返回==

3. 带接收者(Receiver)的函数类型:A.(B,C) -> D

为什么要引出这个东西呢?

大家看上面的扩展函数中,l1后面还有个函数类型的参数,l2

image.png 发现 l1 和 l2 的区别了吗,函数类型申明方式不同

我们在定义高阶扩展函数的时候 ,某个参数是函数类型,假如想要把 扩展对象传递给 这个函数类型 。可以通过 两种方式 定义 函数类型

1、f: (T) -> Unit , 函数类型的参数为 扩展对象类型。 2、f: T.() -> Unit ,函数类型为带接收者的函数类型,接收者和扩展对象一致

先不考虑这两者的区别

要想 l2 执行,应该怎么做呢?直接看代码:

image.png 看出区别了吗

由上面可以知道: 1、{ }花括号里调用必须加(),()里调用直接调用对象引用,无需加()。 2、l1 是不带接受者的,也就是 l1 不被view对象持有,所以不能通过this调用 l1 ,l1要想执行,得自己去执行,而 l2是带接受者的,被view对象持有,而view对象又可以用this表示,即可以用this去调用 l2,也可以 l2 自己执行。

看到这大家可能疑问,那都自己执行自己不就行了吗,其实真正的区别不是体现在这里,而是体现在调用方的回调里:

我先把 l1的第二个参数去掉

image.png

image.png 所以:this 用于 带接受者的函数类型中,表示接受者,接收者和扩展对象一致,没有回调返回

虽然他们的对象地址都一样,但是描述的意义不一样。

image.png 如果将 l2 的返回值类型Boolean换成Unit,apply还可以简写:

image.png 如果接收者不和扩展对象一致,会怎么样呢

image.png this就调用不了 l2

image.png 在这里插入图片描述 调用方的this就不再是调用方的对象

如果在此基础上,给 l2加个传入参数呢?

image.png 调用方:

image.png 这里怎么会同时有个 this 和 it呢,前面已经说过了,this代表了函数类型接受者,it 代表了唯一一个函数类型的传入参数,当然,也可以这么写:

image.png 结果显而易见:

image.png