Kotlin-inline, noinline, crossinline关键字解析——高阶函数详解(第一行代码Kotlin学习笔记7)

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()
}

下面我们来分析一下问什么会有这个提示:

  1. 该函数中,我们创建了Runnable对象,我们知道Runnable的Lambda表达式在编译时会转换成匿名内部类。
  2. 我们在该Lambda中传入了函数类型参数。
  3. 内联函数的函数类型参数中是可以使用return返回的,并且是将最外层调用函数返回。
  4. 但上面代码最多只能对匿名类中的函数调用进行返回,也就是最多返回到Runnable层。而且我们知道高阶函数的实现类中不允许使用return关键字。
  5. 这样条件3和条件4明显冲突,所以就有了这个错误提示。

我们使用了crossinline关键字之后,Runnable的Lambda表达式中就不能再使用return关键字进行返回了,但是我们仍然可以使用return@runRunnable的写法进行局部的返回。除此之外,其它内联函数的特性仍然保留。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值