【码上开学】Kotlin 的高阶函数、匿名函数和 Lambda 表达式

据说……Kotlin 能够用 Lambda?java

不错不错,Java 8 也有 Lambda,挺好用的。app

据说……Kotlin 的 Lambda 还能当函数参数?ide

啊挺好挺好,我也来写一个!函数

哎,报错了?我改!工具

哎?post

我……再改?开发工具

我……再……改?ui

啊!!!!!!!!!!!!this

视频先行

这里是视频版本:【码上开学】Kotlin 的高阶函数、匿名函数和 Lambda 表达式 spa

看了视频就不用看后面的文字了(但若是喜欢,点个赞再溜啊)。

Kotlin 的高阶函数

你们好,我是扔物线朱凯。Kotlin 很方便,但有时候也让人头疼,并且越方便的地方越让人头疼,好比 Lambda 表达式。不少人由于 Lambda 而被 Kotlin 吸引,但不少人也由于 Lambda 而被 Kotlin 吓跑。其实大多数已经用了好久 Kotlin 的人,对 Lambda 也只会简单使用而已,甚至至关一部分人不靠开发工具的自动补全功能,根本就彻底不会写 Lambda。今天我就来跟你们唠一唠 Lambda。不过,要讲 Lambda,咱们得先从 Kotlin 的高阶函数——Higher-Order Function 提及。

在 Java 里,若是你有一个 a 方法须要调用另外一个 b 方法,你在里面调用就能够;

int a() {
  return b(1);
}
a();
复制代码

而若是你想在 a 调用时动态设置 b 方法的参数,你就得把参数传给 a,再从 a 的内部把参数传给 b:

int a(int param) {
  return b(param);
}
a(1); // 内部调用 b(1)
a(2); // 内部调用 b(2)
复制代码

这均可以作到,不过……若是我想动态设置的不是方法参数,而是方法自己呢?好比我在 a 的内部有一处对别的方法的调用,这个方法多是 b,多是 c,不必定是谁,我只知道,我在这里有一个调用,它的参数类型是 int ,返回值类型也是 int ,而具体在 a 执行的时候内部调用哪一个方法,我但愿能够动态设置:

int a(??? method) {
  return method(1);
}
a(method1);
a(method2);
复制代码

或者说,我想把方法做为参数传到另外一个方法里,这个……能够作到吗?

不行,也行。在 Java 里是不容许把方法做为参数传递的,可是咱们有一个历史悠久的变通方案:接口。咱们能够经过接口的方式来把方法包装起来:

public interface Wrapper {
  int method(int param);
}
复制代码

而后把这个接口的类型做为外部方法的参数类型:

int a(Wrapper wrapper) {
  return wrapper.method(1);
}
复制代码

在调用外部方法时,传递接口的对象来做为参数:

a(wrapper1);
a(wrapper2);
复制代码

若是到这里你以为听晕了,我换个写法你再感觉一下:

咱们在用户发生点击行为的时候会触发点击事件:

// 注:这是简化后的代码,不是 View.java 类的源码
public class View {
  OnClickListener mOnClickListener;
  ...
  public void onTouchEvent(MotionEvent e) {
    ...
    mOnClickListener.onClick(this);
    ...
  }
}
复制代码

所谓的点击事件,最核心的内容就是调用内部的一个 OnClickListener 的 onClick() 方法:

public interface OnClickListener {
  void onClick(View v);
}
复制代码

而所谓的这个 OnClickListener 其实只是一个壳,它的核心全在内部那个 onClick() 方法。换句话说,咱们传过来一个 OnClickListener:

OnClickListener listener1 = new OnClickListener() {
  @Override
  void onClick(View v) {
    doSomething();
  }
};
view.setOnClickListener(listener1);
复制代码

本质上实际上是传过来一个能够在稍后被调用的方法(onClick())。只不过由于 Java 不容许传递方法,因此咱们才把它包进了一个对象里来进行传递。

而在 Kotlin 里面,函数的参数也能够是函数类型的:

fun a(funParam: Fun): String {
  return funParam(1);
}
复制代码

当一个函数含有函数类型的参数的时候——这句话有点绕啊——若是你调用它,你就能够——固然你也必须——传入一个函数类型的对象给它;

fun b(param: Int): String {
  return param.toString()
}
a(b)
复制代码

不过在具体的写法上没有个人示例这么粗暴。

首先我写的这个 Fun 做为函数类型实际上是错的,Kotlin 里并无这么一种类型来标记这个变量是个「函数类型」。由于函数类型不是一「个」类型,而是一「类」类型,由于函数类型能够有各类各样不一样的参数和返回值的类型的搭配,这些搭配属于不一样的函数类型。例如,无参数无返回值(() -> Unit)和单 Int 型参数返回 String (Int -> String)是两种不一样的类型,这个很好理解,就好像 Int 和 String 是两个不一样的类型。因此不能只用 Fun 这个词来表示「这个参数是个函数类型」,就好像不能用 Class 这个词来表示「这个参数是某个类」,由于你须要指定,具体是哪一种函数类型,或者说这个函数类型的参数,它的参数类型是什么、返回值类型是什么,而不能笼统地一句说「它是函数类型」就完了。

因此对于函数类型的参数,你要指明它有几个参数、参数的类型是什么以及返回值类型是什么,那么写下来就大概是这个样子:

fun a(funParam: (Int) -> String): String {
  return funParam(1)
}
复制代码

看着有点可怕。可是只有这样写,调用的人才知道应该传一个怎样的函数类型的参数给你。

一样的,函数类型不仅能够做为函数的参数类型,还能够做为函数的返回值类型:

fun c(param: Int): (Int) -> Unit {
  ...
}
复制代码

这种「参数或者返回值为函数类型的函数」,在 Kotlin 中就被称为「高阶函数」——Higher-Order Functions。

这个所谓的「高阶」,总给人一种神秘感:阶是什么?哪里高了?其实没有那么复杂,高阶函数这个概念源自数学中的高阶函数。在数学里,若是一个函数使用函数做为它的参数或者结果,它就被称做是一个「高阶函数」。好比求导就是一个典型的例子:你对 f(x) = x 这个函数求导,结果是 1;对 f(x) = x² 这个函数求导,结果是 2x。很明显,求导函数的参数和结果都是函数,其中 f(x) 的导数是 1 这其实也是一个函数,只不过是一个结果恒为 1 的函数,因此——啊讲岔了,总之, Kotlin 里,这种参数有函数类型或者返回值是函数类型的函数,都叫作高阶函数,这只是个对这一类函数的称呼,没有任何特殊性,Kotlin 的高阶函数没有任何特殊功能,这是我想说的。

另外,除了做为函数的参数和返回值类型,你把它赋值给一个变量也是能够的。

不过对于一个声明好的函数,无论是你要把它做为参数传递给函数,仍是要把它赋值给变量,都得在函数名的左边加上双冒号才行:

a(::b)
val d = ::b
复制代码

这……是为何呢?

双冒号 ::method 究竟是什么?

若是你上网搜,你会看到这个双冒号的写法叫作函数引用 Function Reference,这是 Kotlin 官方的说法。可是这又表示什么意思?表示它指向上面的函数?那既然都是一个东西,为何不直接写函数名,而要加两个冒号呢?

由于加了两个冒号,这个函数才变成了一个对象。

什么意思?

Kotlin 里「函数能够做为参数」这件事的本质,是函数在 Kotlin 里能够做为对象存在——由于只有对象才能被做为参数传递啊。赋值也是同样道理,只有对象才能被赋值给变量啊。但 Kotlin 的函数自己的性质又决定了它没办法被当作一个对象。那怎么办呢?Kotlin 的选择是,那就建立一个和函数具备相同功能的对象。怎么建立?使用双冒号。

在 Kotlin 里,一个函数名的左边加上双冒号,它就不表示这个函数自己了,而表示一个对象,或者说一个指向对象的引用,但,这个对象可不是函数自己,而是一个和这个函数具备相同功能的对象。

怎么个相同法呢?你能够怎么用函数,就能怎么用这个加了双冒号的对象:

b(1) // 调用函数
d(1) // 用对象 a 后面加上括号来实现 b() 的等价操做
(::b)(1) // 用对象 :b 后面加上括号来实现 b() 的等价操做
复制代码

但我再说一遍,这个双冒号的这个东西,它不是一个函数,而是一个对象,一个函数类型的对象。

对象是不能加个括号来调用的,对吧?可是函数类型的对象能够。为何?由于这实际上是个假的调用,它是 Kotlin 的语法糖,实际上你对一个函数类型的对象加括号、加参数,它真正调用的是这个对象的 invoke() 函数:

d(1) // 实际上会调用 d.invoke(1)
(::b)(1) // 实际上会调用 (::b).invoke(1)
复制代码

因此你能够对一个函数类型的对象调用 invoke(),但不能对一个函数这么作:

b.invoke(1) // 报错
复制代码

为何?由于只有函数类型的对象有这个自带的 invoke() 能够用,而函数,不是函数类型的对象。那它是什么类型的?它什么类型也不是。函数不是对象,它也没有类型,函数就是函数,它和对象是两个维度的东西。

包括双冒号加上函数名的这个写法,它是一个指向对象的引用,但并非指向函数自己,而是指向一个咱们在代码里看不见的对象。这个对象复制了原函数的功能,但它并非原函数。

这个……是底层的逻辑,但我知道这个有什么用呢?

这个知识能帮你解开 Kotlin 的高阶函数以及接下来我立刻要讲的匿名函数、Lambda 相关的大部分迷惑。

好比我在代码里有这么几行:

fun b(param: Int): String {
  return param.toString()
}
val d = ::b
复制代码

那我若是想把 d 赋值给一个新的变量 e:

val e = d
复制代码

我等号右边的 d,应该加双冒号仍是不加呢?

不用试,也不用搜,想想:这是个赋值操做对吧?赋值操做的右边是个对象对吧?d 是对象吗?固然是了,b 不是对象是由于它来自函数名,但 d 已是个对象了,因此直接写就好了。

匿名函数

咱们继续讲。

要传一个函数类型的参数,或者把一个函数类型的对象赋值给变量,除了用双冒号来拿现成的函数使用,你还能够直接把这个函数挪过来写:

a(fun b(param: Int): String {
  return param.toString()
});
val d = fun b(param: Int): String {
  return param.toString()
}
复制代码

另外,这种写法的话,函数的名字其实就没用了,因此你能够把它省掉:

a(fun(param: Int): String {
  return param.toString()
});
val d = fun(param: Int): String {
  return param.toString()
}
复制代码

这种写法叫作匿名函数。为何叫匿名函数?很简单,由于它没有名字呗,对吧。等号左边的不是函数的名字啊,它是变量的名字。这个变量的类型是一种函数类型,具体到咱们的示例代码来讲是一种只有一个参数、参数类型是 Int、而且返回值类型为 String 的函数类型。

另外呢,其实刚才那种左边右边都有名字的写法,Kotlin 是不容许的。右边的函数既然要名字也没有用,Kotlin 干脆就不准它有名字了。

因此,若是你在 Java 里设计一个回调的时候是这么设计的:

public interface OnClickListener {
  void onClick(View v);
}
public void setOnClickListener(OnClickListener listener) {
  this.listener = listener;
}
复制代码

使用的时候是这么用的:

view.setOnClickListener(new OnClickListener() {
  @Override
  void onClick(View v) {
    switchToNextPage();
  }
});
复制代码

到了 Kotlin 里就能够改为这么写了:

fun setOnClickListener(onClick: (View) -> Unit) {
  this.onClick = onClick
}
view.setOnClickListener(fun(v: View): Unit) {
  switchToNextPage()
})
复制代码

简单一点哈?另外大多数(几乎全部)状况下,匿名函数还能更简化一点,写成 Lambda 表达式的形式:

view.setOnClickListener({ v: View ->
  switchToNextPage()
})
复制代码

Lambda 表达式

终于讲到 Lambda 了。

若是 Lambda 是函数的最后一个参数,你能够把 Lambda 写在括号的外面:

view.setOnClickListener() { v: View ->
  switchToNextPage()
}
复制代码

而若是 Lambda 是函数惟一的参数,你还能够直接把括号去了:

view.setOnClickListener { v: View ->
  switchToNextPage()
}
复制代码

另外,若是这个 Lambda 是单参数的,它的这个参数也省略掉不写:

view.setOnClickListener {
  switchToNextPage()
}
复制代码

哎,不错,单参数的时候只要不用这个参数就能够直接不写了。

其实就算用,也能够不写,由于 Kotlin 的 Lambda 对于省略的惟一参数有默认的名字:it:

view.setOnClickListener {
  switchToNextPage()
  it.setVisibility(GONE)
}
复制代码

有点爽哈?不过咱们先停下想想:这个 Lambda 这也不写那也不写的……它不迷茫吗?它是怎么知道本身的参数类型和返回值类型的?

靠上下文的推断。我调用的函数在声明的地方有明确的参数信息吧?

fun setOnClickListener(onClick: (View) -> Unit) {
  this.onClick = onClick
}
复制代码

这里面把这个参数的参数类型和返回值写得清清楚楚吧?因此 Lambda 才不用写的。

因此,当你要把一个匿名函数赋值给变量而不是做为函数参数传递的时候:

val b = fun(param: Int): String {
  return param.toString()
}
复制代码

若是也简写成 Lambda 的形式:

val b = { param: Int ->
  return param.toString()
}
复制代码

就不能省略掉 Lambda 的参数类型了:

val b = {
  return it.toString() // it 报错
}
复制代码

为何?由于它没法从上下文中推断出这个参数的类型啊!

若是你出于场景的需求或者我的偏好,就是想在这里省掉参数类型,那你须要给左边的变量指明类型:

val b: (Int) -> String = {
  return it.toString() // it 能够被推断出是 Int 类型
}
复制代码

另外 Lambda 的返回值不是用 return 来返回,而是直接取最后一行代码的值:

val b: (Int) -> String = {
  it.toString() // it 能够被推断出是 Int 类型
}
复制代码

这个必定注意,Lambda 的返回值别写 return,若是你写了,它会把这个做为它外层的函数的返回值来直接结束外层函数。固然若是你就是想这么作那没问题啊,但若是你是只是想返回 Lambda,这么写就出错了。

另外由于 Lambda 是个代码块,它总能根据最后一行代码来推断出返回值类型,因此它的返回值类型确实能够不写。实际上,Kotlin 的 Lambda 也是写不了返回值类型的,语法上就不支持。

如今我再停一下,咱们想一想:匿名函数和 Lambda……它们究竟是什么?

Kotlin 里匿名函数和 Lambda 表达式的本质

咱们先看匿名函数。它能够做为参数传递,也能够赋值给变量,对吧?

可是咱们刚才也说过了函数是不能做为参数传递,也不能赋值给变量的,对吧?

那为何匿名函数就这么特殊呢?

由于 Kotlin 的匿名函数不——是——函——数。它是个对象。匿名函数虽然名字里有「函数」两个字,包括英文的原名也是 Anonymous Function,但它其实不是函数,而是一个对象,一个函数类型的对象。它和双冒号加函数名是一类东西,和函数不是。

因此,你才能够直接把它当作函数的参数来传递以及赋值给变量:

a(fun (param: Int): String {
  return param.toString()
});
val a = fun (param: Int): String {
  return param.toString()
}
复制代码

同理,Lambda 其实也是一个函数类型的对象而已。你能怎么使用双冒号加函数名,就能怎么使用匿名函数,以及怎么使用 Lambda 表达式。

这,就是 Kotlin 的匿名函数和 Lambda 表达式的本质,它们都是函数类型的对象。Kotlin 的 Lambda 跟 Java 8 的 Lambda 是不同的,Java 8 的 Lambda 只是一种便捷写法,本质上并无功能上的突破,而 Kotlin 的 Lambda 是实实在在的对象。

在你知道了在 Kotlin 里「函数并不能传递,传递的是对象」和「匿名函数和 Lambda 表达式其实都是对象」这些本质以后,你之后去写 Kotlin 的高阶函数会很是轻松很是舒畅。

Kotlin 官方文档里对于双冒号加函数名的写法叫 Function Reference 函数引用,故意引导你们认为这个引用是指向原函数的,这是为了简化事情的逻辑,让你们更好上手 Kotlin;但这种逻辑是有毒的,一旦你信了它,你对于匿名函数和 Lambda 就怎么也搞不清楚了。

广告时间:

你们若是喜欢个人视频,也能够了解一下个人 Android 高级进阶系列化课程,扫码加小助理丢丢让她给你发试听课:

白嫖党记得点赞转发,也是对个人支持。

对比 Java 的 Lambda

再说一下 Java 的 Lambda。对于 Kotlin 的 Lambda,有不少从 Java 过来的人表示「好用好用但不会写」。这是一件颇有意思的事情:你都不会写,那你是怎么会用的呢?Java 从 8 开始引入了对 Lambda 的支持,对于单抽象方法的接口——简称 SAM 接口,Single Abstract Method 接口——对于这类接口,Java 8 容许你用 Lambda 表达式来建立匿名类对象,但它本质上仍是在建立一个匿名类对象,只是一种简化写法而已,因此 Java 的 Lambda 只靠代码自动补全就基本上能写了。而 Kotlin 里的 Lambda 和 Java 本质上就是不一样的,由于 Kotlin 的 Lambda 是实实在在的函数类型的对象,功能更强,写法更多更灵活,因此不少人从 Java 过来就有点搞不明白了。

另外呢,Kotlin 是不支持使用 Lambda 的方式来简写匿名类对象的,由于咱们有函数类型的参数嘛,因此这种单函数接口的写法就直接不必了。那你还支持它干吗?

不过当和 Java 交互的时候,Kotlin 是支持这种用法的:当你的函数参数是 Java 的单抽象方法的接口的时候,你依然可使用 Lambda 来写参数。但这其实也不是 Kotlin 增长了功能,而是对于来自 Java 的单抽象方法的接口,Kotlin 会为它们额外建立一个把参数替换为函数类型的桥接方法,让你能够间接地建立 Java 的匿名类对象。

这就是为何,你会发现当你在 Kotlin 里调用 View.java 这个类的 setOnClickListener() 的时候,能够传 Lambda 给它来建立 OnClickListener 对象,但你照着一样的写法写一个 Kotlin 的接口,你却不能传 Lambda。由于 Kotlin 指望咱们直接使用函数类型的参数,而不是用接口这种折中方案。

总结

好,这就是 Kotlin 的高阶函数、匿名函数和 Lambda。简单总结一下:

  • 在 Kotlin 里,有一类 Java 中不存在的类型,叫作「函数类型」,这一类类型的对象在能够当函数来用的同时,还能做为函数的参数、函数的返回值以及赋值给变量;

  • 建立一个函数类型的对象有三种方式:双冒号加函数名、匿名函数和 Lambda;

  • 必定要记住:双冒号加函数名、匿名函数和 Lambda 本质上都是函数类型的对象。在 Kotlin 里,匿名函数不是函数,Lambda 也不是什么玄学的所谓「它只是个代码块,无法归类」,Kotlin 的 Lambda 能够归类,它属于函数类型的对象。

固然了这里面的各类细节还有不少,这个你能够本身学去,我就无论你了。下期内容是 Kotlin 的扩展属性和扩展函数,关注我,不错过个人任何新内容。你们拜拜~

推荐阅读

【码上开学】Kotlin 的变量、函数和类型

【码上开学】Kotlin 的泛型

【码上开学】Kotlin 协程的挂起好神奇好难懂?今天我把它的皮给扒了

【码上开学】到底什么是「非阻塞式」挂起?协程真的更轻量级吗?

【扔物线】消失这半年,我去哪了

相关文章
相关标签/搜索