【Kotlin】内联函数

news2024/11/27 12:47:52

文章目录

        • 内联函数
        • noinline: 避免参数被内联
        • 非局部返回
        • 使用标签实现Lambda非局部返回
          • 为什么要设计noinline
        • crossinline
        • 具体化参数类型

Kotlin中的内联函数之所以被设计出来,主要是为了优化Kotlin支持Lambda表达式之后所带来的开销。然而,在Java中我们似乎并不需要特别关注这个问题,因为在Java 7之后,JVM引入了一种叫做 invokedynamic的技术,它会自动帮助我们做Lambda优化。但是为什么Kotlin要引入内联函数这种手动的语法呢? 这主要还是因为Kotlin要兼容Java 6。

在Kotlin中每声明一个Lambda表达式,就会在字节码中产生一个匿名类(也就是说我们一直使用的Lambda表达式在底层被转换成了匿名类的实现方式)。该匿名类包含了一个invoke方法,作为Lambda的调用方法,每次调用的时候,还会创建一个新的匿名类对象。

可想而知,Lambda语法虽然简洁,但是额外增加的开销也不少。并且,如果Lambda捕捉了某个变量,那么每次调用的时候都会创建一个新的对象,这样导致效率较低。尤其对Kotlin这门语言来说,它当今优先要实现的目标,就是在Android这个平台上提供良好的语言特性支持。Kotlin要在Android中引入Lambda语法,必须采用某种方法来优化Lambda带来的额外开销,也就是内联函数。

内联函数

Kotlin拥抱了内联函数,在C++、C#等语言中也支持这种特性。简单的来说,我们可以用inline关键字来修饰函数,这些函数就称为了内联函数。他们的函数体在编译期被嵌入每一个被调用的地方,以减少额外生成的匿名类数,以及函数执行的时间开销。所以内联函数的工作原理并不复杂,就是Kotlin编译器会将内敛函数中的代码在编译的时候自动替换到调用它的地方,这样也就不存在运行时的开销了。 。

看看Kotlin的内联函数是具体如何操作的:

fun main(args: Array<String>) {
    foo {
        println("dive into Kotlin...")
    }
}

fun foo(block: () -> Unit) {
    println("before block")
    block()
    println("end block")
}

首先,我们声明了一个高阶函数foo,可以接受一个类型为() -> Unit的Lambda,然后在main函数中调用它。以下是通过字节码反编译的相关Java代码:

public static final void main(@NotNull String[] args) {
    Intrinsics.checkParameterIsNotNull(args, "args");
    foo((Function0)null.INSTANCE);	        
}

public static final void foo(@NotNull Function0 block) {
    Intrinsics.checkParameterIsNotNull(block, "block");
    String var1 = "before block";
    System.out.println(var1);
    block.invoke();
    var1 = "end block";
    System.out.println(var1);
}

据我们所知,调用foo就会产生一个Function()类型的block类,然后通过invovke方法来执行,这会增加额外的生成类和调用开销。现在,我们给foo函数加上inline修饰符,如下:

inline fun foo(block: () -> Unit) {
    println("before block")
    block()
    println("end block")
}

再来看看相应的Java代码:

public static final void main(@NotNull String[] args) {
    Intrinsics.checkParameterIsNotNull(args, "args");
    String va1 = "before block";
    System.out.println(var1);
    // block函数体在这里开始粘贴
    String var2 = "dive into Kotlin...";
    System.out.println(var2);
    // block函数体在这里结束粘贴
    var1 = "end block";
    System.out.println(var1);
}

public static final void foo(@NotNull Function0 block) {
    Intrinsics.checkParameterIsNotNull(block, "block");
    String var2 = "before block";
    System.out.println(var2);
    block.invoke();
    var2 = "end block";
    System.out.println(var2);
}

foo函数体代码及被调用的Lambda代码都粘贴到了相应调用的位置。试想下,如果这是一个工程中公共的方法,或者被嵌套在一个循环调用的逻辑体中,这个方法势必会被调用很多次。通过inline的语法,我们可以彻底消除这种额外调用,从而节省了开销。

内联函数典型的一个应用场景就是Kotlin的集合类。如果你看过Kotlin的集合类API文档或者源码实现就会发现,集合函数式API,如map、filter都被定义成内联函数,如:

inline fun <T, R> Array<out T>.map {
    transform: (T) -> R
}: List<R>

inline fun <T> Array<out T>.filter {
    predicate: (T) -> Boolean
}: List<T>

这个很容易理解,由于这些方法都接收Lambda作为参数,同时都需要对集合元素进行遍历操作,所以把相应的实现进行内联无疑是非常适合的。

但是内联函数不是万能的,以下情况我们应避免使用内联函数:

  • 由于JVM对普通的函数已经能够根据实际情况智能地判断是否进行内联优化,所以我们并不需要对其使用Kotlin的inline语法,那只会让字节码变得更加复杂。
  • 尽量避免对具有大量函数体的函数进行内联,这样会导致过多的字节码数量。
  • 一旦一个函数被定义为内联函数,便不能获取闭包类的私有成员,除非你把他们声明为internal。
noinline: 避免参数被内联

通过上面的例子我们已经知道,如果在一个函数的开头加上inline修饰符,那么它的函数体及Lambda参数都会被内联。然而现实中的情况比较复杂,有一种可能是函数需要接受多个参数,但我们只想对其中部分Lambda参数内联,其他的则不内联,这个又该如何处理?

解决这个问题也很简单,Kotlin在引入inline的同时,也新增了noinline关键字,我们可以把它加在不想要被内联的参数开头,该参数便不会具有内联的效果:

fun main(args: Array<String>) {
    foo ( {
        println("I am inlined...")	     	
    }, {
        println("I am not inlined...")
    })
}

inline fun foo(block1: () -> Unit, noinline block2: () -> Unit) {
    println("before block")
    block1()
    block2()
    println("end block")
}

同样的方法,再来看看反编译的Java版本:

public static final void main(@NotNull String[] args) {
    Intrinsics.checkParameterIsNotNull(args, "args");
    Function0 block2$iv = (Function0)null.INSTANCE;
    String var2 = "before block";
    System.out.println(var2);
    // block1 被内联了
    String var3 = "I am inlined...";
    System.out.println(var3);
    // block2 还是原样
    block2$iv.invoke();
    System.out.println(var2);
}
public static final void foo(@NotNull Function0 block1, @NotNull Function0 block2) {
    Intrinsics.checkParameterIsNotNull(block1, "block1");
    Intrinsics.checkParameterIsNotNull(block2, "block2");
    String var3 = "before block";
    System.out.println(var3);
    block1.invoke();
    block2.invoke();
    var3 = "end block";
    System.out.println(var3);
}

可以看出,foo函数的block2参数在带上noinline之后,反编译后的Java代码中并没有将其函数体代码在调用处进行替换。

非局部返回

Kotlin中的内联函数除了优化Lambda开销之外,还带来了其他方面的特效,典型的就是非局部返回和具体化参数类型。我们先来看下Kotlin如何支持非局部返回。

以下是我们常见的局部返回的例子:

fun main(args: Array<String>) {
    foo()
}
fun localReturn() {
    return
}
fun foo() {
    println("before local return")
    localReturn()
    println("after local return")
    return
}
// 运行结果
before local return
after local return

正如我们所熟知的,localReturn执行后,其函数体中的return只会在该函数的局部生效,所以localReturn()之后的println函数依旧生效。我们再把这个函数换成Lambda表达式的版本:

fun main(args: Array<String>) {
    foo { return }
}
fun foo(returning: () -> Unit) {
    println("before local return")
    returning()
    println("after local return")
    return 
}
// 运行结果
Error:(2, 11)Kotlin: 'return' is not allowed here

这时,编译器报错了,就是说在Kotlin中,正常情况下Lambda表达式不允许存在return关键字。这时候,内联函数又可以排上用场了。我们把foo进行内联后再试试看:

fun main(args: Array<String>) {
    foo { return }
}
inline fun foo(returning: () -> Unit) {
    println("before local return")
    returning()
    println("after local return")
    return
}
// 运行结果
before local return 

编译顺利通过了,但结果与我们的局部返回效果不同,Lambda的return执行后直接让foo函数退出了执行。如果你仔细考虑一下,可能很快就想出了原因。因为内联函数foo的函数体及参数Lambda会直接替代具体的调用。所以实际产生的代码中,retrurn相当于是直接暴露在main函数中,所以returning()之后的代码自然不会执行,这个就是所谓的非局部返回。

使用标签实现Lambda非局部返回

另外一种等效的方式,是通过标签利用@符号来实现Lambda非局部返回。同样以上的例子,我们可以在不声明inline修饰符的情况下,这么做来实现相同的效果:

fun main(args: Array<String>) {
    foo { return@foo }
}
fun foo(returning: () -> Unit) {
    println("before local return")
    returning()
    println("after local return")
    return
}
// 运行结果
before local return

非局部返回尤其在循环控制中显得特别有用,比如Kotlin的forEach接口,它接收的就是一个Lambda参数,由于它也是一个内联函数,所以我们可以直接在它调用的Lambda中执行return退出上一层的程序。

fun hasZeros(list: List<Int>): Boolean {
    list.forEach {
        if (it == 0) return true // 直接返回foo函数结果
    }
    return false
}
为什么要设计noinline

这里我已经蒙了,前面已经说了内联函数的好处,那为什么Kotlin还要提供一个noinline关键字来排除内联功能呢?
这是因为内联的函数类型参数在编译的时候会被进行代码替换,因此它没有真正的参数属性。
非内联的函数类型参数可以自由地传递给其他任何函数,因为它就是一个真实的参数,而内联的函数类型参数只允许传递给另外一个内联函数,这也是它最大的局限性。
另外,内联函数和非内联函数还有一个重要的区别,那就是内联函数所引用的Lambda表达式中是可以使用return关键字来进行函数返回的,而非内联函数只能进行局部返回。为了说明这个问题,我们来看下面的例子:

fun printString(str: String, block: (String) -> Unit) {
    println("printString begin")
    block(str)
    println("printString end")
}

fun main() {
    println("main start")
    val str = ""
    printString(str) { s ->
        println("lambda start")
        if (s.isEmpty()) return@printString
        println(s)
        println("lambda end")
    }
    println("main end")
}

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

main start
printString begin
lambda start
printString end
main end

可以看到,除了Lambda表达式中return@printString语句之后的代码没有打印,其他的日志是正常打印的,说明return@printString确实只能进行局部返回。但是如果我们将printString()函数声明成一个内联函数,那么情况就不一样了,如下所示:

inline fun printString(str: String, block: (String) -> Unit) {
    println("printString begin")
    block(str)
    println("printString end")
}

fun main() {
    println("main start")
    val str = ""
    printString(str) { s ->
        println("lambda start")
        if (s.isEmpty()) return
        println(s)
        println("lambda end")
    }
    println("main end")
}

现在printString()函数变成了内联函数,我们就可以在Lambda表达式中使用return关键字了。此时的return代表的是返回外层的调用函数,也就是main()函数,如果想不通为什么的话,可以回顾一下在上一小节中学习的内联函数的代码替换过程。现在重新运行一下程序,打印结果如下:

main start
printString begin 
lambda start

可以看到,不管是main()函数还是printString()函数,确实都在return关键字之后停止执行了,和我们所预期的结果一致。
将高阶函数声明成内联函数是一种良好的编程习惯,事实上,绝大多数高阶函数是可以直接声明成内联函数的,但是也有少部分例外的情况。观察下面的代码示例:

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

这段代码在没有加上inline关键字声明的时候绝对是可以正常工作的,但是在加上inline关键字之后就会提示如下:

Image

这个错误出现的原因解释起来可能会稍微有点复杂。首先,在runRunnable()函数中,我们创建了一个Runnable对象,并在Runnable的Lambda表达式中调用了传入的函数类型参数。而Lambda表达式在编译的时候会被转换成匿名类的实现方式,也就是说,上述代码实际上是在匿名类中调用了传入的函数类型参数。

而内联函数所引用的Lambda表达式允许使用return关键字进行函数返回,但是由于我们是在匿名类中调用的函数类型参数,此时是不可能进行外层调用函数返回的,最多只能对匿名类中的函数调用进行返回,因此这里就提示了上述错误。
也就是说,如果我们在高阶函数中创建了另外的Lambda或者匿名类的实现,并且在这些实现中调用函数类型参数,此时再将高阶函数声明成内联函数,就一定会提示错误。

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

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

可以看到,这里在函数类型参数的前面加上了crossinline的声明,代码就可以正常编译通过了。
那么这个crossinline关键字又是什么呢?前面我们已经分析过,之所以会提示上面所示的错误,就是因为内联函数的Lambda表达式中允许使用return关键字,和高阶函数的匿名类实现中不允许使用return关键字之间造成了冲突。而crossinline关键字就像一个契约,它用于保证在内联函数的Lambda表达式中一定不会使用return关键字,这样冲突就不存在了,问题也就巧妙地解决了。

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

crossinline

值得注意的是,非局部返回虽然在某些场合下非常有用,但可能也存在危险。因为有时候,我们内联的函数所接收的Lambda参数常常来自于上下文其他地方。为了避免带有return的Lambda参数产生破坏,我们还可以使用crossinline关键字来修饰该参数,从而杜绝此类问题的发生。就像这样子:

fun main(args: Array<String>) {
    foo { return }
}
inline fun foo(crossinline returning: () -> Unit) {
    println("before local return")
    returning()
    println("after local return")
    return
}
// 运行结果
Error: (2, 11) Kotlin: 'return' is not allowed here
具体化参数类型

除了非局部返回之外,内联函数还可以帮助Kotlin实现具体化参数类型。Kotlin与Java一样,由于运行时的类型擦除,我们并不能直接获取一个参数的类型。然而,由于内联函数会直接在字节码中生成相应的函数体实现,这种情况下我们反而可以获得参数的具体类型。我们可以用reified修饰符来实现这一效果。

fun main(args: Array<String>) {
    getType<Int>()
}
inline fun <reified T> getType() {
    print(T::class)
}
// 运行结果
class kotlin.Int

这个特性在Android开发中也格外有用。比如在Java中,当我们要调用startActivity时,通常需要把具体的目标视图类作为一个参数。然而,在Kotlin中,我们可以用reified来进行简化:

inline fun <refied T : Activity> Activity.startActivity() {
    startActivity(Intent(this, T::class.java))
}

这样,我们进行视图导航就非常容易了,如:

startActivity<DetailActivity>()

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

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

相关文章

NBIot 平台篇③OneNET物联网平台

NBIot 平台篇③OneNET物联网平台 1、平台介绍:2、平台注册与登录:3、创建产品:4、添加设备:5、设备调试:1、平台介绍: (1)、OneNET物联网开放平台是中国移动打造的面向产业互联和智慧生活应用的物联网PaaS平台,OneNET支持适配各种网络环境和协议类型,可实现各种传感…

【腾讯云云上实验室】用向量数据库在金融信用数据库分析中的实战运用

一、前言 这篇文章将带领读者探索数据库的多样化解决方案及其演进历程&#xff0c;特别关注向量数据库的重要性和在实际项目中的应用。 通过深入剖析腾讯云向量数据库及其在金融信用数据库分析中的实战运用&#xff0c;为读者提供全面而实用的指南&#xff0c;帮助他们理解、…

GitHub 信息动态统计:让你的 Readme 更具吸引力 | 开源日报 No.87

anuraghazra/github-readme-stats Stars: 59.4k License: MIT GitHub Readme Stats 是一个开源项目&#xff0c;它可以在 README 文件中动态生成 GitHub 统计数据。该项目的主要功能包括&#xff1a; GitHub 统计卡片&#xff1a;显示 stars、commits 和 pull requests 等公共…

2016年五一杯数学建模C题二孩政策问题解题全过程文档及程序

2016年五一杯数学建模 C题 二孩政策问题 原题再现 多年来实施的严、紧计划生育政策对控制人口增长起到关键作用。在优生优育政策的指引下&#xff0c;我国人口质量显著提高&#xff0c;但也带来了不利影响&#xff0c;生育率偏低、男女比例失衡、人口老龄化情况严重等问题。2…

数组与链表

作者简介&#xff1a;大家好&#xff0c;我是smart哥&#xff0c;前中兴通讯、美团架构师&#xff0c;现某互联网公司CTO 联系qq&#xff1a;184480602&#xff0c;加我进群&#xff0c;大家一起学习&#xff0c;一起进步&#xff0c;一起对抗互联网寒冬 除了HashMap&#xff0…

MATLAB中corrcoef函数用法

目录 语法 说明 示例 矩阵的随机列 两个随机变量 矩阵的 P 值 相关性边界 NaN 值 corrcoef函数的功能是返回数据的相关系数。 语法 R corrcoef(A) R corrcoef(A,B) [R,P] corrcoef(___) [R,P,RL,RU] corrcoef(___) ___ corrcoef(___,Name,Value) 说明 R corrc…

geemap学习笔记014:加载本地的tif文件

前言 Colab中似乎没法直接加载云盘中的数据&#xff0c;但是可以先上传到GEE中的assets中&#xff0c;再加载本地的数据。下面是以这个数据为例进行展示。 1 上传数据 首先将本地的tif数据上传到Asset中&#xff0c;得到独一的Image ID。 2 加载数据 使用ee.Image加载数据 …

【Web】PHP反序列化刷题记录

目录 ①[NISACTF 2022]babyserialize ②[NISACTF 2022]popchains ③[SWPUCTF 2022 新生赛]ez_ez_unserialize ④[GDOUCTF 2023]反方向的钟 再巩固下基础 ①[NISACTF 2022]babyserialize <?php include "waf.php"; class NISA{public $fun"show_me_fla…

python爬取招聘网站信息

废话不多说&#xff0c;直接上代码&#xff0c;开箱即用。该文件抓取的是智联招聘网站的招聘信息&#xff0c;可以根据需要设置输入搜索关键词和查找页数&#xff0c;就会得到结果&#xff0c;可以搜索到每个岗位的岗位名称、公司名称、学历要求、公司规模、福利待遇、行业、薪…

STL: 容器适配器stack 与 queue

目录 1.容器适配器 1.1 STL标准库中stack和queue的底层结构 1.2 deque的简单介绍(了解) 1.2.1 deque的原理介绍 1.2.2 deque的缺陷 1.2.3 为什么选择deque作为stack和queue的底层默认容器 2. stack的介绍和使用 2.1 stack的介绍 2.2 stack的使用 2.3 利用deque模拟实现…

一起学docker系列之十一使用 Docker 安装 Redis 并配置持久化存储

目录 前言1 基本安装步骤安装Redis镜像&#xff1a;查看已下载的Redis镜像&#xff1a;运行Redis容器&#xff1a;进入Redis容器&#xff1a;使用Redis CLI进行基本操作&#xff1a; 2 配置文件同步准备配置文件&#xff1a;修改Redis配置文件 /app/redis/redis.conf&#xff1…

【Java+SQL Server】前后端连接小白教程

目录 &#x1f4cb; 流程总览 ⛳️【SQL Server】数据库操作 1. 新建数据库text 2. 新建表 3. 编辑表 ⛳️【IntelliJ IDEA】操作 1. 导入jar包 2. 运行显示错误 &#x1f4cb; 流程总览 ⛳️【SQL Server】数据库操作 打开SQL Server数据库-->sa登录-->新建数据库…

如何处理枚举类型(下)

作者简介&#xff1a;大家好&#xff0c;我是smart哥&#xff0c;前中兴通讯、美团架构师&#xff0c;现某互联网公司CTO 联系qq&#xff1a;184480602&#xff0c;加我进群&#xff0c;大家一起学习&#xff0c;一起进步&#xff0c;一起对抗互联网寒冬 上一篇我们通过编写MyB…

百面深度学习-自然语言处理

自然语言处理 神经机器翻译模型经历了哪些主要的结构变化&#xff1f;分别解决了哪些问题&#xff1f; 神经机器翻译&#xff08;Neural Machine Translation, NMT&#xff09;是一种使用深度学习技术来实现自动翻译的方法。自从提出以来&#xff0c;NMT模型经历了几个重要的…

Couchdb 命令执行漏洞复现 (CVE-2017-12636)

Couchdb 命令执行漏洞复现 &#xff08;CVE-2017-12636&#xff09; 1、下载couchdb.py 2、修改目标和反弹地址 3、Python3调用执行即可 couchdb.py文件下载地址: https://github.com/vulhub/vulhub/blob/master/couchdb/CVE-2017-12636/exp.py ‍ 在VULFocus上开启环境 …

指针(1)

指针基本概念&#xff1a; 1.指针变量是一个变量&#xff0c;用来存放地址&#xff0c;地址唯一标识一块地址空间。 2.指针的大小固定是4/8个字节&#xff08;32位平台/64位平台&#xff09;。 3.指针有类型&#xff0c;指针的类型决定-整数的步长&#xff0c;以及解引用时的…

数据分析项目实战!Python分析员工为何离职

大家好&#xff0c;我是小F&#xff5e; 今天给大家介绍一个Python数据分析项目实战&#xff0c;不仅包含代码&#xff0c;还提供分析数据集。 员工流失或是员工离开公司的比率是公司关注的一个重要问题。它不仅会导致宝贵人才的流失&#xff0c;还会产生成本并破坏生产力。了解…

Nginx(资源压缩)

建立在动静分离的基础之上&#xff0c;如果一个静态资源的Size越小&#xff0c;那么自然传输速度会更快&#xff0c;同时也会更节省带宽&#xff0c;因此我们在部署项目时&#xff0c;也可以通过Nginx对于静态资源实现压缩传输&#xff0c;一方面可以节省带宽资源&#xff0c;第…

vim+xxd编辑十六进制的一个大坑:自动添加0x0a

问题描述 今天在做一个ctf题&#xff0c;它给了一个elf文件&#xff0c;我要做的事情是修复这个elf文件&#xff0c;最后执行它&#xff0c;这个可执行文件会计算它自身的md5作为这道题的flag。我把所有需要修复的地方都修复了&#xff0c;程序也能成功运行&#xff0c;但是fl…

打包SpringBoot 项目为本地应用

使用工具&#xff1a;exe4j、Inno Setup Compiler 步骤&#xff1a; 1&#xff0c;将dll包放入项目根路径下&#xff1b; 2&#xff0c;idea 使用Maven打jar包&#xff1b; 3&#xff0c;使用exe4j 工具进行打包&#xff1b; 打开工具首页不动&#xff08;直接 next&#xff…