lambda表达式,或简称lambda,本质上就是可以传递给其他函数的一小段代码。我们常常需要表达这样一种想法:“当某件事发生的时候做出处理”,在老版本的Java中,我们可以用匿名内部类实现,这种语法可以工作,但是有点啰嗦,而lambda提供了一种更加优雅与简洁的实现。
Lambda语法
如前所述,一个lambda把一小段行为进行编码,你能把它当作值到处传递。它可以被独立的声明并存储到一个变量中,但更常见的还是直接声明它并传递给函数。
下图展示了声明lambda表达式的语法:
Kotlin的lambda表达式始终用或括号包围(注意实参并没有用括号括起来),箭头把实参列表和 lambda 的函数体隔开。
可以把 lambda 表达式存储在一个变量中,把这个变量当成普通函数对待:
val sum = { x: Int, y: Int -> x + y }
println(sum(1,3)) //输出: 4
接下来看看直接声明,这个代码是寻找列表中年龄最大的人:
data class Person(val name: String, val age: Int)
val people = listOf(Person("xiaoming",18),
Person("xiaogang",20))
println(people.maxBy { it.age })
//输出: Person(name=xiaogang, age=20)
如果不使用任何简明语法,它的完整形态是这样的:
people. maxBy ({ p: Person -> p.age })
这段代码的意思就很明显了——接受一个Person实参并返回它的年龄。
让我们慢慢改进。Kotlin中有一个语法约定。如果lambda表达式是函数调用的最后一个实参,它可以放在括号外面,这个例子中lambda就是唯一的实参,所以可以放到括号的外面:
people. maxBy(){ p: Person -> p.age }
当lambda是函数唯一的实参时,还可以省略调用代码中的空括号:
people.maxBy { p: Person -> p.age }
和局部变量一样,如果lambda参数的类型可以被推导出来,那么你就不需要显示的指定它,因为maxby函数的参数类型始终与集合的元素类型相同。所以可以省略参数类型:
people.maxBy { p -> p.age }
最后,如果当前上下文期望的是只有一个参数的lambda且这个参数的类型可以推断出来,就会生成一个默认参数名称 it 代替命名参数。于是这个lambda就变成了我们开始使用时的样子:
people.maxBy { it.age }
仅在实参名称没有显示的指定时,这个默认的名称才会生成。如果你用变量存储lambda,那么就没有可以推断出参数类型的上下文,所以你必须显式指定参数类型。
访问作用域变量
如果在函数内使用lambda,可以访问这个函数的参数,还有在lambda之前定义的局部变量。Kotlin和Java的一个显著区别就是,在Kotlin中不会仅限于访问final变量,在lambda中也可以修改这些变量。
var sum = 0
val list = listOf(1,2,3,4,5)
list.forEach { sum+=it }
println(sum) //输出: 15
当从lambda中访问外部变量时,我们称这些变量被 lambda 捕捉。默认情况下,局部变量的生命期被限制在声明这个变量的函数中。但是如果它被lambda捕捉了,那么使用这个变量的代码会被储存并稍后执行。对于非final变量来说,这时它的值被封装在一个特殊的包装器中,对这个包装器的引用会和lambda代码一起储存。
注意:如果lambda被用作异步执行时,对局部变量的修改只会在lambda执行的时候才会发生:
fun tryToCountButtonClicks(button: Button): Int {
var clicks = 0
button.onClick { clicks++ }
return clicks
}
此时返回的clicks始终为0。想要得到正确的值,应该把它声明成类属性之类的函数外也能访问到的地方。
成员引用
Kotlin和Java 8一样,如果把函数装换成一个值,你就可以传递它。你可以通过::运算符来完成转换。
val getAge = Person::age
这种表达式被称为成员引用。它创建了一个调用单个方法或这访问单个属性的函数值。双冒号把类名称和你要引用的成员(一个方法或一个属性)名称隔开。
上面这个语句等价于:
val getAge = { person: Person -> person.age }
成员引用和调用该函数的lambda具有相同的类型,可以互相换用。
如果是引用顶部函数的话,就直接以::开头:
fun salute() = println ("Salute !")
run (::salute) //输出:Salute!
声明高阶函数
以另一个函数作为参数或者返回值的函数,被称为高阶函数,在Kotlin中,函数可以用lambda或者函数引用来表示。因此,使用了任何lambda式或者函数引用作为参数或返回值的函数都是高阶函数。
为了声明一个以lambda作为实参的函数,我们需要知道如何声明对应形参的类型。前面我们已经声明过一个sum函数:
val sum = { x: Int, y: Int -> x + y }
因为Kotlin的类型推导,这个函数省略了声明类型,现在让我们看看它的完整声明:
val sum: (Int,Int) -> Int = { x, y -> x + y }
声明函数类型,需要将函数参数类型放到括号中,紧接着是一个箭头和函数的返回类型。这里需要注意一下,在声明一个普通的函数时,Unit类型的返回值可以省略,但一个函数类型声明总是需要一个显式地返回类型,这时候的 Unit 是不能省略的。同时参数的类型已经在声明中指定了,所以不需要再lambda中重复声明了。
就像其他函数一样,函数类型的返回值也可以为可空类型:
//如果 lambda 表达式的参数未使用,那么可以用下划线取代其名称
var canReturnNull: (Int , Int) -> Int? = { _ , _ -> null }
也可以定义一个函数类型的可空变量:
var funOrNull: ((Int, Int)-> Int)? = null
注意,为了明确表示是变量本身可空,而不是函数类型的返回值可空,我们需要把整个函数类型的定义包含在括号内,再在后面加上一个问号。
知道怎么声明一个函数类型之后,我们可以实现一个高阶函数了。下面使用原先的 lambda sum 来声明一个实现两个数字2、3的任意操作的高阶函数:
fun twoAndThree(operation: (Int,Int) -> Int) {
val result = operation(2, 3)
println ("The result is $result")
}
twoAndThree{ a, b -> a + b } //The result is 5
twoAndThree{ a, b -> a * b } //The result is 6
调用作为参数的函数和调用普通函数的语法是一样的:把括号放在函数名后,并把参数放在括号内。
函数类型一样可以添加默认参数,也可以作为返回值。语法和普通参数类型是一样的:
//添加默认参数
fun twoAndThree(operation: (Int,Int) -> Int = { a , b -> a + b } ) {
val result = operation(2, 3)
println ("The result is $result")
}
//返回一个函数类型
fun twoAndThree(operation: (Int,Int) -> Int): () -> Int {
val result = operation(2, 3)
return {result}
}
内联函数与lambda
lambda表达式会被编译成匿名类,这表示每调用一个lambda,就会有一个额外的类被创建。并且如果lambda捕捉了某个变量,那么每次调用的时候都会创建一个新的对象。这带来的运行时开销会导致每次使用lambda要比一个直接使用相同代码的函数效率更低。
为了生成同样效率的lambda,我们可以使用 inline 修饰符标记一个函数:
inline fun twoAndThree(operation: (Int,Int) -> Int) {
val result = operation(2, 3)
println ("The result is $result")
}
在内联函数被使用的时候编译器不会生成函数调用的代码,而是使用函数实现的真实代码替换每一次的函数调用。因为这种实现方式,如果在两个不同的位置使用不同的lambda调用这个内联函数,那么内联函数会被内联两次,函数代码被拷贝到这两个地方,并把不同的lambda分别替换其中。
这种运作方式使得不是所有lambda的函数都可以被内联:当函数被内联的时候,作为参数的 lambda 表达式的函数体会被直接替换到最终生成的代码中,所以如果有lambda参数在函数某个地方被用变量保存起来了,从而必须有一个包含这些代码的对象存在,那么这样ambda 表达式的代码将不能被内联,编译器会报错:
var aLambda:((Int,Int) -> Int)? = null
inline fun twoAndThree(operation: (Int,Int) -> Int) {
val result = operation(2, 3)
aLambda = operation //Illegal usage of inline-parameter
println ("The result is $result")
}
控制流
当你开始使用lambda去替换像循环这样的命令式代码结构时,很快便会遇到return 表达式的问题。把一个 return 语句放在循环的中间是很简单的事情。但是如果将循环转换成一个类似 filter 的函数呢?在这种情况下 return 会如何工作?我们来看一个例子 :
val people = listOf(Person("Alice", 29) , Person("Bob", 31))
//使用for循环
fun lookForAlice(people: List <Person>){
for (person in people) {
if (person.name == "Alice"){
println ("Found!")
return
}
}
println ("Alice is not found")
}
//使用lambda
fun lookForAlice(people: List<Person>){
people.forEach {
if (it.name =="Alice") {
println("Found!")
return
}
}
println("Alice is not found")
}
事实上,这两个函数的返回结果是一样的,这是因为如果你在lambda中使用return关键字,它会从调用lambda的函数中返回,并不只是从lambda中返回,这样的return语句被称为非局部返回。return出现这种行为的原因很简单:return从最近的使用 fun 关键字声明的函数返回。lambda表达式中没有使用fun关键字,所以lambda中的return从它外层的函数返回。所以如果你想使用return进行 局部返回,即终止lambda的运行,并接着从调用lambda的代码处执行,那么你可以使用 匿名函数,看下面的例子:
fun lookForAlice(people: List<Person>){
people.forEach (fun (person) {
if (person.name == "Alice")
return
println("${person.name} is not Alice")
})
}
lookForAlice(people)//输出:Bob is not Alice
匿名函数和普通函数很类似,只是省略了名字和参数类型。但事实上,它其实是lambda表达式的另一种语法形式,关于lambda表达式怎么实现,以及内联规则都同样使用于匿名函数。除了匿名函数之外,我们还可以通过标签在lambda表达式中实现局部返回;
fun lookForAlice(people: List<Person>){
people.forEach lable@{
if (it.name == "Alice") {
return@lable
}
}
println("Alice might be somewhere")
}
lookForAlice(people) //Alice might be somewhere
要标记一个 lambda 表达式,在 lambda 的花括号之前放一个标签名(可以是任何标识符),接着放一个 @ 符号。要从一个 lambda 返回,在 return 关键字后放一个@符号,接着放标签名。另外一种选择是,使用 lambda 作为参数的函数的函数名可以作为标签:
fun lookForAlice(people: List<Person>){
people.forEach{
if (it.name == "Alice") {
return@forEach
}
}
println("Alice might be somewhere")
}
lookForAlice(people) //Alice might be somewhere
如果你显式地指定了 lambda 表达式的标签,再使用函数名作为标签没有任何效果。一个 lambda 表达式的标签数量不能多于一个。
最后需要注意的是,只有在以 lambda 作为 参数的函数是内联函数的时候才能从更外层的函数返回。上面的代码中的forEach的函数体和lambda的函数体一起被内联了,所以在编译的时候可以这么做。而在一个非内联函数的 lambda 中使用 return表达式是不允许的,因为一个非内联函数可以把传给它的 lambda 保存在变量中,以便在函数返回以后可以继续使用,这个时候 lambda 想要去影响函数的返回己经太晚了。
总结
本文的主要内容包括lambda表达式的声明和使用,以及如何在高阶函数中声明一个lambda参数。这篇文章没有涉及到新的关键字。