《Kotlin核心编程》 ——2.3.6 Lambda是语法糖
2.3.6 Lambda是语法糖
提到Lambda表达式,也许你听说过所谓的Lambda演算。其实这是两个不同的概念,Lambda演算和图灵机一样,是一种支持理论上完备的形式系统,也是理解函数式编程的理论基础。古老的Lisp语言就是基于Lambda演算系统而来的,在Lisp中,匿名函数是重要的组成部分,它也被叫作Lambda表达式,这就是Lambda表达式名字的由来。所以,相较Lambda演算而言,Lambda表达式是更加简单的概念。你可以把它理解成简化表达后的匿名函数,实质上它就是一种语法糖。
我们先来分析下代码清单2-2中的filterCountries方法的匿名函数,会发现:
fun(country:Country)显得比较啰唆,因为编译器会推导类型,所以只需要一个代表变量的country就行了;
return关键字也可以省略,这里返回的是一个有值的表达式;
模仿函数类型的语法,我们可以用->把参数和返回值连接在一起。
因此,简化后的表达就变成了这个样子:
countryApp.filterCountries(countries, {
country ->
country.continient == "EU" && country.population > 10000
})
是不是非常简洁?这个就是Lambda表达式,它与匿名函数一样,是一种函数字面量。我们再来讲解下Lambda具体的语法。现在用Lambda的形式来定义一个加法操作:
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
由于支持类型推导,我们可以采用两种方式进行简化:
val sum = { x: Int, y: Int -> x + y }
或者是:
val sum: (Int, Int) -> Int = { x, y -> x + y }
现在来总结下Lambda的语法:
一个Lambda表达式必须通过{}来包裹;
如果Lambda声明了参数部分的类型,且返回值类型支持类型推导,那么Lambda变量就可以省略函数类型声明;
如果Lambda变量声明了函数类型,那么Lambda的参数部分的类型就可以省略。
此外,如果Lambda表达式返回的不是Unit,那么默认最后一行表达式的值类型就是返回值类型,如:
val foo = { x: Int ->
val y = x + 1
y // 返回值是 y
}
>>> foo(1)
2
Lambda看起来似乎很简单。那么再思考一个场景,如果用fun关键字来声明Lambda表达式又会怎么样?如代码清单2-3所示。
代码清单 2-3
1.单个参数的隐式名称
首先,也许你在it这个关键字上停留了好几秒,然后依旧不明其意。其实它也是Kotlin简化Lambda表达的一种语法糖,叫作单个参数的隐式名称,代表了这个Lambda所接收的单个参数。这里的调用等价于:
listOf(1, 2, 3).forEach { item -> foo(item) }
默认情况下,我们可以直接用it来代表item,而不需要用item->进行声明。
其次,这行代码的结果可能出乎了你的意料,执行后你会发现什么也没有。为什么会这样?这一次,我们必须要借助IDE的帮助了,以下是把foo函数用IDE转化后的Java代码:
@JvmStatic
@NotNull
public static final Function0 foo(final int var0) {
return (Function0)(new Function0 () {
// $FF: synthetic method
// $FF: bridge method
public Object invoke() {
this.invoke();
return Unit.INSTANCE;
}
public final void invoke () {
int var1 = var0;
System.out.print(var1);
}
});
}
以上是字节码反编译的Java代码,从中我们可以发现Kotlin实现Lambda表达式的机理。
2. Function类型
Kotlin在JVM层设计了Function类型(Function0、Function1……Function22)来兼容Java的Lambda表达式,其中的后缀数字代表了Lambda参数的数量,如以上的foo函数构建的其实是一个无参Lambda,所以对应的接口是Function0,如果有一个参数那么对应的就是Function1。它在源码中是如下定义的:
package kotlin.jvm.functions
interface Function1<in P1, out R> : kotlin.Function<R> {
fun invoke(p1: P1): R
}
可见每个Function类型都有一个invoke方法,稍后会详细介绍这个方法。
设计Function类型的主要目的之一就是要兼容Java,实现在Kotlin中也能调用Java的Lambda。在Java中,实际上并不支持把函数作为参数,而是通过函数式接口来实现这一特性。所以如果我们要把Java的Lambda传给Kotlin,那么它们就必须实现Kotlin的Function接口,在Kotlin中我们则不需要跟它们打交道。在第6章我们会介绍如何在Kotlin中调用Java的函数式接口。
神奇的数字—22
也许你会问一个问题:为什么这里Function类型最大的是Function22?如果Lambda的参数超过了22个,那该怎么办呢?
虽然22个参数已经够多了,然而现实中也许我们真的需要超过22个参数。其实,在Scala的设计中也遵循了22个数字的设计,这似乎已经成了业界的一种惯例。然而,这个22的设计也给Scala开发者带来了不少麻烦。所以,Kotlin在设计的时候便考虑到了这种情况,除了23个常用的Function类型外,还有一个FunctionN。在参数真的超过22个的时候,我们就可以依靠它来解决问题。更多细节可以参考https://github.com/JetBrains/kotlin/blob/master/spec-docs/function-types.md。
3. invoke方法
代码清单2-3中的foo函数的返回类型是Function0。这也意味着,如果我们调用了foo(n),那么实质上仅仅是构造了一个Function0对象。这个对象并不等价于我们要调用的过程本身。通过源码可以发现,需要调用Function0的invoke方法才能执行println方法。所以,我们的疑惑也迎刃而解,上述的例子必须如下修改,才能够最终打印出我们想要的结果:
fun foo(int: Int) = {
print(int)
}
>>> listOf(1, 2, 3).forEach { foo(it).invoke() } // 增加了invoke调用
123
也许你觉得invoke这种语法显得丑陋,不符合Kotlin简洁表达的设计理念。确实如此,所以我们还可以用熟悉的括号调用来替代invoke,如下所示:
>>> listOf(1, 2, 3).forEach { foo(it)() }
123
- 点赞
- 收藏
- 关注作者
评论(0)