¶主要内容
- lambda表达式和成员引用
- 函数式风格的集合使用
- Sequences: 集合的惰性处理
- Kotlin中使用Java函数接口
- 带有receiver的lambda函数的写法
¶Lambda expressions and member references
¶Introduction to lambdas: blocks of code as function parameters
¶Syntax for lambda expressions
截止目前为止,所有编程语言的lambda的表达式语法是通用和类似的,
1 | >>> val sum = { x: Int, y: Int -> x + y } |
1 | >>> val people = listOf(Person("Alice", 29), Person("Bob", 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 | >>> val getAge = {p: Person -> p.age } |
¶Accessing variables in scope
对于所有函数式编程而言,基本上,能够修改外部开放项的lambda表达式就是闭包(也叫方法,或者带有副作用的函数);否则就是纯函数。
1 | fun tryToCountButtonClicks(button: Button): Int { |
¶Member references
Kotlin的成员引用( member reference )和Java8的 ::
一样。
1 | val getAge = Person::age |
对于top-level的函数可以省略文件名(top-level 成员的类名就是文件名),譬如:
1 | fun salute() = println("Salute!") |
成员引用的写法比lambda的写法更加便捷,因为它不需要传递参数:
1 | val action = {person: Person, message: String -> sendEmail(person, message)} // This lambda delegates to a sendEmail function. |
你可以推迟这种访问用作构造器引用:
1 | data class Person(val name: String, val age: Int) |
类似地,也可以用于扩展函数:
1 | fun Person.isAdult() = age >= 21 |
¶Functional APIs for collections
现代语言集合的所有API基本上都是通用的,因为数据结构不会相差太大。
¶Essentials: filter and map
filter
和 map
作为集合常见的函数api。
1 | data class Person(val name: String, val age: Int) |
filter
函数过滤偶数:
1 | >>> val list = listOf(1, 2, 3, 4) |
过滤超过30岁的人,
1 | >>> val people = listOf(Person("Alice", 29), Person("Bob", 31)) |
map
的作用相当于transformer,譬如:
1 | >>> val list = listOf(1, 2, 3, 4) |
转换类型
1 | >>> people = listOf(Person("Alice", 29), Person("Bob", 31)) |
也可以替换为成员引用的写法更加简洁:
1 | people.map(Person::name) |
可以链式调用,
1 | >>> people.filter { it.age > 30 }.map(Person::name) |
或者组合lambda和成员引用,
1 | people.filter { it.age == people.maxBy[Person::age).age } |
改进写法,仅计算一次,
1 | val maxAge = people.maxBy(Person::age).age |
¶“All”, “Any”, “count”, and “find”: applying a predicate to a collection
实现上和Java 8 的predicate无差别,
1 | >>> val canBeInClub27 = { p: Person -> p.age <= 27 } |
如果仅检测至少一个匹配,使用any
,
1 | >>> println(people.any(canBeInClub27)) |
!all
的逆命题就是any
,所以下面的写法是等价的,
1 | >>> val list = listOf(1, 2, 3) |
count
统计满足的predicate,
1 | >>> val people = listOf(Person("Alice", 27), Person("Bob", 31)) |
¶GroupBy: converting a list to a map of groups
groupBy
在统计计算的场景被经常用到,顾名思义“分组”,
按年龄分组,
1 | >>> val people = listOf(Person("Alice", 31), Person("Bob", 29), Person("Carol", 31)) |
和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
类似于map
和filter
这一类函数执行后立即计算的称为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
,返回真实的结构。
intermediate operation总是lazy的,譬如,
1 | >>> listOf(1, 2, 3, 4).asSequence().map { print("map{$it) "); it * it }.filter { print("filter($it) "); it % 2 == 0 } |
执行该代码片段不会在控制台打印任何结果。意味着map
和filter
被延迟执行(仅当terminal operation被调用才执行):
1 | >>> listOf(1, 2, 3, 4).asSequence().map { print("map{$it) "); it * it }.filter { print("filter($it) "); it % 2 == 0 }.toList() |
eager(collection) 和 lazy(sequences) 的区别在于,某些计算在sequence不会被执行,如下图:
在第一种case,集合中的list会被转换为另外一个list,因此map
会作用于每一个元素,包括3和4。之后,在计算得到4。
在第二种case,find
的对原始元素一个接一个地处理。对于原来的集合元素,会执行map
转换,再执行find
操作。所以,当达到2时,满足条件,返回结果,不必关系后面的3、4、5…。
这种对元素的处理顺序操作对于性能是有明显影响的。譬如一下代码,
1 | >>> val people = listOf(Person("Alice", 29), Person("Bob", 31), Person("Charles", 31), Person("Dan", 21)) |
如果map
先行,每个元素都被转换(transform)了。如果filter
先行,不恰当的元素被尽可能地filter掉以减少转换。
¶Creating sequences
处理在集合上使用asSequence()
创建一个sequence,还可以通过generateSequence()
函数来创建。例如,
1 | >>> val naturalNumbers = generateSequence(0) { it + 1 } |
¶Using Java functional interfaces
Kotlin调用Java的接口函数比较简单,假设有Java接口如下,
1 | public class Button { |
functional interface声明如下,
1 | public interface OnClickListener { |
在kotlin中直接使用lambda表达式,
1 | button.setOnClickListener { view -> ... } |
之所以生效是因为,接口OnClickListner
仅包含一个抽象方法。这种接口被称为 functional interfaces
或 SAM interfaces
,SAM就是 single abstract method 的缩写。Java的标准API包含大量的这类接口,譬如常见的Runnable
和Callable
。
¶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 | postponeComputation(1_000, object : Runnable { // Passes an object expression as an implementation of a functional interface |
不同的是,如果现实声明一个对象表达式,每次调用都会创建一个新的实例。如果使用lambda,并且不是闭包的情况下,对应的匿名实例会被重用:
1 | postponeComputation(1_000) { println(42) } // One instance of Runnable is created for the entire program. |
因此,等价的写法应该写为,
1 | val runnable = Runnable { println(42) } // Compiled to a global variable; only one instance in the program |
注意,如果将lambda表达式传递给一个带有inline
标记的函数,则不会有匿名类的创建。
¶SAM constructors: explicit conversion of lambdas to functional interfaces
SAM constructors其实就是functional interface!!!
1 | fun createAllDoneRunnable(): Runnable { |
炒概念,不作介绍!!
¶Lambdas with receivers: “with” and “apply”
kotlin 的lambda表达式不同于Java,可以带receivers。
¶The “with” function
with
为inline函数,后面跟着的receiver相当于自由变量,
1 | fun alphabet(): String { |
这个例子中,result
被重复调用了很多次。可以用with
重写为,
1 | fun alphabet(): String { |
with
的底层实际为inline函数,接收两个参数:stringBuilder
、和一个lambda。逻辑上没有任何区别,仅仅为了提高可读性。甚至可以更加简洁,
1 | fun alphabet() = with(StringBuilder()) { |
¶The “apply” function
apply
函数和with
的用法类似;不同在于,apply
总是返回传递进来的参数对象。我们使用apply
重构上述代码,
1 | fun alphabet() = StringBuilder().apply ( |
apply
最常用到的地方就是创建一个实例,并初始化某些属性。譬如,
1 | fun createViewWithCustomAttributes(context: Context) = |
apply
传入的lambda表达式被执行后将返回初始化后的实例。另外,kotlin提供了更简洁的inline函数,
1 | fun alphabet(): String = buildString { |
¶Summary
- lambda可以将代码块作为参数传入函数。
- kotlin可以传递无括号的lambda,并用
it
作为引用。 - 作为闭包时,lambda表达式可以访问和修改外部变量。
- member reference语法使用
::
表示,可以引用方法、构造函数、属性。 - 集合提供了丰富的functional api,譬如
filter
、map
、all
、any
等。 - Sequences 表示集合的操作作为中间操作部分,在真正调用的地方按顺序执行。
- functional interface和SAM interface作为参数的用法。
- lambda带有receivers的用法写在了kotlin的标准inline function中使用。
with
标准库函数允许方法可以多次重复调用同一个实例;apply
可以让你创建实例的同时并初始化成员属性。