如果一个函数接收另一个函数作为参数,或者返回值的类型是另一个函数,那么该函数就称为高阶函数。
以前开发的时候,入参或返回值都是基本类型或者对象,但是在Kotlin中出现了一个新的类型:函数类型。就是函数也可以像String
这种类型一样做入参或者返回值。
语句结构:
(String, Int) -> Unit
->
左边的部分是用来声明该函数接收什么参数的,多个参数之间使用逗号隔开,如果不接收任何参数,写一对空括号就可以了。
->
右边的部分用于声明该函数的返回值是什么类型,如果没有返回值就使用Unit
,相当于Java 中的void
。
前面铺垫了这么久,先来个例子学习下高阶函数
1、高阶函数实例
fun numOperation(num1: Int, num2: Int, operationFunc: (Int, Int) -> Int): Int {
return operationFunc(num1, num2)
}
fun plus(num1: Int, num2: Int): Int {
return num1 + num2
}
fun minus(num1: Int, num2: Int): Int {
return num1 - num2
}
上面声明了一个对数字操作的函数numOperation
,传入了一个函数类型的入参(Int, Int) -> Int
对前面输入的数字进行操作。后面plus
和minus
是定义了两个对数字操作的函数,看下使用。
fun main() {
val plusResult = numOperation(1, 3, ::plus)
val minusResult = numOperation(7, 2, ::minus)
println("plusResult:$plusResult | minusResult:$minusResult")
}
//运行结果
plusResult:4 | minusResult:5
::plus
和::minus
这种是一种函数引用方式的写法,通过传入的函数类型参数来决定具体的运算逻辑。
2、Lambda表达式
上面实例中,每次调用高阶函数的时候都还得先定义一个与其函数类型参数相匹配的函数,太复杂了。Kotlin还支持其他多种方式来调用高阶函数,比如Lambda表达式、匿名函数、成员引用等。让我们换成Lambda表达式看看。
fun main() {
val plusResult1 = numOperation(2, 3) { n1, n2 -> n1 + n2 }
val plusResult2 = numOperation(5, 2) { n1, n2 -> n1 - n2 }
println("plusResult1:$plusResult1 | plusResult2:$plusResult2")
}
//运行结果
plusResult1:5 | plusResult2:3
换成Lambda表达式就简单多了。我们都知道,Kotlin 的代码最终还是要编译成Java 字节码的,但Java 中并没有高阶函数的概念。所以高阶函数是怎么实现的呢?让我们看看字节码反编译后的Java代码:
public static final void main() {
int plusResult1 = numOperation(2, 3, (Function2)null.INSTANCE);
int plusResult2 = numOperation(5, 2, (Function2)null.INSTANCE);
}
public static final int numOperation(int num1, int num2, @NotNull Function2 operationFunc) {
Intrinsics.checkNotNullParameter(operationFunc, "operationFunc");
return ((Number)operationFunc.invoke(num1, num2)).intValue();
}
首先,先解释下这个Function2,Kotlin内部在Function.kt文件中定义了一系列FunctionX接口,开发者定义的Lambda表达式(匿名函数)在Kotlin底层会通过实现FunctionX接口的方式实现。
确定用哪个FunctionX接口是根据Lambda中定义的入参和返回值个数来决定的。因为我们是两个入参一个返回值,所以是Function2。
public interface Function1<in P1, out R> : Function<R> {
public operator fun invoke(p1: P1): R
}
public interface Function2<in P1, in P2, out R> : Function<R> {
public operator fun invoke(p1: P1, p2: P2): R
}
再来看下调用的时候传入的值(Function2)null.INSTANCE
,其实是Function2接口类型的匿名内部类的实例。
JVM中,Lambda表达式是以对象实例(匿名内部类)的形式存在,JVM会为所有同Lambda打交道的变量分配内存。因此Lambda表达式虽然用起来简单,但是实现上对内存和性能并不友好。
为了解决这个问题,Kotlin 提供了内联函数的功能。
内联函数的工作原理:Kotlin 编译器会将内联函数中的代码在编译的时候自动替换到调用它的地方。
使用方式,在高阶函数前面加上关键字inline
:
inline fun numOperation(num1: Int, num2: Int, operationFunc: (Int, Int) -> Int): Int {
return operationFunc(num1, num2)
}
再次看下字节码反编译后的Java代码:
public static final void main() {
byte num1$iv = 2;
int num1$iv = 3;
int $i$f$numOperation = false;
int var6 = false;
//plus函数替换
int plusResult1 = num1$iv + num1$iv;
num1$iv = 5;
int num2$iv = 2;
int $i$f$numOperation = false;
int var7 = false;
//minus函数替换
int plusResult2 = num1$iv - num2$iv;
String var9 = "plusResult1:" + plusResult1 + " | plusResult2:" + plusResult2;
$i$f$numOperation = false;
System.out.println(var9);
}
直接把代码替换到了引用的地方,这样就是解决了高阶函数Lambda 表达式带来的运行时开销。
注意:使用Lambda的递归函数是无法内联的,会导致复制黏贴无限循环,编译会发出警告。
高阶函数中如果接收了两个或者更多函数类型的参数,这时我们给函数加上了inline
关键字,那么Kotlin 编译器会自动将所有引用的Lambda 表达式全部进行内联。
如果我们只想内联其中的一个Lambda 表达式的话,就可以使用noinline
关键字了,语法格式所示:
inline fun inlineTest(block1: () -> Unit, noinline block2: () -> Unit) {
}
这样只会对block1参数所引用的Lambda 表达式进行内联。
1、关键字noinline出现的意义
话说前面已经学习了内联函数的好处,那么为什么Kotlin 还要提供一个noinline关键字来排除内联功能呢?
因为内联的函数类型参数在编译的时候会被代码替换,因此它没有真正的参数属性。内联的函数类型参数可以自由地传递给其他任何函数,因为它就是一个真实的参数,而内联的函数类型参数只允许传递给另外一个内联函数,这也是它最大的局限性。
上面这段话摘至郭霖大神的《第一行代码》第三版。讲真的我第一次读完,没明白啥意思,写代码试了下才知道,接下来我们先上代码:
fun numOperation(num1: Int, num2: Int, operationFunc: (Int, Int) -> Int): Int {
return operationFunc(num1, num2)
}
fun func1(operationFunc: (Int, Int) -> Int) {
var a = 3;
var b = 2;
println("func1 result: " + operationFunc(a, b))
}
我又定义了一个高阶函数func1
,入参也是函数类型的,直接复制的numOperation
函数中的入参operationFunc
,然后我们在numOperation
函数中去调用func1
试试
fun numOperation(num1: Int, num2: Int, operationFunc: (Int, Int) -> Int): Int {
func1(operationFunc)
return operationFunc(num1, num2)
}
fun func1(operationFunc: (Int, Int) -> Int) {
var a = 3;
var b = 2;
println("func1 result: " + operationFunc(a, b))
}
上面代码在numOperation
函数中去调用func1
,将函数类型的入参又转手传给了func1
,运行可正常出结果。那如果我们想优化下给numOperation
函数加上inline
试试
inline fun numOperation(num1: Int, num2: Int, operationFunc: (Int, Int) -> Int): Int {
//func1调用报红
func1(operationFunc)
return operationFunc(num1, num2)
}
fun func1(operationFunc: (Int, Int) -> Int) {
var a = 3;
var b = 2;
println("func1 result: " + operationFunc(a, b))
}
加上inline
关键字后,func1
报红,并提示错误:Illegal usage of inline-parameter ‘operationFunc’ in ‘public inline fun numOperation(num1: Int, num2: Int, operationFunc: (Int, Int) -> Int): Int defined in com.jane.kotlinlearning.test in file Test3.kt’. Add ‘noinline’ modifier to the parameter declaration。提示需要使用noinline
关键字单独修饰。
先不管这个提示,我们把func1
加上inline
关键字试试:
inline fun numOperation(num1: Int, num2: Int, operationFunc: (Int, Int) -> Int): Int {
func1(operationFunc)
return operationFunc(num1, num2)
}
inline fun func1(operationFunc: (Int, Int) -> Int) {
var a = 3;
var b = 2;
println("func1 result: " + operationFunc(a, b))
}
果然两个高阶函数同时变为内联函数后报错消失,可正常运行。好,现在我们在回头看看那段话,为方便看我复制过来了。
因为内联的函数类型参数在编译的时候会被进行代码替换,因此它没有真正的参数属性。非内联的函数类型参数可以自由地传递给其他任何函数,因为它就是一个真实的参数,而内联的函数类型参数只允许传递给另外一个内联函数,这也是它最大的局限性。
总结下:
1、非内联的函数它的函数型的入参可以随意再作为参数传递给内部引用到的其他的高阶函数。
2、内联的函数不能随意传递函数型的入参,除非内部引用到的高阶函数全部变为内联函数,或者函数型的入参使用noinline
关键字修饰。
上面报错提示我们要用这个关键字,那让我们加上试试
inline fun numOperation(num1: Int, num2: Int, noinline operationFunc: (Int, Int) -> Int
, print: (Int, Int) -> Unit): Int {
print(num1,num2)
func1(operationFunc)
return operationFunc(num1, num2)
}
fun func1(operationFunc: (Int, Int) -> Int) {
var a = 3;
var b = 2;
println("func1 result: " + operationFunc(a, b))
}
为了代码更合理,我又加了一个函数类型的入参print
,这样operationFunc
可以当参数随意传递,print
函数进行内联提高运行效率。
noinline
关键字的作用就是实现局部内联及函数型参数的传递。
2、return
返回区别
上面啰里吧嗦讲了一堆noinline
存在的意义。下面再看个内联函数和非内联函数return
返回区别。
非内联函数中的Lambda表达式实际就是个匿名内部类,因此在Lambda表达式中return
,只能退出Lambda这个闭包,真正调用Lambda的主体函数还是会继续跑。
在内联函数中,相当于将Lambda表达式中的代码复制了一份到主体函数中,变成了主体函数的中一部分,因此直接return
的话,主体函数也就直接停掉返回了。
下面看下代码:
fun printContent(content: String, printFunc: (String) -> Unit) {
println("printContent start")
printFunc(content)
println("printContent end")
}
fun main() {
println("main start")
var param = ""
printContent(param) { s ->
println("lambda start")
if (s.isEmpty()) return@printContent
println(s)
println("lambda end")
}
println("main end")
}
Kotlin中,在Lambda表达式中无法直接return,会报错:‘return’ is not allowed here,因此上面代码中使用return@printString
表示在Lambda表达式中局部返回。@后面是调用的函数名称,注意return和@之前没有空格。当前例子中的高阶函数是非内联的,打印结果:
main start
printContent start
lambda start
printContent end
main end
从结果看到,Lambda表达式中return
后,会返回到printContent
函数继续执行。
修改下代码加上内联关键字
inline fun printContent(content: String, printFunc: (String) -> Unit) {
println("printContent start")
printFunc(content)
println("printContent end")
}
fun main() {
println("main start")
var param = ""
printContent(param) { s ->
println("lambda start")
if (s.isEmpty()) return
println(s)
println("lambda end")
}
println("main end")
}
当高阶函数有inline
关键字修饰后,可直接调用return
返回,在看下执行结果:
main start
printContent start
lambda start
从结果看到,Lambda表达式中return后,整个主函数main
完全退出。看下字节码反编译后的Java代码会更清楚:
public static final void main() {
String param = "main start";
boolean $i$f$printContent = false;
System.out.println(param);
//入参赋值为空串
param = "";
$i$f$printContent = false;
String var2 = "printContent start";
boolean var3 = false;
System.out.println(var2);
int var5 = false;
String var6 = "lambda start";
boolean var7 = false;
System.out.println(var6);
//var10 变量也是空字符串
CharSequence var10 = (CharSequence)param;
var7 = false;
//判断无法进入
if (var10.length() != 0) {
boolean var11 = false;
System.out.println(param);
var6 = "lambda end";
var7 = false;
System.out.println(var6);
var2 = "printContent end";
var3 = false;
System.out.println(var2);
String var8 = "main end";
boolean var9 = false;
System.out.println(var8);
}
}
因为param = ""
所以if语句无法进入,主函数main
直接退出了。
先看个代码:
fun main() {
var param = ""
printContent(param) { s ->
if (s.isEmpty()) return
println(s)
}
}
inline fun printContent(content: String, printFunc: (String) -> Unit) {
val runnable = Runnable {
//下面这句代码报红
printFunc(content)
}
}
我把上面例子中的打印都给去掉了,这样看代码更清晰。这里我修改下,在内联函数中先新建个匿名内部类Runnable对象,然后printFunc放在对象中执行。
上面代码编译器提示错误:Can’t inline ‘printFunc’ here: it may contain non-local returns. Add ‘crossinline’ modifier to parameter declaration ‘printFunc’。
内联函数所引用的Lambda 表达式允许使用return
关键字进行函数返回,但是由于我们是在匿名类Runnable中调用的函数类型参数,此时是不可能进行外层调用函数返回的,最多只能对匿名类中的函数调用进行返回。这样返回是容易引发错误的,Kotlin编译器会提前识别并提示错误,防止开发者没有意识到这个问题。
那你可以会说,我把Lambda 表达式入参中的return
去掉呢?也是一样的,编译器会报红,因为无论Lambda 表达式中有无return
,Kotlin编译器检查到在内联函数中的匿名内部类中引用了函数型入参就会报错,有潜在的风险。
这里我们锊一下:
1、Lambda表达式不允许使用return,除非这个Lambda表达式是内联函数的参数。
2、在内联函数中的匿名类中不允许调用函数类型参数。
那要是这样的话,是不是就没办法使用内联函数了?那怎么可能,内联函数这么优秀,所以出现了上面编译器中提示的关键字crossinline
。加上试试:
fun main() {
var param = "aaa"
printContent(param) { s ->
//if (s.isEmpty()) return
println(s)
}
}
inline fun printContent(content: String, crossinline printFunc: (String) -> Unit) {
val runnable = Runnable {
printFunc(content)
}
}
上面的代码我改了两个地方,第一,把return
语句去掉了,第二,在函数型入参前面加上了crossinline
关键字。
加上crossinline
关键字后,编译器允许你在内联函数的匿名内部类中引用函数型入参,但是,要确保函数型入参中没有return
语句。crossinline关键字就像一个契约,它用于保证在内联函数的Lambda 表达式中一定不会使用return
关键字。
那要是这样Lambda 表达式想局部退出咋办呢?我们可以使用@printContent
的写法进行局部返回。这样想要的操作都有了。