定义

如果一个函数接受另外一个函数作为参数,或者返回值的类型是另外一个函数,那么该函数称为高阶函数

函数类型的定义如下

1
(String, Int) -> Unit 

定义一个函数类型,最关键的就是声明该函数接受什么样的参数,以及它得返回值是什么。因此 -> 左边的的是声明函数接受什么样的参数,右边返回的是什么类型。多个参数之间用逗号隔开,如果没有参数的话,就是一个括号,这里得 Unit 返回值相当于 Java 中得 void。

下面举一个例子看看如何使用一个高阶函数,以及作用是什么,其实高阶函数的作用就是大体允许让函数类型参数来决定具体得执行逻辑。看如下实例,定义一个 num1AndNum2() 的高阶函数。

1
2
3
4
fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
    val result = operation(num1, num2)
    return result
}

然后再定义两个可以匹配函数类型的函数

1
2
3
4
5
6
7
fun plus(num1: Int, num2: Int): Int {
    return num1 + num2
}

fun minus(num1: Int, num2: Int): Int {
    return num1 - num2
}

然后具体的使用如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
fun main() {
    val a = 10;
    val b = 5;
    // 匹配函数的方式
    val result1 = num1AndNum2(a, b, ::plus)
    val result2 = num1AndNum2(a, b, ::minus)

		// 也可以使用 Lambda 表达式的方式
    val result3 = num1AndNum2(a, b) { i: Int, i2: Int ->
        return@num1AndNum2 i * i2
    }

    println(result1)
    println(result2)
    println(result3)
}

可以看到 num1AndNum2 结果的返回值,完全是根据我们传入的参数中的高阶函数来决定的。

但是由于 Java 中又是没有高阶函数的,我们来看看他的实现原理到底是怎么样的。编译后的源码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public static final int num1AndNum2(int num1, int num2, @NotNull Function2 operation) {
      Intrinsics.checkParameterIsNotNull(operation, "operation");
      int result = ((Number)operation.invoke(num1, num2)).intValue();
      return result;
}

public static void main(){
			int a = 10;
			int b = 5;
			int result3 = num1AndNum2(a, b, new Function(){
					@Override
					public Integer invoke(Integer n1, Integer n2){
								return n1 + n2;
					}
			});
}

为了可读性,对部分代码进行了些许的调整,大致的源码就是这样,在这里可以发现, Kotlin 实现的高阶函数,在 Java 当中就是创建了一个匿名内部类对象,需要注意的是由于多创建了一个对象,在一定的程度上会造成额外的开销。不过在一般情况下是可以忽略的,不过要是在 for 循环的情况下,情况是不一样了。

所以 Kotlin 中引入了另外一个关键字, inline ,只需要在定义了高阶函数的函数上加上这个关键字。其实就是 Kotlin 编译器会将内联函数中的代码在编译的时候自动替换到调用他的地方,这样就不存在额外的内存开销了。

1
2
3
4
inline fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
    val result = operation(num1, num2)
    return result
}

再次编译源码后看看, 精简部分代码

1
2
3
4
5
6
7
8
public static final void main() {
      int a = 10;
      int b = 5;
		
      int result3 = a * b;
    
      System.out.println(result3);
}

noinline 与 crossinline

一个高阶函数中如果接受了两个或者更多的函数类型参数,这时如果加上了 inline 关键字,那么 Kotlin 将会自动将所有的 Lambda 表达式进行内联。

如果我们其中一个参数不想内联怎么办,那么就可以使用 noinline 关键字。

1
2
inline fun inlineTest(block1: () -> Unit, noinline block2: () -> Unit) {    
}

这样的话,block1 函数就会被内联,而 block2 就不会被内联了, 那么 Kotlin 为什么要提供一个 noinline 关键字呢,这是因为内联的函数类型参数在编译的时候会被进行代码替换,因为他没有真正的参数类型。非内联的函数可以自由的传递给其他任何函数,因为他就是一个真实的参数,而内联的函数类型只允许传递给另外一个内联函数,这也就是他最大的局限性。

另外,内联函数和非内联函数还有一个重要的区别,因为内联函数在编译的时候代码会进行替换,所以再内联函数中,是可以直接使用 return 的,而非内联函数在代码编译后,由于是匿名内部类的实现,在匿名内部类里面是不能直接使用 return 关键字的,但是可以进行匿名内部类中的局部 return 返回。

将高阶函数声明成内联函数在大多数的情况下都是可用的, 并且也不会产生额外的性能开销,但是在有一种情况下,加上 inline 关键字是无法通过编译的。看如下示例:

1
2
3
4
5
6
inline fun runRunnable(block: () -> Unit) {
    val runnable = Runnable {
    		// 报错信息: Can't inline 'block' here: it may contain non-local returns. Add 'crossinline' modifier to parameter declaration 'block'
        block()
    }
}

出现这个错误的原因也比较好解释,都是我们上面解释过的。

首先 Runnable 他是一个 lambda 表达式的高阶函数,他在编译后,也是一个匿名内部类的实现的方式, 然后由于 runRunnable 是一个内联函数,编译后的代码会被替换,但是真正的原因是因为替换后的代码可以直接使用 return 关键字了。但是这里由于 block 函数的实现是在匿名类中的,block 函数这里面是不能直接使用 return 关键字的,只能使用局部返回(类似 return@xxx)。

为了解决这个问题也很好解决,只需要告诉编译器,这里的代码我是不会使用 return 关键字的,那么只需要加上 crossinline 关键字,即可,代码如下。

1
2
3
4
5
inline fun runRunnable(crossinline block: () -> Unit) {
    val runnable = Runnable {
        block()
    }
}

总结

  • inline 编译后的代码会被替换,常在高阶函数中使用,可以使用 return 关键字
  • noinline 被标记了的函数在内联函数中,不会参与内联,也就是编译后,代码不会被直接替换
  • crossinline 表示在内联函数中的 lambda 表达式中一定不会使用 return 关键字