高阶函数的意思是使用函数作为变量或者返回值的函数。
// 这就是一个高阶函数,它的参数是一个函数类型的对象
// (Int) -> Unit 表示这是一个参数为 Int,无返回值的函数类型的对象
fun high(function: (Int) -> Unit) {
// 函数类型的对象通过 invoke 来调用其函数
function.invoke(666)
}
fun main() {
// 调用高阶函数时传入一个函数即可,传入的函数没有名字,所以又被称之为匿名函数
high(fun(number: Int): Unit {
println("number = $number")
})
}
fun high(function: (Int) -> Unit) {
function.invoke(666)
}
fun main() {
// 传入的匿名函数可以简化为 Lambda 表达式的形式
high({ number: Int ->
println("number = $number")
})
// 如果 Lambda 表达式是高阶函数的最后一个参数,则可以把 Lambda 表达式写在括号外面
high() { number: Int ->
println("number = $number")
}
// 如果 Lambda 表达式是高阶函数的唯一参数,则可以省略括号
high { number: Int ->
println("number = $number")
}
// 如果 传入的这个参数是单参数的,则可以省略不写这个参数,使用默认名字 it 表示此参数即可
high {
println("number = $it")
}
}
Kotlin 的匿名函数和 Lambda 表达式本质上就是一个函数类型的对象,它和函数不是同一个东西,而是一个 Kotlin 帮我们自动生成的,我们看不见的一个和函数具有相同功能的对象。
除了使用匿名函数和 Lambda 之外,我们还可以通过双冒号 “::” + 函数名的方式来构造一个函数类型的对象。
fun high(function: (Int) -> Unit) {
function.invoke(666)
}
fun main() {
// “::” + 函数名可以使函数变成函数类型的对象,使得它可以被传入高阶函数中
high(::normal)
}
fun normal(number: Int) {
println("number = $number")
}
函数类型的对象说起来比较抽象,如果用 Java 来表示的话,高阶函数的代码大致是这样的:
public static void high(Function function) {
function.invoke(666);
}
public static void main() {
// Kotlin 内部会帮我们建立一个和函数功能相同的对象,这个对象就被称之为函数类型的对象
high(new Function() {
@Override
public void invoke(int number) {
System.out.println("number = " + number);
}
});
}
也就是说,每当我们使用函数类型的对象时,都会创建一个新的对象,这就会造成额外的内存和性能开销。为了解决此问题,Kotlin 提供了内联函数。
// 添加 inline 关键字使其变成内联函数
inline fun high(function: (Int) -> Unit) {
function.invoke(666)
}
fun main() {
// 在编译时,Kotlin 首先会将内联函数中 Lambda 表达式的内容替换到其调用的地方,然后再将内联函数中的全部代码移到调用处
// 也就是说,这里最终的代码会变成直接调用 println("number = 666"),所以说内联函数完全消除了 Lambda 表达式带来的运行时开销
high {
println("number = $it")
}
}
如果给非高阶函数添加 inline 关键字,我们会看到一条提示:Expected performance impact from inlining is insignificant. Inlining works best for functions with parameters of functional types
,意思是 inline 对普通函数的性能提升微乎其微,inline 关键字主要用于高阶函数。这是因为 JVM 已经对普通的函数进行了内联优化。
如果一个高阶函数接收了两个或者更多的函数类型的对象作为参数,在这个高阶函数添加了 inline 关键字后,所有函数类型的对象均会被内联,如果我们想要某个函数类型的对象不被内联,则可以为此参数添加 noinline 关键字。
// 给 block1 参数添加 noinline 关键字使其不要内联
inline fun high(noinline block1: () -> Unit, block2: () -> Unit){}
既然内联函数这么好用,为什么还要提供 noinline 呢?
上文中说到,内联函数在编译时会被替换为实际运行的代码,所以说它的参数没有真正的参数属性,在函数内部需要传参时,只能传递给另一个内联函数,这就是它最大的局限性。而非内联函数的参数是真实的参数,可以自由的进行传递。
在 Kotlin 的 Lambda 表达式中,return 只能用于局部返回,并且加上 @返回范围
:
thread {
// 这里是局部返回,终止 thread 中的代码,但 thread 之后的代码不受影响
return@thread
}
// thread 之后的代码可以继续执行
但由于内联函数中的 Lambda 会在编译时替换为实际代码,所以内联函数的 Lambda 中,可以使用 return 全局返回:
inline fun high(block: () -> Unit) {
block.invoke()
}
fun main() {
high {
println("hello")
// 这里是全局返回,Lambda 之后的代码无法再被运行。(这里也可以用 return@high 局部返回)
return
}
// 这里的代码不会被执行
println("test")
}
如果内联函数中的 Lambda 表达式在函数内另一个 Lambda 表达式中使用,return 就可能产生歧义:
inline fun high(block: () -> Unit) {
thread {
// 这里是无法编译通过的,因为这里会产生歧义
// 如果 block 的代码中使用了 return,想要进行全局返回,但由于 block 的代码是在这里的 Lambda 表达式中执行的,无法全局返回,就会导致出错。
block.invoke()
}
}
为了解决这个问题,我们需要给 block 参数加上 crossinline 关键字,禁止 block 的代码中使用全局返回:
// 为 block 参数添加 crossinline 关键字,表示不允许 block 中使用全局返回。
inline fun high(crossinline block: () -> Unit) {
thread {
block.invoke()
}
}
fun main() {
high {
println("hello")
// 由于这里的 Lambda 加上了 crossinline 关键字,所以这里无法再使用全局返回,只能局部返回
return@high
}
// 这里的代码依旧能够执行
println("test")
}