Table of Contents
什么是 Lambda?
Lambda 简答来说就是一小段代码块,并且我们可以将这个代码块在函数之间传递,这是函数式编程的一个重要特性。通常我们会需要一个函数,但是又不想定义一个函数那么费事,这个时候就可以使用 lambda 表达式来完成工作。这就是 lambda 函数,概念清晰简单。
Java 8 中非常重要的一个特性就是引入了 lambda,这受到广大工程师的热烈欢迎。为什么 Lambda 如此受欢迎,下面我们来一一解释下。
Lambda 使用
让我们先看一个例子,在 java 的开发中,通常会见到下面的代码:
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
// 点击后执行的动作
}
});
对于这样的代码,我们早已司空见惯,习以为常,觉得这样的代码没啥毛病。但是,如果你看到 Lambda 版本的代码的话,可能就不这么想了:
button.setOnClickListener {
// 点击之后的操作
}
看到没,是不是清爽很多了?下面我们就来看下在 kotlin 中的 lambda 的语法:
{ x: Int, y: Int -> x + y }
上面的形式就是 kotlin 中定义个 lambda 的形式,有如下要点:
- lambda 始终被花括号包围
- 实参不用括号包围
- 使用箭头将参数列表和代码块分隔开
对于如上定义的 lambda,我们在 kotlin 中可以如下方式来使用:
val sum = { x: Int, y: Int -> x + y }
我们可以定义个变量来保存 lambda,然后就像调用函数那样调用 lambda。另外你也可以直接调用 lambda:
println({ x: Int, y: Int -> x + y }(1, 2))
不要怀疑,确实可以这样使用,lambda 后面的括号在 kotlin 中被称为 invoke 机制。
接下来,我们看一个例子来学习一下 lambda 的更多用法。
假设我们需要重一个人员列表中找到年龄最大的那一位,在 kotlin 中使用 lambda 可以如下实现:
val personList = listOf(
Person("Jack", 18),
Person("Tom", 19),
Person("Jim", 24))
val oldestPerson = personList.maxBy({ p: Person -> p.age })
这里我们借用 kotlin 系统提供的集合扩展函数 maxBy 来实现查找,通过查看 maxBy 函数的定义我们知道,我们需要给它传递一个 lambda 即可,因此上面我们给他传递一个 lambda,这个 lambda 的类型是:
(Person) -> Int
也即是将一个 Person 对象转换为 Int 类型,方便比较大小。因为我们是要找年龄最大的,因此我们只要将 Person 对象转为年龄值就行。
上面的调用虽然一目了然,但是确实有点啰嗦。首先过多的标点符号破坏了可读性,其次类型可以从上下文中推断出来,我们不用明显声明这是 Person 类型,最后这种情况下我们不需要给 lambda 分配一个参数名称,因为我们只有一个。
在kotlin 中有这样的约定,如果 lambda 表达式是函数的最后一个实参,他可以放到括号的外面,因此上面的调用简化如下:
val oldestPerson = personList.maxBy(){ p: Person -> p.age }
当 lambda 是唯一的实参时,连圆括号都可以省略:
val oldestPerson = personList.maxBy { p: Person -> p.age }
因为 lambda 表达式中的实参 Person 类型可以从 personList 中推断出来,因此我们还可以简化:
val oldestPerson = personList.maxBy { it.age }
这里使用 it 表达形式,it 是 person 对象在 lambda 中的迭代元素,可以简单理解为:it 就是 personList 中的每一个 person 对象的代表。
以上四种形式功能完成等同,但是最后一种可读性是最好的,是最具有 kotlin style 的。
但是如果你有两个以上的 lambda 参数需要传递,那么你只能把最后一个 lambda 放到括号外面。通常这种情况下,常规的语法形式是比较好的选择。
到目前位置,我们看到的 lambda 都是单语句的形式,但是 lambda 中可以有多个语句,这种情况下,最后一个表达式的值就是 lambda 的结果,比如:
val sum = { x: Int, y: Int ->
println("hello world.")
x + y
}
上面我们看到了 lambda 的基本用法,下面我们看下和 lambda 表达式息息相关的概念:从上下文中捕捉变量。
在作用域中访问变量
我们知道,lambda 的主要使用场景是在函数之间传递。假如我们将 lambda 作为参数传递给一个函数,然后我们在 lambda 中需要访问函数中定义的局部变量,例如参数,怎么办呢?在 kotlin 中,没关系,你可以直接访问。
为了说明,我们使用 kotlin 库函数中最常使用的 forEach 来展示这种行为,forEach 是最基本的集合操作函数之一,它需要一个 lambda 表达式,然后针对集合中的每一个元素都执行该表达式。下面是使用例子:
data class Person(val name: String, val age: Int)
fun main() {
val personList = listOf(
Person("Jack", 18),
Person("Tom", 19),
Person("Jim", 24))
printNames(personList, "Name")
}
fun printNames(personList: Collection<Person>, prefix: String) {
personList.forEach {
println("$prefix: ${it.name}")
}
}
在这个例子中,我们可以看到 kotlin 和 java 的一个显著区别就是,在 kotlin 中不会仅限于访问 final 类型的变量,我们在 lambda 中可以自由地访问参数变量。同时 lambda 不仅可以访问,还可以修改函数的局部变量值:
fun printNames(personList: Collection<Person>, prefix: String) {
var count = 0
personList.forEach {
count++
println("$prefix: ${it.name} at $count")
}
}
这里定义的非 final 变量 count,我们可以在 lambda 中自由访问,并且修改值。
从 lambda 中访问外部变量,在 kotlin 中称为 lambda 捕捉。在上面的例子中,forEach 中的 lambda 捕捉了 prefix 和 count 两个局部变量。
在默认情况下,函数局部变量的声明周期是限制在这个函数的内部的,一旦函数运行结束,这个变量在内存中也就是消失了。但是如果它被 lambda 捕捉了,使用这个变量的代码可以被存储并稍后在执行。这背后的原理很简单,就是被捕捉的值和 lambda 代码一起被存储了起来。对于 final 变量(val 类型)来说,他的值和使用的 lambda 一起存储起来;而对于非 final(var 类型)的变量来说,他的值被封装在一个特殊的包装器中,这样你就可以改变这个值,而这个包装器对象的引用就是一个 final 变量,它会和 lambda 一起被存储。
我们知道 kotlin 是可以运行在 jvm 上面的,因此在 jvm 这个层面上没有任何魔法。既然在 java 中不能在函数的匿名内部类对象访问非 final 类型的变量,那么 kotlin 是如何做到访问非 final 类型的变量呢?答案就在变量的包装类中,这个包装类的实现差不多如下:
class Ref<T> (var value: T)
使用这样的包装类将数据包装起来,然后将这个包装类的对象引用变为 final 类型的,这个时候我们就可以在兼容 jvm 的同时又可以访问非 final 类型的变量了。
需要注意的是,如果 lambda 被用作事件处理器或者用在其他异步执行的情况,对布局的修改只会在 lambda 执行的时候发生,因此我们不可以使用如下的代码来统计按钮被点击的次数:
fun countClick(button: Button): Int {
var count = 0
button.onClick { count++ }
return count
}
因为这里的 count 的累加操作是按钮被点击的时候才会触发的,所以这个函数每次调用都会返回 0。
成员引用
我们已经看到 lambda 是如何让你把代码块作为参数传递给函数,但是我们想要用作参数传递的代码已经被定义成了函数,这个时候我们怎么办呢?当然,我们可以传递一个调用这个函数的 lambda,但是这样显得有点蠢。在 kotlin 中提供了类似 C 中的解决方案,我们可以类似 C 中那样,使用一个变量来保存函数名称,然后访问这个变量就可以来访问这个函数了。例如:</