Kotlin 中的内联函数

1 inline

内联函数:消除 Lambda 带来的运行时开销。

举例来说:

fun main() {val num1 = 100val num2 = 80val result = num1AndNum2(num1, num2) { n1, n2 ->n1 + n2}
}fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {val result = operation(num1, num2)return result
}

在上面的代码中调用了 num1AndNum2() 函数,并通过 Lambda 表达式指定对传入的两个整型参数进行求和。这段代码在 Kotlin 中非常好理解,因为这是高阶函数最基本的用法。但是,Kotlin 代码最终还是要编译成 Java 字节码的,但是 Java 中没有高阶函数的概念。

将上述的 Kotlin 代码转换成 Java 代码:

public final class TestKt {public static final void main() {int num1 = 100;int num2 = 80;num1AndNum2(num1, num2, (Function2)null.INSTANCE); // 1}// $FF: synthetic methodpublic static void main(String[] var0) {main();}public static final int num1AndNum2(int num1, int num2, @NotNull Function2 operation) {Intrinsics.checkNotNullParameter(operation, "operation");int result = ((Number)operation.invoke(num1, num2)).intValue(); // 2return result;}
}

在注释 1 中可以看到 num1AndNum2 函数的第三个参数变成了一个 Function2 接口,这是一种 Kotlin 内置的接口,里面有一个待实现的 invoke 函数(注释 2),并把 num1 和 num2 传了进去。这样,在调用 num1AndNum2 函数的时候,之前的 Lambda 表达式在这里变成了 Function 接口的匿名类实现,然后在 invoke 函数中实现了 n1 + n2 的逻辑,并将结果返回。

这就是 Kotlin 高阶函数背后的实现原理:Lambda 表达式在底层被转换成了匿名类的实现方式。这表明,我们每调用一次 Lambda 表达式,就会创建一个新的匿名类实例,也就造成了额外的内存和性能开销。

内联函数就是用来消除 Lambda 表达式所带来的运行时开销。

内联函数的实现非常简单,只要在定义高阶函数是加上 inline 关键字的声明即可。 如下所示:

inline fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {val result = operation(num1, num2)return result
}

内联函数的工作原理其实并不复杂,Kotlin 编译器会将内联函数中的代码在编译的时候自动替换到调用它的地方,这样也就不存在运行时开销了。

以下是反编译的 Java 代码:

public final class TestKt {public static final void main() {int num1 = 100;int num2 = 80;int $i$f$num1AndNum2 = false; int var6 = false;int result$iv = num1 + num2; // 1}// $FF: synthetic methodpublic static void main(String[] var0) {main();}public static final int num1AndNum2(int num1, int num2, @NotNull Function2 operation) {int $i$f$num1AndNum2 = 0;Intrinsics.checkNotNullParameter(operation, "operation");int result = ((Number)operation.invoke(num1, num2)).intValue();return result;}
}

从注释 1 处可以看出是将内联函数中的全部代码替换到了函数调用处,也正因为此,内联函数才能完全消除 Lambda 表达式所带来的运行时开销。

2 noinline

比如,一个高阶函数接收了两个或者更多的函数类型的参数,这个时候就需要给这些函数类型的参数加上 inline 关键字。但是,如果我们只想内联其中的一个函数该怎么办呢?这个时候就用到 noinline 关键字了,如下所示:

inline fun inlineTest(block1: () -> Unit, noinline block2: () -> Unit) {  
}

可以看到,原本 block1 和 block2 这两个函数类型的参数所引用的 Lambda 表达式都会被内联。但是,我们在 block2 参数前面又加上了 noinline 关键字,那么现在就只会对 block1 参数所引用的 Lambda 表达式进行内联了。这就是 noinline 关键字的作用。

那么,既然内联函数可以消除 Lambda 带来的运行时开销,为什么还要提供 noinline 关键字来排除内联功能呢?

这是因为,内联的函数类型参数在编译时会被代码替换,因此,它是没有真正的参数属性的。非内联的函数类型参数可以自由地传递给其他的任何函数,因为它是一个真实的参数,而且保留原有函数的特性,而内联函数的类型参数只允许传递给另外的一个内联函数,这就是它最大的局限性。

内联函数和非内联函数还有一个重要的区别:内联函数所引用的 Lambda 表达式中是可以使用 return 关键字来进行函数返回的,而非内联函数只能进行局部返回。

fun main() {println("main start") // 1val str = ""printString(str) { s ->println("lambda start") // 3if (s.isEmpty()) return@printString // 局部返回println(s)println("lambda end")}println("main end") // 5
}// 普通函数
fun printString(str: String, block: (String) -> Unit) {println("printString begin") // 2block(str)println("printString end") // 4
}// main start
// printString begin
// lambda start
// printString end
// main end

在这里定义了一个叫做 printString 的高阶函数,用于在 Lambda 表达式中打印传入的字符串参数。但是如果字符串参数为空,那么就不进行打印。注意,Lambda 表达式中是不允许直接使用 return 关键字的,这里使用了 return@printString 的写法,表示进行局部返回,并且不再执行 Lambda 表达式的剩余部分。

如果将 printString 函数声明称一个内联函数:

fun main() {println("main start") // 1val str = ""printString(str) { s ->println("lambda start") // 3if (s.isEmpty()) return // 可以直接使用 return 关键字println(s)println("lambda end")}println("main end")
}// 内联函数
inline fun printString(str: String, block: (String) -> Unit) {println("printString begin") // 2block(str)println("printString end")
}// main start
// printString begin
// lambda start

将 printString 函数声明为内联函数,就可以在 Lambda 表达式中使用 return 关键字了。此时的 return 代表的是返回外层的调用函数,也就 main() 函数。之所以会有这样的效果,是因为内联函数的代码替换。

3 corssinline

将高阶函数声明称内联函数是一种良好的习惯。事实上,绝大多数的高阶函数是可以直接声明成内联函数的,但是也有少部分例外的情况:
内联函数
在这里插入图片描述

我们首先在内联函数 runRunnable 中,创建了一个 Runnable 对象,并在 Runnable 的 Lambda 表达式中调用了传入的函数类型参数。而 Lambda 表达式在编译的时候会被转换成匿名内部类的实现方式,也就是说, 上述代码实际上是在匿名内部类中调用了传入的函数类型参数。

这是因为,内联函数的 Lambda 表达式中允许使用 return 关键字(也就是 block 函数中允许 return),和高阶函数的匿名内部类实现中不允许出现 return 关键字之间造成了冲突。

也就是说,如果我们在高阶函数中创建了另外的 Lambda 表达式或者匿名类的实现,并且在这些实现中也调用了函数类型的参数,此时再将高阶函数声明成内联函数,就一定提示错误。

那么是不是在这种情况下就无法使用内联函数了呢?也不是,借助 corssinline 关键字就可以很好的解决这个问题:

inline fun runRunnable(crossinline block: () -> Unit) {val runnable = Runnable {block()}runnable.run()
}

可以看到,在函数类型参数的前面加上 crossinline 声明,代码就可以正常编译通过了。

那么这个 crossinline 关键字是什么意思呢?crossinline 关键字就像是一个契约,用于保证内联函数的 Lambda 表达式中一定不会出现 return 关键字,这样就不存在冲突了。

声明了 crossinline 之后,我们就无法在调用 runRunnable 函数时的 Lambda 表达式中使用 return 关键字进行函数表达式返回了,但是仍然可以使用 return@runRunnable 的写法进行局部返回。总体来说,除了在 return 关键字的使用上有所区分外,crossinline 保留了内联函数的其他所有特性。

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

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

相关文章

一个项目学习Vue3---NVM和NPM安装

内容资源下载:关注公众号(资小库)回复vue下载本内容资源 1.Windows安装NVM包管理工具 公众号回复:nvm 获取nvm下载地址 步骤1:删除本机Node.js 设置->应用->安装的应用->搜索node.js->删除 清理目录文件 C:\Program Files…

微型导轨:手术机器人的高精度“骨骼”

微型导轨精度高,摩擦系数小,自重轻,结构紧凑,被广泛应用在医疗器械中,尤其是在手术机器人中的应用,通过手术机器人,外科医生可以远离手术台操纵机器人进行手术。可以说,是当之无愧的…

Unity2D - 碰撞检测及边界检测

1. 地面检测 1.1 地面检测的逻辑及代码 一般情况下,对于手人物进行事件处理或动作处理时,我们需要判定人物是否在地面上,这个时候最好的方式是设定地面碰撞器,只有角色在地面时才可以进行跳跃; 我们可以想象物体的重心向地面延伸…

uniapp或安卓对接扫码枪

背景介绍 最近老板又随便丢过来一个扫码枪让我研究快速上线,我心想着又是什么串口通信吗,结果发现是usb的,我想着是不是有什么协议,结果直接插上电脑或者手机 均可在输入框直接输入,不用任何的代码编写 但结合了一下…

【Proteus仿真】【Arduino单片机】基于物联网新能源电动车检测系统设计

文章目录 一、功能简介二、软件设计三、实验现象联系作者 一、功能简介 本项目使用Proteus8仿真Arduino单片机控制器,使用LCD1602液晶显示模块、WIFI模块、蜂鸣器、LED按键、ADC、DS18B20温度传感器等。 主要功能: 系统运行后,LCD1602显示温…

试题与研究杂志试题与研究杂志社试题与研究编辑部2024年第16期目录

教海纵横 互动式教学模式在初中道德与法治课的应用探究 陈文海; 1-3 基于跨学科项目式学习的地理研学旅行课程设计——以“佛山梁园”为例 周红艳; 4-6 育人导向下道德与法治教学与社会实践活动的融合探索 李鹤群; 7-9 合作学习模式下的初中数学教学策略探究 张…

视频融合平台LntonCVS视频监控汇聚平台:构建多元接入与智能管理的安防新生态

一、视频融合平台概述 视频融合平台支持多种协议和设备类型的接入,包括GB28181、Onvif、RTSP、RTMP、海康SDK、Ehome、大华SDK、宇视SDK等。它能够统一整合和管理来自不同品牌、不同协议的视频资源,构建视频数据资源池,并通过视频资源目录为…

pdf文件太大如何压缩变小?pdf文件变小的简单方法

pdf作为目前一种常用的文件格式,通过这种格式的文件展示内容,能够保证在不同设备上显示基本一致的效果,无论是计算机、平板还是手机,都能保持原始的布局、字体和图像效果。PDF是一种分享、存档和打印最合适的选择,那么…

ffmpeg音视频开发从入门到精通——ffmpeg 视频数据抽取

文章目录 FFmpeg视频处理工具使用总结环境配置主函数与参数处理打开输入文件获取流信息分配输出文件上下文猜测输出文件格式创建视频流并设置参数打开输出文件并写入头信息读取、转换并写入帧数据写入尾信息并释放资源运行程序注意事项源代码 FFmpeg视频处理工具使用总结 环境…

PointCloudLib-滤波模块(Filtering)-使用体素网格过滤器对点云进行降采样

在本教程中,我们将学习如何缩减采样——即减少数量 点 – 使用体素化格网方法的点云数据集。 我们将要介绍的类在输入上创建一个 3D 体素网格(将体素网格视为空间中的一组微小 3D 框) 点云数据。然后,在每个体素(即 3D 框)中,所有点都存在 将用它们的质心近似(即下采样…

【遇到的问题】集群上查看gpu的使用情况

流程: 查看bme_cpu所有节点的详细情况scontrol show node bme_gpu[12-23] 下面这个看起来分配出去较少 查看bme_cpu空闲节点sinfo -p bme_gpu -o "%n %G %C %m %e NVIDIAA10080GBPCIe 卡 gpu 13看起来最少 在命令中选择这个节点 #!/bin/bash #SBATCH -J rati…

别再盲目生产了!精益KPI管理让你事半功倍!

在竞争日益激烈的制造业领域,如何提升生产效率、降低成本、确保产品质量,是每个企业都需要面对的重要课题。而研华科技作为工业自动化领域的领军企业,凭借其独特的精益生产KPI分析与管理平台,为企业提供了一套行之有效的解决方案。…

OpenAI突然宣布停止向中国提供API服务!

标题 🌟 OpenAI突然宣布停止向中国提供API服务! 🌟摘要 📜引言 📢正文 📝1. OpenAI API的重要性2. 停止服务的原因分析3. 对中国市场的影响4. 应对措施代码案例 📂常见问题解答(QA)❓…

Java-HashMap和ConcurrentHashMap的区别

Java-HashMap和ConcurrentHashMap的区别 一、关键区别1.数据结构2.线程安全3.性能4.扩容机制 二、源码简析1.并发控制机制2.数据结构转换:链表转红黑树3.扩容机制触发hashMap和concurentHashMap扩容机制的条件 三、putIfAbsent方法computeIfAbsent方法区别 ​ 在 J…

Linux(简单概述)

目录 第一章 初识Linux 第四章 文件管理与常用命令 1.文件基础知识 2.文件显示命令 3.文件内容查询 4. 文件和目录基本操作 5. 文件复制、移动、删除 7. 链接 8. 文件访问权限 9. 文件查找命令 10. 压缩和解压缩 第五章用户与用户组 第六章软件包管理RPM和YUM数据库…

面试-synchronized(java5以前唯一)和ReentrantLock的区别

1.ReentrantLock(再入锁): (1).在java.util.concurrent.locks包 (2).和CountDownLatch,FutureTask,Semaphore一样基于AQS实现。 AQS:AbstractQueuedSynchronizer 队列同步器。Java并发用来构建锁或其他同步主键的基础框架,是j.u.c…

如何使用命令提示符查询电脑相关序列号等信息的操作方法

如何使用命令提示符查询硬盘的序列号? 如果出于保修或其他目的,你想知道硬盘驱动器的序列号,你不想使用第三方应用程序,或者如果你更喜欢命令行方法,则可以使用带有命令提示符的命令来显示硬盘驱动器的序列号。 1. 按…

【SQL Server数据库】数据的增删改操作

目录 一、用SQL语句完成下列功能。 1、新开设一门课程,名叫网络安全与防火墙,学时40,编号为“0118”,主要介绍网络的安全与主要的防火墙软件。 2、先建立monitor表,其结构与student表大致一样.…

图书管理系统(详解版 附源码)

目录 项目分析 实现页面 功能描述 页面预览 准备工作 数据准备 创建数据库 用户表 创建项目 导入前端页面 测试前端页面 后端代码实现 项目公共模块 实体类 公共层 统一结果返回 统一异常处理 业务实现 持久层 用户登录 用户注册 密码加密验证 添加图书…