Kotlin之高阶函数
1、高阶函数
1.1、高阶函数的定义
高阶函数的定义:如果一个函数接收另一个函数作为参数,或者返回值的类型是另一个函数,那么该函数称为高阶函数。你可能会有疑问,一个函数怎么能接收另一个函数作为参数呢?因为Kotlin中新增了
函数类型
,如果我们将这种函数类型添加到一个函数的参数声明后者返回值声明当中,那么该函数就成为高阶函数。
1.2、函数类型的定义
函数类型的定义的基本规则如下:
methodName:(Int,String)->Unit
- 1、methodName是函数类型的名称,名称不限制。
- 2、(Int,String)代表函数接收的类型,多个参数类型用逗号隔开。
- 3、->右边表示函数的返回值,Unit类似于Java的void表示无返回值。
下面看下如何将这个函数类型添加到一个函数的参数声明中:
fun example(block:(String,Int)->Unit){
block("test",123)
}
这里example()
函数就接收了一个函数类型的参数了,该函数就是高阶函数了。函数类型的参数使用就和调用函数一样,传入相应的参数即可。
1.3、高阶函数的用途
高阶函数允许让函数类型参数决定函数的执行逻辑,即使同一个高阶函数,传入的函数类型参数不同,那么函数的执行逻辑和返回结果可能也是完全不同的。下面我们举例说明下:
这里准备定义一个函数num1AndNum2()
接收2个Int参数和一个函数类型的参数,由函数类型的参数决定这两个Int参数具体执行的运算。
新建一个HighFuncFile文件,在其中定义高阶函数
fun num1AndNum2(num1: Int, num2: Int, block: (Int, Int) -> Int): Int {
return block(num1, num2)
}
该函数前两个参数没什么好说的,第三个参数是函数类型的接收两个Int变量并且返回值为Int类型,将前两个Int类型的参数传递给第三个函数类型作为参数,高阶函数中没有其他逻辑,将具体的逻辑交由第三个函数类型的参数来完成。
那么第三个参数应该传什么呢?我们可以在同文件下定义与其匹配的函数或者使用其他类中相匹配类型的函数作为参数,这里我们现在HighFuncFile文件
下定义函数。
fun plusFunc(num1: Int, num2: Int): Int {
return num1 + num2
}
fun minusFunc(num1: Int, num2: Int): Int {
return num1 - num2
}
高阶函数的调用
num1AndNum2(20, 30, ::plusFunc)
num1AndNum2(20, 30, ::minusFunc)
可以看到第三个参数我们使用了::plusFunc
这种写法,这是一种函数引用的写法,表示将函数plusFunc()
来作为参数传递给高阶函数。如果这两个函数是定义在某个类中,那么该怎么引用这个函数呢?
在HighFuncTest.class中定义函数
class HighFuncTest {
fun plusFunc(num1: Int, num2: Int): Int {
return num1 + num2
}
fun minusFunc(num1: Int, num2: Int): Int {
return num1 - num2
}
}
我们上面使用了::plusFunc
来引用函数,那此时我们该怎么引用函数呢?
val highFuncTest: HighFuncTest = HighFuncTest()
num1AndNum2(20, 30, highFuncTest::plusFunc)
num1AndNum2(20, 30, highFuncTest::minusFunc)
先创建对象,然后使用highFuncTest::plusFunc
来引用HighFuncTest类
中的函数作为参数传递给高阶函数。
像这种每次调用高阶函数都需要定义与其函数类型参数匹配的函数,使用起来确实很麻烦,为此Kotlin提供了其他方式调用高阶函数,比如:Lambda表达式、匿名函数、成员引用
等,Lambda表达式是最常用的高阶函数调用方式。下面我们就来学习下如何使用Lambda表达式来调用高阶函数,我们把上面的例子改成Lambda表达式的方法。
val plusResult = num1AndNum2(20, 30) { n1: Int, n2: Int -> n1 + n2 }
Log.e(tag, "$plusResult")
val minusResult = num1AndNum2(20, 30) { n1: Int, n2: Int -> n1 - n2 }
Log.e(tag, "$minusResult")
可以发现使用Lambda表达式同样可以完整的表达函数类型的参数和返回值,Lambda表达式的最后一行代码的返回值作为函数的返回值返回。
下面对高阶函数继续探究,回顾一下apply
标准函数的用法
val stringBuilder = StringBuilder()
val ss = stringBuilder.apply {
append("hello")
append("how are you")
}
Log.e(tag,ss.toString())
apply
标准函数会把调用对象传递到Lambda表达式中作为上下文,并且返回调用对象。下面我们就用高阶函数来实现类似的功能。
fun StringBuilder.otherApply(block: StringBuilder.() -> Unit): StringBuilder {
block()
return this
}
这里给StringBuilder类
定义了一个扩展函数,扩展函数接收一个函数类型的参数,并且返回值为StringBuilder。
注意:这里定义的函数类型的参数和我们前面学习的语法还是有区别的,在函数类型的前面加上了StringBuilder.
,其实这才是完整的函数类型的定义规则,加上ClassName.
表示在哪个类中定义函数类型。使用StringBuilder.
表示在StringBuilder类中定义的函数类型,那么在传入Lambda表达式时将会自动拥有StringBuilder的上下文。下面看下otherApply()
的使用
val stringBuilder = StringBuilder()
val result = stringBuilder.otherApply {
append("hello")
append("123")
}
Log.e(tag, result.toString())
可以看到和apply
标准函数的使用完全一样,只不过apply
适用所有类的使用,而otherApply
只局限于StringBuilder的使用,如果想实现apply
的函数的这个功能,就需要借助Kotlin泛型才可以。
2、内联函数
2.1、高阶函数的实现原理
学习内联函数前我们先来学习一下高级函数的实现原理。这里仍然使用刚才编写的num1AndNum2()
函数为例。
fun num1AndNum2(num1: Int, num2: Int, block: (Int, Int) -> Int): Int {
return block(num1, num2)
}
//调用
val minusResult = num1AndNum2(20, 30) { n1: Int, n2: Int -> n1 - n2 }
Log.e(tag, "$minusResult")
上面是Kotlin中高阶函数的基本用法,我们知道Kotlin代码最终会编译成Java字节码的,而Java中是没有高阶函数概念的,其实Kotlin编译器最终会把Kotlin中高阶函数的语法转换成Java支持的语法结构,上述的Kotlin代码大致被转换成如下Java代码。
public static int num1AndNum2(int num1, int num2, Function operation){
return (int)operation.invoke(num1,num2);
}
public void test(){
int minusResult=num1AndNum2(10, 20, new Function() {
@Override
public Integer invoke(Integer num1,Integer num2) {
return num1+num2;
}
});
}
考虑到可读性,我们对代码做了调整,并不是严格对应了Kotlin转成Java的代码。这里第三个参数变成了Function
接口,这是Kotlin的内置接口,里面有一个待实现的invoke()
函数。而num1AndNum2()
其实就是调用了Function接口的invoke()函数,并把num1和num2参数传了进去。
在调用num1AndNum2
函数时,之前的Lambda表达式变成了Function接口的匿名类实现,然后在invoke函数中实现了num1+num2
的逻辑。
这就是高阶函数背后的原理,原来传入的Lambda表达式在底层被匿名类所代替,这也就说明我们每调用一次Lambda表达式就会创建一个匿名类对象,当然会带来额外的内存和性能开销。
而Kotlin中的内联函数
就是为了解决这个问题的,它可以将使用Lambda表达式运行时的开销完全消除。
2.2、内联函数的使用以及原理
内联函数的使用比较简单就是在定义的高阶函数时加上inline
关键字即可。
inline fun num1AndNum2(num1: Int, num2: Int, block: (Int, Int) -> Int): Int {
return block(num1, num2)
}
内联函数的原理
内联函数的原理也很简单:Kotlin编译器在编译时把内联函数内代码自动替换到要调用的地方,这样就解决了运行时的内存开销。
下面看下替换的过程
步骤一、将Lambda表达式的代码替换到函数类型参数调用的地方
步骤二、将内联函数中全部代码替换到函数调用的地方
image.png
最终替换后的代码为
val minusResult =20-30
正是如此内联函数才能完全消除Lambda表达式运行时带来的额外内存开销。
3、noinline和crossinline
3.1、noinline
一个高阶函数中接收两个或更多函数类型的参数,如果高阶函数被inline
修饰了,那么所有函数类型的参数均会被内联,如果想某个函数类型的参数不被内联,可以用关键字noinline
修饰。
inline fun test(block1: () -> Unit, noinline block2: () -> Unit) {
}
可以看到test
被inline
修饰,本来block1和block2
这两个函数类型参数所引用Lambda表达式均被内联。由于我们在block2
前加上了noinline
关键字,那么只有block1
这个函数类型参数所引用的Lambda表达式被内联。
既然内联函数能消除Lambda表达式运行时带来的内存的额外开销,那么为什么还提供了一个noinline
来排除内联呢?
- 原因一:内联函数类型的参数在编译期间会进行代码替换,所以内联的函数类型的参数算不上真正的参数,非内联的函数类型的参数可以作为真正的参数传递给任何函数。内联函数类型的参数只能传递给另一个内联函数。这也是它最大的局限性。
- 原因二:内联函数和非内联函数有一个重要的区别:内联函数所引用的Lambda表达式中可以使用return来进行函数的返回,而非内联函数只能进行局部返回。
fun printString(str: String, block: (String) -> Unit) {
Log.e("LoginActivity", "printString begin")
block(str)
Log.e("LoginActivity", "printString end")
}
fun main(){
Log.e(tag, "mainbegin")
printString("") {
Log.e(tag, "lambda begin")
if (it.isEmpty()) return@printString
Log.e(tag, "lambda end")
}
Log.e(tag, "mainend")
}
这里定义了一个非内联的高阶函数,在Lambda表达式中如果传入的字符串为空,则直接返回,此时Lambda表达式中只能使用return@printString
进行局部返回。打印结果如下:
main begin
printString begin
lambda begin
printString end
main end
可以看到lambda end
并没有输出,因为输入的字符串为空,则局部返回不再执行Lambda表达式中的函数,所以Log.e(tag, "lambda end")
没有执行。
下面我们声明一个内联函数printStr
inline fun printStr(str: String, block: (String) -> Unit) {
Log.e("LoginActivity", "printString begin")
block(str)
Log.e("LoginActivity", "printString end")
}
fun main(){
Log.e(tag, "main begin")
printStr("") {
Log.e(tag, "lambda begin")
if (it.isEmpty()) return
Log.e(tag, "lambda end")
}
Log.e(tag, "main end")
}
由于printStr
是内联函数,我们可以在Lambda表达式中使用return
进行返回,打印结果如下:
main begin
printString begin
lambda begin
在传入的字符串为空时,返回出最外层的函数,所以lambda end和printString end和click end
将不会被输出。
3.2、crossinline
将高阶函数声明成内联函数是一种良好的习惯,事实上绝大多数高阶函数是可以被声明成内联函数的,但是也有例外的情况。观察下面的代码
inline fun runRunnable(block:()->Unit){
val runnable= Runnable {
block()
}
runnable.run()
}
这段代码如果没有加上inline
关键字是完全可以正常工作的,但是加上inline之后就会报如下错误:
首先我们在内联函数
runRunnable
中创建一个runnable对象,并在Runnable的Lambda表达式中传入的函数类型参数,而Lambda表达式在编译的时候会被转换成匿名类的实现方式,也就是说上面代码是在匿名类中传入了函数类型的参数。而内联函数所引用的Lambda表达式允许使用return进行函数的返回,但是由于我们是在匿名类中调用的函数类型参数,此时不能进行外层调用函数的返回,最多只能进行匿名类中的方法进行返回,因此就提示了上述错误。
也就是说:如果我们在高阶函数中创建了Lambda或匿名类的实现,在这些实现中调用函数类型参数,此时再将高阶函数声明成内联,肯定会报上面的错误。
那么如何在这种情况下使用内联函数呢?这就需要关键字
crossinline
inline fun runRunnable(crossinline block:()->Unit){
val runnable= Runnable {
block()
}
runnable.run()
}
经过前面的分析可知,上面错误的原因:内联函数中允许使用return关键字和高阶函数的匿名类的实现中不能使用return之间造成了冲突。而crossinline关键字用于保证在Lambda表达式中一定不使用return关键字,这样冲突就不存在了。但是我们仍然可以使用return@runRunnable
进行局部返回。总体来说,crossinline
除了return用法不同外,仍然保留了内联函数的所有特性。