Kotlin 高阶函数

定义高阶函数

接收 Lambda 参数的函数可以称为具有函数式编程风格的 API。

如果一个函数接收另一个函数作为参数,或者返回值的类型是另一个函数,该函数就称为高阶函数。函数类型的语法规则:

(String, Int) -> Unit

-> 左边的部分用来声明该函数接收什么参数,多个参数之间使用逗号隔开,如果不接受任何参数写一对空括号就可以了。-> 右边的部分用于声明该函数的返回值是什么类型,如果没有返回值就使用 Unit,大致相当于 Java 中的 void。

将上述函数类型添加到某个函数的参数声明或者返回值声明上,这个函数就是一个高阶函数了:

fun example(func: String, Int) -> Unit){
func("hello",123)
}

高阶函数的使用

高阶函数允许让函数类型的参数来决定函数的执行逻辑。即使是同一个高阶函数,只要传入不同的函数类型参数,那么它的执行逻辑和最终的返回结果就可能是完全不同的。举个例子:

fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
val result = operation(num1, num2)
return result
}

第三个参数是一个接收两个整型参数并且返回值也是整型的函数类型参数,在 num1AndNum2 () 中没有进行任何具体的运算操作,而是将 num1 和 num2 参数传递给第三个函数类型参数并获取它的返回值,最终将得到的返回值返回。

fun plus(num1: Int, num2: Int): Int {
return num1 + num2
}

fun minus(num1: Int, num2: Int): Int {
return num1 - num2
}

fun main() {
val num1 = 100
val num2 = 80
val result1 = num1AndNum2(num1, num2, ::plus)
val result2 = num1AndNum2(num1, num2, ::minus)
println("result1 is $result1")
println("result2 is $result2")
}

::plus::minus 是一种函数引用的写法,表示将 plus () 和 minus () 函数作为参数传递给 num1AndNum2 () 函数。

Kotlin 还支持其他多种方式调用高阶函数,比如 Lambda 表达式、匿名函数、成员引用等。其中 Lambda 表达式是最常见也是最普通的高阶函数调用方式。上述代码使用 Lambda 表达式的写法实现如下所示:

fun main() {
val num1 = 100
val num2 = 80
val result1 = num1AndNum2(num1, num2) { n1, n2 -> n1 + n2 }
val result2 = num1AndNum2(num1, num2) { n1, n2 -> n1 - n2 }
println("result1 is $result1")
println("result2 is $result2")
}

模仿实现 apply 函数

fun StringBuilder.build(block: StringBuilder.() -> Unit): StringBuilder {
block()
return this
}

这里给 StringBuilder 类定义了一个 build 扩展函数,这个扩展函数接收一个函数类型参数,并且返回值类型也是 StringBuilder。在函数类型的前面加上 ClassName. 表示这个函数类型是定义在哪个类中的,这是定义高阶函数完整的语法规则。这里将函数类型定义到 StringBuilder 类中,当调用 build 函数时传入的 Lambda 表达式将会自动拥有 StringBuilder 的上下文。

fun main() {
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
val result = StringBuilder().build {
append("Start eating fruits.\n")
for (fruit in list) {
append(fruit).append("\n")
}
append("Ate all fruits.")
}
println(result.toString())
}

内联函数的作用

Kotlin 内联函数可以将使用 Lambda 表达式带来的运行时开销完全消除。在定义高阶函数时加上 inline 关键字即可使用内联函数:

inline fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
val result = operation(num1, num2)
return result
}

noinline 与 crossinline

noinline

一个高阶函数中如果接收了两个或者更多函数类型的参数,这时加上 inline 关键字 Kotlin 编译器会自动将所有引用的 Lambda 表达式全部进行内联,如果只想内联其中一个 Lambda 表达式可以使用 noinline 关键字:

inline fun inlineTest(block: () -> Unit, noinline block2: () -> Unit) {}

内联的函数类型参数在编译的时候会被进行代码替换,因此它没有真正的参数属性。非内联的函数类型可以自由地传递给其他任何函数,因为它就是一个真实的参数,而内联的函数类型参数只允许传递给另外一个内联函数,这也是它最大的局限性。

内联函数所引用的 Lambda 表达式中是可以使用 return 关键字来进行函数返回的,而非内联函数只能进行局部返回。

fun printString(str: String, block: (String) -> Unit) {
println("printString begin")
block(str)
println("printString end")
}
fun main() {
println("main start")
val str = ""
printString(str) { s ->
println("lambda start")
if (s.isEmpty()) return@printString
println(s)
println("lambda end")
}
println("main end")
}

这里定义了一个叫做 printString ( ) 的高阶函数,由于在 Lambda 表达式中打印传入的字符串参数。如果字符串参数为空就不进行打印。Lambda 表达式中不允许直接使用 return 关键字,这里使用了 return@printString 的写法表示进行局部返回,并且不再执行 Lambda 表达式的剩余代码。打印结果如下:

main start
printString begin
lambda start
printString end
main end

将 printString ( ) 函数声明成一个内联函数:

inline fun printString(str: String, block: (String) -> Unit) {
println("printString begin")
block(str)
println("printString end")
}

fun main() {
println("main start")
val str = ""
printString(str) { s ->
println("lambda start")
if (s.isEmpty()) return
println(s)
println("lambda end")
}
println("main end")
}

printString () 函数变成了内联函数,现在可以再 Lambda 表达式中使用 return 关键字了,此时的 return 代表的时返回外层的调用函数,也就是 main () 函数。打印结果如下:

main start
printString begin
lambda start

crossinline

将高阶函数声明成内联函数是一种良好的编程习惯,绝大多数高阶函数是可以直接声明成内联函数的,但是也有少部分例外的情况:

inline fun runRunnable(block: () -> Unit) {
val runnable = Runnable {
block()
}
runnable.run()
}

这段代码加上 inline 关键字后会报错:Can't inline 'block' here: it may contain non-local returns. Add 'crossinline' modifier to parameter declaration 'block', 在 runRunnable () 函数中创建了一个 Runnable 对象,并在 Runnable 的 Lambda 表达式中调用了传入的函数类型参数,而 Lambda 表达式在编译的时候会被转换成匿名内部类的实现方式,所以上述代码实际上是在匿名内部类中调用了传入的函数类型参数。

而内联函数所引用的 Lambda 表达式允许使用 return 关键字进行函数返回,但是由于我们实在匿名内部类中调用的函数类型参数,此时是不可能进行外层调用函数返回的,最多只能对匿名类中的函数调用进行返回,因此这里就提示了上述错误。

也就是说,如果在高阶函数中创建了另外的 Lambda 或者匿名类的实现,并且在这些实现中调用函数类型参数,此时再将高阶函数声明成内联函数,就一定会提示错误。借助 crossinline 关键字可以很好地解决这个问题:

inline fun runRunnable(crossinline block: () -> Unit) {
val runnable = Runnable {
block()
}
runnable.run()
}

crossinline 关键字就像一个契约,它用于保证在内联函数的 Lambda 表达式中一定不会使用 return 关键字,这样冲突就不存在了。

声明了 crossinline 关键字后,无法在调用 runRunnable 函数时的 Lambda 表达式中使用 return 关键字进行函数返回,但是仍然可以使用 return@runRunnable 的写法进行局部返回。总体来说,除了在 return 关键字的使用上有所区别外,crossinline 保留了内联函数的其他所有特性。

参考