解析Kotlin中的内联函数,inline、noinline、crossinline【笔记摘要】

用编译时常量的概念,引出本文要讲内联函数inline:

1.编译时常量

  • Java的编译时常量 Compile-time Constant

它有四个要求:1.这个变量需要是 final 的  2.类型只能是字符串或者基本类型  3.这个变量需要在声明的时候就赋值  4.等号右边还不能太复杂

final String name = "hsf";
final int age = 18;
final long current = System.currentTimeMillis();

这种编译时常量,会被编译器以内联的形式进行编译,也就是直接把你的值拿过去替换掉调用处的变量名来编译。这样一来,程序结构就变简单了,编译器和 JVM 也方便做各种优化。这,就是编译时常量的作用。

  • 这种编译时常量,到了 Kotlin 里有了一个专有的关键字,叫 const

一个变量如果以 const val 开头,它就会被编译器当做编译时常量来进行内联式编译:

在这里插入图片描述

当然你得符合编译时常量的特征啊,不然会报错,不给编。
在这里插入图片描述

让变量内联用的是 const;而除了变量,Kotlin 还增加了对函数进行内联的支持。在 Kotlin 里,你给一个函数加上 inline 关键字,这个函数就会被以内联的方式进行编译。
但!虽然同为内联,inline 关键字的作用和目的跟 const 是完全不同

2.inline出现的原因

事实上,inline 关键字不止可以内联自己的内部代码,还可以内联自己内部的内部的代码。什么叫「内部的内部」?就是自己的函数类型的参数

声明一个函数,其有一个函数类型的参数
在这里插入图片描述

我可以填成匿名函数的形式:
在这里插入图片描述

也可以简单点,写成 Lambda 表达式:
在这里插入图片描述

因为 Java 并没有对函数类型的变量的原生支持,Kotlin 需要想办法来让这种自己新引入的概念在 JVM 中落地,就是用一个 JVM 对象来作为函数类型的变量的实际载体,让这个对象去执行实际的代码。

也就是说,在我对代码做了刚才那种修改之后,程序在每次调用 hello() 的时候都会创建一个对象来执行 Lambda 表达式里的代码,虽然这个对象是用一下之后马上就被抛弃,但它确实被创建了。
在这里插入图片描述

但是如果这种函数被放在循环里执行,内存占用一下就飚起来了。而且关键是,你作为函数的创建者,并不知道、也没法规定别人在什么地方调用这个函数,也就是说,这个函数是否出现在循环或者界面刷新之类的高频场景里,是完全不可控的。

3.inline的作用

函数在被加了 inline 关键字之后,编译器在编译时不仅会把函数内联过来,而且会把它内部的函数类型的参数也内联过来。换句话说,这个函数被编译器贴过来的时候是完全展开铺平的:
在这里插入图片描述

经过这种优化,就避免了函数类型的参数所造成的临时对象的创建。这样的话,就不怕在循环或者界面刷新这样的高频场景里调用它们了
在这里插入图片描述

这就是 inline 关键字的用处:高阶函数(Higher-order Functions)有它们天然的性能缺陷,我们通过 inline 关键字让函数用内联的方式进行编译,来减少参数对象的创建,从而避免出现性能问题

4.noinline

当一个函数被内联之后,它内部的那些函数类型的参数就不再是对象了,因为它们的壳被脱掉了。换句话说,对于编译之后的字节码来说,这个对象根本就不存在。一个不存在的对象,你怎么使用?
所以当你要把一个这样的参数当做对象使用的时候,Android Studio 会报错,告诉你这没法编译:
在这里插入图片描述

那……我如果真的需要用这个对象怎么办?加上 noinline:
在这里插入图片描述

加了 noinline 之后,这个参数就不会参与内联了:
在这里插入图片描述

noinline 的作用:用来局部地、指向性地关掉函数的内联优化的。使得函数中的函数类型的参数可能被当做对象来使用

5.在Lamdba中使用return

//情况一
fun hello(postAction: () -> Unit) {LogUtil.d("Hello!")postAction()
}
//情况二
inline fun hello(postAction: () -> Unit) {LogUtil.d("Hello!")postAction()
}hello {LogUtil.d("Bye!")return  //实际上对于情况一,这里编译过不了LogUtil.d("Bye!2")
}

对于函数参数中的Lambda 的 return,我们有这样的直观感受。如果是在非内联函数中,return的应该是hello;如果是在内联函数中,return的应该是hello的外层函数。这就造成了一种歧义,那我一个 return 结束哪个函数,竟然要看这个函数是不是内联函数!那岂不是我每次写这种代码都得钻到原函数里去看看有没有 inline 关键字,才能知道我的代码会怎么执行?那这也太难了吧!

为了消除在Lamdba中return所带来的歧义,Kotlin指定了Lambda中return的规则:

  • 规则1、只有内联函数的 Lambda 参数可以使用 return。
fun hello(postAction: () -> Unit) {LogUtil.d("Hello!")postAction()
}hello {LogUtil.d("Bye!")return  //编译不通过,提示return' is not allowed hereLogUtil.d("Bye!2")
}

注意:如果给函数参数又加上了noinline,那么lambda中的return又报错了,很简单,因为它不属于内联的参数了,它又不是铺平的了,此时它的return又变得有歧义了

  • 规则2、Lambda 里的 return,结束的不是直接的外层函数,而是外层再外层的函数(因为内联函数已经被铺平了)
inline fun hello(postAction: () -> Unit) {LogUtil.d("Hello!")postAction()
}hello {LogUtil.d("Bye!")return  //编译通过,这个return结束的是hello外层的函数LogUtil.d("Bye!2")
}

注意:非Lamdba,也就是那种用fun来写的函数类型参数,在是否内联函数中都可以使用return,因为它都结束的是自己的这个fun

fun hello(postAction: () -> Unit) {LogUtil.d("Hello!")postAction()
}hello(fun() {LogUtil.d("Bye!")return  //这个return结束的是helloLogUtil.d("Bye!2")
})

6.crossinline

如果我要对内联函数里的函数类型的参数进行间接调用,例如:

fun ppp(runnable: Runnable) {...
}inline fun hello(postAction: () -> Unit) {LogUtil.d("Hello!")ppp{//实际这里编译不通过,提示: Can't inline 'postAction' here: it may contain non-local returns. Add 'crossinline' modifier to parameter declaration 'postAction'postAction()  }
}fun main(){hello {LogUtil.d("Bye!")returnLogUtil.d("Bye!2")}
}

这就带来了一个麻烦:本来在调用处行的 return 是要结束它外层再外层的 main() 函数的,但现在因为它被放在了 ppp() 里,hello() 对它的调用就变成了间接调用。所谓间接调用,直白点说就是它和外层的 hello() 函数之间的关系被切断了。和 hello() 的关系被切断,那就更够不着更外层的 main() 了,也就是说这个间接调用,导致 Lambda 里的 return 无法结束最外面的 main() 函数了。
因此Kotlin选择了,干脆内联函数里的函数类型的参数,不允许这种间接调用

那我如果真的有这种需求呢?如果我真的需要间接调用,怎么办?使用 crossinline。crossinline 也是一个用在参数上的关键字。当你给一个需要被间接调用的参数加上 crossinline,就对它解除了这个限制,从而就可以对它进行间接调用了:

inline fun hello(crossinline postAction: () -> Unit) {LogUtil.d("Hello!")ppp{postAction()}
}

不过这就又会导致前面说过的return歧义的问题,它结束的是谁?是包着它的 ppp(),还是依然是hello的外层?

hello {LogUtil.d("Bye!")returnLogUtil.d("Bye!2")
}

对于这种不一致,Kotlin 增加了一条额外规定:内联函数里被 crossinline 修饰的函数类型的参数,将不再享有「Lambda 表达式可以使用 return」的福利。所以这个 return 并不会面临「要结束谁」的问题,而是直接就不许这么写。

fun ppp(runnable: Runnable) {}inline fun hello(crossinline postAction: () -> Unit) {LogUtil.d("Hello!")ppp{postAction() }
}fun main(){hello {LogUtil.d("Bye!")return //这里编译不通过,提示:'return' is not allowed hereLogUtil.d("Bye!2")}
}

也就是说,间接调用和 Lambda 的 return,你只能选一个。
所以什么时候需要 crossinline?当你需要突破内联函数的「不能间接调用参数」的限制的时候,但伴随着就要放弃Lambda中使用return了

7.总结

  • inline 可以让你用内联(也就是函数内容直插到调用处)的方式来优化代码结构,从而减少函数类型的对象的创建;
  • noinline 是局部关掉这个优化,来摆脱 inline 带来的「不能把函数类型的参数当对象使用」的限制;
  • crossinline 是局部加强这个优化,让内联函数里的函数类型的参数可以被间接调用

8.扩展:inline的另类用法,在函数里直接去调用 Java 的静态方法

在这里插入图片描述

用偷天换日的方式来去掉了这些 Java 的静态方法的前缀,让调用更简单:
在这里插入图片描述

这种用法不是 inline 被创造的初衷,也不是 inline 的核心意义,这属于一种相对偏门的另类用法。不过这么用没什么问题啊,因为它的函数体简洁,并不会造成字节码膨胀的问题。你如果有类似的场景,也可以这么用。




参考文章:
Kotlin 源码里成吨的 noinline 和 crossinline 是干嘛的?看完这个视频你转头也写了一吨

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

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

相关文章

git提交实战

以新项目为例,如何在新项目新分支提交代码。 1.查看文件所在位置 git init 2.克隆项目到本地并完成身份配置 3.将需要新增的文件放到指定目录路径下 4.进入新克隆的文件 cd XXX 5.切换分支 git checkout XXX 6.标红者即为新提交的文件 git status 7.加入 git …

Java_多线程:线程池

1、线程池优点: 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行。提高线程的可管理性:线程是稀缺资源,如果无限…

泰雷茲具有首个通过FIPS 140-3 三级认证的HSMs

泰雷兹LunaHsm是业界首款通过FIPS140-33级认证的解决方案,安策引进泰雷兹HSM产品可以帮助您满足您的数据安全合规性需求,阻力企业提高竞争力。 安策提供泰雷茲ThalesLunaHSMs成为首个通过FIPS140-3三级认证的硬件安全模块图 我们很高兴地宣布&#xff0c…

面试题 1:阐述Python:except的用法和作用?

欢迎莅临我的博客 💝💝💝,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

微软关闭中国所有线下店,并不影响全球第一

​关注卢松松,会经常给你分享一些我的经验和观点。 微软没有被时代淘汰,时代也没有告别微软!中国市场对微软可有可无,即便没有中国市场,微软市值也在全球前三,这是事实!a 5月中旬,微软azure解散中国分部…

分享六款免费u盘数据恢复工具,U盘恢复工具集合【工具篇】

U盘里面的数据丢失了怎么找回?随着数字化时代的深入发展,U盘已成为我们日常生活中不可或缺的数据存储工具。然而,由于各种原因,如误删除、格式化、病毒攻击等,U盘中的数据可能会丢失,给用户带来极大的困扰。…

加装德国进口高精度主轴 智能手机壳「高质量高效率」钻孔铣槽

在当前高度智能化的社会背景下,智能手机早已成为人们生活、工作的必备品,智能手机壳作市场需求量巨大。智能手机壳的加工过程涉及多个环节,包括钻孔和铣槽等。钻孔要求精度高、孔位准确,而铣槽则需要保证槽位规整、深度适宜。这些…

IDEA中SpringBoot项目数据库连接加密方法

1. maven添加相应版本的依赖 这里注意,不能使用太新的版本,本人开发环境使用3.0.3版本时,报以下错误,使用2.1.2时报错消失。 *************************** APPLICATION FAILED TO START ***************************Description:…

Python容器 之 字典--字典的常用操作方法

1.增加和修改 字典[键] 值 键 存在, 修改 键 不存在, 添加 # 定义非空字典, 姓名, 年龄, 身高, 性别 my_dict {"name": "小明", "age": 18, "height": 1.78, "isMen": True} print(my_dict) # {name: 小明, age: 18, h…

企业如何管理安全生产工作?(附模板)

总结一下在企业内管理安全中遇到的一些问题: 1、 管理方式落后,还在使用纸质记录 2、 人员信息杂乱无章,无人整理 3、出现问题找不到源头和负责人 我做系统管理已经7年了,题主说的这些问题我之前也遇到过,相信也有…

API接口测试/Swgger-ui未授权访问

目录 API接口 接口文档 接口测试的方法 单流程 多流程 Swgger-ui未授权访问 在之间的一次面试中面试官问到了API接口测试,我回答的不好,因为自己确实不太会,后面才下去学习了,这里复习和练习一下 API接口 API(…

ANSYS新能源汽车动力电池仿真应用案例

燃料电池是一种非燃烧过程的电化学能转换装置,将氢气(等燃料)和氧气的化学能连续不断地转换为电能,是发电设备而非储能设备。 根据电解质的不同,分为碱性燃料电池AFC、磷酸燃料电池PAFC、熔融碳酸盐燃料电池MCFC、固体…

解析商场智能导视系统背后的科技:AR导航与大数据如何助力商业运营

在布局复杂的大型商场中,顾客常常面临寻找特定店铺的挑战。商场的规模庞大,店铺众多,使得顾客在享受购物乐趣的同时,也不得不面对寻路的难题。维小帮商场智能导航导视系统的电子地图、AR导航营销能为顾客提供更加便捷的购物体验。…

震惊!张宇强化36讲1200页,暑期强化高效利用指南!

特别喜欢张宇老师的讲课风格 如果你打算跟张宇老师,那么基础——>强化——>冲刺,你应该这么买书! 张宇老师25版课程大改版,其中,36讲的变动是最大的,张宇老师25版课程把以往的强化课程前移&#xff0…

spark on k8s两种方式的原理与对比

spark on k8s两种方式的原理与对比 1、spark on k8s 方式 spark-submit可以直接用来向 Kubernetes 集群提交 Spark 应用,提交机制如下: 1、Spark 创建一个在Kubernetes pod中运行的 Spark 驱动程序。 2、驱动程序创建在 Kubernetes Pod 中运行的执行器…

01 Docker 概述

目录 1.Docker简介 2.传统虚拟机 vs 容器 3.Docker运行速度快的原因 4.Docker基本组成三要素 5.Docker 平台架构 入门版 架构版 1.Docker简介 Docker是基于Go语言实现的云开源项目。 Docker的主要目标是:Build, Ship and Run Any App, Anywhere&#xff0c…

我不小心把生产的数据改错了!同事帮我用MySQL的BinLog挽回了罚款

之前在生产做修改数据的时候不小心改错了一行数据,本来以为会被通报批评,但是同事利用binlog日志查看到了之前的旧数据,并且帮我回滚了,学到了,所以写了一篇binlog的文章分享给大家。 MySQL的Binary Log(简…

Java--Map集合

Map 映射(map)是存储键和值间关联(即,键值对)的对象。给定一个键,可以找到其值。键和值都是对象。键必须唯一,但是值可以重复。 支持映射的接口 接 口 描 述 Map 将唯一键映射到值 NavigableMap 扩展SortedMap接口,以处理…

使用PID算法实现DAC模拟量输出的快速调节

目录 概述 1 系统框架和算法 1.1 框架结构介绍 1.2 PID算法实现 1.2.1 理论介绍 1.2.2 离散化位置式PID 1.2.3 位置式PID算法 2 STM32Cube 配置项目 2.1 配置参数 2.2 GENERATE项目 3 功能实现 3.1 ADC采样数据功能 3.2 DAC数据转换 3.3 PID相关的调制函数 4 …

基于YOLOv10深度学习的CT扫描图像肾结石智能检测系统【python源码+Pyqt5界面+数据集+训练代码】深度学习实战、目标检测

《博主简介》 小伙伴们好,我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 ✌更多学习资源,可关注公-仲-hao:【阿旭算法与机器学习】,共同学习交流~ 👍感谢小伙伴们点赞、关注! 《------往期经典推…