相信大部分安卓开发同学现在都已经在使用kotlin语言进行安卓程序的开发了。今天和大家分享一下kotlin中的三个关键字:inline、noinline和crossinline的使用和区别。
当然想彻底搞明白这三个关键字首先你得有一定的kotlin语法基础,对kotlin中的高阶函数和lambda表达式要有一定的了解,所以这里先引出高阶函数的定义和使用。
下面我们先来看一下高阶函数的定义:如果一个函数接收另一个函数作为参数,或者返回值的类型是另一个函数,那么该函数就称为高阶函数。
也就是说在kotlin中,允许定义一个函数类型的参数,具体语法如下:
(Int,String) - > String
- >左边的部分就是要声明该函数接收的参数类型,而- >右边的部分用于声明该函数的返回值类型。下面我们将上面的函数类型的参数运用到具体的函数中:
private fun fold(block:(Int,String) -> String) {
val start = 0
val end = "fold"
block.invoke(start,end)
}
这里fold函数接收了一个函数类型的参数block,那么fold函数就是一个高阶函数。而调用一个函数类型的参数,它的语法类似于调用一个普通的函数,只需要在函数名的后面加上一对括号,并在括号中传入必要的参数即可(invoke是调用操作符()的重载,这里等价于()。读者可以理解为block()就可以了)。注意这里下图中调用高阶函数flod的地方,如果有多个参数,和声明的地方类似,->左边的参数用逗号分隔开,-> 右边是这个函数类型参数的方法体。如果只有一个参数可以省略->,使用隐式名称it。lambda表达式最后一行代码也就是花括号中的最后一行代码,默认为该lambda表达式的返回值。
fun main() {
fold { start, end ->
return@fold "$start , $end"
}
}
private fun fold(block:(Int,String) -> Unit) {
val start = 0
val end = "fold"
block.invoke(start,end)
}
由于lambda表达式只支持局部返回,也就是使用@标签限制的return语句进行局部返回,这里我们暂时没有办法做到在lambda表达式中使用裸return进行外部函数的返回。如果你直接使用裸retrun编译器就会提示语法错误:
那么如果我们想在flod函数的lambda表达式中进行外部main函数的返回,我们该如何做到呢?此时inline关键字就可以帮助我们很好的解决这个问题。当我们使用inline关键字对一个函数修饰时我们就称这个函数是内联函数。下面我们使用inline关键字对flod函数进行修饰,我们再去在main函数中调用flod函数,此时我们就可以直接对main函数进行返回:
fun main() {
println("main called: start")
fold { start: Int, end: String ->
return
}
println("main called: end")
}
private inline fun fold(block: (Int, String) -> String){
val start = 0
val end = "end"
block.invoke(start, end)
}
这是因为 lambda 表达式传给的函数是内联的,该return也可以内联。现在编译器不会再报错了,而flod函数此时就可以完成对外部函数的返回。
这种返回(位于 lambda 表达式中,但退出包含它的函数)称为非局部返回。
那么内联函数是如何做到可以支持让lambda表达式进行非局部返回呢?我们先在上面的示例中添加一个normal函数:
fun main() {
println("main called: start")
normal { start: Int, end: String ->
return@normal "$start$end"
}
fold { start: Int, end: String ->
return
}
println("main called: end")
}
private fun normal(block: (Int, String) -> String) {
val start = 0
val end = "end"
block.invoke(start, end)
}
private inline fun fold(block: (Int, String) -> String){
val start = 0
val end = "end"
block.invoke(start, end)
}
在Android Studio中 打开Tools - > Kotlin -> ShowKotlin ,然后在打开的窗口中点击Decompile按钮。
我们知道Kotlin的代码最终还是要编译成Java字节码的,Kotlin编译器会将内联函数中的字节码在编译的时候自动替换到调用的地方。没有用inline修饰的normal函数在转换成Java代码的时候lambda表达式使用匿名内部类实现。而fold函数仅仅是用了代码替换,这就是为什么内联函数可以支持裸return的原因。
我们每调用一次lambda表达式都会创建一个新的匿名类,这样就会造成额外的内存和性能开销。
normal((Function2)null.INSTANCE);
在实际开发过程中就是等价与下面的实现方式:
private void main() {
normal(new Function() {
@Override
public String invoke(Integer start, String end) {
return start + "," + end;
}
});
}
所以在使用高阶函数时,使用inline关键字,可以在内存上节约一定的空间。当然内联函数也不是一点缺点都没有,试想一下如果需要内联的函数中的逻辑代码十分复杂的话,就会导致编译时调用的地方代码看起来比较臃肿。
关于内联函数我们就讲解到这里。下面我们来看下关键字noinline:禁用内联
既然内联函数有这么多的好处,那么我们为什么还要禁用内联呢?
这是因为内联函数的参数在实际编译的时候没有具体的类型,只是进行代码替换。所以就有这么一个约束内联高阶函数的函数类型参数只能传递给内联高阶函数。假设我们有下面一段逻辑的代码:
高阶函数fold拥有两个函数类型的参数:block1和block2。我们需要将block2传递给normal函数。这个时候编译器给出了语法错误提示 ,当我们给函数类型的参数block2加上noinline关键字的时候编译器不再报错了。
在fold函数中我们可以正常调用normal函数了。 在实际开发中如果你需要将一个内联高阶函数的函数类型参数传递给另外一个非内联高阶函数的函数类型参数时,那么我们就可以使用noinline来禁用内联。
关于crossinline这个关键字可能不是那么好理解。我想尽可能详细的把这个关键字说的明白一些。假设我们有如下面一段代码:
normal函数是一个使用inline关键字修饰的内联函数,而在normal函数内部我们创建了一个Function的匿名类。在这个Function匿名类内部我们调用了函数类型的参数block。此时我们看到编译器又给我们的block函数提示了语法错误。那么为什么在这个匿名内部类中就无法调用block这个函数呢?
这是因为内联函数在编译期间仅仅是代码替换,它支持在lambda表达式中进行外部函数的返回,而高阶函数的匿名类实现中是不允许对外部函数进行返回的,这在语法上是一个错误,kotlin编译器识别出了这个问题,发出了错误提示。那么我们该如何解决这个问题呢?而在这里crossinline关键字就派上了用场。下面我们对noraml函数的函数类型参数block加上crossinline关键字进行修饰:
可以在上面的代码中看到编译器不在提示错误了。
这就是crossinline关键字的作用,它就像是一个约定告诉编译器。我保证不会在内联高阶函数的匿名类实现中使用裸return。
而在normal函数调用的地方main函数中。内联函数normal现在是无法使用裸return了,我们知道lambda表达式的最后一行代码默认是这个lambda的返回值。而上面的block函数中并没有显示的使用return进行返回,下面我们在normal函数中显示加上return:
此时编译器又提示错误了,这就是因为我们加上了crossinline关键字导致的。因为我们已经明确的告诉了编译器,不会在内联高阶函数的匿名类实现中使用裸return。而此时我们使用@标签限制的return就可以正常返回:
到这里,这篇关于inline、noinline、crossinline这三个关键字的介绍就说完了。
说实话刚开始从Java转到Kotlin的时候真的有点不适应,但是写了接近快1年的Kotlin后才发现,Kotin使用起来还是很简单和方便的,就是代码的可读性可能没有Java那么高。
可能有些地方说的还是有些欠缺,希望读者也能提出你宝贵的建议,让我们一起学习,一起进步。