Kotlin 高阶函数详解

news2024/12/29 2:43:20

高阶函数

在 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.coloradmin.cn/o/919309.html

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

相关文章

CGAL 点云分类

文章目录 一、简介二、实现代码三、实现效果参考资料一、简介 点云分类一直是点云数据应用的永恒课题,它包含很多,如地面点分类、建筑物分类、植被分类等。CGAL中也为我们提供了一种点云分类的方式,其具体的计算过程如下所述: 首先,使用点云中所携带的一些几何特征来对数据…

三、数据库索引

1、索引介绍 索引是一种用于快速查询和检索数据的数据结构&#xff0c;其本质可以看成是一种排序好的数据结构。 常见的索引结构有&#xff1a;B数&#xff0c;B树&#xff0c;Hash和红黑树等。在MySQL中&#xff0c;无论是 InnoDB还是MyISAM&#xff0c;都使用了B树作为索引…

西班牙卡瓦起泡酒的风味搭配

卡瓦是一种对食物友好的西班牙起泡酒&#xff0c;它的制作方法和香槟一样&#xff0c;可以和类似的食物搭配。卡瓦食物搭配包括各种食物&#xff0c;从海鲜和鱼到火腿&#xff0c;以及不同类型的小吃&#xff0c;也可以将卡瓦酒与甜点、水果和奶酪搭配。 卡瓦酒是世界上最著名的…

IDEA常用插件之注解插件

文章目录 注解插件JavaDoc插件安装修改配置生成文档加入自己信息 Easy JavaDoc安装插件在线安装离线安装中文名自动转英文加注释默认快捷键&#xff08;可通过IDEA快捷键设置修改&#xff09; 注解插件 JavaDoc插件 安装 修改配置 生成文档加入自己信息 Easy JavaDoc 中文文…

一种pug与html相互转换的工具

有时候看pug很不方便&#xff0c;这个语言虽然简洁&#xff0c;但可读性与维护性较差&#xff0c;所以需要进行转换&#xff0c;这个是win工具&#xff0c;比较方便。 这个工具的下载地址如下&#xff1a; https://download.csdn.net/download/qq_40032778/88244980 解压后如下…

PDFPrinting.Net Crack

PDFPrinting.Net Crack 它能够轻松灵活地预测完美的打印结果以及用户文件的示例性显示。在.NET的PDF打印中&#xff0c;可以快速浏览最关键的元素。如果用户需要获得更详细的概述&#xff0c;那么他可以查看快速入门手册&#xff0c;甚至现有文档的详细概述参考。 在这种情况下…

atxserver bug记录

8. 解决无法点击屏幕 原因&#xff1a;remotecontrol_android.html为按比例自动缩放&#xff0c;play.html&#xff08;Django&#xff09;显示的屏幕大小不会随页面放大缩小。有个h265方式获取的宽高是720*448&#xff0c;电脑上显示的大小是545*339&#xff0c;这个对不上&am…

理解机制,再探单元工厂的实现原理

最近有点忙,好久没更新文章了,今天继续再研究一下单元工厂的实现机制。为什么我们要这么重视这一块的内容呢?因为用计算机的目的是为了处理大量数据,如果数据量不大,大多情况下用纸就好了,专门用个计算设备的便捷性也就体现不出来。而大量数据的呈现方式的多样性精髓就在…

cuda编程002—流

没有使用同步的情况&#xff1a; #include <stdio.h> #include <cuda_runtime.h>__global__ void test_kernel(){printf("Message from Device.\n"); } void test(){test_kernel<<<1, 1>>>(); } #include <cuda_runtime.h> #i…

蓝蓝设计-UI设计公司作品-博晖创新原子吸收光谱仪软件交互及界面设计

博晖创新原子吸收光谱仪软件交互及界面设计 图标设计 | 交互设计 | 界面设计 博晖公司拥有强大的自主研发实力&#xff0c;建立了专业的研发团队&#xff0c;通过不断的技术创新&#xff0c;形成了分子诊断、免疫诊断、原子吸收、原子荧光及质谱五大技术平台&#xff0c;并成功…

ESP8266显示gif动态图,使用U8g2库

一.代码 #include <U8g2lib.h> //实现gif火柴人跑步动画// 定义GIF动画的帧数据 const unsigned char frame1[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0…

安装docker服务及docker基本操作

一、docker安装&#xff08;yum安装&#xff09; 基于centos7 1.添加docker-ce 源信息 安装依赖包&#xff08;yum-utils 提供了 yum-config-manager &#xff0c;并且 device mapper 存储驱动程序需要device-mapper-persistent-data 和 lvm2&#xff09; yum install yum-…

阿丹千问V1.5-迭代升级-使用人工智能提炼文字-java代码生成流程图-百度千帆大模型

阿丹&#xff1a; 今天在学习新技的时候发现了一个可以偷懒的地方&#xff0c;在学习新的知识体系的时候需要去理解文档中的逻辑关系等等&#xff0c;那么如果有一个东西可以支持输入一段具有逻辑的文字就可以帮我提炼起其中的逻辑&#xff0c;并帮助我绘制一个流程图岂不是美哉…

Node基础--Node基础使用体验

在上一篇文章中提到我们按照好Node.js之后&#xff0c;就可以在控制台看到其版本。那么下面我们一起来看看如何使用node执行js文件代码。 (1).在本地创建一个名称为hello.js的文件&#xff0c;输入内容如下所示: console.log("helloworld");var a 1;var b 2;cons…

leetcode 118.杨辉三角

⭐️ 题目描述 &#x1f31f; leetcode链接&#xff1a;https://leetcode.cn/problems/pascals-triangle/description/ 代码&#xff1a; class Solution { public:vector<vector<int>> generate(int numRows) {// 先开空间vector<vector<int>> v;v.…

Dockerfile推送私有仓库的两个案例

一&#xff0c;编写Dockerfile制作Web应用系统nginx镜像&#xff0c;生成镜像nginx:v1.1&#xff0c;并推送其到私有仓库。 具体要求如下&#xff1a; &#xff08;1&#xff09;基于centos基础镜像&#xff1b; &#xff08;2&#xff09;指定作者信息&#xff1b; &#xff…

driver‘s license exam train

驾考训练 driver‘s license exam 1_spencer_tseng的博客-CSDN博客 driver‘s license exam 2_spencer_tseng的博客-CSDN博客 driver‘s license exam 3_spencer_tseng的博客-CSDN博客 driver‘s license exam 4_spencer_tseng的博客-CSDN博客 car indicator light_spencer…

10个必备的IntelliJ IDEA插件,提高开发效率

WakaTime 安装链接&#xff1a;https://plugins.jetbrains.com/plugin/7425-wakatime WakaTime 是一个能够根据你的编程活动&#xff0c;自动生成各种数据的工具&#xff0c;可以让你看到在编码过程中的指标、见解和时间跟踪&#xff0c;从而评估你的编码效率。 你可以准确地知…