定义: 如果一个函数接收另一个函数作为参数,或者返回值的类型是另一个函数,那么该函数就称为高阶函数。
一个函数怎么能接收另一个函数作为参数呢?这就涉及到另外一个概念:函数类型。类似于整型、布尔型。
定义一个函数类型:
(String, Int) -> Unit
->左边的部分用来声明该函数接受什么参数的,多个参数用逗号隔开,如果不接收参数则写一对空括号即可。->右边的部门用于声明该函数的返回值是什么,如果没有返回值就使用Unit,大致相当于Java中的void。
现在将上诉函数类型添加到某个函数的声明或者返回值声明上,那么这个函数就是一个高阶函数。
示例:
fun example(func: (String, Int) -> Unit) {
func("Hello", 123)
}
上诉代码中,example()函数接收了一个函数类型的参数,因此example()函数就是一个高阶函数。而调用一个函数类型的参数,语法类似于调用一个普通的函数,只需要在后面加上一对括号,在括号中传入必要的参数即可。
简单点说,高阶函数允许让函数类型的参数来决定函数的执行逻辑。
这里再定义一个num1AndNum2()的高阶函数,并让其接收两个整型和一个函数类型的参数,然后在num1AndNum2()函数中对传入的两个整型参数进行某种运算,并返回最终的运算结果,具体进行什么运算由传入的函数类型参数决定。
fun num1AddNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
return operation(num1, num2)
}
再定义两个匹配上诉函数类型的函数:
fun plus(num1: Int, num2: Int): Int {
return num1 + num2
}
fun minus(num1: Int, num2: Int): Int {
return num1 - num2
}
调用num1AndNum2(),传入不同的函数类型参数:
val result1 = num1AndNum2(100, 80, ::plus) //180
val result = num1AndNum2(100, 80, ::minus) //20
上诉代码的第三个参数写法是一种函数引用的写法,表示将plus()和minus()函数作为参数传递给num1AndNum2()函数。
Kotlin还支持其它多种方式来调用高阶函数,比如Lambda表达式、匿名函数、成员引用等。例如我们可以使用Lambda表达式来调用。
示例:
num1AndNum2(100, 80) { n1, n2 ->
n1 + n2
}
num1AndNum2(100, 80) { n1, n2 ->
n1 - n2
}
因为Lambda表达式同样可以完整地表达一个函数的参数声明和返回值类型声明(Lambda表达式最后一行代码会自动作为返回值)。
使用高阶函数模仿一个类似于apply()函数的功能:
fun StringBuilder.build(bolck: StringBuilder.() -> Unit): StringBuilder {
block()
return this
}
上述函数类型参数的声明方式有点不同,它在函数类型前面加上了一个StringBuilder.的语法结构。其实这个才是定义高阶函数完整的语法,函数类型前面加上ClassName.就表示这个函数类型是定义在哪个类当中的。
将函数类型定义到StringBuilder类当中时,当我们调用build函数时传入的Lambda表达式将会自动拥有StringBuilder的上下文,这也是apply()函数的功能。
调用方法示例:
StringBuilder().build {
append("aaa")
append("bbb")
}
Kotlin编译器会将高阶函数的语法转化成Java支持的语法。函数类型参数变成了Function()接口,里面有一个待实现的invoke()函数。而在调用高阶函数的时候,之前的Lambda表达式变成了Function接口的匿名类实现。
Kotlin高阶函数使用的Lambda表达式在底层被转换成了匿名类的实现方式,这会造成额外的内存和性能开销。为了解决这个问题,Kotlin提供了内联函数的功能,它可以将使用Lambda表达式带来的运行开销完全消除。
内联函数用法只需要在定义高阶函数时加上inline关键字的声明即可。
示例:
inline fun num1AddNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
return operation(num1, num2)
}
内联函数的原理:Kotlin编译器会将内联函数中的代码在编译的时候自动替换到调用它的地方,这样就不存在运行时开销了。
高阶函数存在着一些特殊情况:一个高阶函数中如果接收了两个或者更多的函数类型的参数,这时给函数加上了inline关键字,Kotlin编译器会自动将所有引用的Lambda表达式全部进行内联。
如果只想内联其中的一个Lambda表达式,这时可以使用noinline关键字。
示例:
inline fun inlineTest(block1: () -> Unit, noinline block2: () -> Unit) {
}
上诉代码就只会对block1参数所引用的Lambda表达式进行内联了。
内联函数既然有好处,为什么Kotlin还要提供一个noinline关键字来排除内联功能呢?这是因为内联的函数参数类型在编译的时候会被进行代码替换,因此它没有真正的参数属性。非内联的函数类型参数可以自由传递给其它任何函数,因为它是一个真正的参数,而内联的函数类型参数只允许传递给另外一个内联函数,这就是最大的局限性。
内联函数和非内联函数还有一个重要的区别,就是内联函数所引用的Lambda表达式中是可以使用return关键字来进行函数返回的,而非内联函数只能进行局部返回。
示例:
fun printString(str: String, block: (String) -> Unit) {
println("printString begin")
block(str)
println("printString end")
}
fun main() {
println("main start")
printString("") { s ->
println("lambda start")
if (s.isEmpty()) return@printString
println("lambda end")
}
println("main end")
}
运行上诉代码结果:
main start
printString begin
lambda start
printString end
main end
这说明return@printString确实只能进行局部返回
如果将printString()函数声明成一个内联函数,再将return@printString改成return,运行结果如下:
main start
printString begin
lambda start
这是因为printString()变成了内联函数,我们就可以再Lambda表达式中使用return关键字了,这时由于内联函数的替换,return代表的就是返回main()函数,所以return关键字后面的代码都不执行了。
绝大多数时候都能将高阶函数声明成内联函数的,但是有少数的例外情况,例如:
inline fun runRunnable(block: () -> Unit) {
Runnable {
block()
}.run()
}
上诉代码会报错,首先我们再runRunnable()函数中创建了一个Runnable对象,并在这个对象的Lambda表达式中调用了传入的函数类型参数,而Lambda表达式在编译的时候会被转换成匿名的实现方式,也就是说上诉代码实际上是在匿名类中调用了传入的函数类型参数。
而内联函数所引用的Lambda表达式允许使用return关键字进行返回,但是由于是在匿名类中调用的函数参数类型,此时是不可能进行外层调用函数返回的,最多能对匿名类中的函数调用进行返回,因此会报错。
也就是说,如果在高阶函数中创建了另外的Lambda表达式或者匿名类的实现,并且在这些实现中调用函数参数类型,此时再将高阶函数声明成内联函数会报错。
这时就可以使用crossinline关键字了。
示例:
inline fun runRunnable(crossinline block: () -> Unit) {
Runnable {
block()
}.run()
}
上诉代码中,在函数类型参数前面加上了crossinline关键字,就不会报错了。
之前会报错是因为内联函数的Lambda表达式中允许使用return关键字,和高阶函数的匿名类实现中不允许使用return关键字之间造成了冲突。而crossinline关键字用于保证内联函数的Lambda表达式中一定不会使用return关键字。
本文整理自《第一行代码 Android》第三版,笔记文章,希望对大家有所帮助!