lambdas编程

主要内容

  1. lambda表达式和成员引用
  2. 函数式风格的集合使用
  3. Sequences: 集合的惰性处理
  4. Kotlin中使用Java函数接口
  5. 带有receiver的lambda函数的写法

Lambda expressions and member references

Introduction to lambdas: blocks of code as function parameters

Syntax for lambda expressions

截止目前为止,所有编程语言的lambda的表达式语法是通用和类似的,

1
2
3
>>> val sum = { x: Int, y: Int -> x + y }
>>> println(sum(1, 2))
3
1
2
3
>>> val people = listOf(Person("Alice", 29), Person("Bob", 31))
>>> println(people.maxBy { it.age })
Person(name=Bob, age=31)

上面用了内建的结构体,下面是一般的lambda语法:

1
people.maxBy({ p: Person -> p.age })

或者写成:

1
people.maxBy() { p: Person -> p.age }

或者写成:

1
people.maxBy { p: Person -> p.age }

简化的类型推断:

1
people.maxBy { p -> p.age }

如果将lambda存储为一个变量,在没有上下文环境的情况下,是无法推断的。必须显式指定:

1
2
>>> val getAge = {p: Person -> p.age }
>>> people.maxBy(getAge)

Accessing variables in scope

对于所有函数式编程而言,基本上,能够修改外部开放项的lambda表达式就是闭包(也叫方法,或者带有副作用的函数);否则就是纯函数。

1
2
3
4
5
fun tryToCountButtonClicks(button: Button): Int {
var clicks = 0
button.onClick { clicks++ }
return clicks
}

Member references

Kotlin的成员引用( member reference )和Java8的 ::一样。

1
val getAge = Person::age

对于top-level的函数可以省略文件名(top-level 成员的类名就是文件名),譬如:

1
2
3
fun salute() = println("Salute!")
>>> run(::salute)
Salute!

成员引用的写法比lambda的写法更加便捷,因为它不需要传递参数:

1
2
val action = {person: Person, message: String -> sendEmail(person, message)}  // This lambda delegates to a sendEmail function.
val nextAction = ::sendEmail // You can use a member reference instead.

你可以推迟这种访问用作构造器引用:

1
2
3
4
5
6
data class Person(val name: String, val age: Int)

>>> val createPerson = ::Person // An action of creating an instance of "Person" is saved as a value.
>>> val p = createPerson("Alice", 29)
>>> println(p)
Person(name=Alice, age=29)

类似地,也可以用于扩展函数:

1
2
fun Person.isAdult() = age >= 21
val predicate = Person::isAdult

Functional APIs for collections

现代语言集合的所有API基本上都是通用的,因为数据结构不会相差太大。

Essentials: filter and map

filtermap 作为集合常见的函数api。

1
data class Person(val name: String, val age: Int)

filter函数过滤偶数:

1
2
3
>>> val list = listOf(1, 2, 3, 4)
>>> println(list.filter { it % 2 == 0 }) // Only even numbers remain.
[2, 4]

过滤超过30岁的人,

1
2
3
>>> val people = listOf(Person("Alice", 29), Person("Bob", 31))
>>> println(people.filter { it.age > 30 })
[Person(name=Bob, age=31)]

map的作用相当于transformer,譬如:

1
2
3
>>> val list = listOf(1, 2, 3, 4)
>>> println(list.map { it * it })
[1, 4, 9, 16]

转换类型

1
2
3
>>> people = listOf(Person("Alice", 29), Person("Bob", 31))
>>> println(people.map { it.name })
[Alice, Bob]

也可以替换为成员引用的写法更加简洁:

1
people.map(Person::name)

可以链式调用,

1
2
>>> people.filter { it.age > 30 }.map(Person::name)
[Bob]

或者组合lambda和成员引用,

1
people.filter { it.age == people.maxBy[Person::age).age }

改进写法,仅计算一次,

1
2
val maxAge = people.maxBy(Person::age).age
people.filter { it.age == maxAge }

“All”, “Any”, “count”, and “find”: applying a predicate to a collection

实现上和Java 8 的predicate无差别,

1
2
3
4
>>> val canBeInClub27 = { p: Person -> p.age <= 27 }
>>> val people = listOf(Person("Alice", 27), Person("Bob", 31))
>>> println(people.all(canBeInClub27))
false

如果仅检测至少一个匹配,使用any

1
2
>>> println(people.any(canBeInClub27))
true

!all的逆命题就是any,所以下面的写法是等价的,

1
2
3
4
5
>>> val list = listOf(1, 2, 3)
>>> println(!list.all { it == 3 }) // The negation ! isn't noticeable, so it's better to use "any" in this case.
true
>>> println(list.any { it != 3 }) // The condition in the argument has changed to its opposite.
true

count统计满足的predicate,

1
2
3
>>> val people = listOf(Person("Alice", 27), Person("Bob", 31))
>>> println(people.count(canBeInClub27))
1

GroupBy: converting a list to a map of groups

groupBy在统计计算的场景被经常用到,顾名思义“分组”,

按年龄分组,

1
2
>>> val people = listOf(Person("Alice", 31), Person("Bob", 29), Person("Carol", 31))
>>> println(people.groupBy { it.age })

和Java一样,分组后放在一个Map<Int, List<Person>>里面。

flatMap and flattern: processing elements in nested collections

flatMap的步骤分为两步,即flatten+map。首先将所有元素进行转换(map),然后在组合(flattern)为一个。它属于集合内嵌操作。

1
data class Book(val title: String, val authors: List<String>)

计算集合,

1
books.flatMap { it.authors }.toSet()  // Set of all authors who wrote books in the "books" collection

Lazy collectino operations: sequences

类似于mapfilter这一类函数执行后立即计算的称为eagerly。相对于的类似于sequences提供了惰性求值的可能。

1
people.asSequence().map(Person::name).filter { it.startsWith("A") }.toList()

Sequence在Kotlin 中作为接口,提供了丰富的api和语法糖。

Executing sequence operations: intermediate and terminal operations

操作一个sequence被分为两个步骤:

  • intermediate operation,返回另外一个sequence实例,
  • terminal operation,返回真实的结构。

Figure 5.7

intermediate operation总是lazy的,譬如,

1
>>> listOf(1, 2, 3, 4).asSequence().map { print("map{$it) "); it * it }.filter { print("filter($it) "); it % 2 == 0 }

执行该代码片段不会在控制台打印任何结果。意味着mapfilter被延迟执行(仅当terminal operation被调用才执行):

1
2
>>> listOf(1, 2, 3, 4).asSequence().map { print("map{$it) "); it * it }.filter { print("filter($it) "); it % 2 == 0 }.toList()
map(1) filter(1) map(2) filter(4) map(3) filter(9) map(4) filter(16)

eager(collection) 和 lazy(sequences) 的区别在于,某些计算在sequence不会被执行,如下图:

Figure 5.8

在第一种case,集合中的list会被转换为另外一个list,因此map会作用于每一个元素,包括3和4。之后,在计算得到4。

在第二种case,find的对原始元素一个接一个地处理。对于原来的集合元素,会执行map转换,再执行find操作。所以,当达到2时,满足条件,返回结果,不必关系后面的3、4、5…。

这种对元素的处理顺序操作对于性能是有明显影响的。譬如一下代码,

1
2
3
4
5
6
7
>>> val people = listOf(Person("Alice", 29), Person("Bob", 31), Person("Charles", 31), Person("Dan", 21))
>>> println(people.asSequence().map(Person::name).filter { it.length < 4 }.toList()) // "map" goes first, then "filter".

[Bob, Dan]
>>> println(people.asSequence().filter { it.name.length < 4 }.map(Person::name).toList() // "map" goes after "filter".

[Bob, Dan]

Figure 5.9

如果map先行,每个元素都被转换(transform)了。如果filter先行,不恰当的元素被尽可能地filter掉以减少转换。

Creating sequences

处理在集合上使用asSequence()创建一个sequence,还可以通过generateSequence()函数来创建。例如,

1
2
3
4
>>> val naturalNumbers = generateSequence(0) { it + 1 }
>>> val numbersTo100 = naturalNumbers.takeWhile { it <= 100 }
>>> println(numbersTo100.sum()) // All the delayed operations are performed when the result "sum" is obtained.
5050

Using Java functional interfaces

Kotlin调用Java的接口函数比较简单,假设有Java接口如下,

1
2
3
public class Button {
public void setOnClickListner(OnClickListener l) {... }
}

functional interface声明如下,

1
2
3
public interface OnClickListener {
void onClick(View v);
}

在kotlin中直接使用lambda表达式,

1
button.setOnClickListener { view -> ... }

Figure 5.10

之所以生效是因为,接口OnClickListner仅包含一个抽象方法。这种接口被称为 functional interfaces 或 SAM interfaces ,SAM就是 single abstract method 的缩写。Java的标准API包含大量的这类接口,譬如常见的RunnableCallable

Passing a lambda as a parameter to a Java method

对于Java中包含有functional interface的方法参数可以直接传递lambda表达式。

1
void postponeComputation(int delay, Runnable computation);

kotlin中的编译器会自动根据表达式进行转换,

1
postponeComputation(1_000) { println(42) }

它等效于下面的写法,

1
2
3
4
5
postponeComputation(1_000, object : Runnable {  // Passes an object expression as an implementation of a functional interface
override fun run() {
println(42)
}
}

不同的是,如果现实声明一个对象表达式,每次调用都会创建一个新的实例。如果使用lambda,并且不是闭包的情况下,对应的匿名实例会被重用:

1
postponeComputation(1_000) { println(42) }  // One instance of Runnable is created for the entire program.

因此,等价的写法应该写为,

1
2
3
4
val runnable = Runnable { println(42) }  // Compiled to a global variable; only one instance in the program
fun handleComputation() {
postponeComputation(1_000, runnable) // One object is used for every handleComputation call.
}

注意,如果将lambda表达式传递给一个带有inline标记的函数,则不会有匿名类的创建。

SAM constructors: explicit conversion of lambdas to functional interfaces

SAM constructors其实就是functional interface!!!

1
2
3
4
5
6
fun createAllDoneRunnable(): Runnable {
return Runnable { println("All done!") }
}

>>> createAllDoneRunnable().run()
All done!

炒概念,不作介绍!!

Lambdas with receivers: “with” and “apply”

kotlin 的lambda表达式不同于Java,可以带receivers。

The “with” function

with为inline函数,后面跟着的receiver相当于自由变量,

1
2
3
4
5
6
7
8
9
10
11
fun alphabet(): String {
val result = StringBuilder()
for (letter in 'A'..'Z') {
result.append(letter)
}
result.append("\nNow I know the alphebet!")
return result.toString()
}
>>> println(alphabet())
ABCDEFGHIJKLMNOPQRSTUVWXYZ
Now I know the alphabet!

这个例子中,result被重复调用了很多次。可以用with重写为,

1
2
3
4
5
6
7
8
9
10
fun alphabet(): String {
val stringBuilder = StringBuilder()
return with(stringBuilder) { // Specifies the receiver value on which you're calling the methods
for (letter in 'A'..'Z') {
this.append(letter) // Calls a method on the receiver value though an explicit "this"
}
append("\nNow I know the alphabet!") // Calls a method ommiting "this"
this.toString() // Returns a value from the lambda
}
}

with的底层实际为inline函数,接收两个参数:stringBuilder、和一个lambda。逻辑上没有任何区别,仅仅为了提高可读性。甚至可以更加简洁,

1
2
3
4
5
6
7
fun alphabet() = with(StringBuilder()) {
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I know the alphabet!")
toString()
}

The “apply” function

apply函数和with的用法类似;不同在于,apply总是返回传递进来的参数对象。我们使用apply重构上述代码,

1
2
3
4
5
6
fun alphabet() = StringBuilder().apply (
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I know the alphabet!")
}.toString()

apply最常用到的地方就是创建一个实例,并初始化某些属性。譬如,

1
2
3
4
5
6
fun createViewWithCustomAttributes(context: Context) =
TextView(context).apply {
text = "Sample Text"
textSize = 20.0
setPadding(10, 0, 0, 0)
}

apply传入的lambda表达式被执行后将返回初始化后的实例。另外,kotlin提供了更简洁的inline函数,

1
2
3
4
5
6
fun alphabet(): String = buildString {
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I know the alphabet!")
}

Summary

  • lambda可以将代码块作为参数传入函数。
  • kotlin可以传递无括号的lambda,并用it作为引用。
  • 作为闭包时,lambda表达式可以访问和修改外部变量。
  • member reference语法使用::表示,可以引用方法、构造函数、属性。
  • 集合提供了丰富的functional api,譬如filtermapallany等。
  • Sequences 表示集合的操作作为中间操作部分,在真正调用的地方按顺序执行。
  • functional interface和SAM interface作为参数的用法。
  • lambda带有receivers的用法写在了kotlin的标准inline function中使用。
  • with标准库函数允许方法可以多次重复调用同一个实例;apply可以让你创建实例的同时并初始化成员属性。