Kotlin inline、noinline、crossinline 深入解析

主要内容:

  • inline
    • 高价函数的原理分析
    • Non-local returns
  • noinline
  • crossinline

inline

如果有C语言基础的,inline 修饰一个函数表示该函数是一个内联函数。编译时,编译器会将内联函数的函数体拷贝到调用的地方。我们先看下在一个普通的 kotlin 函数上使用 inline 关键字:

inline fun inlineFun() {println("from inlineFun")
}

会发现 IDE 会给出警告:

在这里插入图片描述
建议我们在高阶函数上使用 inline 关键字。

好,那我们来看下高阶函数。

高价函数的原理分析

下面是一个简单的高阶函数,函数参数是一个 function type 类型:

private fun proxy(action: () -> Unit) {println("start logging")action()println("end logging")
}

编译后对应的 Java 代码为:

private final void proxy(Function0 action) {String var2 = "start logging";System.out.println(var2);action();var2 = "end logging";System.out.println(var2);
}

会将 function type 编译成 Function0 类型,因为 action: () -> Unit括号内是无参的,所以是 Function0,如果是一个参数对应 Function1,以此类推。然后,我们调用上面的高阶函数 proxy:

fun invokeProxy() {proxy {println("eating")}
}

查看对应的字节码:

  public final invokeProxy()VL0LINENUMBER 34 L0ALOAD 0GETSTATIC inline/InlineTest$invokeProxy$1.INSTANCE : Linline/InlineTest$invokeProxy$1;CHECKCAST kotlin/jvm/functions/Function0INVOKESPECIAL inline/InlineTest.proxy (Lkotlin/jvm/functions/Function0;)VL1LINENUMBER 38 L1RETURNL2LOCALVARIABLE this Linline/InlineTest; L0 L2 0MAXSTACK = 2MAXLOCALS = 1

可以看出,编译后会生成一个内部类:inline/InlineTest$invokeProxy$1,然后我们看下这个内部类长什么样:

final class inline/InlineTest$invokeProxy$1 extends kotlin/jvm/internal/Lambda implements kotlin/jvm/functions/Function0 {// access flags 0x1041public synthetic bridge invoke()Ljava/lang/Object;L0LINENUMBER 15 L0ALOAD 0INVOKEVIRTUAL inline/InlineTest$invokeProxy$1.invoke ()VGETSTATIC kotlin/Unit.INSTANCE : Lkotlin/Unit;ARETURNMAXSTACK = 1MAXLOCALS = 1// access flags 0x11public final invoke()VL0LINENUMBER 36 L0LDC "eating"ASTORE 1L1GETSTATIC java/lang/System.out : Ljava/io/PrintStream;ALOAD 1INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)VL2L3LINENUMBER 37 L3RETURNL4LOCALVARIABLE this Linline/InlineTest$invokeProxy$1; L0 L4 0MAXSTACK = 2MAXLOCALS = 2// access flags 0x0<init>()VALOAD 0ICONST_0INVOKESPECIAL kotlin/jvm/internal/Lambda.<init> (I)VRETURNMAXSTACK = 2MAXLOCALS = 1// access flags 0x19public final static Linline/InlineTest$invokeProxy$1; INSTANCE// access flags 0x8static <clinit>()VNEW inline/InlineTest$invokeProxy$1DUPINVOKESPECIAL inline/InlineTest$invokeProxy$1.<init> ()VPUTSTATIC inline/InlineTest$invokeProxy$1.INSTANCE : Linline/InlineTest$invokeProxy$1;RETURNMAXSTACK = 2MAXLOCALS = 0@Lkotlin/Metadata;(mv={1, 8, 0}, k=3, d1={"\u0000\u0008\n\u0000\n\u0002\u0010\u0002\n\u0000\u0010\u0000\u001a\u00020\u0001H\n\u00a2\u0006\u0002\u0008\u0002"}, d2={"<anonymous>", "", "invoke"})OUTERCLASS inline/InlineTest invokeProxy ()V// access flags 0x18final static INNERCLASS inline/InlineTest$invokeProxy$1 null null// compiled from: InlineTest.kt
}

上面的字节码,类似下面的伪代码:

final class InlineTest$invokeProxy$1 extends kotlin/jvm/internal/Lambda implements kotlin/jvm/functions/Function0 {public final static InlineTest$invokeProxy$1 INSTANCE;static {INSTANCE = new InlineTest$invokeProxy$1()}public void invoke(){System.out.println("eating")}
}

可以看出,调用高阶函数 proxy,会生成内部类(该内部类是一个单例)将lambda体里的代码,拷贝到 invoke 函数里面。

小结:简单来说,就是有多少个调用点(call site 调用高阶函数的地方)就会产生多少个内部类。

我们继续往下看,如果在 lambda 表达式体里访问外部的变量呢:

class InlineTest {var age = 18private fun proxy(action: () -> Unit) {println("start logging")action()println("end logging")}fun invokeProxy() {proxy {age = 11 // 访问外部的成员变量println("eating")}}
}

invokeProxy对应的字节码如下:

  public final invokeProxy()VL0LINENUMBER 34 L0ALOAD 0NEW inline/InlineTest$invokeProxy$1DUPALOAD 0INVOKESPECIAL inline/InlineTest$invokeProxy$1.<init> (Linline/InlineTest;)VCHECKCAST kotlin/jvm/functions/Function0INVOKESPECIAL inline/InlineTest.proxy (Lkotlin/jvm/functions/Function0;)VL1LINENUMBER 38 L1RETURNL2LOCALVARIABLE this Linline/InlineTest; L0 L2 0MAXSTACK = 4MAXLOCALS = 1

对应的 Java 伪代码如下:

public final void invokeProxy(){InlineTest$invokeProxy$1 function0 = new InlineTest$invokeProxy$1()proxy(function0)
}

该内部类 InlineTest$invokeProxy$1 变成如下:

final class InlineTest$invokeProxy$1 extends kotlin/jvm/internal/Lambda implements kotlin/jvm/functions/Function0 {public void invoke(){InlineTest$invokeProxy$1.this.setAge(11)System.out.println("eating")}
}

可以看出每调用一次高阶函数 proxy 会都会创建一个内部类对象。

小结:

  1. 每个调用高阶函数地方(调用点),编译时,编译器都会生成一个内部类
  2. 调用高阶函数时,如果传入的 lambda 表达式体没有使用外部变量,那么只会用到内部类常量对象;如果 lambda 表达式体使用了外部变量,那么每次调用该高阶函数都会创建一个内部类对象。
    如果在一个频繁触发的地方调用高阶函数,如自定义 draw 方法里,刚好 lambda 体又实用到了外部成员变量,这样就会隐性地在 draw 方法里频繁创建对象。

这样时候 inline 关键字就派上用场了。

将上面的高阶函数 proxy 使用 inline 修饰:

private inline fun proxy(action: () -> Unit) {println("start logging")action()println("end logging")
}

调用该高阶函数:

fun invokeProxyInline() {proxy {println("eating")}
}

字节码对应的 Java 代码如下:

public final void invokeProxyInline() {int $i$f$proxyInline = false;String var3 = "start logging";System.out.println(var3);int var4 = false;String var5 = "eating";System.out.println(var5);var3 = "end logging";System.out.println(var3);
}

可以看出调用 proxy 函数的地方不会创建内部类,而是将高阶函数的函数体拷贝到调用点。

Non-local returns

什么是 Non-local returns?我们先来看下什么是 return,下面是 kotlin 对 return 的定义:

by default returns from the nearest enclosing function or anonymous function.

意思就是:从离 return 最近的封闭函数或匿名函数中返回。举个例子:

fun test(age:Int) {if (age < 0) {return}println(age)
}

其中 test就是 enclosing function. 也就是说 return 的作用就是从一个函数中返回(离它最近的函数)。

搞清楚 return 关键字之后,我们来看下在 lambda 中使用 reutrn:

// 定义一个普通的高阶函数
private fun normalFunction(action: () -> Unit){println("start......")action()println("end......")
}main(){// 调用高阶函数normalFunction {return // 使用 return, 编译器报错}
}

发现编译器报错了,为啥不能在 lambda 中使用 return 呢?

首先,上面代码中里离 return关键字最近的 enclosing function是 main函数,有人可能会问,离 return最近的不是 normalFunction么?normalFunction只是一个函数调用,它不是一个封闭的函数,封闭的是 lambda 表达式。

其次,return也无法控制 normalFunction函数的 return的。因为 return所处的代码块只是 normalFunction的 lambda 参数而已,return控制的是 lambda。正如 Kotlin 官网所说的:

A bare return is forbidden inside a lambda because a lambda cannot make the enclosing function return.

因为在 lambda 中使用return,无法实现 return 的定义,所以无法在 lambda 中使用 return,如果是内联函数,则可以在 lambda 中使用 return:

private inline fun normalFunction(action: () -> Unit){println("start......")action()println("end......")
}main(){// 调用高阶函数normalFunction {return}
}

上面的代码是合法的(因为内联 normalFunction,编译时会将代码体拷贝到main函数中).

Kotlin 中把这种 return 称之为 Non-local returns(located in a lambda, but exiting the enclosing function).
Non-local returns 名字很好理解:return 的 local 是 lambda,而此处的 return 返回的是 lambda 外面的 main 函数(non-local),所以称之为 non-local returns.

noinline

noinline 顾名思义就是不内联。那是什么时候使用 noinline 呢?我们在上面 proxy 函数基础上做一个小修改:

private inline fun proxy(action: () -> Unit, action2: () -> Unit) {println("start logging")action()println("end logging")// action2 作为参数传递给另一个高阶函数cleanResource(action2)
}private fun cleanResource(execution: () -> Unit) {execution()println("cleaning resource1")println("cleaning resource2")
}

为了 proxy 新增了另一个参数 action2,然后将 action2 传递给高阶函数 cleanResource. 但是 IDEA 会提示如下错误:
在这里插入图片描述
提示我们使用 noinline 修饰 action2 参数:

private inline fun proxy(action: () -> Unit, noinline action2: () -> Unit) {println("start logging")action()println("end logging")// action2 作为参数传递给另一个高阶函数cleanResource(action2)
}

为什么不加 noinline 就会报错呢?

其实很好理解,因为 proxy 是一个 inline 函数,那么调用 proxy 的地方,编译器都会将函数体拷贝过去,包括传入的 lambda 参数(如上面的 action,action2),例如:

fun invokeProxy() {proxy({println("eating...")}, {println("eating...2")})
}

action对应的代码块是:println("eating..."),action2 对应的代码块是:println("eating...2")

因为action2传递给了 cleanResource,要想将代码块当做参数传递给函数,那么代码块用什么来表示,目前只能使用Class类。然而 proxy又是 inline 的,所以需要对 action2 参数单独处理,将其不要 inline。所以需要使用 oninline关键字来修饰 action2参数。

小结:如果需要将 inline 高阶函数的 lambda 参数传递给另一个高阶函数或作为函数的返回值,均需要使用 noinline 关键字修饰该参数。

crossinline

介绍完了 inline 和 noinline,我们来看下 crossinline。将上面的 proxy 函数,稍作修改:

private fun wrap(action: () -> Unit) {action.invoke()
}private inline fun proxy(action: () -> Unit) {println("start logging")wrap {action()}println("end logging")
}

编译器会报错,给出如下提示:

在这里插入图片描述
编译器报错原因:action 可能包含 Non-local returns,Kotlin 中不允许在非内联的 lambda 中使用 return(原因已经在 Non-local returns 章节已介绍了),也就是说 action 代码块中可能存在 return 关键字,需要使用 crossinline 来修饰 action 参数:

private inline fun proxy(crossinline action: () -> Unit) {println("start logging")wrap {action()}println("end logging")
}// 调用 proxy
private fun invokeProxy() {proxy{println("invoke acrossinline")}
}

我们看下 invokeProxy字节码:

// 内部类
public final class InlineTest$invokeProxy$$inlined$proxy$1 extends Lambda implements Function0 {public InlineTest$invokeProxy$$inlined$proxy$1() {super(0);}// $FF: synthetic method// $FF: bridge methodpublic Object invoke() {this.invoke();return Unit.INSTANCE;}public final void invoke() {int var1 = false;String var2 = "invoke acrossinline";System.out.println(var2);}
}private final void invokeProxy() {int $i$f$proxy = false;String var3 = "start logging";System.out.println(var3);// 每次都 new 一个内部类对象access$wrap(this, (Function0)(new InlineTest$invokeProxy$$inlined$proxy$1()));var3 = "end logging";System.out.println(var3);
}

可以看出会生成一个内部类

  • InlineTest$invokeProxy$$inlined$proxy$1

并且每次调用都会创建一个内部类对象。crossinline 阻止了 action 参数内联。

可以到看 crossinline 的核心作用是阻止内联,那我们将 crossinline 换成 noinline 是不是也可以呢?

private inline fun proxy(noinline action: () -> Unit) {println("start logging")wrap {action()}println("end logging")
}

编译器不会报错,代码运行也是OK,貌似可以使用 noinline 代替 crossinline,既然可以用 noinline,为啥还搞个新的 crossinline 关键字?上面的代码虽然可以使用 noinline 代替 crossinline,但是底层还差别的。我们看下使用 noinline 对应的字节码:

  public final invokeProxy()VL0LINENUMBER 135 L0ALOAD 0ASTORE 1GETSTATIC inline/InlineTest$invokeProxy$1.INSTANCE : Linline/InlineTest$invokeProxy$1;CHECKCAST kotlin/jvm/functions/Function0// 省略其他代码L5LINENUMBER 184 L5ALOAD 1NEW inline/InlineTest$proxy$1DUPALOAD 2INVOKESPECIAL inline/InlineTest$proxy$1.<init> (Lkotlin/jvm/functions/Function0;)VCHECKCAST kotlin/jvm/functions/Function0INVOKESTATIC inline/InlineTest.access$wrap (Linline/InlineTest;Lkotlin/jvm/functions/Function0;)V// 省略其他...

可见,使用 noinline 的话,会创建两个内部类:

  • inline/InlineTest$invokeProxy$1
  • inline/InlineTest$proxy$1

至此,inline、noinline 和 crossinline 就介绍完毕了。

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

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

相关文章

mac idea启动没反应 无法启动

遇到的问题如下&#xff1a; 启动idea&#xff0c;没反应 无法启动&#xff0c;不论破解还是别的原因&#xff0c;总之无法启动了 应用程序–找到idea–右击显示包内容–Contents–MacOS–打开idea 弹出框提示如下&#xff1a; 双击这个idea可执行文件 1&#xff09;先查看日志…

Mac安装brew、mysql、redis

mac安装brew mac安装brewmac安装mysql并配置开机启动mac安装redis并配置开机启动 mac安装brew 第一步&#xff1a;执行. /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"第二步&#xff1a;输入开机密码 第三…

正规黄金代理的三大要素

对于现货黄金投资来说&#xff0c;寻找一个正规的黄金代理是十分重要的问题。在目前的现货黄金投资市场中&#xff0c;现货黄金代理的数量很多&#xff0c;他们都致力于耕耘现货黄金投资市场。当越来越多的专业人士加入到现货黄金投资的市场中当中时&#xff0c;这个市场将会越…

mybatis源码学习-2-项目结构

写在前面,这里会有很多借鉴的内容,有以下三个原因 本博客只是作为本人学习记录并用以分享,并不是专业的技术型博客笔者是位刚刚开始尝试阅读源码的人,对源码的阅读流程乃至整体架构并不熟悉,观看他人博客可以帮助我快速入门如果只是笔者自己观看,难免会有很多弄不懂乃至理解错误…

ZMTP协议

ZoreMQ Transport Protocol是一个传输层协议&#xff0c;用于ZMQ的连接的信息交互&#xff0c;本文档描述的是3.0协议&#xff0c;主要分析基于NULL Security Mechanism 协议语法 ZMTP由三部分组成&#xff0c;分别是 greeting、handshake、traffic 部分描述构成greeting描述…

从零开始,探索C语言中的字符串

字符串 1. 前言2. 预备知识2.1 字符2.2 字符数组 3. 什么是字符串4. \04.1 \0是什么4.2 \0的作用4.2.1 打印字符串4.2.2 求字符串长度 1. 前言 大家好&#xff0c;我是努力学习游泳的鱼。你已经学会了如何使用变量和常量&#xff0c;也知道了字符的概念。但是你可能还不了解由…

BuhoCleaner for mac:让你的Mac重获新生

你是否曾经因为电脑运行缓慢而感到困扰&#xff1f;是否曾经因为大量的垃圾文件和无效的临时文件而感到头疼&#xff1f;如果你有这样的烦恼&#xff0c;那么BuhoCleaner for mac就是你的救星&#xff01; BuhoCleaner for mac是一款专门为Mac用户设计的系统清理工具&#xff…

Linux中Tomcat发布war包后无法正常访问非静态资源

事故现象 在CentOS8中安装完WEB环境&#xff0c;首次部署WEB项目DEMO案例&#xff0c;发现可以静态的网页内容&#xff0c; 但是无法向后台发送异步请求&#xff0c;全部出现404问题&#xff0c;导致数据库数据无法渲染到界面上。 原因分析 CentOS请求中提示用来获取资源的连…

跨模态可信感知

文章目录 跨模态可信感知综述摘要引言跨协议通信模式PCP网络架构 跨模态可信感知跨模态可信感知的概念跨模态可信感知的热点研究场景目前存在的挑战可能改进的方案 参考文献 跨模态可信感知综述 摘要 随着人工智能相关理论和技术的崛起&#xff0c;通信和感知领域的研究引入了…

URL重定向漏洞

URL重定向漏洞 1. URL重定向1.1. 漏洞位置 2. URL重定向基础演示2.1. 查找漏洞2.1.1. 测试漏洞2.1.2. 加载完情况2.1.3. 验证漏洞2.1.4. 成功验证 2.2. 代码修改2.2.1. 用户端代码修改2.2.2. 攻击端代码修改 2.3. 利用思路2.3.1. 用户端2.3.1.1. 验证跳转 2.3.2. 攻击端2.3.2.1…

睿趣科技:开抖音小店挣钱吗到底

在当今数字化时代&#xff0c;社交媒体平台成为了创业者们寻找商机和赚钱的新途径。而抖音作为一款风靡全球的短视频分享平台&#xff0c;自然也成为了许多人开设小店、进行创业的选择之一。那么&#xff0c;开抖音小店能否真正实现盈利&#xff0c;成为了一个备受关注的话题。…

【UE5】用法简介-使用MAWI高精度树林资产的地形材质与添加风雪效果

首先我们新建一个basic工程 然后点击floor按del键&#xff0c;把floor给删除。 只留下空白场景 点击“地形” 在这个范例里&#xff0c;我只创建一个500X500大小的地形&#xff0c;只为了告诉大家用法&#xff0c;点击创建 创建好之后有一大片空白的地形出现 让我们点左上角…

uniapp实现移动端的视频图片轮播组件

1、视频轮播组件app体验地址 https://tt.appc02.com/nesxr6 2、支持小程序、H5、APP&#xff0c;在小程序上运行的效果图 3、使用方法 第一步&#xff0c;按照截图步骤配置好 第二步&#xff1a;参考以下代码&#xff0c;使用视频图片轮播组件 <template><view>…

JavaScript -【第一周】

文章来源于网上收集和自己原创&#xff0c;若侵害到您的权利&#xff0c;请您及时联系并删除~~~ JavaScript 介绍 变量、常量、数据类型、运算符等基础概念 能够实现数据类型的转换&#xff0c;结合四则运算体会如何编程。 体会现实世界中的事物与计算机的关系理解什么是数据并…

工程制造领域:企业IT架构

一、IT组织规划架构图 1.1 IT服务保证梯队与指导思想 二、整体业务规划架构图 三、数据化项目规划架构图 四、应用系统集成架构图

Shell-AI:基于LLM实现自然语言理解的CLI工具

一、前言 随着AI技术的普及&#xff0c;部分技术领域的门槛逐步降低&#xff0c;比如非科班出身&#xff0c;非技术专业&#xff0c;甚至从未涉足技术领域&#xff0c;完全不懂服务器部署和运维&#xff0c;如今可以依托AI大模型非常轻松的掌握和使用相关技术&#xff0c;来解…

网络地址转换技术NAT(第九课)

一 什么是NAT? NAT是网络地址转换的缩写,是一种在计算机网络中使用的技术,可以将私有地址转换为公共地址,从而实现本地网络与公共网络的互联。NAT工作在网络层,可以隐藏内部网络中的IP地址和端口号,从而增强网络的安全性和灵活性。在家庭网络、企业网络、公共WIFI热点等…

MongoDB实验——MongoDB shell操作

MongoDB shell操作 实验原理 MongoDB shell是一个可执行文件&#xff0c;是MongoDB自带的一个交互式JavaScript shell&#xff0c;位于MongoDB安装路径下的/bin文件夹中。要启动MongoDB shell&#xff0c;可执行命令mongo。这将在控制台提示符中启动该shell&#xff0c;Mongo…

获取Linux内核源码

在嵌入式平台上做Linux开发的时候&#xff0c;我们用的kernel都是芯片厂家移植到自家平台上的&#xff0c;但是最初的原生Linux内核的源码是从哪里来的呢&#xff1f;下面我们介绍一下怎么获取原生的Linux源码。 从Linux社区获取内核kernel源码 Linux社区的官方网站是 https:…

构造函数和析构函数(个人学习笔记黑马学习)

构造函数:主要作用在于创建对象时为对象的成员属性赋值&#xff0c;构造函数由编译器自动调用&#xff0c;无须手动调用。析构函数:主要作用在于对象销毁前系统自动调用&#xff0c;执行一些清理工作。 #include <iostream> using namespace std;//对象初始化和清理class…