解析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 here
    LogUtil.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结束的是hello
    LogUtil.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!")
        return
        LogUtil.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!")
    return
    LogUtil.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 here
        LogUtil.d("Bye!2")
    }
}

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

7.总结

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

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

在这里插入图片描述

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

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




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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

西部智慧健身小程序+华为运动健康服务

1、 应用介绍 西部智慧健身小程序为用户提供一站式全流程科学健身综合服务。用户通过登录微信小程序,可享用健康筛查、运动风险评估、体质检测评估、运动处方推送、个人运动数据监控与评估等公益服务。 2、 体验介绍西部智慧健身小程序华为运动健康服务核心体验如…

文心一言 VS 讯飞星火 VS chatgpt (294)-- 算法导论21.4 3题

三、根据练习 21.4-2 的结论,对于每个结点x,需要多少位(bit)来存储x.rank?21.4-2 的内容是:“证明:每个结点的秩最多为 ⌊lgn⌋ 。”。如果要写代码,请用go语言。 文心一言: 在问题21.4-2中&a…

YOLO-world论文阅读笔记

论文:https://arxiv.org/abs/2401.17270 code: https://github.com/AILab-CVC/YOLO-World 1、为什么要做这个研究(理论走向和目前缺陷) ? 之前的开集检测器大多比较慢,不利于部署。 2、他们怎么做这个研究 (方法&…

C++编程(九)异常处理机制、模板、容器库

文章目录 一、异常处理机制(一)概念(二)语法格式(三)使用示例(四)异常处理机制库 二、模板(一)概念(二)函数模板1. 语法格式2. 函数模…

zxing-cpp+OpenCV根据字符串生成条形码

编译构建 需要使用到 CMake、Git、GCC 或 MSVC。 github 链接:https://github.com/zxing-cpp/zxing-cpp 编译之前请确保: 确保安装了 CMake 版本 3.15 或更高版本。 确保安装了与 C17 兼容的编译器(最低VS 2019 16.8 / gcc 7 / clang 5)。 编译构建…

Linear Regression线性回归

目录 前言 Housing Price Prediction Linear Regression Loss/Cost Function Learning Rate 作业 前言 在本专栏中,我们将学习机器学习的知识。 Housing Price Prediction 在开始线性回归的研究之前,我们先来考虑一个实际的问题:房屋…

茗鹤APS高级计划排程系统,在集团多工厂协同生产下的应用

随着业务规模的扩大和市场的全球化,越来越多的企业选择“总部多工厂基地”的模式,此种模式大幅提升企业的产能与产量,有效分散风险。然后,与之而来的是对企业的管理提出更高的管理要求。多个生产基地不仅面临集团下发的周期性计划…

electron教程(一)创建项目

一、方式① 根据官网描述将electron/electron-quick-start项目克隆下来并启动 electron/electron-quick-start地址: GitHub - electron/electron-quick-start: Clone to try a simple Electron app git clone https://github.com/electron/electron-quick-start…

回溯 | Java | LeetCode 39, 40, 131 做题总结

Java Arrays.sort(数组) //排序 不讲究顺序的解答,都可以考虑一下排序是否可行。 39. 组合总和 错误解答 在写的时候需要注意,sum - candidates[i];很重要,也是回溯的一部分。 解答重复了。是因为回溯的for循环理解错了。 class Solutio…

JavaSE简易版扫雷小游戏

描述:用户输入二维雷区的高和宽,输入确定地雷数,随机在地雷区生成地雷。用户输入横竖坐标进行挖雷,挖到地雷游戏以失败结束,并让用户选择是否再次游戏;没挖到雷,显示该区域8个方向地雷数。如果8…

去中心化社会的崛起:探索区块链对社会结构的影响

随着区块链技术的发展和应用,我们正逐步迈向一个去中心化的社会结构。本文将深入探讨区块链技术如何影响社会结构,从经济、政治到文化等多个方面进行探索和分析,揭示其可能带来的革命性变革。 1. 区块链技术的基本原理回顾 1.1 分布式账本与…

放大器的输入电容Cin对放大电路的影响

1、OPA859构成的放大电路的设计 图中OPA859的增益G设定为1.16 ,OPA859的增益带宽积GBP 900M , 放大器的带宽BW GBP / Acl 900 / 1.16 775.86M。 图:OPA859放大电路 由于需要在放大电路上加带宽的限制,所以在OPA859放大电路上有个低通限…

Elasticsearch基础(二):阿里云Elasticsearch快速入门

文章目录 阿里云Elasticsearch快速入门 一、资源领取 二、访问实例 三、创建索引 四、插入数据 五、搜索数据 1、全文搜索 2、按查询条件搜索 六、删除数据 阿里云Elasticsearch快速入门 一、资源领取 这里资源领取只针对新用户,如果是老用户按需购买&am…

vue3.0(十六)axios详解以及完整封装方法

文章目录 axios简介1. promise2. axios特性3. 安装4. 请求方法5. 请求方法别名6. 浏览器支持情况7. 并发请求 Axios的config的配置信息1.浏览器控制台相关的请求信息:2.配置方法3.默认配置4.配置的优先级5.axios请求响应结果 Axios的拦截器1.请求拦截2.响应拦截3.移…

太阳辐射系统日光全光谱模拟太阳光模拟器

太阳光模拟器是一种用于评估太阳能电池性能的重要设备。它能够模拟太阳光的特性,通过测试电池的短路电流、开路电压、填充因子和光电转化效率等关键指标,来评估电池的性能优劣。 设备型号:KYF-GC004品牌制造商:科迎法电气太阳光模…

bigNumber的部分使用方法与属性

场景:最近做IoT项目的时候碰到一个问题,涉及到双精度浮点型的数据范围的校验问题。业务上其实有三种类型:int、float和double类型三种。他们的范围分别是: //int int: [-2147483648, 2147483647],//float float: [-3402823466385…

idea xml ctrl+/ 注释格式不对齐

处理前 处理后 解决办法 取消这两个勾选

【C++题解】1456. 淘淘捡西瓜

问题:1456. 淘淘捡西瓜 类型:贪心 题目描述: 地上有一排西瓜,每个西瓜都有自己的重量。淘淘有一个包,包的容量是固定的,淘淘希望尽可能在包里装更多的西瓜(当然要装整个的,不能切开…

Go语言--运算符

算术运算符 关系运算符 不能写0<a<10&#xff0c;要判断必须0<a&&a<10。因为int和bool不兼容 逻辑运算符 位运算符 赋值运算符 其他 运算符的优先级

数字化精益生产系统--RD研发管理系统

R&D研发管理系统是一种用于管理和监督科学研究和技术开发的软件系统&#xff0c;其设计和应用旨在提高企业研发活动的效率、质量和速度。以下是对R&D研发管理系统的功能设计&#xff1a;