定义高阶函数
在 Kotlin 中,Lambda 表达式是函数式编程风格的灵魂所在。如果你想要定义出使用 Lambda 的函数式 API,就需要通过高阶函数来实现。
我们先来看看高阶函数的定义:如果一个函数接收另一个函数作为参数,或者其返回值是一个函数,那么该函数就是高阶函数。
为了理解这个定义,我们先来了解一下函数类型,它的基本语法如下:
kotlin
// 语法:(参数类型1,参数类型2,...) -> 返回值类型
(String, Int) -> Unit
其中:
-
->
左侧的部分,表示函数接收的参数类型列表。如果不接收任何参数,就只要写一对空括号即可。 -
->
右侧的部分,表示函数的返回值类型。如果没有返回值,可以声明为Unit
。
当一个函数的参数类型是函数类型时,那么它就是一个高阶函数。比如:
kotlin
fun example(action: (String, Int) -> Unit) {
action("hello", 123)
}
在函数内部,我们可以像调用普通函数一样调用这个函数类型的参数。
那这有什么用?简单来说,高阶函数可以让函数的一部分执行逻辑由函数的调用方来决定。
举个计算器的例子。我们定义一个高阶函数 calculate()
,它接收两个整数和一个"操作"函数。函数并不关心操作过程是什么,它只会调用这个操作,获取到结果并返回:
kotlin
fun calculate(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
val result = operation(num1, num2)
return result
}
为了调用这个高阶函数,首先得有符合其函数类型的函数,我们定义两个函数:
kotlin
fun plus(num1: Int, num2: Int): Int {
return num1 + num2
}
fun minus(num1: Int, num2: Int): Int {
return num1 - num2
}
然后,在 main()
函数中测试一下:
kotlin
fun main() {
val num1 = 20
val num2 = 80
val result1 = calculate(num1, num2, ::plus)
println("result1 is $result1")
val result2 = calculate(num1, num2, ::minus)
println("result2 is $result2")
}
其中我们通过 ::函数名
函数引用的方式,来将函数作为参数进行传递。
运行结果:
csharp
result1 is 100
result1 is -60
虽然函数引用的方式可行,但每次调用高阶函数时,就需要定义一个函数,就显得有些繁琐了。所以,Kotlin 还支持通过 Lambda 表达式来调用高阶函数,我们来看看它的用法。
上述调用如果使用 Lambda 表达式来完成的话,是这样的:
kotlin
fun main() {
val num1 = 80
val num2 = 20
val result1 = calculate(num1, num2) { n1, n2 -> n1 + n2 }
println("result1 is $result1")
val result2 = calculate(num1, num2) { n1, n2 -> n1 - n2 }
println("result2 is $result2")
}
这时,我们并不需要定义额外的函数,就能调用高阶函数。
带接收者的函数类型
现在,我们使用高阶函数,来模仿标准库中的 apply
函数:
kotlin
fun StringBuilder.myApply(block: StringBuilder.() -> Unit): StringBuilder {
block()
return this
}
我们为 StringBuilder
定义了一个名为 myApply
的扩展函数。它接收一个函数类型的 block
参数,不过,这个类型比较特殊,是一个带接收者的函数类型。
其中 StringBuilder.
这部分即是接收者(Receiver) ,这样我们在传入的 Lambda 表达式中会自动拥有 StringBuilder
的上下文,可以访问 StringBuilder
类中的公有成员。
我们来测试一下:
kotlin
fun main() {
val list: List<String> = listOf("Sally", "Jack", "Martin", "Tommy", "Asher", "Alan")
val builderInstance = StringBuilder().myApply {
append("All People:\n")
for (people in list) {
append(people)
append("\n")
}
append("There are ${list.size} people in total.")
}
val result = builderInstance.toString()
println(result)
}
运行结果:
ini
All People:
Sally
Jack
Martin
Tommy
Asher
Alan
There are 6 people in total.
但官方的 apply
函数似乎能被任何类型的对象调用,这就需要使用到 Kotlin 的泛型了,我们来修改一下:
kotlin
fun <T> T.myApply(block: T.() -> Unit): T {
block()
return this
}
这样,任何类型的对象都可以调用它了。不过,它距离官方的 apply
还差在性能优化上。
内联函数的作用
高阶函数这么好用,有没有什么代价?当然是有的。
我们先来看看它背后的实现原理。以下面的 Kotlin 代码为例:
kotlin
fun calculate(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
val result = operation(num1, num2)
return result
}
fun main() {
val result = calculate(100, 80) { n1, n2 -> n1 + n2 }
}
这段 Kotlin 代码被编译成 JVM 字节码,再反编译为 Java 代码,大致会是下面这个样子(简化便于理解):
java
public static int calculate(int num1, int num2, Function2<Integer, Integer, Integer> operation) {
int result = operation.invoke(num1, num2);
return result;
}
public static void main() {
int result = calculate(100, 80, new Function2<Integer, Integer, Integer>() {
@Override
public Integer invoke(Integer n1, Integer n2) {
return n1 + n2;
}
});
}
可以看到,operation
参数的函数类型被转换成了 Function2
接口,而我们传入的 Lambda 表达式则变为了实现了 Function2
接口的匿名类实例。
这意味着我们每次调用高阶函数并传入 Lambda 表达式时,其实都会创建一个新的匿名类对象,这会造成额外的内存和性能开销。
为了解决这个问题,Kotlin 提供了 inline
(内联)关键字,将高阶函数变为内联函数,可消除因使用 Lambda 表达式带来的运行时开销。
使用它很简单,只需在高阶函数的声明前加上 inline
关键字即可,像这样:
kotlin
inline fun calculate(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
val result = operation(num1, num2)
return result
}
那内联又是怎么实现的?代码替换。
在编译期间,编译器会将内联函数中的代码,以及传入的 Lambda 表达式中的代码,"粘贴" 到调用处。
我们来说说这个过程,以上述的 calculate
的调用为例:
-
首先会将 Lambda 表达式中的代码,替换掉函数类型参数调用的地方:
-
然后内联函数的调用,会被替换成它的函数体:
最终编译后 main
函数中的代码会变为:
kotlin
fun main() {
val result = 100 + 80
}
就是因为这样,内联函数才能彻底消除因使用 Lambda 表达式带来的运行时开销。
注意:如果内联函数很长,且在很多地方被调用,那么会导致生成的字节码体积增加。所以,内联函数只适合接收 Lambda 参数且函数体不多的高阶函数。
现在我们就可以来改进我们之前的 myApply
函数了,标准库中的 apply
函数是一个内联函数,可以消除因使用 Lambda 带来的性能开销,所以优化后的 myApply
函数为:
kotlin
inline fun <T> T.myApply(block: T.() -> Unit): T {
block()
return this
}
noinline与crossinline
非局部返回
内联的 Lambda 表达式中,可以直接使用 return
关键字来退出其 外层的函数,这被称为非局部返回 。而非内联函数只能进行局部返回,使用 return@label
的语法来退出 Lambda 表达式本身。
请看这个例子:
kotlin
data class User(val name: String)
fun findUser(users: List<User>, action: (User) -> Unit) {
println("--- Start search ---")
for (user in users) {
if (user.name == "Admin") {
action(user)
}
}
println("--- End search ---")
}
fun main() {
val users = listOf(User("Alice"), User("Admin"), User("Bob"))
findUser(users) { user ->
println("Found: ${user.name}")
return@findUser // 只能局部返回,退出 Lambda
}
println("Main function continues...")
}
我们定义了一个函数 findUser
,用于查找用户列表中的用户,找到了就使用 return@find
这种写法进行局部返回,不再执行 Lambda 表达式剩余的代码。
注意:在该 Lambda 表达式中不能使用 return
关键字进行返回,否则会报错:'return' is not allowed here。
运行结果:
lua
--- Start search ---
Found: Admin
--- End search ---
Main function continues...
现在,我们将 findUser
函数声明为内联函数:
kotlin
data class User(val name: String)
inline fun findUser(users: List<User>, action: (User) -> Unit) {
println("--- Start search ---")
for (user in users) {
if (user.name == "Admin") {
action(user)
}
}
println("--- End search ---")
}
fun main() {
val users = listOf(User("Alice"), User("Admin"), User("Bob"))
findUser(users) { user ->
println("Found: ${user.name}")
return // 直接 return 并结束 main 函数
}
println("Main function continues...")
}
此时在 Lambda 表达式中,就可以使用 return
关键字了,并且会退出 main
函数。
运行结果:
diff
--- Start search ---
Found: Admin
因为 Lambda 表达式中的代码最终会被放("粘贴")到 main
函数中,所以 return
自然是返回 main
函数了。
noinline
默认情况下,内联函数会将其所有的 Lambda 参数进行内联。但如果你只想内联其中一部分,该怎么办?
这时,就需要用到 noinline
关键字了。只需在不想被内联的 Lambda 参数之前加上即可,如下所示:
kotlin
inline fun processBlocks(block1: () -> Unit, noinline block2: () -> Unit) {
}
内联可以消除因使用 Lambda 表达式所带来的运行时开销,那我们为什么要非内联的函数类型参数呢?
因为内联的 Lambda 参数在编译后,会被其内部代码进行替换,并不是一个函数对象。在内联函数的内部,不能传递给另一个需要函数对象的普通函数,而非内联的 Lambda 参数可以作为函数对象自由传递。
例如:
kotlin
fun executeInRunnable(block: () -> Unit) {
println("--- Executing runnable ---")
Runnable {
block()
}.run()
}
inline fun processBlocks(block1: () -> Unit, noinline block2: () -> Unit) {
println("Executing block1 (inlined)")
block1()
// // 内联 lambda 参数无法传递给普通函数
// executeInRunnable(block1) // 报错 Illegal usage of inline-parameter 'block1' ... Add 'noinline' modifier to the parameter declaration
println("Passing block2 as an object to another function")
// 非内联 lambda 参数可以作为函数对象自由传递
executeInRunnable(block2)
}
crossinline
如果内联函数的 Lambda 参数在一个新的作用域中被调用,比如在另一个线程中或者另一个匿名类中,编译会不通过,因为无法处理其中可能存在的非局部返回。比如:
kotlin
inline fun runRunnable(block: () -> Unit) {
val runnable = Runnable {
block()
}
runnable.run()
}
其中 block()
这行代码会报错:Can't inline 'block' here: it may contain non-local returns. Add 'crossinline' modifier to parameter declaration 'block'。
上述代码,我们在一个匿名类的实现中,调用了内联的函数类型参数。如果在该 Lambda 表达式中使用 return
进行非局部返回,这个 return
将试图穿过一个作用域边界 (Runnable
匿名类),去返回外层的函数(Lambda 表达式所在的函数)。这种返回会导致混乱的控制流,所以编译器禁止了这样的使用方式。
这种情况下,我们可以使用 crossinline
关键字,使 runRunnable
函数仍然是内联函数。如下所示:
kotlin
inline fun runRunnable(crossinline block: () -> Unit) {
val runnable = Runnable {
block()
}
runnable.run()
}
它会向编译器做出一个保证:在这个 Lambda 表达式中我不会使用非局部返回 return。
当使用了 crossinline
后,你在 Lambda 表达式中将不允许使用 return
关键字进行非局部返回,比如:
kotlin
fun main() {
runRunnable {
return
}
}
会报错:'return' is not allowed here。
但还是可以使用 return@runRunnable
进行局部返回的,比如:
kotlin
fun main() {
runRunnable {
return@runRunnable
}
}
总的来说,除了 return
关键字使用上的差别,crossinline
保留了内联带来的性能优势。