【17】Kotlin语法进阶——Kotlin高阶函数基础

提示:此文章仅作为本人记录日常学习使用,若有存在错误或者不严谨得地方,欢迎各位在评论中指出。

文章目录

  • 一、高阶函数详解
    • 1.1 函数类型的结构
    • 1.2 高阶函数的定义
    • 1.3 通过Lambda表达式调用高阶函数
    • 1.4 apply函数与高阶函数
  • 二、函数的内联优化
    • 2.1 inline和noinline

一、高阶函数详解

1.1 函数类型的结构

如果一个函数接收另一个函数作为参数或者返回值的类型是另一个函数,那么该函数就称为高阶函数。接下来我们来学习一个新概念:函数类型。在前面你已经接触了Int整型、Boolean布尔类型、String字符串类型等,而Kotlin中新增加了一个函数类型。如果我们将一个函数类型当作一个函数的参数或者返回值,那么这就是一个高阶函数了。下面我们来看一下函数类型的结构

 参 数 类 型    返回值类型
(String,Int) -> Unit

->左边的部分用来声明该函数的参数类型,而->右边的部分用来声明该函数的返回值类型。当返回值为Unit时,表示该函数没有返回值,大致相当于Java语言中的void。

1.2 高阶函数的定义

在知道了函数类型的声明后,现在我们就可以尝试声明一个高阶函数了:

/*高阶函数*/
                    参数名   参数类型      返回值类型
fun higherExample(func: (String: Int) -> Unit) {

}

/*普通函数*/
          参数名  参数类型
fun normalExample(str: String, int: Int) {

}

可以看到,我们首先声明了一个higherExample()方法,并且让它接收一个函数类型作为参数:func是参数名,(String: Int)代表参数类型,Unit表示函数类型参数没有返回值。与普通函数相比,高阶函数就是在参数名后面加上一对括号,在括号内传入参数类型,然后添加了一个类似于Lambda表达式的结构->,并添加了一个返回值类型。接下来我们尝试根据该结构定义一个高阶函数吧!新建一个HigherOrderFunction.kt文件,然后在这个文件中编写如下代码:

               参数1        参数2          参数3
fun num1Andnum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
    val result = operation(num1, num2)
    return result
}

这里我们定义了一个num1Andnum2()高阶函数。这是一个非常简单的高阶函数,它一共要求接收三个参数。我们重点来看第三个参数,第三个参数位要求传入一个函数类型的参数。该函数类型参数需要传入两个Int类型参数,并返回一个Int类型的返回值。而num1Andnum2()高阶函数的函数体没有进行任何运算,只是将前两个参数传递给第三个参数,并获取第三个参数的返回值。高阶函数现在定义好了,那么该如何使用呢?我们还需要定义与其函数类型相匹配的方法才行,在HigherOrderFunction.kt文件中添加如下代码:

//加运算
fun plus(num1: Int, num2: Int): Int {
    return num1 + num2
}
//减运算
fun minus(num1: Int, num2: Int): Int {
    return num1 - num2
}

这里定义了两个用于运算的方法,并且这两个方法的参数和返回值类型都与num1Andnum2()高阶函数中函数类型的参数和返回值类型是一样的。到这里我们就可以调用num1Andnum2()高阶函数了,在main()方法中编写如下代码:

val num1 = 100
val num2 = 80
val result1 = num1Andnum2(num1, num2, ::plus)
val result2 = num1Andnum2(num1, num2, ::minus)
println("result1 is :${result1}")
println("result2 is :${result2}")

——>输出
result1 is :180
result2 is :20

可以看到,我们在调用高阶函数num1Andnum2()的时候,第三个参数使用了::plus和::minus这种写法。这是一种函数引用方式的写法,表示将plus()和minus()方法作为参数传递给num1Andnum2()函数。所以num1Andnum2()高阶函数会通过传入的参数类型来决定具体的运算逻辑,当我们传入::plus时会执行加运算,当我们传入::minus时会执行减运算。

1.3 通过Lambda表达式调用高阶函数

虽然上面这种写法是完全符合语法规则并且可以运行的,但是如果每次调用高阶函数时还得先定义一个与其函数类型相匹配的函数,这也太麻烦了。不用担心,Kotlin还支持通过Lambda表达式来调用高阶函数!上面的代码如果要用Lambda表达式来实现的话,代码如下:

main(){
    val num1 = 100
    val num2 = 80
    val result1 = num1Andnum2(num1, num2) { n1, n2 -> n1 + n2 }
    val result2 = num1Andnum2(num1, num2) { n1, n2 -> n1 - n2 }
    println("result1 is :${result1}")
    println("result2 is :${result2}")
}     

——>输出
result1 is :180
result2 is :20

可以看到,Lambda表达式同样可以完整的表达一个函数的参数声明和返回值声明(Lambda表达式的最后一行代码会自动作为返回值),但是写法却更加清晰和简介。

1.4 apply函数与高阶函数

在之前文章中我们有介绍过apply标准函数,让我们先来回顾一下吧!


apply函数是要通过某个对象来调用的,并且接收一个Lambda表达式作为参数,然后会将调用apply函数的对象作为Lambda表达式的上下文。

val myList = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
val result = StringBuilder().apply {
    append("Start eating fruits.\n")
    for (fruit in myList) {
        append(fruit).append("\n")
    }
    append("Ate all fruits.")
    //自动返回一个StringBuilder对象 然后被保存在result中
}
println(result.toString())

输出->
Start eating fruits.
Apple
Banana
Orange
Pear
Grape
Ate all fruits.

apply函数是没有办法指定返回值的,只能返回调用apply对象本身。所以这里的result对象其实是一个StringBuilder对象,我们打印的时候时需要通过StringBuilder.toString()方法才行。


接下来我们就通过高阶函数来模仿实现一个类似的功能吧!修改HigherOrderFunction.kt的代码:

       扩展函数            将函数类型定义到StringBuilder类中
fun StringBuilder.build(block: StringBuilder.() -> Unit): StringBuilder {
    return this
}

我们为StringBuilder定义了一个build()扩展函数,它可以接受一个lambda表达式作为参数,也可以接受一个普通函数作为参数,并且返回值类型是StringBuilder。注意,这里我们在build()扩展函数前面加上了一个StringBuilder.的语法结构。其实这才是定义高阶函数完整的语法规则,在函数类型前面加上类名.的结构表示将这个函数类型定义到哪个类中
这里将函数类型定义到StringBuilder类中的好处是:我们在调用build()函数时传入的Lambda表达式将会自动拥有StringBuilder的上下文,同时这也是apply标准函数的实现方式。
现在我们就用自己创建的build函数来简化StringBuilder构建字符串的方式了:

val myList = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
val result = StringBuilder().build{
    append("Start eating fruits.\n")
    for (fruit in myList) {
        append(fruit).append("\n")
    }
    append("Ate all fruits.")
    //自动返回一个StringBuilder对象 然后被保存在result中
}
println(result.toString())

输出->
Start eating fruits.
Apple
Banana
Orange
Pear
Grape
Ate all fruits.

可以看到build函数的用法和apply函数基本一模一样,只不过我们编写的build函数目前只能用在StringBuilder类上面,而apply函数时可以作用在所有类上面。

二、函数的内联优化

为了更好的讲解内联函数,我们需要先了解一下高阶函数的实现原理。拿之前的num1Andnum2()函数来举例,代码如下:

               参数1        参数2          参数3
fun num1Andnum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
    val result = operation(num1, num2)
    return result
}

main(){
    val num1 = 100
    val num2 = 80
    val result1 = num1Andnum2(num1, num2) { n1, n2 -> n1 + n2 }
    println("result1 is :${result1}")
}   

我们知道,Kotlin代码最终还是会被编译成Java字节码的,但Java中并没有高阶函数的概念。其实,我们一直使用的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编译器将内联函数中的代码在编译时自动替换到调用它的地方,这样就不会存在性能开销了。下面我们来看一下内联函数内部是如何替换的。
第一步替换过程:
【17】Kotlin语法进阶——Kotlin高阶函数基础_第1张图片
第二步替换过程:
【17】Kotlin语法进阶——Kotlin高阶函数基础_第2张图片
最终代码:
【17】Kotlin语法进阶——Kotlin高阶函数基础_第3张图片
正是如此,内联函数才可以完全消除Lambda表达式带来的性能开销。

2.1 inline和noinline

在Kotlin中,有inline、noinline和crossinline这三个关键字,它们都是用来优化函数的。

  • inline:内联,用于修饰函数。被修饰的函数称为内联函数,内联函数中的代码可以直接嵌入到调用处,从而减少方法栈的层级与函数类型对象的创建。
  • noinline:不内联,用于修饰内联函数中的函数类型参数,关闭对函数类型参数的内联优化。这样就可以摆脱inline带来的“不能把函数类型参数当对象使用”的限制。
  • crossinline:交叉内联,用于修饰内联函数中的函数类型参数,使函数类型参数能被间接调用。

在前面的内容中,我们已经学会了通过inline关键字来声明一个内联函数,接下来我们看一些更加特殊的情况。当一个高阶函数中接收了两个或多个函数类型的参数时,如果我们给高阶函数加上inline关键字,那么Kotlin编译器会自动将所有引用的Lambda表达式全部进行内联。
但是,如果我们只想内联其中的一个Lambda表达式该怎么办呢?我们可以通过noinline关键字来实现高阶函数的部分内联:

//部分内联示例代码
                       内联                 不内联
inline fun inlineTest(block1: () -> Unit, noinline block2: () -> Unit) {
}

通过inline关键字声明了inlineTest()函数,原本block1和block2两个参数所引用的Lambda表达式都会被内联。但是我们给block2参数前面加上了noinline关键字,现在就只有block1参数所引用的Lambda表达式是内联的。前面我们已经讲了很多内联的好处,现在为什么又要不内联呢?这是因为:

  1. 内联的函数参数类型只允许传递给另外一个内联函数,非内联的函数类型参数可以自由的传递给其他任何函数。
  2. 内联函数所引用的Lambda表达式可以使用return关键字来进行函数返回的,而非内联函数只能进行局部返回

下面我们通过一个例子重点看一下第2点:

fun printString(str: String, block: (String) -> Unit) {
    println("printString begin")
    block(str)
    println("printString end")
}

fun main() {
    println("main start")
    val str = ""
    printString(str) { s ->
        println("lambda start")
        if (s.isEmpty()) return@printString
        println(s)
        println("lambda end")
    }
    println("main end")
}

这里我们定义了一个printString()高阶函数,用于在Lambda表达式中打印所传入的字符串参数。但是如果字符串为空,就不进行打印。需要注意的是,Lambda表达式中是不允许直接使用return关键字的,这里使用了return@printString的写法,用来表示进行局部返回(只退出printString()函数),并且不再执行Lambda表达式的剩余部分代码。如果我们刚好传入一个空的字符串,会有如下打印:

——>输出
main start
printString begin
lambda start
printString end
main end

可以看到,在Lambda表达式中return@printString语句之后的代码都没有执行,其他日志是可以正常打印的。这就说明return@printString确实只能进行局部返回(返回printString()函数)。但是,如果我们将printString()函数声明成一个内联函数,那么情况就不一样了:

inline fun printString(str: String, block: (String) -> Unit) {
    println("printString begin")
    block(str)
    println("printString end")
}

fun main() {
    println("main start")
    val str = ""
    printString(str) { s ->
        println("lambda start")
        if (s.isEmpty()) return
        println(s)
        println("lambda end")
    }
    println("main end")
}

会有如下打印:

——>输出
main start
printString begin
lambda start

可以看到,不管是main()函数还是printString()函数都在return关键字后终止执行了。由于crossinline关键字的使用场景很少,只在特殊场景下使用,暂时不做讲解。

你可能感兴趣的:(奇妙的Kotlin之旅,kotlin,微信,开发语言)