Kotlin 高阶函数详解

高阶函数

在 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)} 进行简化:

  1. Kotlin 有类型推到机制,所以 Int 可以去掉
val action: (Int) -> Unit = {value -> println(value)}
  1. Lambda 表达式如果只有一个参数,可以直接用 it 来代替,并且不需要声明参数名
val action: (Int) -> Unit = {println(it)}

将简化后的代码代入,现在上述的代码就变成如下这样:

fun main() {intArray.forEach({println(it)})
}

这个代码还可以进行简化:

  1. 当 Lambda 参数是函数的最后一个参数时,可以将 Lambda 表达式移到函数括号的外面
fun main() {intArray.forEach(){println(it)}
}
  1. 如果 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 当中 User,User.() -> 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 {i++println(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 文件中定义了几个我们平时会经常用到的高阶函数,可以去看一看。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/50495.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

好用的电容笔有哪些推荐?开学季便宜好用电容笔推荐

开学马上要来了&#xff0c;想必很多学生党都在为开学而做准备&#xff0c;要知道&#xff0c;原装的Apple Pencil&#xff0c;虽然功能很强&#xff0c;但是价格却很贵&#xff0c;不是一般人能够承受得起的。所以&#xff0c;是否也有类似于Apple Pencil这样的电容笔&#xf…

php开发websocket笔记(1)

1.运行server1.php文件 Windows命令行运行 php server1.php<?phperror_reporting(E_ALL); set_time_limit(0); //ob_implicit_flush(); $address 0.0.0.0;//可以监听网络上的请求 $address 127.0.0.1;//只能监听本机的请求$port 10005; //创建端口 $socket1 socket_cr…

6个比较火的AI绘画生成工具

随着人工智能技术的发展&#xff0c;市场上出现了越来越多的人工智能图像生成工具。这些人工智能图像生成工具可以自动创建惊人的图像、艺术作品和设计&#xff0c;以帮助设计师和创意人员更快地实现他们的创造性想法。在本文中&#xff0c;我们将推荐7种最近流行的人工智能图像…

【0基础入门Python Web笔记】三、python 之函数以及常用内置函数

三、python 之函数以及常用内置函数 函数函数定义函数调用函数参数返回值 常用内置函数input()函数range()函数其它 更多实战项目可进入下方官网 函数 函数是一种用于封装可重复使用代码块的工具&#xff0c;能够将一系列操作组织成一个逻辑单元。 函数定义 在Python中&…

创建R包-2.1:在RStudio中使用Rcpp制作R-Package(更新于2023.8.23)

目录 0-前言 1-在RStudio中创建R包项目 2-创建R包 2.1通过R函数创建新包 2.2在RStudio通过菜单来创建一个新包 2.3关于R包创建的说明 3-添加R自定义函数 4-添加C函数 0-前言 目标&#xff1a;在RStudio中创建一个R包&#xff0c;这个R包中包含C函数&#xff0c;接口是Rc…

【Unity自制手册】游戏基础API大全

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;Uni…

嵌入式Linux开发实操(十):ADC接口开发

#前言 ADC就是模数转换,可以用来接一些模拟量设备,所谓模拟量就是波形不是方波而是各种包络形状的波形的信号,比如电压、电流等电信号或压力、温度、湿度、位移、声音等非电信号,ADC就是将这些信号转换为数字方波信号,以便于信息传递的。 #ADC硬件设计 key按键连接了AD…

根据源码,模拟实现 RabbitMQ - 网络通讯设计,实现客户端Connection、Channel(完结)

目录 一、客户端代码实现 1.1、需求分析 1.2、具体实现 1&#xff09;实现 ConnectionFactory 2&#xff09;实现 Connection 3&#xff09;实现 Channel 二、编写 Demo 2.1、实例 2.1、实例演示 一、客户端代码实现 1.1、需求分析 RabbitMQ 的客户端设定&#xff…

Mybatis的动态SQL及关键属性和标识的区别(对SQL更灵活的使用)

&#xff08; 虽然文章中有大多文本内容&#xff0c;想了解更深需要耐心看完&#xff0c;必定大有受益 &#xff09; 目录 一、动态SQL ( 1 ) 是什么 ( 2 ) 作用 ( 3 ) 优点 ( 4 ) 特殊标签 ( 5 ) 演示 二、#和$的区别 2.1 #使用 ( 1 ) #占位符语法 ( 2 ) #优点 2.…

朴素贝叶斯==基于样本特征来预测样本属于的类别y

目录 朴素贝叶斯基于样本特征来预测样本属于的类别y 朴素贝叶斯算法的基本概念与核心思想 假设两个特征维度之间是相互独立的 拉普拉斯平滑增加出现次数保证0不出现 ​编辑 基于样本特征来预测样本属于的类别y 什么是拉普拉斯平滑 朴素贝叶斯基于样本特征来预测样本属于的…

软考高项(九)项目范围管理 ★重点集萃★

&#x1f451; 个人主页 &#x1f451; &#xff1a;&#x1f61c;&#x1f61c;&#x1f61c;Fish_Vast&#x1f61c;&#x1f61c;&#x1f61c; &#x1f41d; 个人格言 &#x1f41d; &#xff1a;&#x1f9d0;&#x1f9d0;&#x1f9d0;说到做到&#xff0c;言出必行&am…

C#反编译工具ILSPY

ILSPY ILSpy 是一个开源的.Net程序集浏览器和反编译工具。 Visual Studio 2022附带了默认情况下启用的F12反编译支持&#xff08;使用我们的引擎v7.1&#xff09;。 在Visual Studio 2019中&#xff0c;您必须手动启用F12支持。转到“工具”/“选项”/“文本编辑器”/C#/Adva…

c#设计模式-结构型模式 之 外观模式

概述 外观模式&#xff08;Facade Pattern&#xff09;又名门面模式&#xff0c;隐藏系统的复杂性&#xff0c;并向客户端提供了一个客户端可以访问系统的接口。这种类型的设计模式属于结构型模式&#xff0c;它向现有的系统添加一个接口&#xff0c;来隐藏系统的复杂性。该模式…

【洛谷算法题】P1000-超级玛丽游戏【入门1顺序结构】

&#x1f468;‍&#x1f4bb;博客主页&#xff1a;花无缺 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 花无缺 原创 收录于专栏 【洛谷算法题】 文章目录 【洛谷算法题】P1000-超级玛丽游戏【入门1顺序结构】&#x1f30f;题目描述&#x1f30f;输入格…

Android 多渠道打包及VasDolly使用

目录 1.添加productFlavors的配置buildConfigFieldmanifestPlaceholdersresValue 2.设置apk文件的名称&#xff0c;便于识别3.添加vasdolly、添加gradle脚本&#xff08;windows&#xff09; 作用&#xff1a;一次性可以打多个apk包&#xff0c;名字、包名、logo等可以不相同。…

jstat(JVM Statistics Monitoring Tool):虚拟机统计信息监视工具

jstat&#xff08;JVM Statistics Monitoring Tool&#xff09;&#xff1a;虚拟机统计信息监视工具 用于监视虚拟机各种运行状态信息的命令行工具。 它可以显示本地或者远程虚拟机进程中的类加载、内存、垃圾收集、即时编译等运行时数据&#xff0c;在没有GUI图形界面、只提…

如何使用CSS实现一个水平居中和垂直居中的布局?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ 水平居中布局⭐ 垂直居中布局⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 记得点击上方或者右侧链接订阅本专栏哦 几何带你启航前端之旅 欢迎来到前端入门之旅&#xff01;这个专栏是为那些对Web开发感兴趣…

共创无线物联网数字化新模式|协创数据×企企通采购与供应链管理平台项目成功上线

近日&#xff0c;全球无线物联网领先者『协创数据技术股份有限公司』&#xff08;以下简称“协创数据”&#xff09;SRM采购与供应链项目全面上线&#xff0c;并于近日与企企通召开成功召开项目上线总结会。 基于双方资源和优势&#xff0c;共同打造了物联网特色的数字化采购供…

齐聚众力,中国移动以“百川”定乾坤

近日&#xff0c;由工业和信息化部、宁夏回族自治区人民政府主办的2023中国算力大会在宁夏银川举办。中国移动党组书记、董事长杨杰参加开幕式&#xff0c;并在大会主论坛作题为《算网筑基锻引擎 数实融合创未来》的主旨演讲。在演讲中&#xff0c;杨杰表示&#xff1a;未来&am…