6.5.1 定义高阶函数

如果我们想定义自己的函数式API ,那就得借助高阶函数来实现了。

如果一个函数接收另一个函数作为参数,或者返回值的类型是另一个函数,那么这个函数就称为高阶函数。

一个函数怎么能接收另一个函数作为参数呢?这就设计另外一个概念了:函数类型。我们知道,编程语言中有整型、布尔型等字段类型,而Kotlin 又增加了一个函数类型概念。如果我们将这种函数类型添加到一个函数的参数声明或者返回值声明当中,那么这就是一个高阶函数了。

接下来就学习一下如何定义一个函数类型。不同于定义一个普通的字段类型,函数类型的语法规则是有点特殊的,基本规则如下:

(String,Int) -> Unit 

这是啥???既然是定义一个函数类型,那么最关键的就是要声明该函数接收什么参数,以及它的返回值是什么。 因此, -> 左面的部分就是用来声明该函数接收什么参数的,多个参数之间使用逗号隔开,如果不接收任何参数,写一对空括号就可以了。而 -> 右边的部分用于声明该函数的返回值是什么类型,没如果没有返回值就使用Unit ,大致相当于 Java 的 void 。

再解释一下  (String,Int) -> Unit  这就是一个函数类型参数的声明方式,看好了是一个参数, param : (String,Int) -> Unit  这种格式。

fun main() {
    example({s: String, i: Int -> 
        println("param1:$s,param2:$i")
    })
}
fun main() {
    example(fun (s,i){
        println("$s$i")
    })
}
fun main() {
    example(::funParam)
}
fun funParam(s: String,i: Int){
    println("$s$i")
}
fun example(func:(String,Int) -> Unit){
    func("Hello",123)
}

wow,wow,wow,是不是有点像Java 中的接口调用。其实就是将一个可传入的函数类型参数 进行定义,然后在外部将一个同样格式的函数 当做实参传入。

看这三个main 函数是三种不同的传递参数形式第三个是 一种函数引用方式的写法。表示将funParam 函数作为参数传递给example 函数。

其实第二中和第三种都是一样的,只有第一种是Lambda表达式。毕竟Kotlin 支持Lambda 表达式,这里说一下为什么能传递一个普通函数 还能传递Lambda 表达式,这普通函数和Lambda 表达式难道有什么关联吗?对没错,其实Lambda 表达式就是一个类中的函数,如果编译成Java 的话我们会更直观的发现,任何Lambda 表达式都会编译成一个 Function 类中的 invoke 函数。我们传递进的Lambda 表达式也可以理解为传递了个函数,这样理解就通顺了。

在第三章学习的apply 标准函数可以给予Lambda 表达式中提供一个上下文,当需要连续调用同一个对象的多个方法时,apply 函数让代码更加的精简,那么这个是怎么实现的呢?我们来实现一下吧:

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

请注意看,我们定义了一个扩展函数,其中内部的参数为一个函数类型,形参为 block ,() 函数类型不需要传入参数,返回值为空,那么这个StringBuilder. 是什么呢(注意后面有个点 )? 

其实这才是定义高阶函数的完整语法规则,在函数类型前面加上ClassName.  就表示这个函数类型定义在哪个类当中。

这样定义的好处就是 我们的Lambda 表达式会拥有这个类的上下文(环境),如果不理解就去这样定义一个高阶函数,然后去调用一下就知道了。

 

6.5.2 内联函数的作用

当我们使用Lambda表达式时,它会被正常地编译成匿名类。这表示每调用一次Lambda表达式,一个额外的类就会被创建,并且如果Lambda捕捉了某个变量,那么每次调用的时候都会创建一个新的对象,这会带来运行时的额外开销,导致使用Lambda比使用一个直接执行相同代码的函数效率更低。

为了解决这个问题,Kotlin 提供了内联函数的功能,它可以将使用Lambda 表达式带来的运行时开销完全消除。

内联函数的用法非常简单,只需要在定义高阶函数时加上 inline 关键字的声明即可:

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

那么内联函数的工作原理是什么呢?其实并不复杂,就是Kotlin 编译器会将内联函数中的代码在编译的时候自动替换到调用它的地方(就是直接替换成为了Lambda函数体中的代码),这样也就不存在运行时的开销了(直接直接替换成为了Lambda函数体中内部的代码就不用创建内部类了)。

替换一共分为两步,第一步将传入Lambda 表达式在调用处直接替换为表达式函数体中的代码,第二部 将内联函数 函数体中替换后的代码直接替换在调用的内联函数的位置。

6.5.3 oninline 和 crossinline 

oninline

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

我们知道内联函数的好处就是在编译阶段将Lambda 表达式内的函数体直接编译在调用处,无需在运行调用时频繁的创建匿名内部类,缺点也是有那么一点的,class字节码变大了。

既然内联函数这么好,为什么要有非内联这个oninline 关键字用于排除内联功能呢?一个正常的Lambda 非内联 函数类型参数是内部是创建了一个匿名内部类来传递的,这是有实体实例的,可以自由的传递给任何函数,但是内联函数的函数类型参数编译后就是一堆代码了,没有真正的参数,而且内联的函数类型参数只允许传递给另一个内联函数,这也是它最大的局限性。

另外内联函数和非内联函数还有一个重要的区别,就是内联函数引用的Lambda 表达式中是可以使用return 关键来进行函数返回的(就是如果内联函数中的函数类型参数函数体中如果调用了return 关键字,同时内联函数调用了这个函数类型参数,那么也就相当于调用者调用了return的效果,因为内联函数的原因最后编译会把函数类型参数函数体的代码直接编译到调用处!),而非内联函数就只能局部返回(局部返回相当于正常的方法进行返回,return的只是当前的方法)。

 

我们上面说了,内联函数的函数类型参数只允许传递给另一个内联函数,如果传递给一个非内联函数就会编译报错,报错的原因就是 return 的返回,非内联函数类型是不能return返回的,只能局部返回,这个时候我们crossinline 的关键字就有用武之地了,将crossinline 添加到函数类型 参数前进行修饰,告诉编译器我们保证内联函数的Lambda表达式不会出现 return 关键字,这时编译器就会禁止我们在函数体的return行为,如果return 就会编译报错,这样就可以正常编译通过了,虽然无法调用 return 返回,但是还可以调用局部返回。

这个部分挺难的,多看几遍,还是不会就去翻阅一下他人的资料再做吸收和理解。