``引入lambda是Java 8这门语言演化历程中期待已经的改变。为什么这是一件大事?在这一章节,你将会发现为什么lambda会如此有用,以及lambda表达式的语法在Kotlin中长什么样。
5.1.1 Lambda介绍: 作为方法参数的代码块
在你的代码中,传递并存储行为块是一个高频的任务。举个例子,你经常需要表达像这样的想法:当某个事件发生时,运行这个句柄。或者是,对某个数据结构中的所有元素应用某个操作。在旧版的Java中,你可以通过匿名内部类来实现。这个技术凑效,但是需要冗余的语法。
函数式编程为你解决这个问题提供了一个方法:把函数当做一个值的能力。你可以直接传递一个函数,而不是声明一个类然后把这个类的实例传递给方法。你不需要声明一个函数:相反的,你可以直接有效的传递一个代码块作为一个方法参数。
让我们来看一个例子。想象一下你需要定义一个点击一个按钮的行为。你添加了一个侦听器来负责处理点击。侦听器实现了对应的OnClickListener
接口,以及它的onClick
方法:
/* Java */
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
/* actions on click */
}
});
这些冗长的信息要求你声明一个匿名内部类,当它多次重复的时候会让人感到气愤。你想在这表达的只是在点击时应该发生的行为。在Kotlin中,和在Java 8一样,你可以为此使用lambda:
button.setOnClickListener { /* actions on click */ }
这份Kotlin代码跟Java中的匿名内部类做同样的事情。但是它更加的精简、可读。我们将会在这一节的后续部分讨论这个例子的细节。 你已经看到了lambda可以如何只用一个方法来用作匿名对象的一个替代方案。我们现在继续lambda表达式的另一个典型的用法:跟集合一起使用。
5.1.2 Lambda与集合
一个好的编程风格的一个主要原则是在你的代码中避免重复。我们使用集合执行的大部分任务都遵循某些通用的模式。所以实现它们的代码应该放入一个库中。但是,没有lambda,为集合提供一个好用、便捷的库就会变得很困难。因此,如果你用Java(Java 8以前的)编写你的代码,你很可能有一个自己实现所有东西的习惯。使用Kotlin,这个习惯必须改掉!
让我们来看一个例子。你将会使用一个包含有关一个人的姓名和年龄信息的Person
类:
data class Person(val name: String, val age: Int)
假定你有一列人,你需要找出其中年纪最大的一个。如果你没有lambda编程经验,你可能会急着去手动实现搜索。你已经引入了两个立即变量--一个保存最大年龄,另一个存储这个年龄对应的人。然后遍历这个列表,更新这两个变量:
fun findTheOldest(people: List<Person>) {
var maxAge = 0 // 存储最大年龄
var theOldest: Person? = null // 存储最大年龄的人
for (person in people) {
if (person.age > maxAge) { // 如果下一个人比当前最年长的人年纪更大,修改最大值
maxAge = person.age
theOldest = person
}
}
println(theOldest)
}
>>> val people = listOf(Person("Alice", 29), Person("Bob", 31))
>>> findTheOldest(people)
Person(name=Bob, age=31)
如果有足够的经验,你可以相当快速的实现这样的循环。但是这里的代码有点多。而且也很容易出错。比如,你可能在比较时出错,最后找到的是最小值而不是最大值。 Kotlin有更好的方法。你可以使用一个函数库:
>>> val people = listOf(Person("Alice", 29), Person("Bob", 31))
>>> println(people.maxBy { it.age }) // 通过比较年龄找出最大值
Person(name=Bob, age=31)
maxBy
函数可以在任何集合中被调用。而它只需要一个参数:指定什么样的值应该进行比较来找到最大值元素的函数。大括号中的代码{ it.age }
是一个实现这样的逻辑的lambda。它接受一个集合元素作为一个参数(使用it
引用)。同时返回一个值进行比较。在这个例子中,集合元素是一个Person
对象。比较的值是它存储在age
属性的年龄。
如果lambda只是委托一个方法或者属性,它可以被一个成员引用所取代:
people.maxBy(Person::age)
这段代码表达的东西跟前面的例子相同。 我们通常用Java(Java 8之前的版本)集合所做的大部分事情可以使用接受lambda或者成员引用的库函数更好的表达。 最后的代码更加简短、更容易理解。为了帮助你开始使用它,让我们来看看lambda表达式的语法。
5.1.3 Lambda表达式语法
正如我们已经提及的那样,lambda编码了一小块你可以用作一个值进行传递的行为。它可以被独立的声明并存储在一个变量中。但是更常见的是,当它被传递给一个函数时直接声明。图5.1展示了声明lambda表达式的语法。
图 5.1 Lambda表达式语法
在Kotlin中,一个lambda表达式往往被一个大括号所包围。注意,参数周围没有圆括号。箭头将参数列表从lambda主体中分隔开来。 你可以在一个变量中保存一个lambda表达式。然后把这个变量当做一个正常的函数(使用对应的参数调用它):
>>> val sum = { x: Int, y: Int -> x + y }
>>> println(sum(1, 2)) // 调用存储在一个变量中的lambda
3
如果你想的话,你可以直接调用lambda表达式:
>>> { println(42) }()
42
但是这样的语法并不可读,而且没有多大意义(它等价于立即执行lambda主体)。如果你需要在一个块中包含一块代码,你可以使用执行传递给它的lambda的库函数run
:
>>> run { println(42) } // 运行Lambda中的代码
42
在8.2节,你将会了解为什么这样的调用没有运行时负担,而且跟语言内建的构造有着同样的效率。让我们回到之前查找列表中最年长的人的例子:
>>> val people = listOf(Person("Alice", 29), Person("Bob", 31))
>>> println(people.maxBy { it.age })
Person(name=Bob, age=31)
如果你重写了这个例子,而没有使用任何的语法糖,你将会得到下面的代码:
people.maxBy({ p: Person -> p.age })
这里边发生了什么应该很明显了:大括号里面的代码是lambda表达式。你把它当做一个参数传递给函数。lambda表达式接受一个Person
类型的参数并返回它的年龄。
但是这份代码是繁琐的。首先,这里有太多的标点,影响了可读性。第二,类型可以从上下文推断出来,一次可以省略掉。最后,在这种情况下,你不需要为lambda参数分配一个名称。
我们来做些提升吧。首先从大括号开始。在Kotlin中,有一个语法约定允许你将lambda表达式移出圆括号外部,如果它是函数调用的最后一个参数。在这个例子中,lambda是唯一的参数,所以它可以放在圆括号外部:
people.maxBy() { p: Person -> p.age }
当lambda是函数的唯一参数时,你也可以删掉空的圆括号:
people.maxBy { p: Person -> p.age }
这三种语法形式都表达同样的意思。但是最后一个是最容易阅读的。如果lambda是唯一的参数,你绝对想写成没有圆括号的。当你有多个参数时,你可以通过把它放在圆括号内部来强调lambda是一个参数,或者你把它放在括号外面--两个选项都是有效的。如果你传递两个或更多的lambda,你不能把两个以上的lambda移到圆括号外部。所以通常最好使用常规的语法来传递它们。
为了看到看上去带有复杂调用的选项,让我们回到你在第三章大面积使用的joinToString
函数。它也定义在了Kotlin标准库中。不同的是标准库版本接受函数作为一个额外的参数。这个函数可以用来将一个函数转换为一个字符串,但是跟toString()
函数不一样。下面是你可以如何使用它来只打印名字:
>>> val people = listOf(Person("Alice", 29), Person("Bob", 31))
>>> val names = people.joinToString(separator = " ",
... transform = { p: Person -> p.name })
>>> println(names)
Alice Bob
下面是你可以如何使用圆括号外部的lambda来重写这个调用:
people.joinToString(" ") { p: Person -> p.name }
第二个变体更加精简地做了同样的事情。但是它没有显式的表达lambda用来做什么。所以对被调用的函数不熟悉的人来说,更加难以理解。
TIP IntelliJ IDEA 提示 把一种语法形式转换成其他形式,你可以使用这个动作:“把lambda表达式移到圆括号外面”
我们继续简化成最短的语法,并除去类型参数:
people.maxBy { p: Person -> p.age } // 显式的写出参数类型
people.maxBy { p -> p.age } // 参数类型通过推断得出
由于带有本地变量,如果lambda参数的类型可以推断出来,你不需要显式的指定。使用maxBy
函数,参数类型总是跟集合元素类型相同。编译器知道你在一个Person
对象集合调用maxBy
。所以它可以理解,lambda参数也会是Person
类型。
有一些情况下,编译器无法推断lambda参数类型。但是我们不会讨论这些情况。你可以遵循一个简单的规则:总是先 不指定类型,如果编译器报错,你就指定类型。
你可以仅仅指定其中的某些参数的类型,其他参数只写名字。如果编译器无法推断出某个类型,或者如果显式的类型可以提升可读性,这样做可能会方便一点。
在这个案例中,你可以达到的最简形式是使用参数默认名:it,来替代一个参数。如果上下文预期衣蛾只有一个参数的lambda,同时它的类型可以推断出来,(编译器)就会生成这个名字:
people.maxBy { it.age } // “it”是一个自动生成的参数名
只有在你没有显式的指定参数名时,默认名称才会生成。
NOTE 注意
it
约定对于缩短你的代码是非常有用的。但是你也不应该滥用它。特别是,嵌套lambda的情况下,最好是显式的说明每个lambda的参数。否则,难以理解it
指的是哪一个值。如果参数的类型或者含义在上下文中不清晰的话,显式的声明参数也是很有用的。
如果你把lambda保存在一个变量中,那就没有上下文来推断参数类型。所以你必须显式的指定它们:
>>> val getAge = { p: Person -> p.age }
>>> people.maxBy(getAge)
目前为止,你只看到由一个表达式或语句组成的lambda案例。但是lambda没有局限在这样的一个小规模,而且它也可以包含多个语句。在这个例子中,结果是最后的一个表达式:
>>> val sum = { x: Int, y: Int ->
... println("Computing the sum of $x and $y")
... x + y
... }
>>> println(sum(1, 2))
Computing the sum of 1 and 2...
3
接下来,我们讨论一个经常跟lambda表达式同时出现的概念:从上下文中捕获变量。
5.1.4 在作用域内访问变量
你知道,当你在一个方法中声明一个匿名内部类时,你可以在(匿名内部)类的内部通过这个方法引用参数和本地变量。你可以使用lambda做同样的事。如果你在一个函数内使用lambda,你可以和声明在lambda之前的本地变量那样,访问那个函数的参数。
为了解释这一点,我们使用标准库函数forEach
。它是最基本的集合操作函数之一。它所做的是对集合中的每一个元素调用给定的lambda。forEach
多多少少比常规的for
循环更简洁一些。但是它并没有许多其他的优势。所以你不需要急着把你所有的循环转换为lambda。
下面的例子接受一列消息并打印每一个加了前缀的消息:
fun printMessagesWithPrefix(messages: Collection<String>, prefix: String) {
messages.forEach { // 接受一个指定每个元素做什么的lambda作为参数
println("$prefix $it") // 在lambda中访问"prefix"参数
}
}
>>> val errors = listOf("403 Forbidden", "404 Not Found")
>>> printMessagesWithPrefix(errors, "Error:")
Error: 403 Forbidden
Error: 404 Not Found
Kotlin和Java的一个重要的不同点是:Kotlin没有严格限制你访问final
变量。你也可以从lambda总修改变量。下面的一个案例统计了客户端和服务器端在给定响应状态码中的错误个数:
fun printProblemCounts(responses: Collection<String>) {
var clientErrors = 0 // 1
var serverErrors = 0 // 1
responses.forEach {
if (it.startsWith("4")) {
clientErrors++ // 2
} else if (it.startsWith("5")) {
serverErrors++ // 2
}
}
println("$clientErrors client errors, $serverErrors server errors")
}
>>> val responses = listOf("200 OK", "418 I'm a teapot",
... "500 Internal Server Error")
>>> printProblemCounts(responses)
1 client errors, 1 server errors
// 1 声明通过lambda访问的变量 // 2 修改lambda中的变量
如你所见,跟Java不同,Kotlin允许你访问非不可更改的变量,甚至在lambda中修改它们。通过lambda来访问外部变量,比如案例中的prefix
、clientErrors
、serverErrors
,可以说被lambda捕获了。
注意,本地变量的生命周期默认是限制在声明这个变量的函数内部的。但是如果它被lambda捕获了,使用这个变量的代码可以被存储起来并在后期执行。你可能会问,这是怎么做到的。当你捕获到一个不可修改的变量时,它的值跟用到这个值的lambda代码一同保存。对于不可更改的变量来说,(它的)值依附在一个特殊的包装器内。这个包装器允许你修改它。包装器的引用跟lambda同时存储。
SIDEBAR 捕获一个可修改的变量:实现细节 Java只允许你捕获不可修改的变量。当你想要捕获可修改变量时,你可以使用以下技巧中的一个:声明一个存储可变值元素的数组,或者创建一个存储可改变引用的包装器类的实例。下面是Kotlin中的相似的代码:
class Ref<T>(var value: T) // 用来模拟捕捉一个可修改变量的类 >> val counter = Ref(0) >> val inc = { counter.value++ } // 形式上,一个不可变变量被捕获了。但是实际的值存储在一个字段中而且可以被修改。
在Kotlin中,你不需要创建这样的包装器。相反的,你可以直接的修改变量:
var counter = 0 val inc = { counter++ }
这是怎么实现的呢?第一个案例展示了第二个案例的底层工作原理。在任意时刻,你捕获了一个不可修改变量(val),它的值就被复制了,就像在Java中那样。当你捕获一个可修改变量(var)时,它的值被保存为一个
Ref
类的实例。Ref
变量是不可被修改的。它很容易被捕获。然而真实值保存在一个字段中,并且可以通过lambda进行修改。
一个重要的告诫:如果lambda用作一个事件句柄,或者异步执行,本地变量的修改只会在lambda被执行的时候发生。举个例子,下面的代码不是统计按钮点击次数的正确方式:
fun tryToCountButtonClicks(button: Button): Int {
var clicks = 0
button.onClick { clicks++ }
return clicks
}
这个函数总会返回0。尽管onClick
句柄会修改clicks
的值,但是你观察不到修改,因为onClick
句柄将会在函数返回之后被调用。这个函数的一个正确的实现,需要保存点击次数到非本地变量,而是在一个依然可以在函数外部进行访问的位置--例如,类的属性。
我们已经讨论了声明lambda的语法和变量如何在lambda中被捕获的。现在,我们讨论成员引用,一个允许你方便的传递引用给现有函数的特性。
5.1.5 成员引用
你已经看到了lambda是如何允许你向函数传递一块代码作为参数的。但是如果你需要作为参数传递的代码是已经定义了的函数呢?当然,你可以传递一个调用那个函数的lambda。但是这样做有点多此一举。你可以直接传递函数吗?
跟在Java 8一样,如果你把那个函数转换为一个值的话,你可以在Kotlin中这样做。你可以为此使用::
:
val getAge = Person::age
这个表达式叫做成员引用。它为创建一个直接调用方法或访问属性的函数值提供了一种简短的语法。双冒号将类名从你需要引用的成员(方法或属性)名中分隔出来。如图5.2所示:
这个更为精简的lambda表达式做了同样的事情:
val getAge = { person: Person -> person.age }
要注意,不管你引用函数还是属性,你都不应该在函数引用中,为被引用的函数或方法在它的名字后面放一对圆括号。 成员引用拥有和调用那个函数的lambda同样的类型。所以你可以互换的使用这两者:
people.maxBy(Person::age)
你也可以有一个声明在顶层的函数引用(并非类的成员):
fun salute() = println("Salute!")
>>> run(::salute) // 引用顶层函数
Salute!
在这个案例中,你可以忽略类名并以::
为开头。成员引用::salute
作为一个参数传递给库函数run
。它将会调用对应的函数。
提供一个成员引用非常方便,但是委托给一个接收多个参数的函数却是相反的:
val action = { person: Person, message: String -> // 1. 这个lambda委托为sendEmail函数
sendEmail(person, message)
}
val nextAction = ::sendEmail // 2. 相反,你可以使用一个成员引用
你可以使用构造函数引用来保存或者延迟创建类实例的动作。构造函数引用通过在双冒号后面指定类名来构成:
data class Person(val name: String, val age: Int)
>>> val createPerson = ::Person // 创建"Person"类实例的动作被保存为一个值
>>> val p = createPerson("Alice", 29)
>>> println(p)
Person("Alice", 29)
注意,你也可以用同样的方式来引用扩展函数:
fun Person.isAdult() = age >= 21
val predicate = Person::isAdult
尽管isAdult
并不是Person
类的成员,但是你可以通过引用来访问它,就像你可以访问实例中的成员那样:person.isAdult()
。
SIDEBAR 绑定引用 在Kotlin 1.0中,当你引用类的方法或属性时,在你调用这个引用时,你总是需要提供那个类的实例。Kotlin 1.1计划支持绑定方法引用。这将允许你使用方法引用语法来捕捉特定对象实例中的方法引用。
>> val p = Person("Dmitry", 34) >> val personsAgeFunction = Person::age >> println(personsAgeFunction(p)) 34 >> val dmitrysAgeFunction = p::age // 1 你可以在Kotlin 1.1 使用的绑定方法引用 >> println(dmitrysAgeFunction()) 34
注意,
personsAgeFunction
函数只接收一个参数(它返回一个给定的人的年龄),然而,dmitrysAgeFunction
函数没有参数(它返回指定的人的年龄)。在Kotlin 1.1之前的版本,你需要显式的写上{ p.age }
,而不是使用绑定成员引用p::age
。
在接下来的章节中,我们会看到很多库函数。它们能够跟lambda表达式和成员引用很好的工作。