Android开发基础——Kotlin:高阶函数

高阶函数

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

同时Kotlin中还增加了函数类型的概念,因此如果将函数类型添加到一个函数的参数声明或返回值声明中,该函数就是一个高阶函数了。

函数类型的语法规则为:

(String, Int) -> Unit

上边的定义中,->左边的部分用于声明该函数接收什么参数,多个参数之间使用逗号隔开,如果不接收任何参数,则使用空括号即可,->右边的部分用于声明该函数的返回值是什么类型,如果没有返回值就是Unit。

fun num1AndNum2(num1:Int, num2:Int, operator:(Int, Int) -> Int):Int {
    val result = operator(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() {
    println(num1AndNum2(1,2, ::plus))
    println(num1AndNum2(1,2, ::minus))
}

上面的代码中,同时定义了plus和minus方法,并作为参数传入num1AndNum2方法,这意味着num1AndNum2方法可以忽略传入函数类型参数的名称,而只关注函数类型参数的参数,只要是相同的参数类型都可作为参数传入,而在函数体中使用同一接口进行不同的操作。

需要注意的是,在调用时需要使用::plus这样的形式,以表示这是一个函数类型的参数,这是函数引用方式的写法。

当然,高阶函数还可以使用Lambda,匿名函数,成员引用的形式进行调用。

fun main() {
    println(num1AndNum2(1,2) {n1, n2 -> n1 + n2})
    println(num1AndNum2(1,2) {n1, n2 -> n1 - n2})
}

上面的代码就是Lambda的形式,和之前的执行结果是一样的。

再看一个例子:

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

fun main() {
    val list = listOf("Apple","Banana","Orange","Pear","Grape")
    val result = StringBuilder().build {
        append("Start eating fruits.\n")
        for (fruit in list) {
            append(fruit).append("\n")
        }
        append("Ate all fruits.")
    }
    println(result.toString())

    val result2 = StringBuilder().apply {
        append("Start eating fruits.\n")
        for (fruit in list) {
            append(fruit).append("\n")
        }
        append("Ate all fruits.")
    }
    println(result2.toString())
}

上面两种写法是等效的。只是采用高阶函数的写法中,首先为StringBuilder定义了build扩展函数,该扩展函数接收一个函数类型参数,并且返回值类型为StringBuilder。

然后参数类型参数采用了StringBuilder.()的形式,表明该函数是StringBuilder中的函数。而实际调用中传入的Lambda表达式则会自动拥有StringBuilder的上下文,即和apply的执行结果一致了。

内联函数

内联函数只需要在定义高阶函数时加上inline关键字即可,如:

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

内联函数就是在代码编译时执行代码的自动替换,这种方式能够消除使用Lambda表达式作为高阶函数参数时产生的运行时开销。

这里的内联函数定义和C++中的内联函数定义也类似,总之能够消除Lambda表达式所带来的运行时开销。

noinline/crossinline

而如果一个高阶函数接受了两个或多个函数类型的参数,此时给函数添加inline关键字,Kotlin编译器会自动将所有引用的Lambda表达式进行内联。

而如果只需要内联其中的一个Lambda表达式,可以使用noinline关键字:

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

上面的代码因为noinline的作用,只会对block1进行内联。而之所以取消内联则是因为内联的函数类型参数在编译的时候会进行代码替换,因此其没有真正的参数属性,而非内联的函数类型参数则可以自由传递给任何函数,因为其就是一个真正的参数,而内联的函数类型参数只允许传递给另外一个内联函数。

同时,需要注意的是,内联函数所引用的Lambda表达式中是可以使用return关键字进行函数返回的,而非内联函数只能进行局部返回。

fun printString(str:String, block:(String) -> Unit) {
    println("printString start")
    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的写法,表示进行局部返回,并且不再执行Lambda表达式的剩余部分代码。

这里传入了一个空的字符串参数,结果为:

main start
printString start
lambda start
printString end
main end

从上述结果来看,return@printString确实只能进行局部返回。

这里将printString函数声明为一个内联函数:

inline fun printString(str:String, block:(String) -> Unit) {
    println("printString start")
    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 start
lambda start

现在printString函数声明为了内联函数,也就可以在Lambda表达式中使用return关键字,此时return表示返回外层的调用函数,也就是main函数。

inline fun runRunnable(block:() -> Unit) {
    val runnable = Runnable {
        block()
    }
    
    runnable.run()
}

虽然可以使用inline来声明高阶函数以消除Lambda的运行时开销。但上述的代码会报错,这是因为在runRunnable函数中,创建了Runnable对象,并在Runnable的Lambda表达式中调用了传入的函数类型参数。而Lambda表达式在编译的时候会被转换为匿名类的实现方式,也就是说,上述代码实际上是在匿名类中调入了传入的函数类型参数。

而内联函数所引用的Lambda表达式允许使用return关键字进行函数返回,而匿名类中调用的函数类型参数,不可能进行外层调用函数返回,最多只能对匿名类中的函数调用进行返回,因此就会报错。

也就是说,如果在高阶函数中创建了另外的Lambda或者匿名类的实现,并且在这些实现中调用函数类型参数,此时将高阶函数声明为内联函数,就会报错。

而借助crossiinline关键字就可以解决该问题:

inline fun runRunnable(crossinline block:() -> Unit) {
    val runnable = Runnable {
        block()
    }
    
    runnable.run()
}

 之前的报错是因为内联函数中的Lambda表达式允许使用return关键字和高阶函数的匿名类实现中不允许使用return关键字之间产生了冲突。而crossinline关键字则保证在内联函数的Lambda表达式中一定不会使用return关键字,这样冲突就不存在了。

声明了crossinline之后,就无法在调用Lambda表达式中使用return关键字进行函数返回,但是仍然可以使用reutrn@func进行局部返回。

总的来说,除了上述区别之外,绝大多数高阶函数都是可以直接声明为内联函数的,这也是一种良好的编程习惯。

猜你喜欢

转载自blog.csdn.net/SAKURASANN/article/details/127041950