热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Kotlin语言学习(11)内联函数

Kotlin语言学习(1)-Kotlin基础Kotlin语言学习(2)-函数的定义与调用Kotlin语言学习(3)-类、对象和接口Kotlin语言学习(4)-数据类、类委托及

Kotlin 语言学习(1) - Kotlin 基础

Kotlin 语言学习(2) - 函数的定义与调用

Kotlin 语言学习(3) - 类、对象和接口

Kotlin 语言学习(4) - 数据类、类委托 及 object 关键字

Kotlin 语言学习(5) - lambda 表达式和成员引用

Kotlin 语言学习(6) - Kotlin 的可空性

Kotlin 语言学习(7) - Kotlin 的类型系统

Kotlin 语言学习(8) - 运算符重载及其他约定

Kotlin 语言学习(9) - 委托属性

Kotlin 语言学习(10) - 高阶函数:Lambda

Kotlin 语言学习(11) - 内联函数

Kotlin 语言学习(12) - 泛型类型参数


一、本文概要


二、内联函数

当我们使用lambda表达式时,它会被正常地编译成匿名类。这表示每调用一次lambda表达式,一个额外的类就会被创建,并且如果lambda捕捉了某个变量,那么每次调用的时候都会创建一个新的对象,这会带来运行时的额外开销,导致使用lambda比使用一个直接执行相同代码的函数效率更低。

如果使用inline修饰符标记一个函数,在函数被调用的时候编译器并不会生成函数调用的代码,而是 使用函数实现的真实代码替换每一次的函数调用


2.1 内联函数如何运作

当一个函数被声明为inline时,它的函数体是内联的,也就是说,函数体会被直接替换到函数被调用地方,下面我们来看一个简单的例子,下面是我们定义的一个内联的函数:

inline fun inlineFunc(prefix : String, action : () -> Unit) {println("call before $prefix")action()println("call after $prefix")
}

我们用如下的方法来使用这个内联函数:

fun main(args: Array) {inlineFunc("inlineFunc") {println("HaHa")}
}

运行结果为:

>> call before inlineFunc
>> HaHa
>> call after inlineFunc

最终它会被编译成下面的字节码:

fun main(args: Array) {println("call before inlineFunc")println("HaHa")println("call after inlineFunc")
}

lambda表达式和inlineFunc的实现部分都被内联了,由lambda生成的字节码成了函数调用者定义的一部分,而不是被包含在一个实现了函数接口的匿名类中。


传递函数类型的变量作为参数

在调用内联函数的时候,也可以传递函数类型的变量作为参数,还是上面的例子,我们换一种调用方式:

fun main(args: Array) {val call : () -> Unit = { println("HaHa") }inlineFunc("inlineFunc", call)
}

那么此时最终被编译成的Java字节码为:

fun main(args: Array) {println("call before inlineFunc ")action()println("call after inlineFunc")
}

在这种情况,只有inlineFunc的实现部分被内联了,而lambda的代码在内联函数被调用点是不可用的。


在两个不同的位置使用同一个内联函数

如果在两个不同的位置使用同一个内联函数,但是用的是不同的lambda,那么内联函数会在每一个被调用的位置分别内联,内联函数的代码会被拷贝到使用它的两个不同位置,并把不同的lambda替换到其中。


2.2 内联函数的限制

鉴于内联的运作方式,不是所有使用 lambda 的函数都可以被内联。当函数被内联的时候,作为参数的lambda表达式的函数体会被 替换到最终生成的代码中

这将限制函数体中的lambda参数的使用:


  • 如果lambda参数 被调用,这样的代码能被容易地内联。
  • 如果lambda参数 在某个地方被保存起来,以便以后继续使用,lambda表达式的代码 将不能被内联,因此必须要 有一个包含这些代码的对象存在

一般来说,参数如果 被直接调用或者作为参数传递 给另外一个inline函数,它是可以被内联的,否则,编译器会 禁止参数被内联 并给出错误信息Illeagal usage of inline-parameter

例如,许多作用于序列的函数会返回一些类的实例,这些类代表对应的序列操作并接收lambda作为构造方法的参数,以下是Sequence.map函数的定义:

fun Sequence.map(transform : (T) -> R) : Sequence {return TransformingSequence(this, transform);
}

map函数没有直接调用作为transform参数传递进来的函数。而是将这个函数传递给一个类的构造方法,构造方法将它保存在一个属性当中。为了支持这一点,作为transform参数传递的lambda需要 被编译成标准的非内联表示法,即一个实现了函数接口的匿名类。

如果一个函数期望两个或更多的lambda函数,可以选择只内联其中一些参数,因为一个lambda可能会包含很多代码或者 以不允许内联的方式调用,接收这样的非内联lambda的参数,可以用noinline修饰符来标记它:

inline fun foo(inlined : () -> Unit, noinline noinlined : () -> Unit) {}

注意,编译器完全支持 内联跨模块的函数或者第三方库定义的函数,也可以在 Java 中调用绝大部分内联函数


2.3 内联集合操作

大部分标准库中的集合函数都带有lambda参数。例如filter,它被声明为内联函数,这意味着filter函数,以及传递给它的lambda字节码会被内联到filter被调用的地方,因此我们不用担心性能问题。

假如我们像下面这样,连续调用filtermap两个操作:

println(people.filter{ it.age > 30 }.map(Person :: name))

这个例子使用了一个lambda表达式和一个成员引用,filtermap函数都被声明为inline函数,所以不会额外产生类或者对象,但是上面的代码会创建一个中间集合来保存列表过滤的结果。


2.4 决定何时将函数声明成内联

对于普通函数的调用,JVM已经提供了强大的内联支持。它会分析代码的执行,并在任何通过内联能够带来好处的时候将函数调用内联。

带有lambda参数的函数内联能带来好处:


  • 节约了函数调用的开销,节约了为lambda创建匿名类,以及创建lambda实例对象的开销。
  • JVM目前并没有聪明到总是能够将函数调用内联。
  • 内联使得我们可以使用一些不可能被普通lambda使用的特性,例如 非局部返回

但是在使用inline关键字的时候,还是应该注意代码的长度,如果你要内联的函数很大,将它的字节码拷贝到每一个调用点将会极大地增加字节码的长度。在这种情况下,你应该将那些与lambda参数无关的代码抽取到一个独立的非内联函数中。


三、高阶函数中的控制流

当你使用lambda去替换像循环这样的命令式代码结构时,很快就会遇到return表达式的问题,把一个return语句放在循环的中间是很简单的事。但是如果将循环替换成一个类似filter的函数呢?


3.1 lambda 中的返回语句:从一个封闭的函数返回

下面,我们通过一个例子来演示,在集合当中寻找名为Alice的人,找到了就直接返回:

data class Person(val name: String, val age: Int)val people = listOf(Person("Alice", 29), Person("Bob", 31))fun lookForAlice(people: List) {people.forEach {if (it.name == "Alice") {println("Found!")return}}println("Alice is not found")
}fun main(args: Array) {lookForAlice(people)
}

运行结果为:

>> Found !

如果在lambda中使用return关键字,它会 从调用 lambda 的函数 中返回,并不只是 从 lambda 中返回,这样的return语句叫做 非局部返回,因为它从一个比包含return的代码块更大的代码块中返回了。

需要注意的是,只有 以 lambda 作为参数的函数是内联函数 的时候才能从更外层的函数返回。在一个非内联的lambda中使用return表达式是不允许的,一个非内联函数可以把它的lambda保存在变量中,以便在函数返回以后可以继续使用,这个时候lambda想要去影响函数的返回已经太晚了。


3.2 从 lambda 中返回:使用标签返回

也可以在lambda表达式中使用局部返回,类似于for循环中的break表达式,它会终止lambda的执行,并接着从调用lambda的代码处执行。

要区分局部返回和非局部返回,要用到标签。想从一个lambda表达式处返回你可以标记它,然后在return关键字后面引用这个标签。

data class Person(val name: String, val age: Int)val people = listOf(Person("Alice", 29), Person("Bob", 31))fun lookForAlice(people: List) {people.forEach label@{if (it.name == "Alice") return@label}println("Alice might be somewhere")
}fun main(args: Array) {lookForAlice(people)
}

运行结果为:

>> Alice might be somewhere

另一种选择是,使用lambda作为参数的函数的函数名可以作为标签,也就是上面的forEach,如果你显示地指定了lambda表达式的标签,再使用函数名作为标签没有任何效果。


3.3 匿名函数:默认使用局部返回

匿名函数是一种不同的用于编写传递给函数的代码块的方式,先来看一个示例:

data class Person(val name: String, val age: Int)val people = listOf(Person("Alice", 29), Person("Bob", 31))fun lookForAlice(people: List) {people.forEach(fun (person) {if (person.name == "Alice") returnprintln("${person.name} is not Alice")})
}fun main(args: Array) {lookForAlice(people)
}

运行结果为:

>> Bob is not Alice

匿名函数和普通函数有相同的指定返回值类型的规则,代码块匿名函数 需要显示地指定返回类型,如果使用 表达式函数体,就可以省略返回类型。

在匿名函数中,不带return表达式会从匿名函数返回,而不是从包含匿名函数的函数返回,这条规则很简单:return从最近的使用fun关键字声明的函数返回。


  • lambda表达式没有使用fun关键字,所以lambda中的return从最外层的函数返回。
  • 匿名函数使用了fun,因此return表达式从匿名函数返回。

尽管匿名函数看起来和普通函数很相似,但它其实是lambda表达式的另一种语法形式而已。关于lambda表达式如何实现,以及在内联函数中如何被内联的讨论同样适用于匿名函数。


see you


推荐阅读
  • 本文将继续探讨 JavaScript 函数式编程的高级技巧及其实际应用。通过一个具体的寻路算法示例,我们将深入分析如何利用函数式编程的思想解决复杂问题。示例中,节点之间的连线代表路径,连线上的数字表示两点间的距离。我们将详细讲解如何通过递归和高阶函数等技术实现高效的寻路算法。 ... [详细]
  • 本文介绍了如何利用ObjectMapper实现JSON与JavaBean之间的高效转换。ObjectMapper是Jackson库的核心组件,能够便捷地将Java对象序列化为JSON格式,并支持从JSON、XML以及文件等多种数据源反序列化为Java对象。此外,还探讨了在实际应用中如何优化转换性能,以提升系统整体效率。 ... [详细]
  • 开发日志:201521044091 《Java编程基础》第11周学习心得与总结
    开发日志:201521044091 《Java编程基础》第11周学习心得与总结 ... [详细]
  • Java中不同类型的常量池(字符串常量池、Class常量池和运行时常量池)的对比与关联分析
    在研究Java虚拟机的过程中,笔者发现存在多种类型的常量池,包括字符串常量池、Class常量池和运行时常量池。通过查阅CSDN、博客园等相关资料,对这些常量池的特性、用途及其相互关系进行了详细探讨。本文将深入分析这三种常量池的差异与联系,帮助读者更好地理解Java虚拟机的内部机制。 ... [详细]
  • 本指南从零开始介绍Scala编程语言的基础知识,重点讲解了Scala解释器REPL(读取-求值-打印-循环)的使用方法。REPL是Scala开发中的重要工具,能够帮助初学者快速理解和实践Scala的基本语法和特性。通过详细的示例和练习,读者将能够熟练掌握Scala的基础概念和编程技巧。 ... [详细]
  • 本文介绍了如何在iOS平台上使用GLSL着色器将YV12格式的视频帧数据转换为RGB格式,并展示了转换后的图像效果。通过详细的技术实现步骤和代码示例,读者可以轻松掌握这一过程,适用于需要进行视频处理的应用开发。 ... [详细]
  • 深入理解 Java 控制结构的全面指南 ... [详细]
  • 本文深入解析了Python在处理HTML过滤时的实现方法及其应用场景。通过具体实例,详细介绍了如何利用Python代码去除HTML字符串中的标签和其他无关信息,确保内容的纯净与安全。此外,文章还探讨了该技术在网页抓取、数据清洗等领域的实际应用,为开发者提供了宝贵的参考。 ... [详细]
  • 如何使用和示例代码解析 org.semanticweb.owlapi.model.OWLSubPropertyChainOfAxiom.getPropertyChain() 方法 ... [详细]
  • 在探讨C语言编程文本编辑器的最佳选择与专业推荐时,本文将引导读者构建一个基础的文本编辑器程序。该程序不仅能够打开并显示文本文件的内容及其路径,还集成了菜单和工具栏功能,为用户提供更加便捷的操作体验。通过本案例的学习,读者可以深入了解文本编辑器的核心实现机制。 ... [详细]
  • 本次发布的Qt音乐播放器2.0版本在用户界面方面进行了细致优化,提升了整体的视觉效果和用户体验。尽管核心功能与1.0版本保持一致,但界面的改进使得操作更加直观便捷,为用户带来了更为流畅的使用体验。此外,我们还对部分细节进行了微调,以确保软件的稳定性和性能得到进一步提升。 ... [详细]
  • 如何使用 net.sf.extjwnl.data.Word 类及其代码示例详解 ... [详细]
  • 深入理解Java中的多态性概念及其应用
    多态是面向对象编程中的三大核心特性之一,与封装和继承共同构成了面向对象的基础。多态使得代码更加灵活和可扩展,封装和继承则为其提供了必要的支持。本文将深入探讨多态的概念及其在Java中的具体应用,帮助读者全面理解和掌握这一关键知识点。 ... [详细]
  • GDI+ 进阶指南:深入解析 IGPFont 接口
    本文深入探讨了 GDI+ 中 IGPFont 接口的应用与实现,通过具体的代码示例展示了如何在 Delphi 中使用 GDI+ 进行高质量的文本渲染。文章详细解析了 IGPFont 接口的各项属性和方法,并提供了实用的编程技巧,帮助开发者更好地理解和掌握 GDI+ 的字体处理功能。 ... [详细]
  • 在处理多个玩家的相机控制时,我遇到了一个挑战,即无法在运行时动态添加播放器子对象以转换数组类型。为了解决这个问题,我在 `CameraControl.cs` 脚本中采取了临时措施。该脚本负责根据玩家的数量动态调整相机的缩放范围,确保所有玩家都能被相机捕捉到。 ... [详细]
author-avatar
l清笛l
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有