当前位置: 首页 > news >正文

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 保留了内联函数的其他所有特性。

相关文章:

  • AI与音乐:共创未来乐章还是终结艺术的颂歌?
  • Docker容器导出导入
  • Python发送Email的性能怎么样?如何配置?
  • Unity定时(延迟)管理器实现
  • 数据结构-线性表的顺序表示
  • Webstorm vue项目@路径不能跳转到对应资源,提示Cannot find declaration to go to
  • Android记录19-朋友圈动态发布时间计算
  • 事件传播机制 与 责任链模式
  • Matlab 入门学习
  • .net core使用EPPlus设置Excel的页眉和页脚
  • G7易流赋能化工物流,实现安全、环保与效率的共赢
  • Java延迟初始化Logger日志对象
  • 【C++11 之nullptr关键字 用以消除空指针和0歧义】基础知识必须了解
  • 【Python教程】压缩PDF文件大小
  • Vue3中的常见组件通信之`provide`、`inject`
  • 【前端学习】-粗谈选择器
  • 【许晓笛】 EOS 智能合约案例解析(3)
  • Android组件 - 收藏集 - 掘金
  • Apache Pulsar 2.1 重磅发布
  • codis proxy处理流程
  • DataBase in Android
  • egg(89)--egg之redis的发布和订阅
  • FineReport中如何实现自动滚屏效果
  • JavaScript 事件——“事件类型”中“HTML5事件”的注意要点
  • JavaScript-Array类型
  • PaddlePaddle-GitHub的正确打开姿势
  • Protobuf3语言指南
  • SAP云平台运行环境Cloud Foundry和Neo的区别
  • Spring核心 Bean的高级装配
  • vue 个人积累(使用工具,组件)
  • Wamp集成环境 添加PHP的新版本
  • 不用申请服务号就可以开发微信支付/支付宝/QQ钱包支付!附:直接可用的代码+demo...
  • 服务器之间,相同帐号,实现免密钥登录
  • 离散点最小(凸)包围边界查找
  • 实现菜单下拉伸展折叠效果demo
  • 腾讯大梁:DevOps最后一棒,有效构建海量运营的持续反馈能力
  • 微信端页面使用-webkit-box和绝对定位时,元素上移的问题
  • 微信开放平台全网发布【失败】的几点排查方法
  • Hibernate主键生成策略及选择
  • JavaScript 新语法详解:Class 的私有属性与私有方法 ...
  • 积累各种好的链接
  • 浅谈sql中的in与not in,exists与not exists的区别
  • ​如何使用ArcGIS Pro制作渐变河流效果
  • $NOIp2018$劝退记
  • (2/2) 为了理解 UWP 的启动流程,我从零开始创建了一个 UWP 程序
  • (pytorch进阶之路)CLIP模型 实现图像多模态检索任务
  • (转)Linq学习笔记
  • (转)linux下的时间函数使用
  • (转)Spring4.2.5+Hibernate4.3.11+Struts1.3.8集成方案一
  • **CentOS7安装Maven**
  • .bat批处理(十一):替换字符串中包含百分号%的子串
  • .NET 的程序集加载上下文
  • .NET6使用MiniExcel根据数据源横向导出头部标题及数据
  • //解决validator验证插件多个name相同只验证第一的问题
  • /usr/bin/python: can't decompress data; zlib not available 的异常处理