Kotlin 核心编程 (28):基础语法 2.3.6

阅读数:1 2019 年 12 月 25 日 15:33

Kotlin核心编程(28):基础语法 2.3.6

(Lambda 是语法糖)

内容简介
本书不是一本简单介绍 Kotlin 语法应用的图书,而是一部专注于帮助读者深入理解 Kotlin 的设计理念,指导读者实现 Kotlin 高层次开发的实战型著作。书中深入介绍了 Kotlin 的核心语言特性、设计模式、函数式编程、异步开发等内容,并以 Android 和 Web 两个平台为背景,演示了 Kotlin 的实战应用。
全书共 13 章,分为 4 个部分:
热身篇—Kotlin 基础(第 1~2 章),简单介绍了 Kotlin 设计哲学、生态及基础语法,其中包括 Kotlin 与 Scala、Java 之间的关联与对比,以及 Kotlin 的类型声明的特殊性、val 和 var 的使用、高阶函数的使用、面向表达式编程的使用、字符串的定义与操作等内容;
下水篇—Kotlin 核心(第 3~8 章),深入介绍了面向对象、代数数据类型、模式匹配、类型系统、Lambda、集合、多态、扩展、元编程等 Kotlin 开发核心知识,这是本书的重点,其中涉及很多开发者特别关心的问题,比如多继承问题、模式匹配问题、用代数数据类型抽象业务问题、泛型问题、反射问题等。
潜入篇—Kotlin 探索(第 9~11 章),探索 Kotlin 在设计模式、函数式编程、异步和并发等编程领域的应用,其中包括对 4 大类设计模式、Typeclass 实现、函数式通用结构设计、类型替代异常处理、共享资源控制、CQRS 架构等重点内容的深入剖析;
遨游篇—Kotlin 实战(第 12~13 章),着重演示了 Kotlin 在 Android 和 Web 平台的实战案例,其中涉及架构方式、单向数据流模型、解耦视图导航、响应式编程、Spring 5 响应式框架和编程等内容。

提到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
复制代码
fun foo(int: Int) = {
print(int)
}
>>> listOf(1, 2, 3).forEach { foo(it) } // 对一个整数列表的元素遍历调用 foo,猜猜结果是什么
  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 表达式的机理。

  1. 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

  1. 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

Kotlin核心编程(28):基础语法 2.3.6

购书地址 https://item.jd.com/12519581.html?dist=jd

评论

发布