解析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解散中国分部…

mindspore打卡机器学习正则化与优化器

机器学习正则化 这段代码实现了一个深度学习实验,目的是使用不同的正则化技术(包括dropout、批量归一化、L2正则化以及早期停止策略)来训练神经网络模型,以拟合一个带有噪声的余弦波形数据集。代码使用MindSpore框架进行编写&…

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

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

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

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

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

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

【Ubuntu】Ubuntu系统的127.0.1.1有什么用

参考Debian参考手册,Debian/Ubuntu系统下/etc/hosts内定义的127.0.1.1 IP为本机回环地址。 对于有静态IP地址的系统,应用静态IP替代这里的127.0.1.1. Debian参考手册部分截取: 5.1.1. 主机名解析 主机名解析,目前也是由 NSS (名字服务转换 Name Service Switch) 机制来支…

Yarn Scripts自动化:高效运行JavaScript任务的指南

引言 Yarn是一个现代的包管理工具,它不仅提供了快速可靠的包安装机制,还内置了一个强大的脚本运行工具。通过Yarn的脚本功能,开发者可以自动化执行各种任务,如测试、构建、清理等。本文将详细介绍如何使用Yarn的脚本运行工具来提…

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

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

基于Hadoop平台的电信客服数据的处理与分析③项目开发:搭建基于Hadoop的全分布式集群---任务4:安装并配置JDK

任务描述 Hadoop 2.8需要JDK 1.6及以上版本的JDK,建议安装JDK 1.8,如果Cent OS是最小化安装,可能没有Open JDK,即使已经安装过Open JDK也可以使用JDK 1.8替换系统自带的Open JDK。 任务的内容为检查各个节点的JDK的安装情况&…

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

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

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

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

自下而上的选股与自上而下的选股

一起学习了《战胜华尔街》,不知道大家有没有这么一种感受:林奇的选股方法是典型的自下而上的选股方法。虽然这一点没有单独拎出来讨论过,但在《从低迷中寻找卓越》《如何通过财务指标筛选股票?》《边逛街边选股?》《好…

拓扑学习系列(6)pq环面结实与卫星结实

(p, q)-torus knots (p, q)-torus knots 是一类特殊的结实,它们可以通过在环面(torus)上进行 p 次纵向绕线和 q 次横向绕线来构造。Trichotomy 是指将 (p, q)-torus knots 分为三类:可解的(solvable)、非可…

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

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

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

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

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

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