Kotlin很方便,但有时候也让人头疼,而且越方便的地方越让人头疼,比如Lambda表达式。很多人因为Lambda而被Kotlin吸引,但很多人也因为Lambda而被Kotlin吓跑。其实大多数已经用了很久Kotlin的人,对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);
b(wrapper2);
如果到这里你觉得听晕了,我换个写法你再感受一下:
我们在用户发生点击行为的时候会触发点击事件:
// 注:这是简化后的代码,不是View.java类的源码
public class View {
OnClickListener mOnClickListener;
...
public void onTouchEvent(MotionEvent e) {
...
mOnClickListener.onClick(this);
...
}
}
所谓的点击事件,最核心的内容就是调用内部的一个OnClickListener的onClick()方法:
publc interface OnClickListener {
void onClick(View v);
}
而所谓的这个OnClickListener其实只是一个壳,它的核心全在内部那个onClick()方法。换句话说,我们传过来一个`OnClickListener:
OnClickListener listener1 = new OnClickListener() {
@Override
void onClick(View ) {
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);
}
puublic 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()
})
Lammbda表达式
终于讲到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
}
所以,当你要把一个匿名函数赋值给变量而不是作为函数参数传递的时候:
val b = fun(param: Int): String {
return param.toString()
}
如果也简写成Lambda的形式:
val b = { param: Int ->
return param.toString()
}
就不能省略掉Lambda的参数类型了:
val b = {
return it.toString()
}
为什么?因为它无法从身上下文中推断出这个参数的类型啊!
如果你处于场景的需求或者个人偏好,就是想在这里省掉参数类型,那你需要给左边的变量指明类型:
val b: (Int) -> String = {
return it.toString()// it可以被推断出是Int类型
}
另外Lambda的返回值不是用return来返回,而是直接取最后一行代码的值:
val b: (Int) -> String = {
it.toString()// it可以被推断出是Int类型
}
这个一定注意,Lambda的返回值别写return,如果你写了,它会把这个作为它外层的函数的返回值来直接结束外层函数。当然如果你就是想那么做那没问题啊,但如果你是只是想返回Lambda,这么写就出错了。
另外因为Lambda是个代码块,它总能根据最后一行代码来推断出返回值类型,所以它的返回值类型确实可以不写。实际上,Kotin的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函数引用,故意引导大家认为这个引用是指向原函数的,这是为了简化事情的逻辑,让大家更好上手;但这种逻辑是有毒的,一旦你信了它,你对于匿名函数和Lambda就怎么也搞不清楚了。
对吧Java的Lambda
再说一下Java的Lambda。对于Kotlin的Lamnda,有很多从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可以归类,它属于函数类型的对象。
我自己理解
匿名函数和Lambda表达式 到底是什么区别呢?
在Kotlin中,匿名函数和Lambda表达式都用于创建匿名函数,但它们在语法上有一些区别。
匿名函数是一种没有名称的函数,可以在需要函数作为参数的地方使用。匿名函数使用关键字 fun 声明,可以指定参数列表和返回类型。在匿名函数中,可以使用 return 关键字从函数中返回值。
下面是一个使用匿名函数的示例:
val sum: (Int, Int) -> Int = fun(x: Int, y: Int): Int {
return x + y
}
在上述示例中,sum 是一个类型为 (Int, Int) -> Int 的变量,它引用了一个匿名函数。这个匿名函数接收两个 Int 类型的参数,并返回它们的和。
Lambda表达式是一种更简洁的语法形式,用于创建匿名函数。它由一个箭头 -> 分隔的参数列表和函数体组成。Lambda表达式的参数类型通常可以省略,因为编译器会根据上下文推断类型。
下面是相同功能的示例,使用Lambda表达式来实现:
val sum: (Int, Int) -> Int = { x, y -> x + y }
在上述示例中,sum 是一个类型为 (Int, Int) -> Int 的变量,它引用了一个Lambda表达式。Lambda表达式接收两个 Int 类型的参数 x 和 y,并返回它们的和。
主要的区别在于语法形式和返回语法。使用匿名函数时,需要使用 fun 关键字声明函数,并使用 return 关键字返回值。而Lambda表达式则更加简洁,不需要显式声明函数和返回值。它们可以根据使用场景和个人偏好来选择。
版权声明
本文首发于:Kotlin 的 Lambda 表达式,大多数人学得连皮毛都不算
微信公众号:扔物线