天天看点

Kotlin学习路(七):高阶函数与内联函数关系

<本文学习郭神《第三行代码》总结>

定义用法

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

语法规则:(String, Int)-> Unit

1、在->左边的部分就是用来声明该函数接收什么参数,多个参数之间用逗号隔开,如果不接收任何参数,则用空括号,比如: ()-> Unit。

2、在右边则声明该函数返回的值类型,如果没有返回值就使用Unit,它相当于void。

比如,将上述函数类型添加到某个函数的参数声明或者返回值声明上,那么这个函数就是一个高阶函数了:

fun example(func: (String, Int) -> Unit){
func("aaa", 123)
}
           

在这里,example函数接收了一个函数类型的参数,因此example就是一个高阶函数。

高阶函数允许让函数类型的参数来决定函数的执行逻辑。即使是同一个函数参数,那么它的执行逻辑和最终的返回结果都可能是完全不同的。

例如:

定义一个方法num1AndNum2()的高阶函数,并让它接收两个整型和一个函数类型参数,并最终返回运算结果。

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

num1AndNum2()函数的第三个参数是一个接收两个整型参数,并有一个返回值的函数参数,这里还需要定义两个方法和上述函数参数类型匹配。

fun plus(num1: Int, num2: Int) : Int{
    return num1 + num2
}
fun minus(num1: Int, num2: Int) : Int{
    return num1 - num2
}
           

这两个函数的参数返回类型和num1AndNum2()的函数参数类型返回完全一样。

接下来开始使用这个方法:

fun main(){
    val num1 = 100
    val num2 = 10
    val result1 = num1AndNum2(num1, num2, :: plus)
    val result2 = num1AndNum2(num1, num2, :: minus)
    print("result1 is $result1")
    print("result2 is $result2")
}
           

这里第三个参数使用了 :: plus、:: minus这种写法,这是一种函数的引用方法,表示将plus和minus函数作为参数传递给num1AndNum2()函数。

使用这种函数引用的写法虽然能够正常工作,但是每次调用时都还需先定义与之匹配的方法,会很繁琐,所有,这里可以替换成Lambda表达式的方法实现。

上述代码就可以修改为:

fun main(){
    val num1 = 100
    val num2 = 10
    val result1 = num1AndNum2(num1, num2){
        n1, n2 -> n1 + n2
    }
    val result2 = num1AndNum2(num1, num2){
        n1, n2 -> n1 - n2
    }
           

Lambda表达式提供一个指定的上下文,当需要连续调用同一个对象的多个方法时,apply函数就可以让代码更精简,比如StringBuilder。

例如:

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

这里给StringBuilder定义一个build扩展函数,这个扩展函数接收一个函数参数,并且返回一个StringBuilder类型值。在函数前面加上ClassName. 就是表示这个函数类型是定义在哪个类中。

val list  = listOf("a", "b", "c")
val result = StringBuilder().build {
    for (s in list){
        append(s)
    }
}
print(s)
           

这里build函数的用法和apply用法一样,唯一区别就是build函数只是作用在StringBuilder上,而apply函数则是作用在所有类上,这就需要借助Kotlin泛型才行。

原理

现在知道高阶函数怎么用了,但是我们还需要知道它的原理。

还是用上述代码为例,调用num1AndNum2()函数,通过Lambda表达式传入两个整型参数,将代码转换成Java代码则是:

public static int num1AndNum2(int num1, int num2, Function operation){
    int result = (int)operation.invoke(num1, num2);
    return result;
}
public static void main(){
    int num1 = 100;
    int num2 = 10;
    int result = num1AndNum2(num1, num2, new Function() {
        @Override
        public int invoke(Integer n1, Integer n2) {
            return n1 + n2;
        }
    });
}
           

在这里num1AndNum2()函数的第三个参数变成了Function接口,这是Kotlin的内置接口,里面待实现invoke()函数,在调用num1AndNum2()函数时,之前的Lambda表达式在这里变成了Function接口的匿名实现类。

所以,每调用一次Lambda表达式,都会创建一个新的匿名类实例,这也会造成额外的内存和性能开销。

inline

为了解决这个问题,Kotlin提供了内联函数,内联函数的用法非常简单,只需要在定义高阶函数时加上inline关键字即可,上述代码就可修改为

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

首先,Kotlin编译器会将Lambda表达式中的代码替换到函数类型参数调用的地方。

然后,再将内联函数中的全部代码替换到函数调用的地方。

最终,就会替换成两个Int直接相加。

noinline

当一个高阶函数接收了多个函数参数类型时,inline会自动将所有Lambda表达式全部进行内联,如果只是想内联期中一个,inline就不满足,所以这里需要用到noinline关键字。

比如:

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

}
           

这里使用了inline声明inlineTest函数,原本block1、block2都会被内联,但是在block2前加关键字noinline,那么只会对block1内联。

inline与oninline区别:

关键字inline:内联的函数参数类型在编译的时候回呗进行代码替换,因此没有真正的参数属性,它所引用的表达式中可以使用return关键字进行函数返回。

关键字noinline:非内联函数参数类型可以自由传递给其他任何函数,因为它就是一个真实的参数,而内联的函数参数只允许传递给另外一个内联函数,它只能进行局部返回。

比如:

fun printString(str: String, block: (String) -> Unit){
    block(str)
}
fun main(){
    val str = ""
    printString(str){
        s -> 
        if (s.isEmpty())
            [email protected]
        print(s)
 print("END")
    }
}
           

这里定义一个printString的高阶函数,用于在Lambda表达式中传入打印的字符串,如果字符串参数为空,则不打印。

在Lambda中不能直接使用return关键之,所以这里[email protected]表示局部返回,并且不执行后面的代码,功能与Java中return一样。

如果传入的参数是一个空字符串,则不会执行return之后的语句。

但是如果将printString函数声明成一个内联函数,则可以再Lambda中使用return关键字。

比如:

inline fun printString(str: String, block: (String) -> Unit){
    block(str)
}
fun main(){
    val str = ""
    printString(str){
        s -> 
        if (s.isEmpty())
            return
        print(s)
 print("END")
    }
}
           

这里return代表的是返回层的调用函数,也就是main函数。

crossinline

绝大多数高阶函数可以声明内联函数,少部分是不行的。

比如:

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

上述代码再没有inline声明是可以正常工作的,但是加上inline后会提示错误。

在runRunable函数中创建一个Runable对象,并在Runable的Lambda表达式中调用了传入的函数参数。

而Lambda表达式在编译的时候会被转换成匿名类的实现方式,实际上上述代码实在匿名类中调用了传入的函数参数。

而内联函数所引用的Lambda允许使用return进行函数返回,但是由于实在匿名类中调用的函数参数,所以不可能进行外层调用函数的返回,最多只能对匿名类中的函数调用进行返回。

也就是说,在高阶函数中创建Lambda或者匿名类的实现,并且在这些实现中调用函数参数,此时再将高阶函数声明成内联函数,一定会报错。

在这种情况下就必须使用crossinline关键字。

上述代码就可修改为:

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

这样就可以正常编译了。

因为内联函数的Lambda表达式可以使用return,但是高阶函数的匿名类中不允许使用return,这就会导致冲突,而crossinline就可以解决这种冲突。

在声明了crossinline后,就无法调用runRunable函数时的Lambda表达式中使用return进行函数返回了,但是仍然可以使用[email protected]的方式进行局部返回。