温州建设工程监理管理学会网站,翻译做网站,新公司做网站多少钱,网站怎么做动态背景图片高阶函数
在 Kotlin 中#xff0c;函数是一等公民#xff0c;高阶函数是 Kotlin 的一大难点#xff0c;如果高阶函数不懂的话#xff0c;那么要学习 Kotlin 中的协程、阅读 Kotlin 的源码是非常难的#xff0c;因为源码中有太多高阶函数了。
高阶函数的定义
高阶函数的…高阶函数
在 Kotlin 中函数是一等公民高阶函数是 Kotlin 的一大难点如果高阶函数不懂的话那么要学习 Kotlin 中的协程、阅读 Kotlin 的源码是非常难的因为源码中有太多高阶函数了。
高阶函数的定义
高阶函数的定义非常简单一个函数如果参数类型是函数或者返回值类型是函数那么这就是一个高阶函数。
函数类型
kotlin 中有整型 Int、字符串类型 String同样函数也有类型举个例子
fun add(num1: Int, num2: Int): Int {return num1 num2
}
这个 add 函数的函数类型就是 (Int, Int) - Int函数类型其实就是将函数的 “参数类型” 和 “返回值类型” 抽象出来既然 (Int, Int) - Int 是函数类型那么它就可以跟整型字符串类型一样将一个变量定义成函数类型如下所示变量 c 的类型就是函数类型这时编译器没有报错所以是可以将变量的类型设置为函数类型的。 那么怎么给 c 这个变量赋值呢类比整型、字符串变量的赋值要给一个函数类型的变量赋值我们需要将一个具有相同函数类型的函数引用赋值给变量就可以了具体写法如下所示
val c: (Int, Int) - Int ::addfun add(num1: Int, num2: Int): Int num1 num2
::add 这种写法是一种函数引用方式的写法。
除了函数引用这种方式外Kotlin 还支持用 Lambda 表达式对一个函数类型的变量进行赋值。如下所示
val c: (Int, Int) - Int {num1: Int, num2: Int - num1 num2}
实际项目中绝大多数情况下我们都是用 Lambda 表达式来调用高阶函数的。
Lambda 表达式语法结构{参数名1: 参数类型, 参数名2: 参数类型 - 函数体} 函数体中可以编写任意行代码最后一行代码会自动作为 Lambda 表达式的返回值 了解了函数类型和高阶函数的定义我们很简单的就可以定义高阶函数了如下所示
// 参数是函数类型的高阶函数
fun higherFunction(func: (Int, Int) - Int) {}
// 返回值是函数类型的高阶函数
fun higherFunction(): (Int, Int) - Int {}
高阶函数的调用
我们以 Kotlin 中数组的遍历为例子来讲高阶函数的调用。
首先我们定义一个 Int 类型的数组如下所示
val intArray intArrayOf(1, 2, 3, 4, 5)
我们不用 for in 的方式来遍历而是用 forEach 方法来遍历forEach 函数就是一个高阶函数源码如下所示
public inline fun IntArray.forEach(action: (Int) - Unit): Unit {for (element in this) action(element)
首先高阶函数肯定是一个函数那么方法的调用如下这样写肯定是没有问题的
intArray.forEach(?)
只是这个 是个函数类型的参数函数类型是 (Int) - Unit那么我就定义一个相同的函数类型的变量传给 forEach 不就好了嘛如下所示
val action: (Int) - Unit ??fun main() {intArray.forEach(action)
}
通过上述的学习我们知道这里的 ?? 可以是函数引用或者是 Lambda 表达式如果我们用函数引用那代码就是这样的
val action: (Int) - Unit ::printValuefun main() {intArray.forEach(action)
}fun printValue(value: Int): Unit {println(value)
}
前面我们已经讲过实际项目中绝大多数情况下我们都是用 Lambda 表达式来调用高阶函数的因为函数引用比较麻烦为了调用高阶函数我们还得特意写一个函数。并且 Lambda 表达式还有很多简便的写法。
我们利用 Lambda 表达式来改写上述代码如下所示
val action: (Int) - Unit {value: Int - println(value)}fun main() {intArray.forEach(action)
}
Lambda 表达式有很多简便的写法现在我们就对 {value: Int - println(value)} 进行简化
Kotlin 有类型推到机制所以 Int 可以去掉
val action: (Int) - Unit {value - println(value)}
Lambda 表达式如果只有一个参数可以直接用 it 来代替并且不需要声明参数名
val action: (Int) - Unit {println(it)}
将简化后的代码代入现在上述的代码就变成如下这样
fun main() {intArray.forEach({println(it)})
}
这个代码还可以进行简化
当 Lambda 参数是函数的最后一个参数时可以将 Lambda 表达式移到函数括号的外面
fun main() {intArray.forEach(){println(it)}
}
如果 Lambda 表达式是函数的唯一一个参数的话还可以将函数的括号省略
fun main() {intArray.forEach{println(it)}
}
到此为止就无法继续简化了这就是最终版本相比较于最开始的样子这个代码已经非常简洁了。
带有接收者的函数类型
前面我们举了 forEach 高阶函数我们再来看一个高阶函数 apply看看这两者有什么区别apply 函数源码如下
public inline fun T T.apply(block: T.() - Unit): T {block()return this
}
apply 函数接收的函数类型是 T.() - Unit相比较于前面我们所见的函数类型多了一个 T.那么这个 T. 有什么作用呢
再说作用之前我们再来看一个高阶函数 also这几个高阶函数都是定义在 Kotlin 标准库中的目的是在对象上下文内执行代码块also 函数的源码如下所示
public inline fun T T.also(block: (T) - Unit): T {block(this)return this
}
also 函数接收的函数类型是 (T) - Unit。
我们来看一下这两个函数实际运用中有哪些不同如下所示 假设这里我们把泛型 T 当中 UserUser.() - Unit 表示这个函数类型是定义在 User 类当中的那么这里将函数类型定义到 User 类当中有什么好处呢好处就是当我们调用 apply 函数时传入的 Lambda 表达式将会自动拥有 User 的上下文以便访问接收者对象的成员而无需任何额外的限定符。
这个说起来确实有点抽象但是结合上面的图片我觉得还是比较容易懂的。
到这里为止高阶函数的理论知识我们已经算是讲完了。
高阶函数的应用
案例一统计文件中各个字符不包括空白字符的个数
fun main() {File(build.gradle).readText() // 读文件直接以 String 的格式返回.toCharArray() // 将字符串转换成字符数组.filter { !it.isWhitespace() } // 过滤空白字符.groupBy { it } // 按照集合中每个字符分组.map {it.key to it.value.size } // 映射重新生成新的集合.let {println(it)}
}
运行结果如下所示: 这个案例中我们用到了 filter、groupBy、map 和 let 这几个高阶函数。如果对这个写法不是很懂的话可以将每一步的结果打印出来看一下。
inline 优化
在讲什么是 inline 优化之前我们先来看一下高阶函数的实现原理。我们知道 Kotlin 和 Java 是完全兼容的最后都会被编译成 .class 文件但是 Java 里面没有高阶函数的概念那么 Kotlin 高阶函数如果被反编译成 Java 代码会是什么样子的呢
例我们来看下面这个高阶函数 foo()
fun main() {var i 0foo {iprintln(i)}
}fun foo(block: () - Unit) {block()
}
反编译之后的 Java 代码
// 主要代码省略了一些没用的代码
public final class HigherFunctionKt {public static final void main() {foo((Function0)(new Function0() {public Object invoke() {this.invoke();return Unit.INSTANCE;}public final void invoke() {int var10001 i.element;int var1 i.element;System.out.println(var1);}}));}public static final void foo(NotNull Function0 block) {Intrinsics.checkNotNullParameter(block, block);block.invoke();}
}
这里的 Function0 是一个接口可以看到高阶函数 foo 的函数类型参数变成了 Function0而 main() 函数当中的高阶函数调用也变成了“匿名内部类”的调用方式。所以高阶函数最终还是以匿名内部类的形式在运行难道 Kotlin 高阶函数只是为了简化“匿名内部类”的写法吗
当然不是Kotlin 高阶函数的性能是远远高于匿名内部类某些极端情况下甚至有几百倍的性能提升。当然我们上面的实现是无法提高性能的不过写法也很简单只需要在函数的前面加上一个 inline 关键字就可以了。
我们来测试一下看看 inline 关键字是不是真的能提高高阶函数的性能这里我们利用 JMH 来进行测试代码如下
BenchmarkMode(Mode.Throughput) // 基准测试的模式采用整体吞吐量的模式
Warmup(iterations 3) // 预热次数
Measurement(iterations 10, time 5, timeUnit TimeUnit.SECONDS) // 测试参数iterations 10 表示进行10轮测试
Threads(8) // 每个进程中的测试线程数
Fork(2) // 进行 fork 的次数表示 JMH 会 fork 出两个进程来进行测试
OutputTimeUnit(TimeUnit.MILLISECONDS) // 基准测试结果的时间类型
open class SequenceBenchmark {// 不用inline的高阶函数fun foo(block: () - Unit) {block()}// 使用inline的高阶函数inline fun fooInline(block: () - Unit) {block()}// 测试无inline的代码Benchmarkfun testNonInlined() {var i 0foo {i}}// 测试inline的代码Benchmarkfun testInlined() {var i 0fooInline {i}}
}fun main() {val options OptionsBuilder().include(SequenceBenchmark::class.java.simpleName).output(benchmark_sequence.log).build()Runner(options).run()
}
测试结果如下分数越高性能越好 从上面的测试结果我们能看出来是否使用 inline它们之间的效率几乎相差 30 倍。而这还仅仅只是最简单的情况如果在一些复杂的代码场景下多个高阶函数嵌套执行它们之间的执行效率会相差上百倍。
如果我们将函数嵌套十层再来测试会发现性能差距更大代码如下所示
BenchmarkMode(Mode.Throughput) // 基准测试的模式采用整体吞吐量的模式
Warmup(iterations 3) // 预热次数
Measurement(iterations 10, time 5, timeUnit TimeUnit.SECONDS) // 测试参数iterations 10 表示进行10轮测试
Threads(8) // 每个进程中的测试线程数
Fork(2) // 进行 fork 的次数表示 JMH 会 fork 出两个进程来进行测试
OutputTimeUnit(TimeUnit.MILLISECONDS) // 基准测试结果的时间类型
open class SequenceBenchmark {// 不用inline的高阶函数fun foo(block: () - Unit) {block()}// 使用inline的高阶函数inline fun fooInline(block: () - Unit) {block()}Benchmarkfun testNonInlined() {var i 0 foo { foo { foo { foo { foo { foo { foo { foo { foo { foo { i } } } } } } } } } }}Benchmarkfun testInlined() { var i 0 fooInline { fooInline { fooInline { fooInline { fooInline { fooInline { fooInline { fooInline { fooInline { fooInline {
}fun main() {val options OptionsBuilder().include(SequenceBenchmark::class.java.simpleName).output(benchmark_sequence.log).build()Runner(options).run()
}
测试结果如下 从上面的性能测试数据我们可以看到在嵌套了 10 个层级以后我们 testInlined 的性能几乎没有什么变化而当 testNonInlined 嵌套了 10 层以后性能也比 1 层嵌套差了 6 倍。并且此时两个函数的性能差距将近 200 倍。
那么 inline 关键字是如何让高阶函数的性能提高这么多的呢
inline 原理
其实内联函数的工作原理很简单就是 Kotlin 编译器会将内联函数中的代码在编译的时候自动替换到调用它的地方这样也就不存在运行时的开销了。
以下面这段代码作为例子
// 使用inline的高阶函数
inline fun fooInline(block: () - Unit) {block()
}Benchmark
fun testInlined() {var i 0fooInline {fooInline {fooInline {fooInline {fooInline {fooInline {fooInline {fooInline {fooInline {fooInline {i}}}}}}}}}}
}
根据内联函数的原理上面的代码等价于下面这样
// 使用inline的高阶函数
inline fun fooInline(block: () - Unit) {block()
}Benchmark
fun testInlined() {var i 0fooInline { i}
}
所以在嵌套了 10 个层级以后testInlined 的性能几乎没有什么变化。把这段代码反编译成 Java 代码也是如此
Benchmark
public final void testInlined() {int i 0;int $i$f$fooInline false;int var4 false;int $i$f$fooInline false;int var7 false;int $i$f$fooInline false;int var10 false;int $i$f$fooInline false;int var13 false;int $i$f$fooInline false;int var16 false;int $i$f$fooInline false;int var19 false;int $i$f$fooInline false;int var22 false;int $i$f$fooInline false;int var25 false;int $i$f$fooInline false;int var28 false;int $i$f$fooInline false;int var31 false;int i i 1;
}
总结
如果一个函数的参数是函数类型或者返回值是函数类型那么这个函数就是高阶函数。高阶函数可以简化我们的代码并且利用 inline 关键字可以提高高阶函数的性能。
在 kotlin 源码的 Standard.kt 文件中定义了几个我们平时会经常用到的高阶函数可以去看一看。