Kotlin inline、noinline、crossinline 深入解析

news2024/12/26 9:33:00

主要内容:

  • 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()V
   L0
    LINENUMBER 34 L0
    ALOAD 0
    GETSTATIC inline/InlineTest$invokeProxy$1.INSTANCE : Linline/InlineTest$invokeProxy$1;
    CHECKCAST kotlin/jvm/functions/Function0
    INVOKESPECIAL inline/InlineTest.proxy (Lkotlin/jvm/functions/Function0;)V
   L1
    LINENUMBER 38 L1
    RETURN
   L2
    LOCALVARIABLE this Linline/InlineTest; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1

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

final class inline/InlineTest$invokeProxy$1 extends kotlin/jvm/internal/Lambda implements kotlin/jvm/functions/Function0 {
  // access flags 0x1041
  public synthetic bridge invoke()Ljava/lang/Object;
   L0
    LINENUMBER 15 L0
    ALOAD 0
    INVOKEVIRTUAL inline/InlineTest$invokeProxy$1.invoke ()V
    GETSTATIC kotlin/Unit.INSTANCE : Lkotlin/Unit;
    ARETURN
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x11
  public final invoke()V
   L0
    LINENUMBER 36 L0
    LDC "eating"
    ASTORE 1
   L1
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 1
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
   L2
   L3
    LINENUMBER 37 L3
    RETURN
   L4
    LOCALVARIABLE this Linline/InlineTest$invokeProxy$1; L0 L4 0
    MAXSTACK = 2
    MAXLOCALS = 2

  // access flags 0x0
  <init>()V
    ALOAD 0
    ICONST_0
    INVOKESPECIAL kotlin/jvm/internal/Lambda.<init> (I)V
    RETURN
    MAXSTACK = 2
    MAXLOCALS = 1

  // access flags 0x19
  public final static Linline/InlineTest$invokeProxy$1; INSTANCE

  // access flags 0x8
  static <clinit>()V
    NEW inline/InlineTest$invokeProxy$1
    DUP
    INVOKESPECIAL inline/InlineTest$invokeProxy$1.<init> ()V
    PUTSTATIC inline/InlineTest$invokeProxy$1.INSTANCE : Linline/InlineTest$invokeProxy$1;
    RETURN
    MAXSTACK = 2
    MAXLOCALS = 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 0x18
  final 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 = 18
    private fun proxy(action: () -> Unit) {
        println("start logging")
        action()
        println("end logging")
    }
 fun invokeProxy() {
        proxy {
            age = 11 // 访问外部的成员变量
            println("eating")
        }
    }
}

invokeProxy对应的字节码如下:

  public final invokeProxy()V
   L0
    LINENUMBER 34 L0
    ALOAD 0
    NEW inline/InlineTest$invokeProxy$1
    DUP
    ALOAD 0
    INVOKESPECIAL inline/InlineTest$invokeProxy$1.<init> (Linline/InlineTest;)V
    CHECKCAST kotlin/jvm/functions/Function0
    INVOKESPECIAL inline/InlineTest.proxy (Lkotlin/jvm/functions/Function0;)V
   L1
    LINENUMBER 38 L1
    RETURN
   L2
    LOCALVARIABLE this Linline/InlineTest; L0 L2 0
    MAXSTACK = 4
    MAXLOCALS = 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 method
   public 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()V
   L0
    LINENUMBER 135 L0
    ALOAD 0
    ASTORE 1
    GETSTATIC inline/InlineTest$invokeProxy$1.INSTANCE : Linline/InlineTest$invokeProxy$1;
    CHECKCAST kotlin/jvm/functions/Function0
    // 省略其他代码
   L5
    LINENUMBER 184 L5
    ALOAD 1
    NEW inline/InlineTest$proxy$1
    DUP
    ALOAD 2
    INVOKESPECIAL inline/InlineTest$proxy$1.<init> (Lkotlin/jvm/functions/Function0;)V
    CHECKCAST kotlin/jvm/functions/Function0
    INVOKESTATIC 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.coloradmin.cn/o/962751.html

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

相关文章

题目有点太简单了,不知道怎么选了

有个公司给了下面一个题目&#xff0c;看了下太简单了&#xff0c;都怕选错了。 后来拿着程序跑了下&#xff0c;就是这个意思嘛。 结论 程序跑出来的结果就是对输入的列表进行倒序排列。 public void testGetPut() throws Exception {List<Integer> numbers List.of(…

【基于交叉注意力的泛锐化深度展开迭代网络】

CADUI: Cross-Attention-Based Depth Unfolding Iteration Network for Pansharpening Remote Sensing Images &#xff08;CADUI&#xff1a;基于交叉注意力的泛锐化深度展开迭代网络&#xff09; 全色锐化是遥感成像系统获取高分辨率多光谱图像的重要技术。它主要通过融合低…

异或和大小比较类问题——抓住最高位:CF1863F

https://codeforces.com/contest/1863/problem/F 因为有等于&#xff0c;所以考虑异或和为0的合法区间&#xff0c;它可以随意切现在考虑切开后左边大于右边&#xff0c;可以发现左右边最高位可以互相抵消&#xff0c;似乎不太可做&#xff1f;此时可以换个考虑&#xff0c;考…

Jetpack业务架构—四件套(Lifecycle、ViewModel、LiveData、DataBinding)

Jetpack 是一个由多个库组成的套件&#xff0c;可帮助开发者遵循最佳做法、减少样板代码并编写可在各种 Android 版本和设备中一致运行的代码&#xff0c;让开发者可将精力集中于真正重要的编码工作。 Android Jetpack组件的优势&#xff1a; Jetpack推出的主要目的是为了能够…

【LeetCode75】第四十四题 省份数量

目录 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 代码&#xff1a; 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 给我们一个二维数组&#xff0c;表示城市之间的连通情况&#xff0c;连在一起的城市为一个省份&#xff0c;问我们一共有多少个省份。 这…

Python实现多子图绘制系统

文章目录 修改DrawTypeDrawType的调用逻辑绘图逻辑源代码 Python绘图系统&#xff1a; &#x1f4c8;从0开始的3D绘图系统&#x1f4c9;一个3D坐标系&#xff0c;多个函数图表类型和风格&#xff1a;&#x1f4c9;极坐标绘图&#x1f4ca;散点图和条形图&#x1f4ca;混合类型…

推进数据要素化,数据云为何是“加速器”?

数据要素化&#xff0c;一个世界性难题。 相比于传统生产要素&#xff0c;数据要素具有获得非竞争性、使用非排他性等独有特征&#xff0c;在流通、产权、安全和使用等方面需要法规制度与基础设施的双重保障。 我国无疑是最早探索数据要素化的国家之一。从早期成立的各种大数…

win10下的CLion控制台中文乱码终极解决方案

win10下的CLion控制台中文乱码终极解决方案 如果你也是&#xff0c;用Clion时候&#xff0c;CPP文件中有中文&#xff0c;然后终端运行会有乱码&#xff0c;改了设置发现&#xff0c;项目中的中文乱码了&#xff0c;但是终端又不乱码了&#xff0c;&#xff0c;&#xff0c;&am…

使用java代码给Excel加水印,保真,新鲜出炉

首先&#xff0c;往表格里贴透明图片&#xff0c;这个很智障&#xff0c;会严重干扰正常阅读和操作 设置文件背景图&#xff1b; 其次&#xff0c;其实就是给excel加一个背景图&#xff0c;但是问题就麻烦在java中基本没有这么干过的&#xff0c;可用方案很少&#xff0c;有spi…

基于java+springboot+vue的置换网站-lw

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql Webstorm Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; ssm mybatis Maven mysql5.7或8.0等等组成&#xff0c;B/S模式 Maven管理等等。 环境需要 1.…

Java-HashMap中put()方法是如何实现的,内含详细流程图

文章目录 Java中的HashMap什么是HashMap&#xff1f;对比其他Map中put()方法HashMap中put()方法使用示例 HashMap中put()源码解析手绘流程图实现原理源码探究&#xff08;JDK 1.8&#xff09; 设计put()的意义总结 Java中的HashMap 什么是HashMap&#xff1f; HashMap是Java中…

ARP欺骗

ARP欺骗定义 ARP欺骗&#xff08;英语&#xff1a;ARP spoofing&#xff09;&#xff0c;又称ARP毒化&#xff08;ARP poisoning&#xff0c;网络上多译为ARP病毒&#xff09;或ARP攻击&#xff0c;是针对以太网地址解析协议&#xff08;ARP&#xff09;的一种攻击技术&#x…

Qt —UDP通信QUdpSocket 简介 +案例

1. UDP通信概述 UDP是无连接、不可靠、面向数据报&#xff08;datagram&#xff09;的协议&#xff0c;可以应用于对可靠性要求不高的场合。与TCP通信不同&#xff0c;UDP通信无需预先建立持久的socket连接&#xff0c;UDP每次发送数据报都需要指定目标地址和端口。 QUdpSocket…

SpringCloudAlibaba Gateway(一)简单集成

SpringCloudAlibaba Gateway(一)简单集成 随着服务模块的增加&#xff0c;一定会产生多个接口地址&#xff0c;那么客户端调用多个接口只能使用多个地址&#xff0c;维护多个地址是很不方便的&#xff0c;这个时候就需要统一服务地址。同时也可以进行统一认证鉴权的需求。那么服…

vcs仿真教程(查看断言)

VCS是在linux下面用来进行仿真看波形的工具&#xff0c;类似于windows下面的modelsim以及questasim等工具&#xff0c;以及quartus、vivado仿真的操作。 1.vcs的基本指令 vcs的常见指令后缀 sim常见指令 2.使用vcs的实例 &#xff08;1&#xff09;新建文件夹&#xff1a; …

linux 开设端口

1&#xff0c;查看端口情况 firewall-cmd --list-all2&#xff0c;开设端口 firewall-cmd --zonepublic --add-port6379/tcp --permanent–zonepublic 表示作用域为公共的 –add-port6379/tcp 添加 tcp 协议的端口端口号为 6379 –permanent 永久生效&#xff0c;如果没有此参…

每日一题 1372二叉树中的最长交错路径

题目 给你一棵以 root 为根的二叉树&#xff0c;二叉树中的交错路径定义如下&#xff1a; 选择二叉树中 任意 节点和一个方向&#xff08;左或者右&#xff09;。如果前进方向为右&#xff0c;那么移动到当前节点的的右子节点&#xff0c;否则移动到它的左子节点。改变前进方…

枚举的简单介绍

目录 概念&#xff1a; 枚举的声明&#xff1a; 枚举的使用&#xff1a; 枚举的取值&#xff1a; 枚举的优点&#xff1a; #define的功能&#xff1a; 而与#define对比&#xff0c;枚举的优点有&#xff1a; 概念&#xff1a; 枚举顾名思义就是⼀⼀列举。 把可能的取值…

WireShark流量抓包详解

目录 Wireshark软件安装Wireshark 开始抓包示例Wireshakr抓包界面介绍WireShark 主要界面 wireshark过滤器表达式的规则 Wireshark软件安装 软件下载路径&#xff1a;wireshark官网。按照系统版本选择下载&#xff0c;下载完成后&#xff0c;按照软件提示一路Next安装。 Wire…

CUDA小白 - NPP(2) - Arithmetic and Logical Operations(2)

cuda小白 原始API链接 NPP GPU架构近些年也有不少的变化&#xff0c;具体的可以参考别的博主的介绍&#xff0c;都比较详细。还有一些cuda中的专有名词的含义&#xff0c;可以参考《详解CUDA的Context、Stream、Warp、SM、SP、Kernel、Block、Grid》 常见的NppStatus&#xf…