Kotlin笔记(四):高阶函数

1. 高阶函数

1.1 定义高阶函数

 高阶函数和Lambda的关系是密不可分的。一些与集合相关的函数式API的用法,如map、filter函数等,Kotlin的标准函数,如run、apply函数等。这几个函数有一个共同的特点:它们都会要求我们传入一个Lambda表达式作为参数。像这种接收Lambda参数的函数就可以称为具有函数式编程风格的API,而如果你想要定义自己的函数式API,那就得借助高阶函数来实现了.

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

 一个函数怎么能接收另一个函数作为参数呢?这就涉及另外一个概念了:函数类型。我们知道,编程语言中有整型、布尔型等字段类型,而Kotlin又增加了一个函数类型的概念。如果我们将这种函数类型添加到一个函数的参数声明或者返回值声明当中,那么这就是一个高阶函数了。

 接下来我们就学习一下如何定义一个函数类型。不同于定义一个普通的字段类型,函数类型的语法规则是有点特殊的,基本规则如下:

(String, Int) -> Unit

 既然是定义一个函数类型,那么最关键的就是要声明该函数接收什么参数,以及它的返回值是什么。因此,->左边的部分就是用来声明该函数接收什么参数的,多个参数之间使用逗号隔开,如果不接收任何参数,写一对空括号就可以了。而->右边的部分用于声明该函数的返回值是什么类型,如果没有返回值就使用Unit,它大致相当于Java中的void。

 现在将上述函数类型添加到某个函数的参数声明或者返回值声明上,那么这个函数就是一个高
阶函数了,如下所示:

fun example(func: (String, Int) -> Unit) {func("hello", 123)
}

 可以看到,这里的example()函数接收了一个函数类型的参数,因此example()函数就是一个高阶函数。而调用一个函数类型的参数,它的语法类似于调用一个普通的函数,只需要在参数名的后面加上一对括号,并在括号中传入必要的参数即可。

 高阶函数允许让函数类型的参数来决定函数的执行逻辑。即使是同一个高阶函数,只要传入不同的函数类型参数,那么它的执行逻辑和最终的返回结果就可能是完全不同的。类似于Java中的回调函数,同样的参数,由于回调函数的实现不同,那么结果也是完全不同的,Kotlin中的高阶函数把设置回调函数和调用回调函数放在了一起.

1.2 高阶函数的使用

 如果每次调用任何高阶函数的时候都还得先定义一个与其函数类型参数相匹配的函数,这是不是有些太复杂了?因此Kotlin还支持其他多种方式来调用高阶函数,比如Lambda表达式、匿名函数、成员引用等。

 回顾之前学习的apply函数,它可以用于给Lambda表达式提供一个指定的上下文,当需要连续调用同一个对象的多个方法时,apply函数可以让代码变得更加精简,比如StringBuilder就是一个典型的例子。接下来我们就使用高阶函数模仿实现一个类似的功能。修改HigherOrderFunction.kt文件,在其中加入如下代码:

fun StringBuilder.build(block: StringBuilder.() -> Unit): StringBuilder {block()return this
}

 这里我们给StringBuilder类定义了一个build扩展函数,这个扩展函数接收一个函数类型参
数,并且返回值类型也是StringBuilder。

 注意,这个函数类型参数的声明方式和我们前面学习的语法有所不同:它在函数类型的前面加上了一个StringBuilder. 的语法结构。这是什么意思呢?其实这才是定义高阶函数完整的语法规则,函数类型的前面加上ClassName. 就表示这个函数类型是定义在哪个类当中的。那么这里将函数类型定义到StringBuilder类当中有什么好处呢?好处就是当我们调用build函数时传入的Lambda表达式将会自动拥有StringBuilder的上下文,同时这也是apply函数的实现方式。

 现在我们就可以使用自己创建的build函数来简化StringBuilder构建字符串的方式了。

fun main() {val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")val result = StringBuilder().build {append("Start eating fruits.\n")for (fruit in list) {append(fruit).append("\n")}append("Ate all fruits.")}println(result.toString())
}

 可以看到,build函数的用法和apply函数基本上是一模一样的,只不过我们编写的build函数目前只能作用在StringBuilder类上面,而apply函数是可以作用在所有类上面的。如果想实现apply函数的这个功能,需要借助于Kotlin的泛型才行.

1.3 高阶函数的原理及内联函数

 我们还是简单分析一下高阶函数的实现原理,使用刚num1AndNum2()函数来举例,代码如下所示:

fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {val result = operation(num1, num2)return result
}
fun main() {val num1 = 100val num2 = 80val result = num1AndNum2(num1, num2) { n1, n2 ->n1 + n2}
}

 可以看到,上述代码中调用了num1AndNum2()函数,并通过Lambda表达式指定对传入的两个整型参数进行求和。这段代码在Kotlin中非常好理解,因为这是高阶函数最基本的用法。可是我们知道,Kotlin的代码最终还是要编译成Java字节码的,但Java中并没有高阶函数的概念。

 那么Kotlin究竟使用了什么魔法来让Java支持这种高阶函数的语法呢?这就要归功于Kotlin强大
的编译器了。Kotlin的编译器会将这些高阶函数的语法转换成Java支持的语法结构,上述的Kotlin代码大致会被转换成如下Java代码:

public static int num1AndNum2(int num1, int num2, Function operation) {int result = (int) operation.invoke(num1, num2);return result;
}
public static void main() {int num1 = 100;int num2 = 80;int result = num1AndNum2(num1, num2, new Function() {@Overridepublic Integer invoke(Integer n1, Integer n2) {return n1 + n2;}});
}

 考虑到可读性,我对这段代码进行了些许调整,并不是严格对应了Kotlin转换成的Java代码。可
以看到,在这里num1AndNum2()函数的第三个参数变成了一个Function接口,这是一种Kotlin内置的接口,里面有一个待实现的invoke()函数。而num1AndNum2()函数其实就是调用了Function接口的invoke()函数,并把num1和num2参数传了进去。在调用num1AndNum2()函数的时候,之前的Lambda表达式在这里变成了Function接口的匿名类实现,然后在invoke()函数中实现了n1 + n2的逻辑,并将结果返回。

 这就是Kotlin高阶函数背后的实现原理。你会发现,原来我们一直使用的Lambda表达式在底层
被转换成了匿名类的实现方式。这就表明,我们每调用一次Lambda表达式,都会创建一个新的匿名类实例,当然也会造成额外的内存和性能开销。为了解决这个问题,Kotlin提供了内联函数的功能,它可以将使用Lambda表达式带来的运行时开销完全消除。

 内联函数的用法非常简单,只需要在定义高阶函数时加上inline关键字的声明即可,如下所示:

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

 内联函数的工作原理又是什么呢?其实并不复杂,就是Kotlin编译器会将内联函数中的代码在编译的时候自动替换到调用它的地方,这样也就不存在运行时的开销了。

 我们通过图例的方式来详细说明内联函数的代码替换过程。首先,Kotlin编译器会将Lambda表达式中的代码替换到函数类型参数调用的地方:
在这里插入图片描述
 接下来,再将内联函数中的全部代码替换到函数调用的地方
在这里插入图片描述
 最终的代码就被替换成了如下图所示的样子
在这里插入图片描述
 正是如此,内联函数才能完全消除Lambda表达式所带来的运行时开销。在编译时,将Lambda表达式中的实现代码替换到内联函数中,从而消除了额外的资源开销.

1.4 noinline与crossinline

 接下来我们要讨论一些更加特殊的情况。比如,一个高阶函数中如果接收了两个或者更多函数类型的参数,这时我们给函数加上了inline关键字,那么Kotlin编译器会自动将所有引用的Lambda表达式全部进行内联。但是,如果我们只想内联其中的一个Lambda表达式该怎么办呢?这时就可以使用noinline关键字了,如下所示:

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

可以看到,这里使用inline关键字声明了inlineTest()函数,原本block1和block2这两
个函数类型参数所引用的Lambda表达式都会被内联。但是我们在block2参数的前面又加上了
一个noinline关键字,那么现在就只会对block1参数所引用的Lambda表达式进行内联了。这就是noinline关键字的作用。

 前面我们已经解释了内联函数的好处,那么为什么Kotlin还要提供一个noinline关键字来排除内联功能呢?这是因为内联的函数类型参数在编译的时候会被进行代码替换,因此它没有真正的参数属性。非内联的函数类型参数可以自由地传递给其他任何函数,因为它就是一个真实的参数,而内联的函数类型参数只允许传递给另外一个内联函数,这也是它最大的局限性

 另外,内联函数和非内联函数还有一个重要的区别,那就是内联函数所引用的Lambda表达式
中是可以使用return关键字来进行函数返回的,而非内联函数只能进行局部返回
, 这一点思考一下内联函数编译替换的过程即可理解。为了说明这个问题,我们来看下面的例子。

fun printString(str: String, block: (String) -> Unit) {println("printString begin")block(str)println("printString end")
}
fun main() {println("main start")val str = ""printString(str) { s ->println("lambda start")if (s.isEmpty()) return@printStringprintln(s)println("lambda end")}println("main end")
}

 这里定义了一个叫作printString()的高阶函数,用于在Lambda表达式中打印传入的字符串参数。但是如果字符串参数为空,那么就不进行打印。注意,Lambda表达式中是不允许直接使用return关键字的,这里使用了return@printString的写法,表示进行局部返回,并且不再执行Lambda表达式的剩余部分代码。现在我们就刚好传入一个空的字符串参数,运行程序,打印结果如图所示。
在这里插入图片描述

 可以看到,除了Lambda表达式中return@printString语句之后的代码没有打印,其他的日志是正常打印的,说明return@printString确实只能进行局部返回。但是如果我们将printString()函数声明成一个内联函数,那么情况就不一样了,如下所示:

inline fun printString(str: String, block: (String) -> Unit) {println("printString begin")block(str)println("printString end")
}
fun main() {println("main start")val str = ""printString(str) { s ->println("lambda start")if (s.isEmpty()) returnprintln(s)println("lambda end")}println("main end")
}

 现在printString()函数变成了内联函数,我们就可以在Lambda表达式中使用return关键字了。此时的return代表的是返回外层的调用函数,也就是main()函数,如果想不通为什么的话,可以回顾一下在上一小节中学习的内联函数的代码替换过程(return语句直接替换到main()函数中了,终止了main()函数的允许)。现在重新运行一下程序,打印结果如图6.17所示。
在这里插入图片描述

 将高阶函数声明成内联函数是一种良好的编程习惯,事实上,绝大多数高阶函数是可以直接声
明成内联函数的,但是也有少部分例外的情况
。观察下面的代码示例:

inline fun runRunnable(block: () -> Unit) {val runnable = Runnable {block()}runnable.run()
}

 这段代码在没有加上inline关键字声明的时候绝对是可以正常工作的,但是在加上inline关
键字之后就会提示如图所示的错误。
在这里插入图片描述
 这个错误出现的原因解释起来可能会稍微有点复杂。首先,在runRunnable()函数中,我们创建了一个Runnable对象,并在Runnable的Lambda表达式中调用了传入的函数类型参数。而Lambda表达式在编译的时候会被转换成匿名类的实现方式,也就是说,上述代码实际上是在匿名类中调用了传入的函数类型参数。而内联函数所引用的Lambda表达式允许使用return关键字进行函数返回,但是由于我们是在匿名类中调用的函数类型参数,此时是不可能进行外层调用函数返回的,最多只能对匿名类中的函数调用进行返回,因此这里就提示了上述错误。

 也就是说,如果我们在高阶函数中创建了另外的Lambda或者匿名类的实现,并且在这些实现中调用函数类型参数,此时再将高阶函数声明成内联函数,就一定会提示错误。那么是不是在这种情况下就真的无法使用内联函数了呢?也不是,比如借助crossinline关键字就可以很好地解决这个问题:

inline fun runRunnable(crossinline block: () -> Unit) {val runnable = Runnable {block()}runnable.run()
}

 可以看到,这里在函数类型参数的前面加上了crossinline的声明,代码就可以正常编译通过了。

 那么这个crossinline关键字又是什么呢?前面我们已经分析过,之所以会提示错误,就是因为内联函数的Lambda表达式中允许使用return关键字,和高阶函数的匿名类实现中不允许使用return关键字之间造成了冲突。而crossinline关键字就像一个契约,它用于保证在内联函数的Lambda表达式中一定不会使用return关键字,这样冲突就不存在了,问题也就巧妙地解决了。

 声明了crossinline之后,我们就无法在调用runRunnable函数时的Lambda表达式中使用return关键字进行函数返回了,但是仍然可以使用return@runRunnable的写法进行局部返回。总体来说,除了在return关键字的使用上有所区别之外,crossinline保留了内联函数的其他所有特性。

1.5 总结

 高阶函数就是参数中有函数类型参数的函数. 在调用时由于会产生额外的接口创建资源消耗,为了避免此缺点,引入inline内联函数的概念, 通过在编译时进行代码替换来避免额外的资源消耗.

 内联函数中的函数类型参数不能像普通参数那样随意传递到其他函数中(因为编译时进行了代码替换,实际中并不存在此函数类型参数了),所以为了解决此缺点,引入noinline关键字,表示对内联函数中的函数类型参数并不进行代码替换.

crossinline关键字是为了解决在高阶内联函数中,创建了另外的Lambda或者匿名类的实现,并且在这些实现中调用函数类型参数导致的错误(主要是内联函数进行代码替换导致的return关键字的使用问题).

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

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

相关文章

4.查询用户的累计消费金额及VIP等级

思路分析: (1)按照user_id及create_date 分组求消费金额total_amount (2)开窗计算同user_id下的累计销售金额sum(total_amount) over(partition by user_id order by create_date ROWS BETWEEN UNBOUNDED PRECEDING AN…

07测试Maven中依赖的范围,依赖的传递原则,依赖排除的配置

依赖的特性 scope标签在dependencies/dependency标签内,可选值有compile(默认值),test,provided,system,runtime,import compile:在项目实际运行时真正要用到的jar包都是以compile的范围进行依赖 ,比如第三方框架SSM所需的jar包test:测试过程中使用的j…

CustomShapes/自定义形状, CustomCurves/自定义曲线, AnimateableData/数据变化动画 的使用

1. CustomShapes 自定义形状视图 1.1 资源图文件 therock.png 1.2 创建自定义形状视图 CustomShapesBootcamp.swift import SwiftUI/// 三角形 struct Triangle: Shape{func path(in rect: CGRect) -> Path {Path { path inpath.move(to: CGPoint(x: rect.midX, y: rect.mi…

云原生网关可观测性综合实践

作者:钰诚 可观测性 可观测性(Observability)是指系统、应用程序或服务的运行状态、性能和行为能够被有效地监测、理解和调试的能力。 随着系统架构从单体架构到集群架构再到微服务架构的演进,业务越来越庞大,也越来…

数字孪生在制造运行管理(MOM)的七大应用场景

数字经济时代,数字孪生作为实现各行各业智能化、数字化的重要手段之一,受到了各方的广泛重视。随着各项关键使能技术的不断发展,数字孪生的应用价值有望得到进一步释放。这些关键使能技术包括建模、渲染、仿真、物联网、虚拟调试、可视化等&a…

02_单片机及开发板介绍

单片机简介 单片机,又称为微控制器(Microcontroller),是一种集成了微处理器核心、存储器、输入/输出接口及各种功能模块的集成电路芯片。它通常由中央处理器(CPU)、存储器、输入/输出接口以及各种外设组成&…

文献阅读快速法-ChatPDF

如题,直接提供给大家一款能够快速阅读文档的好工具——iTextMaster。 iTextMaster是一款免费的pdf阅读浏览器,上传pdf文档后等待几秒钟,AI就会自动反馈给用户关于文档的摘要总结。十分的方便且实用。 ChatPDF为您提供简洁的文档摘要。对于那…

【算法|动态规划No.21】leetcode494. 目标和

个人主页:兜里有颗棉花糖 欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 兜里有颗棉花糖 原创 收录于专栏【手撕算法系列专栏】【LeetCode】 🍔本专栏旨在提高自己算法能力的同时,记录一下自己的学习过程,希望…

8月19日PMP成绩,预计10月16日公布!附查询入口、流程

PMP的考试成绩一般在考后6-8周即可查询,8月PMP的成绩预计会在北京时间10月16日晚上公布,具体时间以官方公告为准。 如何查询8月考试成绩? 渠道一:收到PMI邮件提醒 当你注册PMI所使用的邮箱收到一封PMI发来的,标题为…

『PyQt5-Qt Designer篇』| 13 Qt Designer中如何给工具添加菜单和工具栏?

13 Qt Designer中如何给工具添加菜单和工具栏? 1 创建默认窗口2 添加菜单栏3 查看和调用1 创建默认窗口 当新创建一个窗口的时候,默认会显示有:菜单栏和状态栏,如下: 可以在菜单栏上右键-移除菜单栏: 可以在菜单栏上右键-移除状态栏: 2 添加菜单栏 在窗口上,右键-创建…

软件测试定位bug方法+定位案例(详解)

1、问题bug定位技巧 首先,作为开发也好,测试也好,定位问题有一个总的思路,而这个思路是和数据的走向一致的。 大致是这样: 用户层面问题 -> Web页面/软件界面 -> 中间件 -> 后端服务 -> 代码 -> 数据…

如何正确维护实验室超声波清洗器?

实验室一直被视为一个严谨而严肃的场所,实验应遵循一定的步骤,使用的设备也经历了详细的选择,如实验室超声波清洗机,其特点远强于一般类型的清洗机。专门负责采购的实验室人员一般对优质服务的实验室超声波清洗机印象深刻&#xf…

故障维修无忧服务:OLED透明拼接屏的专业技术支持与保修服务

OLED透明拼接屏作为未来显示技术的领军者,以其卓越的画质和全方位的优势在市场上备受推崇。 本文将深入探讨OLED透明拼接屏的画质特点和独有的优势,并为您提供选购指南、价格表以及故障维修服务,助您了解并选择最适合的OLED透明拼接屏。 一、…

使用CFimagehost源码搭建无需数据库支持的PHP免费图片托管私人图床

文章目录 1.前言2. CFImagehost网站搭建2.1 CFImagehost下载和安装2.2 CFImagehost网页测试2.3 cpolar的安装和注册 3.本地网页发布3.1 Cpolar临时数据隧道3.2 Cpolar稳定隧道(云端设置)3.3.Cpolar稳定隧道(本地设置) 4.公网访问测…

NPM 常用命令(十二)

目录 1、npm unpublish 1.1 使用语法 1.2 描述 2、npm unstar 2.1 使用语法 3、npm update 3.1 使用语法 3.2 描述 3.3 示例 插入符号依赖 波浪号依赖 低于 1.0.0 的插入符号依赖 子依赖 更新全局安装的包 4、npm version 4.1 使用语法 5、npm view 5.1 使用语…

Raven2靶机渗透

1. 信息收集 1.1 主机探测 sudo arp-scan -l1.2 端口扫描 nmap -p- -A 192.168.16.185开放了80端口,尝试登录网址查看信息,通过浏览器插件找出指纹 1.3 目录扫描 访问登录界面,发现remember Me怀疑是shiro界面 登录/vendor/界面&#xff0…

「深入探究Web页面生命周期:DOMContentLoaded、load、beforeunload和unload事件」

🎬 江城开朗的豌豆:个人主页 🔥 个人专栏 :《 VUE 》 《 javaScript 》 📝 个人网站 :《 江城开朗的豌豆🫛 》 ⛺️ 生活的理想,就是为了理想的生活 ! 目录 引言 1. DOMContentLoaded 1.1 属性 1.2 A…

【aloam】ubuntu20.04 配置 aloam 环境,编译过程报错及成功解决方法

为什么写这篇博客 ALOAM是slamer的必经之路,official提供的基础环境推荐ubuntu16.04或者18.04,而我用20.04已经有一段时间了,不方便换,但由于其他原因也不得不去配置。过程中出现了几个问题,在这里也就20分钟&#xf…

04在命令行中使用Maven命令创建Maven版的Web工程,并将工程部署到服务器的步骤

创建Maven版的Web工程 使用命令生成Web工程 使用mvn archetype:generate命令生成Web工程时,需要使用一个专门生成Web工程骨架的archetype(参照官网看到它的用法) -D表示后面要附加命令的参数,字母D和后面的参数是紧挨着的,中间没有任何其它…

记一次Redis Cluster Pipeline导致的死锁问题

作者:vivo 互联网服务器团队- Li Gang 本文介绍了一次排查Dubbo线程池耗尽问题的过程。通过查看Dubbo线程状态、分析Jedis连接池获取连接的源码、排查死锁条件等方面,最终确认是因为使用了cluster pipeline模式且没有设置超时时间导致死锁问题。 一、背…