高阶函数:lambda作为参数和返回

主要内容

  1. 函数类型
  2. 高阶函数以及其结构化代码
  3. 内联函数
  4. 非本地返回以及标签
  5. 匿名函数

higher-order functions , 高阶函数,指函数包含lambda入参或lambda作为返回值的函数。

Declaring higher-order functions

Kotlin中,函数可以被表述为lambda或函数的值的引用。因此,一个高阶函数可以是任何函数,它包含lambda表达式或函数。譬如,标准库中的filter定义为高阶函数:

1
list.filter { x > 0 }

前面已经介绍过了Kotlin标准库常见的几个高阶类型:mapwith等。要声明自定义高阶函数,首先必须引入 函数类型(function types)

Function types

为了声明一个函数的入参为lambda,你需要知道如何声明对应的参数类型。它的显式声明格式如下:

1
2
val sum: (Int, Int) -> Int = { x, y -> x + y }  // Function that takes two Int parameters and returns an Int value
val action: () -> Unit = { println(42) } // Function that takes no arguments and doesn't return a value

Figure 8.1

因为函数类型中已经指定了参数,所以lambda中的声明部分可以省略入参类型。

和其它函数一样,一个函数类型的返回类型可以被标识为可空的(nullable):

1
var canReturnNull: (Int, Int) -> Int? = { null }

又或者标识为可空的函数类型,

1
var funOrNull: ((Int, Int) -> Int)? = null

Calling functions passed as arguments

高阶函数的声明形式如下,

1
2
3
4
5
6
7
8
9
fun twoAndThree(operation: (Int, Int) -> Int) {  // Declares a parameter of a function type
val result = operation(2, 3) // Calls the parameter of a function type
println("The result is $result")
}

>>> twoAndThree { a, b -> a + b }
The result is 5
>>> twoAndThree { a, b -> a * b }
The result is 6

函数作为入参的语法如下,

Figure 8.2

filter函数接收一个predicate作为入参。predicate的类型是一个函数,接收字符串并返回一个boolean结果。

下面是它的完整实现,

1
2
3
4
5
6
7
8
9
10
11
fun String.filter(predicate: (Char) -> Boolean): String {
val sb = StringBuilder()
for (index in 0 until length) {
val element = get(index)
if (predicate(element)) sb.append(element) // Calls the function passed as the argument for the "predicate" parameter
}
return sb.toString()
}

>>> println("ab1c".filter { it in 'a'..'z' }) // Passes a lambda as anargument for "predicate"
abc

Default and null values for parameters with function types

声明函数类型参数时,可以指定一个默认值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
fun <T> Collection<T>.joinToString(
separator: String = ", ",
prefix: String = "",
postfix: String = "",
transform: (T) -> String = { it.toString() } // Declares a parameter of a function type with a lambda as a default value
): String {
val result = StringBuilder(prefix)

for ((index, element) in this.withIndex()) {
if (index > 0) result.append(separator)
result.append(transform(element)) // Calls the function passed as an argument for the "transform" parameter
}

result.append(postfix)
return result.toString()
}

>>> val letters = listOf("Alpha", "Beta")
>>> println(letters.joinToString()) // Uses the default conversion function
Alpha, Beta
>>> println(letters.joinToString { it.toLowerCase() }) // Passes a lambda as an argument
Alpha, beta
>>> println(letters.joinToString(separator = "! ", postfix = "! ",
... transform = { it.toUpperCase() })) // Uses the named argument syntax for passing several arguments including a lambda
ALPHA! BETA!

这里的函数包含泛型:类型参数Ttransform接收该泛型参数。函数类型参数默认值的写法和普通参数默认值的语法一直,在等号后面声明。

另外,入参部分也可以为可空的,

1
2
3
4
5
6
fun foo(callback: (() -> Unit)?) {
// ...
if (callback != null) {
callback()
}
}

一个更简单的版本是,函数类型是包含接口invoke方法的一个实现。作为常规方法,可以通过callback?.invoke()的形式调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fun <T> Collection<T>.joinToString(
separator: String = ", ",
prefix: String = "",
postfix: String = "",
transform: ((T) -> String)? = null // Declares a nullable parameter of a function type
): String {
val result = StringBuilder(prefix)

for ((index, element) in this.withIndex()) {
if (index > 0) result.append(separator)
val str = transform?.invoke(element) ?: element.toString() // safe-call syntax to call the function
result.append(str)
}

result.append(postfix)
return result.toString()
}

Returning functions from functions

一个函数返回另外一个函数的场景并不常见,但某些需要实时计算的场景很有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
enum class Delivery { STANDARD, EXPEDITED }

class Order(val itemCount: Int)

fun getShippingCostCalculator(
delivery: Delivery): (Order) -> Double { // Declares a function that returns a function
if (delivery == Delivery.EXPEDITED) {
return { order -> 6 + 2.1 * order.itemCount } // Returns lambdas from the function
}

return { order -> 1.2 * order.itemCount } // Returns lambdas
}

>>> val calculator = getShippingCostCalculator(Delivery.EXPEDITED) // Stores the returned function in a variable
>>> println("Shipping costs ${calculator(Order(3))}") // Invokes the returned function
Shipping costs 12.3

函数作为返回,使用return关键字带上一个lambda、函数类型的成员引用、或函数类型的表达式(譬如本地变量)。

下面是另外一个例子。假设你有一个GUI联系人应用,你需要决定哪些联系人应该在UI上显式。UI界面允许你输入字符串进行过滤;也允许你隐藏不包含某些字段的联系人。你的对象数据如下:

1
2
3
4
class ContactListFilters {
var prefix: String = ""
var onlyWithPhoneNumber: Boolean = false
}

当用户在界面上敲入Dprefix的值被更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
data class Person(
val firstName: String,
val lastName: String,
val phoneNumber: String?
)

class ContactListFilters {
var prefix: String = ""
var onlyWithPhoneNumber: Boolean = false

fun getPredicate(): (Person) -> Boolean { // Declares a function that returns a function
val startsWithPrefix = { p: Person ->
p.firstName.startsWith(prefix) || p.lastName.startsWith(prefix)
}
if (!onlyWithPhoneNumber) {
return startsWithPrefix // 返回函数类型的一个变量
}
return { startsWithPrefix(it)
&& it.phoneNumber != null } // 返回lambda表达式
}
}

>>> val contacts = listOf(Person("Dmitry", "Jemerov", "123-4567"),
Person("Svetlana", "Isakova", null))
>>> val contactListFilters = ContactLIstFilters()
>>> with (contactListFilters) {
>>> prefix = "Dm"
>>> onlyWithPhoneNumber = true
>>> }
>>> println(contacts.filter(
... contactListFilters.getPredicate())) // 函数作为参数形式传递
[Person(firstName=Dmitry, lastName=Jemerov, phoneNumber=123-4567)]

getPredicate方法返回一个函数值,并作为参数传递给filter函数。

Removing duplication through lambdas

(误,原文思想错误,重复代码并不是通过lambda解决的,代码重构和clean code才是;kotlin的lambda并不能带来clean code的作用)

Inline functions: removing the overhead of lambdas

How inlining works

当声明一个函数为inline,它的body部分是内联的——换句话说,内联函数的语句体会在它真正被调用的地方直接替代。

譬如我们要确保多线程并发时资源不共享。函数使用Lock对象,执行代码块后,最后释放锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
inline fun <T> synchronized(lock: Lock, action: () -> T): T {
lock.lock()
try {
return action()
}
finally {
lock.unlock()
}
}

val l = Lock()
synchronized(l) {
// ...
}

由于你声明的synchronized函数作为inline,编译生成的代码等价于Java的synchronized语句,

1
2
3
4
5
6
7
fun foo(l: Lock) {
println("Before sync")
synchronized(l) {
println("Action")
}
println("After sync")
}

编译的字节码等价于,

Figure 8.3

内联函数也可以作为参数传递,

1
2
3
4
5
class LockOwner(val lock: Lock) {
fun runUnderLock(body: () -> Unit) {
synchronized(lock, body) // A variable of a function type is passed as an argument, not a lambda.
}
}

这里的lambda表达式并不可用,因此它不是内联的。只有synchronized函数体才是内联的;lambda表达式还是常规的调用形式。runUnderLock函数编译对应的字节码形式如下,

1
2
3
4
5
6
7
8
9
10
11
class LockOwner(val lock: Lock) {
fun __runUnderLock__(body: () -> Unit) { // This function is similar to the bytecode the real runUnderLOck is compiled to.
lock.lock()
try {
body() // The body isn't inlined, because there's no lambda at the invocation.
}
finally {
lock.unlock()
}
}
}

如果包含有两个内联函数放置在不同的lambda表达式,每个内联函数的调用仍然是独立的。内联函数的语句体代码会被拷贝到相应的lambda位置中。

Restrictions on inline functions

基于内联函数的执行方式,不是所有用到lambda的函数都可以内联。如果函数是内联的,lambda表达式的语句体将被作为参数直接替换到返回代码中。这种约束可能用于函数对应的传参上。如果该参数被调用,函数代码就可以轻松实现内联。但如果该参数被存储在其它地方,lambda表达式部分的代码就不能被内联,因为必须在函数内部要有一个对象容纳这些代码。

通常地,参数可以被内联的条件是它被直接调用或以参数的形式传递给另外一个inline函数。否则,编译器将阻止参数的内联并抛出错误信息"Illegal usage of inline-parameter."。

例如,Sequence.map函数如下定义,

1
2
3
fun <T, R> Sequence<T>.map(transform: (T) -> R): Sequence<R> {
return TransformingSequence(this, transform)
}

map函数并没有直接调用到入参函数transform。相反,它将它传递给了构造函数,并存储为属性。为了实现标准库的map方法,应该标明让编译器不对其进行内联,你可以使用noinline修改器进行修饰,

1
2
3
inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) {
// ...
}

注意编译器完全支持内联实现,可以跨module、跨函数定义、跨第三方库。也可以在Java中调用内联函数;不过改调用不会被内联,而是被编译成常规函数调用。

Inlining collection operations

Kotlin标准库中的集合相关函数都是实现了inline,目的是为了更好的性能。

Deciding when to declare functions as inline

Using inlined lambdas for resource management

1
2
3
4
5
fun readFirstLineFromFile(path: String): String {
BufferedReader(FileReader(path)).use { br ->
return br.readLine() // Returns the line from the function
}
}

Control flow in higher-rder functions

Return statements in lambdas: return from an enclosing function

高级函数可以返回闭包语句,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
data class Person(val name: String, val age: Int)

val people = listOf(Person("Alice", 29), Person("Bob", 31))

fun lookForAlice(people: List<Person>) {
for (person in people) {
if (person.name == "Alice") {
println("Found!")
return
}
}
println("Alice is not found") // This line is printed if there's no Alice among "people".
}

>>> lookForAlice(people)
Found!

forEach部分可以进行重写,

1
2
3
4
5
6
7
8
9
fun lookForAlice(people: List<Person>) {
people.forEach {
if (it.name == "Alice") {
println("Found!")
return // Returns from a function
}
}
println("Alice is not found")
}

如果你在一个lambda中使用return关键字,returns from the function in which you called the lambda 返回的是函数签名部分,而不是lambda自身。这种return语句被称为 非本地返回(non-local return)

Returning from lambdas: return with a label

如果要另上述代码实现 本地返回(local return) 语义,你需要使用label关键字,也就是标签,语法上在return关键字之后。

1
2
3
4
5
6
7
8
9
fun lookForAlice(people: List<Person>) {
people.forEach label@{
if (it.name == "Alice") return@label // return@label refers to this label.
}
println("Alice might be somewhere") // This line is always printed.
}

>>> lookForAlice(people)
Alice might be somewhere

为了label一个lambda表达式,将标签名(可以是任意唯一标识),跟随在@字符之后,在lambda开括号之前。然后要在lambda表达式内返回,return关键字之后带上@字符+标签标识。如下图:

Figure 8.4

非传统方式,你可以直接使用接收lambda参数的函数名,作为本地标签使用,

1
2
3
4
5
6
fun lookForAlice(people: List<Person>) {
people.forEach {
if (it.name == "Alice") return@forEach // return@forEach returns from the lambda expression.
}
println("Alice might be somewhere")
}

注意,一个函数表达式里面最多只能使用一个标签;如果显式指定了label标签名,就必须使用return@label的形式。

Labeled “this” expression

this表达式也可加标签。如果lambda表达式指定了this的receiver,通过this@label表达式实际访问了它的隐式receiver:

1
2
3
4
5
6
7
>>> println(StringBuilder().apply sb@ {  // This lambda's implicit receiver is accessed by this@sb.
... listOf(1, 2, 3).apply { // "this" refers to the closest implici receiver in the scope.
... this@sb.append(this.toString()) // All implicit receivers can be accessed, the outer ones via explicit labels.
... }
... })

[1, 2, 3]

你可以指定lambda表达式的label,或者使用函数名代替。

non-local return的语法有点啰嗦,如果一个lambda表达式又有多个return表达式的话。为了解决这个问题,kotlin引入了匿名函数(anonymous functions)。

Anonymous functions: local returns by default

匿名函数默认之后local return。

1
2
3
4
5
6
7
8
9
fun lookForAlice(people: List<Person>) {
people.forEach(fun (person) { // Uses an anonymous function instead of a lambda expression
if (person.name == "Alice") return
println("${person.name} is not Alice")
})
}

>>> lookForAlice(people)
Bob is not Alice

匿名函数和普通函数没什么两样,不过它的函数名和参数类型可以省略。

1
2
3
people.filter(fun (person): Boolean {
return person.age < 30
})

作为表达式语句体时,匿名函数的返回类型可以直接省略,

1
people.filter(fun (person) = person.age < 3))

匿名函数和lambda表达式的区别如下,

Figure 8.5

非常明显,lambda表达式没有fun关键字,所以它return到外部函数。

Summary

  • 函数类型允许声明的变量、参数、函数返回是一个函数应用。
  • 高阶函数就是指函数接收的参数或返回值也为函数的函数。
  • 当一个内联函数被编译,编译后的字节码将被直接替换在它所真正被调用的地方。
  • 内联函数可以使用 non-local return ——非本地返回,返回内部lambda表达式中的返回。
  • 匿名函数只有 local return 。因此它有多个退出点。