infix函数,高阶函数,内联函数,noinline,crossinline
抓住今天,尽可能少的信赖明天。 喝汤能补 (* ^ ▽ ^ *)
前言
该文章作为学习交流,如有错误欢迎各位大佬指正 (* ^ ▽ ^ *)
- 自身技能
(1)已具备计算机的基本知识 - 本文简介
主要讲解:高阶函数,内联函数的定义以及用法;infix函数的用法。
infix函数
在Kotlin中,经常会看到A to B的语法结构,这里的to并非关键字,实现的机制是使用了infix函数。
- infix函数,只是把编程语言函数调用的语法规则调整了一下。 如 A to B,就是A.to(B)的写法。
- infix函数,允许将函数调用时的小数点,括号等计算机相关的语法去掉,从而使用一种更接近英语的语法来编写程序,让代码更加具有可读性。
// 使用定义的endWith
if ("hello" endWith "lo"){
}
// 定义一个infix.kt的文件,编写如下代码
infix fun String.endWith(prefix: String) = endsWith(prefix)
- to函数原理的解析。
这里A,B均为泛型,将函数定义在A的类型下,接收B类型参数,然后返回一个Pair对象。也就是A to B得到的就是一个包含A、B类型的Pair对象。而初始化枚举时,使用的mapOf()函数就是接收的一个Pair类型的可变参数列表。
//源代码
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
高级函数
Kotlin中,将接受Lambda参数的函数称为具有函数式编程风格的API,接受Lambda参数的函数都是系统提供了。如果我们想定义自己函数的函数式API,那就需要用到 高阶函数。
- 高阶函数,定义是如果一个函数接收另一函数作为参数,或者返回值的类型是另外一个函数,那么该函数就是高阶函数。一般在定义高阶函数时,在函数名前面加上 类名. ,表示将该函数定义在哪个类中,也就是扩展函数的语法。
既然是做为参数类型,与返回值类型,那么必定有函数的类型,如同”AA“ 的类型是String。
- 函数类型,语法的基本规则是:(String, Int) -> Unit
- ->的左边是声明该函数接收的参数,多个参数用逗号(,)隔开,无参数就是空括号;右边声明返回值是什么类型,Unit表示没有返回值。 类似其他语法,如java中的void。
- 高阶函数允许让函数类型的参数来决定函数的执行逻辑。 让同一个高阶函数,因传入不同的函数类型参数,其执行逻辑与运行结果就可能完全不同。调用时候,使用::函数名,这种函数引用方式的写法。 表示将函数作为参数传给高级函数。
- 调用高级函数,还可以使用其他方式,比如:Lambda表达式,匿名函数,成员引用等。这样可以无需每次调用高阶函数时,都需要定义一个与其函数类型参数相匹配的函数。
调用一个函数类型的参数,语法类似调用一个普通函数,即函数名(param1,param2) 。
高阶函数例子.
fun exanple(funParams: (String,Int) -> Unit){
}
高阶函数根据不同的参数类型,执行不同的逻辑
fun main() {
str1AndStr2("hello","ho", ::plus)
str1AndStr2("hello","ho",::startsWith)
//使用Lambda表达式的方式,简化高阶函数的调用
str1AndStr2("hello","ho"){ str1 , str2 ->
str1 + str2
}
str1AndStr2("hello","ho"){ str1 , str2 ->
str1.startsWith(str2).toString()
}
}
fun str1AndStr2(str1: String, str2: String, operation: (String,String) -> String): String{
val ret = operation(str1,str2)
return ret
}
//在StringTest.kt总定义,函数类型参数相匹配的函数。
fun plus(str1: String,str2: String): String{
return str1 + str2
}
fun startsWith(str1: String,str2: String): String{
return str1.startsWith(str2).toString()
}
高阶函数原理浅析
Kotlin的代码最终会编译成JAVA字节码的,但Java中并没有高阶函数的概念。那么就需要Kotlin编译器将这些高阶函数的语法转为Java支持的语法结构。
在kotlin中使用的Lambda表示式,会在底层被转换成Java的匿名类的实现方式。 也就是说我们每次调用一次Lambda表达式,都会创建一个新的匿名类实例,这样就会造成额外的内存和性能开销。
- 为了避免该问题,Kotlin提供了内联函数。
fun main() {
str1AndStr2("hello","ho"){ str1 , str2 ->
str1 + str2
}
}
fun str1AndStr2(str1: String, str2: String, operation: (String,String) -> String): String{
val ret = operation(str1,str2)
return ret
}
上面的代码会被翻译成类似下面的java代码。其中Function为Kotlin内置的接口,为了方便理解,这里直接自己定义一个接口。
//
interface Function{
String invoke(String str1,String str2);
}
public static String str1AndStr2(String str1, String str2, Function operation) {
String ret = operation.invoke(str1,str2);
return ret;
}
public static void main(){
String ret = str1AndStr2("A", "aSB", new Function() {
@Override
public String invoke(String str1, String str2) {
return str1 + str2;
}
});
}
内联函数inline
- 内联函数,在定义高阶函数时加上inline关键字,这样该高级函数就是内联函数了。
- 内联函数的原理,Kotlin编译器会将内联函数中的代码在编译的时候自动替换到调用它的地方,这样就没有运行时的开销了。
大概替换步骤
1、Kotlin先将Lambda表达式中代码替换到函数类型参数调用地方。
2、将内联函数中的全部代码替换到函数调用的地方。
fun main() {
var ret = str1AndStr2("hello","ho"){ str1 , str2 ->
str1 + str2
}
}
fun str1AndStr2(str1: String, str2: String, operation: (String,String) -> String): String{
val ret = operation(str1,str2)
return ret
}
// 第一次替换
fun main() {
var ret = str1AndStr2("hello","ho")
}
fun str1AndStr2(str1: String, str2: String, operation: (String,String) -> String): String{
val ret = str1 + str2
return ret
}
// 第二次替换
fun main() {
var ret = "hello" + "ho"
}
noinline
定义一个内联函数时,在高阶函数前加入inline关键字就可以了。但是,这样Kotlin编译器会自动将所有引用的Lambda表达式全部进行内联。如果只想内联其中一个Lambda表达式,就需要noinline
- 内联的函数类型在编译的时候会被进行代码替换,因此没有真正的参数属性。非内联的函数类型参数可以自由地传递给其他任何函数,因其是真实的参数,而内联函数类型参数只允许传递给另外一个内联函数。
- 内联函数所引用的Lambda表达式中是可以使用return关键字进行函数返回的,而非内联函数只能进行局部返回(ruturn@函数名或类名)。
如下代码,只会对funParams1参数所引用的Lambda表达式进行内联。
inline fun exanple(funParams1: (Int,Int) -> Unit,noinline funParams2: () -> Unit){
}
如下代码,如果传入countNum函数的字符串是空,那么就不打印;Lambda表达式中不允许直接使用return的,所以使用了return@countNum写法 ,进行局部返回,Lambda表达式后面的代码就不会执行了。
fun main() {
println("main start ")
countNum(""){s ->
if (s.isEmpty()) return@countNum
val sum = s.toInt() + 1
println("sum is = $sum")
println("Lambda is end ")
}
println("main end ")
}
// 结果
// main start
// start count
// count end
// main end
fun countNum(numStr: String,funParams1: (String) -> Unit ){
println("start count")
funParams1(numStr)
println("count end")
}
这里将,countNum变成了内联函数,那么在Lambda表达式中,就可以使用return了。并且这里的return返回的是外层调用函数(main函数)的返回; 因为内联函数的替换规则,此处表达式里面的内容就是写在main函数里面的。
fun main() {
println("main start ")
countNum(""){s ->
if (s.isEmpty()) return
val sum = s.toInt() + 1
println("sum is = $sum")
println("Lambda is end ")
}
println("main end ")
}
// 结果
// main start
// start count
inline fun countNum(numStr: String,funParams1: (String) -> Unit ){
println("start count")
funParams1(numStr)
println("count end")
}
crossinline
kotlin中,绝大对数高阶函数,都可以通过关键字inline变为内联函数,但是也有些特性的情况。
- 在高阶函数中创建了另外的Lambda或者匿名类的实现,并在这些实现中调用函数类型参数,此时再将高阶函数声明成内联函数,就会出错。使用crossinline关键字就能解决问题
- crossinline,用于保证在内联函数的Lambda表达式中一定不会使用return,这样就不会因返回而冲突了。在高阶函数中另外的Lambda或者匿名类中,仍然可以使用局部返回
下面的代码,加上inline之后,block()该行代码就会报错。我们在runRunnable函数中,创建了一个匿名内部类的对象runnable。并在这个匿名内部类中,调用了传入的函数类型参数。
因为内联函数的Lambda表达式中使用的return是对外层调用函数进行返回的;但是我们在匿名类中使用函数类型参数,所以就不可能对外层调用函数进行返回了,最多只能对匿名类中的函数调用进行返回。 即内联函数的Lambda表达式中运行使用return,而高阶函数的匿名类实现中不允许使用return造成了冲突。
fun runRunnable(block: () -> Unit){
val runnable = Runnable {
block()
}
runnable.run()
}
// inline
inline fun runRunnable(block: () -> Unit){
val runnable = Runnable {
block()
}
// runnable.run() // 错误
}
// crossinline
inline fun runRunnable(crossinline block: () -> Unit){
val runnable = Runnable {
block()
}
runnable.run()
}
(* ^ ▽ ^ *)