Kotlin笔记(四):高阶函数

news2024/11/25 6:34:00

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 = 100
 	val num2 = 80
 	val 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() {
 		@Override
 		public 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@printString
 		println(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()) return
 		println(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.coloradmin.cn/o/1097738.html

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

相关文章

从虚拟电厂在上海的实践探索看企业微电网数字化的意义

安科瑞 华楠 作为典型的人口聚集、负荷密集区域,上海市具有外来电比例高、本地资源禀赋不足的特点。从发电侧角度来看,近年来上海风、光等新能源发电装机比例逐年提升,传统的火电逐渐成为调节性发电资源;从负荷侧角度来看上海以第…

什么是补丁管理,如何选择补丁管理软件

补丁管理是识别、测试、部署和安装软件补丁(或更新)到计算机的过程。软件补丁是一段代码,专为修复软件中的现有错误/漏洞、添加新功能或增强其安全性而量身定制。 通常,软件补丁管理过程包括扫描网络中的计算机以查找缺失的补丁&…

WATLOW CAS200 CLS216 释放人工智能(AI)能力用于导航

WATLOW CAS200 CLS216 释放人工智能(AI)能力用于导航 正如本周在自动化2019展示和会议,MiR1000可以在动态环境中自动拾取、运输和交付托盘和其他重达1000千克(2200磅)的重物。与2018年推出的MiR500一样,MiR1000旨在作为工厂车间叉车的协作、安全和灵活…

架构-设计原则

1、面向对象的SOLID 1.1 概述 SOLID是5个设计原则开头字母的缩写,其本身就有“稳定的”的意思,寓意是“遵从SOLID原则可以建立稳定、灵活、健壮的系统”。5个原则分别如下: Single Responsibility Principle(SRP)&am…

c++香甜的黄油(acwing)

农夫John发现了做出全威斯康辛州最甜的黄油的方法:糖。 把糖放在一片牧场上,他知道 N 只奶牛会过来舔它,这样就能做出能卖好价钱的超甜黄油。 当然,他将付出额外的费用在奶牛上。 农夫John很狡猾,就像以前的巴甫洛夫…

双十一购物指南:电视盒子哪个牌子好?口碑电视盒子品牌排行榜

双十一可以说是年度最低价,我们都会在此时买买买,电视盒子作为日常必备销量同样火爆,近来很多朋友咨询小编电视盒子哪个牌子好,因此我整理了用户评价最高的电视盒子品牌排行榜,看完就知道哪些电视盒子最值得买了。 推荐…

mysql优化(关联表查询效率低下问题)

场景:表A和表B关联 A.id B.a_id 查询,并用表A的字段create_time排序(查询条件附带between create_time) 造成结果:explain结果如下,(查询结果耗时长) 原因:尚不明确. 解决方案:给B的a_id字段加上索引(create index index_a_id on B(a_id)),就解决了一部分问题.查询速度提升上…

pg 时间操作方法

1.概念 本质就是个宏定义,只是动态的 2.常用使用形式和函数 current_date :当前时间 current_timestamp:时间戳,更细 date ‘当前日期’ :指定日期 age(日期1,日期2)&#xf…

OpenWrt如何公网ssh远程连接【内网穿透】

文章目录 1.安装cpolar内网穿透2.配置SSH远程访问隧道3.公网远程连接4.固定远程连接地址 简单几步实现openWRT结合cpolar内网穿透工具实现远程ssh连接 1.安装cpolar内网穿透 cpolar是一个非常棒的跨平台、内网穿透工具,可以通过安全隧道将NAT或防火墙后面的本地服…

“5G+北斗”赋能千行百业,中海达亮相2023中国移动全球合作伙伴大会

10月12日,2023中国移动全球合作伙伴大会在广州保利世贸博览馆召开。本次活动以“算启新程 智享未来”为主题,重点展示中国移动全球合作伙伴在5G、终端、数智城市等方面的新产品、新技术和新服务。作为中国移动“朋友圈”战略合作企业及“北斗时空子链”链…

Python实现简易过滤删除数字的方法

嗨喽~大家好呀,这里是魔王呐 ❤ ~! python更多源码/资料/解答/教程等 点击此处跳转文末名片免费获取 如果想从一个含有数字,汉字,字母的列表中滤除仅含有数字的字符, 当然可以采取正则表达式来完成,但是有点太麻烦了…

【前端】vue在Windows平台IIS的部署

系列文章 【C#】IIS平台下,WebAPI发布及异常处理 本文链接:https://blog.csdn.net/youcheng_ge/article/details/126539836 【Vue】vue2与WebApi跨域CORS问题 本文链接:https://blog.csdn.net/youcheng_ge/article/details/133808959 文章目…

了解容器运行时安全:保护你的容器应用

前言 容器是一种虚拟化技术,用于封装和运行应用程序及其依赖项,以便在不同的计算环境中保持一致性和可移植性。自2013年容器诞生至今,容器Docker镜像的下载量超20亿,虽然容器行业发展如火如荼,但是其安全风险却不容乐…

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

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

Mac电脑日历怎么看状农历和事件?小历TinyCal怎么看农历日期

怎么设置电脑日期显示农历?电脑中如何查看万年历,电脑中快速查看万年历的方法。小历TinyCal mac版,是一款菜单栏日历软件,软件简洁大方、使用简单、操作方便,显示农历、节日节气和法定节假日,支持日历事件的…

轻松学会这招,给大量视频批量添加滚动字幕不求人

想要给大量视频批量添加滚动字幕不求人吗?下面就教你一个简单的方法。首先你需要下载并安装一款名为“固乔剪辑助手”的软件,这是一款非常专业的视频剪辑软件,它可以帮助你快速地给大量视频添加滚动字幕。 打开固乔剪辑助手软件后&#xff0c…

4年前,当我进入这家公司,便深感管理无力

有形的东西,看得见摸得着,只要有一道有形的围墙,管理也乱不到哪里去,可是这个行业的核心,是以合同为纽带的信息流管理。以长租合同为例,履约期限长达3年,涉及合同签约、归档、应收登记、实收登记…

Goland Cannot use ‘err‘ (type error) as the type any

问题描述: 用Goland写代码的时候,使用panic总是报错,官方用法也是报错,最后找到官方回复的链接,https://youtrack.jetbrains.com/issue/GO-12179/Cannot-use-err-type-error-as-the-type-any 问题解决方式&#xff1…

idea禁用双击ctrl

Run anything | IntelliJ IDEA Documentation Disable double modifier key shortcuts