这是我参与「第四届青训营 」笔记创作活动的第18天
高阶函数
Kotlin 中有函数类型这一概念,也就是说函数可以和 Int 一样,作为函数的参数和返回值。
而如果一个函数接收另一个函数作为参数,或者返回值的类型是另一个函数,那么该函数就称为高阶函数。
定义函数类型
(String, Int) -> Unit
左边是参数,右边是返回值,没有就写 Unit ,相当于 Java 中的 void
kotlin 的 Lambda 左边的参数好像不能用括号围起来
fun main() {
example { s: String, i: Int ->
println(s)
println(i)
}
}
fun example(func: (String, Int) -> Unit) {
func("hello", 123)
}
反编译成 Java 代码
public final class SixthKt {
public static final void main() {
ExtendFuntionKt.example((Function2)null.INSTANCE);
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
public static final void example(@NotNull Function2 func) {
Intrinsics.checkNotNullParameter(func, "func");
func.invoke("hello", 123);
}
}
其中 Function2 代表着两个参数的 Function 接口,类似的还有 Function1 等等。
函数引用的写法
fun main() {
example(::print) // 函数引用
}
fun print(s: String, i: Int) {
println(s)
println(i)
}
内联函数
Lambda 表达式其实就是匿名内部类的简化写法,具体实现还是用的匿名内部类的形式,这样会带来额外的内存和性能开销。
而使用内联函数可以消除这种开销。
用法:在定义高阶函数时加上 inline 关键字即可。
inline fun example(func: (String, Int) -> Unit) {
func("hello", 123)
}
实现原理
Kotlin 首先会将 Lambda 表达式替换到 函数实现的地方
fun example() {
val s = "hello"
val i = 123
println(s)
println(i)
}
然后将函数替换到调用函数的地方
fun main() {
val s = "hello"
val i = 123
println(s)
println(i)
}
反编译结果(部分逻辑无关代码已省略)
public static final void main() {
int i = 123;
String s = "hello";
System.out.println(s);
System.out.println(i);
}
noinline 与 crossinline
一个高阶函数中如果接收了两个或者更多函数类型的参数,这时我们给函数加上了inline关键字,那么Kotlin编译器会自动将所有引用的Lambda表达式全部进行内联。
如果只想内联其中一个 Lambda 表达式,可以使用 noinline 关键字
inline fun inlineTest(block1: () -> Unit, noinline block2: () -> Unit) {
}
noinline 关键字存在的原因:内联的函数类型参数在编译的时候会被进行代码替换,因此它没有真正 的参数属性。非内联的函数类型参数可以自由地传递给其他任何函数,因为它就是一个真实的参数,而内联的函数类型参数只允许传递给另外一个内联函数,这也是它最大的局限性。
另外,内联函数和非内联函数还有一个重要的区别,那就是内联函数所引用的Lambda表达式 中是可以使用return关键字来进行函数返回的,而非内联函数只能进行局部返回。
fun printString(str: String, block: (String) -> Unit) {
println("printString begin")
block(str)
println("printString end")
}
fun main() {
println("main start")
val str = ""
printString(str) { s ->
println("lambda start")
if (s.isEmpty()) return@printString // 如果字符串为空则不进行打印
println(s)
println("lambda end")
}
println("main end")
}
Lambda表达式中是不允许直接 使用return关键字的,这里使用了return@printString的写法,表示进行局部返回
结果
main start
printString begin
lambda start
printString end
main end
声明为内联函数
inline fun printString(str: String, block: (String) -> Unit) {
println("printString begin")
block(str)
println("printString end")
}
fun main() {
println("main start")
val str = ""
printString(str) { s ->
println("lambda start")
if (s.isEmpty()) return
println(s)
println("lambda end")
}
println("main end")
}
结果
main start
printString begin
lambda start
将高阶函数声明成内联函数是一种良好的编程习惯,事实上,绝大多数高阶函数是可以直接声 明成内联函数的,但是也有少部分例外的情况。
inline fun runRunnable(block: () -> Unit) {
val runnable = Runnable {
block() // 报错
}
runnable.run()
}
IDEA 给的报错信息是 Can't inline 'block' here: it may contain non-local returns. Add 'crossinline' modifier to parameter declaration 'block'
报错原因:Lambda 的表达式默认实现方式是匿名内部类,而内联函数允许使用 return 关键字进行函数返回,但由于是在匿名内部类中调用的函数类型参数,不可能调用外层调用函数返回,最多只能对匿名内部类中的函数进行返回。
按下 Alt + Enter 自动修复
inline fun runRunnable(crossinline block: () -> Unit) {
val runnable = Runnable {
block()
}
runnable.run()
}
此时函数类型参数前加上了 crossinline 关键字,该关键字用于保证在内联函数的Lambda表达式中一定不会使用return关键字。
而此时如果在Lambda表达式里使用了return关键字,会报错:'return' is not allowed here ,而return@Runnable的写法仍可以使用。
参考与鸣谢
郭林《第一行代码》第三版