1. 高阶函数定义
高阶函数和Lambda的关系密不可分,不熟悉Lambda的同学请移步: Kotlin-集合与Lambda编程map,filter,any,all函数的使用(第一行代码Kotlin学习笔记3).我们学过的map,filter,apply等函数,都会要求传入一个Lambda表达式,这样的函数就可以称为具有函数式编程风格的API,如果想要定义自己的函数式API,就需要借助高阶函数来实现。
- 定义:如果一个函数接收另一个函数作为参数,或者返回值的类型是另一个函数,那么该函数就称为高阶函数。
光看定义,我们可能会觉得抽象,现在我们需要了解一个概念,叫做“函数类型”,好比我们知道整型,布尔型等字段类型,函数类型是Kotlin中的一个新增概念。如果我们某个函数的返回值或者参数声明为函数类型,那么该函数就是高阶函数了。
- 格式:(String,Int) -> Int
->左边的()中表示该函数类型的参数,->右边的Int表示该函数类型的返回值类型。这时我们要了解一个关键字Unit,这个关键字大致相当于Java中的void,所以见到返回值类型是Unit时,我们需要知道是什么意思。
接下来我们看代码来具体学习一下:
//定义高阶函数
fun operationTwoNum(num1:Int,num2:Int,operation:(Int,Int) ->Int):Int{
val result = operation(num1,num2)
return result
}
//定义要使用的函数
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 = 40
val resultPlus = operationTwoNum(num1,num2, ::plus)
val resultMinus = operationTwoNum(num1,num2, ::minus)
println("resultPlus is $resultPlus")
println("resultMinus is $resultMinus")
}
//resultPlus is 140
//resultMinus is 60
这是一个简单的高阶函数,并没有什么实际的意义,我们在调用高阶函数时,传入的函数参数使用了::plus和::minus的写法,这是一种函数引用方式的写法,表示将plus()和minus()函数作为参数传递给operationTwoNum()函数。
看到这里,我们会不会觉得和Lambda有异曲同工的感觉呢,接下来我们看下上段代码的Lambda版本:
fun main() {
val num1 = 100
val num2 = 40
val resultPlus = operationTwoNum(num1,num2) {n1,n2-> n1 + n2}
val resultMinus = operationTwoNum(num1,num2) {n1,n2-> n1 - n2}
println("resultPlus is $resultPlus")
println("resultMinus is $resultMinus")
}
此时我们回顾下之前学习的apply函数:Kotlin-标准函数with,run和apply及静态方法@JvmStatic和顶层方法(第一行代码Kotlin学习笔记5).我们接下来可以使用高阶函数的方式模仿一个类似于apply函数的功能:
fun StringBuilder.myApply(block:StringBuilder.() -> Unit):StringBuilder{
block()
return this
}
我们给StringBuilder定义了扩展函数myApply,并且接收一个函数类型的参数,返回值为StringBuilder,但是我们在函数类型参数声明时,也加了StringBuilder.,这表示这个函数类型是定义在StringBuilder中的。我的理解是其实也相当于扩展函数。这里我们将函数类型定义在StringBuilder,我们就可以在传入的Lambda表达式中使用StringBuilder的上下文了。接下来我们就可以使用了:
fun main() {
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
val result = StringBuilder().myApply {
for (fruit in list){
append(fruit).append(" ")
}
}
println("result is ${result.toString()}")
}
//result is Apple Banana Orange Pear Grape Watermelon
2. 高阶函数应用
熟悉Android的童鞋们一定对sp的使用也很熟悉,但是如果没有高阶函数,我们的代码应该是这样的:
fun saveSp(str:String){
val sharedPreferences = getSharedPreferences("data", Context.MODE_PRIVATE)
val edit = sharedPreferences.edit()
edit.putString("key","value")
edit.apply()
}
但是如果使用高阶函数,我们的代码将会简单很多,我们先以扩展函数的方式向SharedPreferences中添加函数open(),当然我么的扩展函数最好定义在SharedPreferences.kt的文件中。
fun SharedPreferences.open(block:SharedPreferences.Editor.() -> Unit){
val editor = edit()
editor.block()
editor.apply()
}
因为block()其实是定义在了SharedPreferences.Editor中,所以我们可以直接在我们的Labmda中使用put()方法,而且我们在扩展函数open()中,已经调用了apply()函数,因此,在我们的Labmda中也就不再需要调用apply()了。
然后我们在使用的时候就可以又可以愉快的少写它个好几行代码了:
fun saveSp(str:String){
getSharedPreferences("data", Context.MODE_PRIVATE).open {
putString("key","value")
}
}
其实google在扩展库KTX中已经帮我们定义了edit()函数来实现同样的功能,并且Androidstudio创建项目时会自动依赖该库,我们也可以自行依赖:
implementation 'androidx.core:core-ktx:1.0.2'
然后我们就可以愉快的使用了:
fun saveSp(str:String){
getSharedPreferences("data", Context.MODE_PRIVATE).edit{
putString("key","value")
}
}
3. 内联函数inline
我们先简单看下高阶函数的大致原理,既然Kotlin最终要编译为Java字节码,但Java中并没有高阶函数,那是怎么转换的呢?
//定义高阶函数
fun operationTwoNum(num1:Int,num2:Int,operation:(Int,Int) ->Int):Int{
val result = operation(num1,num2)
return result
}
fun main() {
val num1 = 100
val num2 = 40
val resultPlus = operationTwoNum(num1,num2) {n1,n2-> n1 + n2}
println("resultPlus is $resultPlus")
}
然后把它翻译成Java代码,大概是这个样子的,此处请不要深究,只说大概原理:
class JavaClass {
public static int operationTwoNum(int num1,int num2,Function<Integer> operation){
int result = operation.invoke(num1,num2);
}
public static void main(){
final int num1 = 100;
final int num2 = 40;
int result = operationTwoNum(num1, num2, new Function<Integer>() {
@Override
public Integer invoke(Integer n1,Integer n2) {
return n1+n2;
}
});
}
}
interface Function<T>{
T invoke(T n1,T n2);
}
之前的Lambda表达式变成了Function接口的匿名类实现,然后在invoke()函数中做需要的操作逻辑,这是我们会发现,原来我们一直用的Lambda表达式,最后备转换成了匿名类的实现方式。这表示我们每用一次Lambda,就会有一个匿名实例被创建,造成额外的内存和性能开销。为了解决这个问题,Kotlin提供了内联函数功能。
内联函数的用法很简单,在定义高阶函数时加上inline关键字的声明即可。如下:
inline fun operationTwoNum(num1:Int,num2:Int,operation:(Int,Int) ->Int):Int{
val result = operation(num1,num2)
return result
}
内联函数的原理其实也很简单,Kotlin会将内联函数中的代码在编译时自动替换到调用它的地方。这样就不会再创建内部类实例,也就不存在运行时开销了。例如:
inline fun operationTwoNum(num1:Int,num2:Int,operation:(Int,Int) ->Int):Int{
val result = operation(num1,num2)
return result
}
fun main() {
val num1 = 100
val num2 = 40
val resultPlus = operationTwoNum(num1,num2) {n1,n2-> n1 + n2}
println("resultPlus is $resultPlus")
}
第一步转化后是这样的:就是把作为参数的函数的代码,替换到了高阶函数中调用该函数的地方。
inline fun operationTwoNum(num1:Int,num2:Int,operation:(Int,Int) ->Int):Int{
val result = num1 + num2
return result
}
fun main() {
val num1 = 100
val num2 = 40
val resultPlus = operationTwoNum(num1,num2)
println("resultPlus is $resultPlus")
}
最后会变成这个样子:
fun main() {
val num1 = 100
val num2 = 40
val resultPlus = num1 + num2
println("resultPlus is $resultPlus")
}
内联函数的原理就是这样,如果我们的高阶函数中有不止一个函数类型参数,使用inline关键字会全部内联。但是内联函数也有它的弊端,因为内联的函数类型参数在编译时会进行代码替换,因此它没有真正的参数属性。非内联的函数类型参数可以自由的传递给其它任何函数,因为它就是一个真实的参数,而内联的函数类型参数只允许传递给另外的内联函数。而且内联函数所引用的Lambda表达式中是可以使用return关键字来进行函数返回的,并且返回的是最外层调用函数。而非内联函数只能进行局部返回。例如:
fun printStr(str:String,block:(String) -> Unit){
println("print begin")
block(str)
println("print end")
}
fun main() {
println("main start")
val str = "str content"
printStr(str){
println("lambda start")
return@printStr
println(str)
println("lambda end")
}
println("main end")
}
//main start
//print begin
//lambda start
//print end
//main end
我们会发现println(str)和println(“lambda end”)都没有执行,但是println(“main end”)执行了。
这里需要注意的是我们此处返回用的是 return@printStr 这样的写法,表示进行局部返回,Lambda剩余代码将不再执行。这么写是因为Lambda表达式中不允许直接使用return关键字进行返回。
接下来我们看下内联函数会有什么样的结果:
inline fun printStr(str:String,block:(String) -> Unit){
println("print begin")
block(str)
println("print end")
}
fun main() {
println("main start")
val str = "str content"
printStr(str){
println("lambda start")
return
println(str)
println("lambda end")
}
println("main end")
}
//main start
//print begin
//lambda start
我们会发现连println(“main end”)都没有执行。
4. noinline与crossinline
4.1 noinline
前面我们说了inline的弊端,那如果我们的内联函数中,只想内联其中的某一个Lambda或者某个Lambda不想内联怎么办呢?这时我们就可以使用noline关键字了,它的用法也同样简单:
inline fun nolineTest(block1:() -> Unit,noinline block2:() ->Unit){
//...
}
此处我们的block2加上了noinline关键字,那么我们在使用的时候,就不会对这个函数类型参数进行内联了。
4.2 crossinline
为了减少内存和性能上的开销,绝大多数的高阶函数都可以直接声明为内联函数,但为什么是绝大多数呢,接下来我们来看一个列外情况:
inline fun runRunnable(block:() -> Unit){
val runnable = Runnable{
block()
}
runnable.run()
}
如果不懂Runnable为什么这么写,请看:Kotlin-集合与Lambda编程map,filter,any,all函数的使用(第一行代码Kotlin学习笔记2)文章中Java 常用的函数式API部分。这段代码会直接报错
可以看到提示使用crossinline关键字来解决这个问题。我们可以将代码改成这个样子,就不会有错误提示了:
inline fun runRunnable(crossinline block:() -> Unit){
val runnable = Runnable{
block()
}
runnable.run()
}
下面我们来分析一下问什么会有这个提示:
- 该函数中,我们创建了Runnable对象,我们知道Runnable的Lambda表达式在编译时会转换成匿名内部类。
- 我们在该Lambda中传入了函数类型参数。
- 内联函数的函数类型参数中是可以使用return返回的,并且是将最外层调用函数返回。
- 但上面代码最多只能对匿名类中的函数调用进行返回,也就是最多返回到Runnable层。而且我们知道高阶函数的实现类中不允许使用return关键字。
- 这样条件3和条件4明显冲突,所以就有了这个错误提示。
我们使用了crossinline关键字之后,Runnable的Lambda表达式中就不能再使用return关键字进行返回了,但是我们仍然可以使用return@runRunnable的写法进行局部的返回。除此之外,其它内联函数的特性仍然保留。