看完上篇,大致能理解 function type 与 lambda receiver 之间的关系。
下面来看一下一个高阶函数在编译之后会变成什么
查看字节码
Decompile
可以看到,高阶函数在编译之后,变成了普通的java函数
接下来看看,如果调用这个高阶函数,那它编译会是什么样子呢
直接给出编译后的解释
INVOKEDYNAMIC invoke()Lkotlin/jvm/functions/Function2; [ //运行时动态生成一个 Function2 对象 InlineKt.main$lambda$0(II)I // 这个对象背后调的静态方法(lambda 体)运行时由 LambdaMetafactory 动态生成一个实现 Function2 的类
然后我进行第二次调用
看看第二次调用编译后的字节码会有什么不一样
区别在于
//第一次 main$lambda$0 //第二次 main$lambda$1所以,每写一个 lambda,就多一个 INVOKEDYNAMIC,就多一个静态方法。 每次调用都会动态生成(或复用)一个 Function2 对象。
还有就是这个function2是 Kotlin 标准库里定义的一个接口,专门用来表示「接收 2 个参数、返回 1 个值」的函数
下面使用inline来定义高阶函数,然后调用它,看看会有什么不同
这里完全没有 INVOKEDYNAMIC,也没有 addDefined 调用
可以看到和普通高阶函数区别很大
INVOKEDYNAMIC 消失了 → 没有动态生成 Function2 对象
addDefined 调用消失了 → 因为 inline 把 addDefined 的函数体直接贴进了 main
ICONST_1 / ICONST_2 / ISTORE → 这是 addDefined 内部的代码,被内联展开后出现在 main 里
这就是 inline 的本质:把被调函数的代码“复制粘贴”到调用处,于是函数调用没了,lambda 对象也没了
比如随便写一个inline函数
看看反编译后是什么
很鲜明
public static final void main() { addDefined(1, 2, InlineKt::main$lambda$0); // ← 没 inline 的:还是调用 addDefined(1, 2, InlineKt::main$lambda$1); // ← 没 inline 的:还是调用 int $i$f$testInline = 1; // ← inline 函数的标记 int b$iv = 2; int $i$f$addDefinedInline = 0; int var5 = 0; int a = $i$f$testInline + b$iv; // ← 函数体直接出现在 main 里! System.out.println(a); $i$f$testInline = 0; System.out.println("测试"); // ← 函数体直接出现在 main 里! System.out.println("测试"); System.out.println("测试"); System.out.println("测试"); System.out.println("测试"); }由此可以看出,inline对高阶函数有重要作用
如果我对一个普通函数使用inline,会有这样的提示
编译器也告诉我们对于普通函数 “内联(inline)带来的性能提升微乎其微。内联最适合用在「参数是函数类型」的函数上。”
对高阶函数用 inline ,就是把它带来的那两样开销——FunctionN 对象 + invoke 调用——彻底消掉
接下来来看关于inline函数的第二点
我这里写一个简单的年龄判断,如果小于18,lambda表达式返回no反之yes
这就奇怪了,为什么会在return处报错呢。
这就涉及到一个概念non-local return
kotlin官方文档对于return的解释
return默认从最近的包围函数(封闭函数)/匿名函数返回,就比如下面这个
没有报错,因为这个return的封闭函数就是上面的ageTest()
那回到上面的
那这个return上面的是不是这个ageJudge,显然不是,因为他只是一个函数的调用,不是封闭函数
那再往上找,是不是return上面这个age()呢,那肯定又做不到,因为它传进来是个lambda表达式,怎么能跳出去来return age()呢,与return的定义就不符。
文档里也有所提及
不过正如之前的文章里讲的,可以使用标签来return(其实编写的时候编译器就提醒了)
打一个这样的标签就可以用了
还有就是给函数加个inline就可以用return了
但为什么会报错呢????接下来我来形象化演示一下加完inline会是什么样,就会理解了
根据上述得到的inline相关知识,会将函数体直接拷贝到调用处,那么
fun age(){ println("begin") println(x(1)) //调用x,即把lambda拿过来 println("end") }接下来就会变成
fun age(){ println("begin") it: Int-> if (it<18){return "no" } "yes" println("end") }这下是不是懂了,加上inline后能return的原因在这里,因为这里return可以找到age()了,那上述的return “no”为什么报红?因为它返回的是age(),但age没有声明返回类型(那就默认Unit),把“no”删了就行了
解释:it<18,直接return,由于age()是:Unit(不需要所有路径都 return),不用担心返回条件不足。 然后lambda 里最后一行自动作为返回值
既然return的是age(),那我想return字符串的话,是不是给age()加个:String就行了
这里还是不建议,
age(): String 才会“条件不足”,所以编译器提示,加一个TODO
it >= 18 lambda 返回 "yes",age() 继续走 末尾 return TODO(...),这样是可以的
不过还是建议:Unit最为方便
补充:
ok,现在来了解一下noinline。我给上面的函数加上一个参数:
然后我再写一个高阶函数
在ageJudge里调用它,看起来传y参数其实没问题,那为什么会报错呢
那调用一下它,然后形象化转换一下看看会变成什么样
现在形象化把函数体拷贝过来,
println("begin") { println("1") //第一个function type }) println("end") { clean(println("2")) //第二个function type }会发现clean()里面直接是拷贝过来的函数体内容,这样根本用不了,所以编译器在写到这的时候会提示加一个noinline
拷贝函数体对这个y不生效,那他就可以用这个clean()(函数了
下面还有个知识点,是crossinline。通过上面了解了inline的非局部返回,即是inline的特权,也是inline的麻烦。为什么呢
当我对lambda进行一个处理,把调用function type放进另一个lambda里
block 可能包含非局部返回(return),但它现在被包进了 other 这个新 lambda 里,编译器没法保证 return 还能正确工作。要么加 crossinline,要么别这么用
crossinline 的作用:给参数加一个“禁止非局部返回”的约束。
加了 crossinline 后,block 里就不能写 return 了,既要内联,又不允许非局部返回,这样才能安全地转手。
以上就是我对从高阶函数到inline的浅薄理解,看个乐呵兄弟们