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 = ::add

fun 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 = ::printValue

fun 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 = 0
    foo {
        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的代码
    @Benchmark
    fun testNonInlined() {
        var i = 0
        foo {
            i++
        }

    }

    // 测试inline的代码
    @Benchmark
    fun testInlined() {
        var i = 0
        fooInline {
            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 = 0
    fooInline {
        fooInline {
            fooInline {
                fooInline {
                    fooInline {
                        fooInline {
                            fooInline {
                                fooInline {
                                    fooInline {
                                        fooInline {
                                            i++
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

根据内联函数的原理,上面的代码等价于下面这样:

// 使用inline的高阶函数
inline fun fooInline(block: () -> Unit) {
    block()
}

@Benchmark
fun testInlined() {
    var i = 0
    fooInline { 
        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.mfbz.cn/a/86912.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

vue中form和table标签过长

form标签过长 效果&#xff1a; 代码&#xff1a; <el-form-item v-for"(item,index) in ticketEditTable1" :label"item.fieldNameCn" :propitem.fieldName :key"item.fieldNameCn" overflow"":rules"form[item.fieldName…

Python土力学与基础工程计算.PDF-土的三项组成

5.3 Python求解 Python 求解代码如下&#xff1a; 1. # 定义已知参数 2. G_s 2.7 # 比重 3. w 0.2 # 含水量 4. e 0.6 # 孔隙比 5. gamma_w 9.81 # 水的重度 6. 7. # 根据公式计算饱和度 8. S_r G_s * w / e 9. print("饱和度为", S_r) 10. 11.…

rust库学习-env_logger(actix-web添加彩色日志、rust添加彩色日志 )

文章目录 介绍actix-web启用彩色日志crate地址&json格式日志 我们在进行rust的web开发时&#xff0c;如果不指定日志&#xff0c;就不会有输出&#xff0c;非常不友好 这里我们使用env_logger进行日志打印 介绍 env_logger 需要配合 log 库使用, env_logger 是 Rust 社区…

SpringCloud学习笔记(四)_ZooKeeper注册中心

基于Spring Cloud实现服务的发布与调用。而在18年7月份&#xff0c;Eureka2.0宣布停更了&#xff0c;将不再进行开发&#xff0c;所以对于公司技术选型来说&#xff0c;可能会换用其他方案做注册中心。本章学习便是使用ZooKeeper作为注册中心。 本章使用的zookeeper版本是 3.6…

Android相机-HAL子系统

引言 应用框架要通过拍照预览摄像获得照片或者视频,就需要向相机子系统发出请求, 一个请求对应一组结果 一次可发起多个请求&#xff0c;并且提交请求是非阻塞的&#xff0c;始终按照接收的顺序以队列的形式先进先出地进行顺序处理 一个请求包含了拍摄和拍照配置的所有信息&…

图为科技-边缘计算在智慧医疗领域的作用

边缘计算在智慧医疗领域的作用 随着科技的进步&#xff0c;智慧医疗已成为医疗行业的重要发展趋势。边缘计算作为新兴技术&#xff0c;在智慧医疗领域发挥着越来越重要的作用。本文将介绍边缘计算在智慧医疗领域的应用及其优势&#xff0c;并探讨未来发展方向。 一、边缘计算…

Skywalking Kafka Tracing实现

背景 Skywalking默认场景下&#xff0c;Tracing对于消息队列的发送场景&#xff0c;无法将TraceId传递到下游消费者&#xff0c;但对于微服务场景下&#xff0c;是有大量消息队列的业务场景的&#xff0c;这显然无法满足业务预期。 解决方案 Skywalking的官方社区中&#xf…

远程调试环境配置

目录 一、准备工作 二、ssh连接和xdebug配置 1.ssh连接 2.xdebug配置 三、xdebug调试&#xff0c;访问 一、准备工作 1.安装vscode里面的两个扩展 2.安装对应PHP版本的xdebug 去xdebug官方&#xff0c;复制自己的phpinfo源码到方框里&#xff0c;再点击Analyse Xdebug: …

Pandas数据清洗和常用函数

数据清洗 数据清洗是对一些没用的数据进行处理的过程。 当数据出现确实、数据格式错误、错误数据或重复数据的情况&#xff0c;如果我们想要分析的更加准确&#xff0c;就要对没用的数据进行处理。 此时我们学习采用菜鸟教程的数据作为案例&#xff0c;如下图所示。 在途中包…

AWS SDK 3.x for .NET Framework 4.0 可行性测试

前言 为了应对日益增长的网络安全挑战, 越来越多的互联网厂商已经陆续开始或者已经彻底停止了对 SSL 3 / TLS 1.0 / TLS1.1 等上古加密算法的支持. 而对于一些同样拥有悠久历史的和 AWS 服务相关联的应用程序, 是否可以通过仅更新 SDK 版本的方式来适应新的环境. 本文将以 Win…

【ElasticSearch】一键安装IK分词器无需其他操作

要注意的时下面命令中的es是我容器的名称&#xff0c;要换成你对应的es容器名 docker exec -it es /bin/bash # 进入容器 ./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis- ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.1…

锚定医学营养 健启星深耕不辍

在生命医学中&#xff0c;营养被称为维持患者生命的物质基础。医学营养&#xff0c;是结合了医学临床营养、营养素与疾病预防等方面&#xff0c;并根据患者的医疗记录、身体检查及心理情况&#xff0c;由医生及专业营养师给出配比完善的营养素&#xff0c;以此来增加患者身体的…

IDEA常用插件之类Jar包搜索Maven Search

文章目录 IDEA常用插件之类Jar包搜索Maven Search说明安装插件使用方法1.搜索自己要搜的jar包2.根据类名搜索 IDEA常用插件之类Jar包搜索Maven Search 说明 它可以帮助用户快速查找和浏览Maven中央存储库中可用的依赖项和插件。它可以帮助用户更方便地管理项目依赖项。 安装…

进行Stable Diffusion的ai训练怎么选择显卡?

Stable Diffusion主要用于从文本生成图像&#xff0c;是人工智能技术在内容创作行业中不断发展的应用。要在本地计算机上运行Stable Diffusion&#xff0c;您需要一个强大的 GPU 来满足其繁重的要求。强大的 GPU 可以让您更快地生成图像&#xff0c;而具有大量 VRAM 的更强大的…

基于YOLOV8模型的课堂场景下人脸目标检测系统(PyTorch+Pyside6+YOLOv8模型)

摘要&#xff1a;基于YOLOV8模型的课堂场景下人脸目标检测系统可用于日常生活中检测与定位课堂场景下人脸&#xff0c;利用深度学习算法可实现图片、视频、摄像头等方式的目标检测&#xff0c;另外本系统还支持图片、视频等格式的结果可视化与结果导出。本系统采用YOLOv8目标检…

uniapp 使用permission获取录音权限

使用前&#xff0c;需要先配置权限 android.permission.RECORD_AUDIO

Linux基础命令2

目录 基础命令 ln命令 grep命令 查看文本内容的五种方式 1.cat命令 2.more命令 3.less命令 4.head命令 5.tail命令 echo命令 alias命令 基础命令 ln命令 作用&#xff1a;创建链接文件 格式&#xff1a;ln 命令选项 目标文件 链接文件名 命令选项&#xff1a;-s…

在Eclipse中创建javaweb工程

新建动态web工程 点击project或other之后&#xff0c;如何快速找到Dynamic Web Project 填写工程名等详细信息 也许会出现下面的对话框 项目结构图

韩顺平java集合

遍历集合方式: public static void main(String[] args) {List<Object> arrayList new ArrayList<>();arrayList.add(1);arrayList.add(3);arrayList.add(111);Iterator<Object> iterator arrayList.iterator();while (iterator.hasNext()){System.out.pri…

黑客自学笔记

谈起黑客&#xff0c;可能各位都会想到&#xff1a;盗号&#xff0c;其实不尽然&#xff1b;黑客是一群喜爱研究技术的群体&#xff0c;在黑客圈中&#xff0c;一般分为三大圈&#xff1a;娱乐圈 技术圈 职业圈。 娱乐圈&#xff1a;主要是初中生和高中生较多&#xff0c;玩网恋…
最新文章