携手创作,共同成长!这是我参与「日新计划 8 月更文挑战」的第22天,点击查看活动详情
前言
Kotlin中的效果域函数是规范库中包括的几个常用函数,let、run、with、apply以及also。 从本篇起来介绍一下 Kotlin 中的效果域函数,分上下两篇。上篇会阐明几个常见效果域函数,剖析一下run函数,以及对比一下 Java 中没有效果域函数状况。
概览
1. 常见的四个效果域函数
学习 Kotlin 肯定会碰到 run/let/apply/also 这四个函数,它们是Kotlin规范库中的几个常用函数,效果在目标上时,履行给定的block代码块。形成一个暂时效果域,在这个效果域中,能够拜访该目标而无需称号,也被称为效果域函数(scope functions)
下面表格展示常用几个效果域函数的对比,依据表格在事务场景选择合适效果域函数。
函数 | 目标引用 | 返回值 | 是否为扩展函数 |
---|---|---|---|
let | it | Lambda表达式成果 | 是 |
run | this | Lambda表达式成果 | 是 |
apply | this | 上下文目标 | 是 |
also | it | 上下文目标 | 是 |
依据预期目的选择合适效果域函数的指南:
- 对一个非空(non-null)目标履行 lambda 表达式:
let
- 将表达式作为变量引进为局部效果域中:
let
- 目标装备:
apply
- 目标装备而且计算成果:
run
- 在需求表达式的当地运行句子:非扩展的
run
- 附加效果:
also
2. run 办法运用
在项目中,有以下一段代码:
public class PlayManager {
/** 初始值为空,需在资源初始化之后再拿到目标 */
private Player player = null;
/** 播映音乐 */
public void play(String path) {
if (player != null) {
player.init(path);
player.prepare();
player.start();
}
}
}
Kotlin等效代码为:
public class PlayManager {
/** 初始值为空,需在资源初始化之后再拿到目标 */
private var player: Player? = null
/** 播映 */
fun play(path: String) {
player?.init(path)
player?.prepare()
player?.start()
}
}
运用 Kotlin 的 run
办法:
public class PlayManager {
/** 初始值为空,需求在资源初始化之后再拿到目标 */
private var player: Player? = null
/** 播映 */
fun play(path: String) {
player?.run { // 目标调用run
init(path)
prepare()
start()
}
}
}
run 调用是一种函数调用的特别写法,即当 lambda 作为函数的最后一个参数时,能够写在函数括号外部,也就是说
object.run { }
和object.run({ })
是等价的。这种代码写起来看起来都更简洁。
run
的功能很简单,主要做两件事:
- 把 lambda 内部的
this
改成了对应调用目标; - run 函数会返回 lambda表达式的返回值。
run
办法到达以下三个效果:
-
因
this
的变化,不再需求重复的输入变量,和链式调用殊途同归; -
把可空目标转换为了非空目标,由于
run
办法是?.
调用,player
不为空才会履行。考虑到并发,Kotlin 要求每次调用可空特点时要进行判空。运用run
办法等效于先把可空特点用暂时变量持有再运用,这样就消除了并发竞争的影响。 -
在一个函数里声明的这个一个小“代码块“,表示和其他无关的代码隔离,完成了函数内部的高内聚。能够添加代码的可读性,让人一看就理解:“这是针对此目标的一系列操作,函数里关于此目标的运用只需求重视这个代码块即可”。
第3点是非常棒的,这样不仅是进步开发效率,更是引导开发者写出好保护的代码。在写 Java代码时,很简单不自觉的写出某个目标在函数头操作一下,隔几行调用一下,隔几行又操作一下的代码。阅读者很简单误以为这些代码之间有着次序上的耦合,从而继续按照这个“隐含的规矩“来保护代码。却不知其时的开发者只是想到哪写到哪,实际并不存在这样的隐含联系。运用 run
能够在函数内部快速建立起一个个代码块,让函数拥有更明晰的结构,又不用花费很大精力保护代码逻辑。
3. run 函数代码剖析
run
源码如下:
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
从上面函数源码看,触及的根本都是编译器相关的。包括了泛型,inline,类扩展 lambda(T.() -> R
),contract 4 个概念。
inline,表示内联函数,在编译期调用这个函数的当地会被替换为函数包括的代码。
inline 的优点是调用该办法不再有调用办法的功能消耗,即不会跳转和发生栈帧;弊端是可能会使二进制文件体积增大,尤其是函数代码量大的时候。所以 inline 适合用在代码量小的函数,run
就很契合这个条件。能够得出结论:编译器编译时会把 inline 函数内联到实际调用方位,所以运用 run
办法时不会有办法调用的功能损耗。
而 @kotlin.internal.InlineOnly
,实际效果为对 Java 不行见(private),由于 Java 不支持 inline。对 Java 不行见后,这个 inline 办法则能够不在字节码里存在,由于调用的当地全部都内联了。
Java 虽没有内联函数,但 JVM 是有内联优化的,只是这个优化无法精确控制。
类扩展 lambda(关键字 lambda with class extension),即入参的声明 T.() -> R
。扩展 lambda 能够理解为给类扩展一个 lambda 函数。它的效果和扩展办法一样,在 扩展 lambda 效果域内,以目标作为 this
来操作这个目标。
contract 契约,指的是代码和 Kotlin 编译器的契约。举一个比如,对局部变量添加了假如为空则 return 的逻辑,Kotlin 编译器便能够智能的辨认出 return 之后的局部变量必定不为空,局部变量的类型会退化为非空类型。但假如把是否为空的代码封装进一个扩展办法如 Any?.isNotNull()
里,那么编译器就无法辨认 return 后边的代码局部变量是否为空,事实上这个局部变量依然是可空类型。
这儿能够声明一个 contract,告知编译器假如Any?.isNotNull()
返回了 true,则表示目标非空。这样在代码里履行了 isNotNull()
办法之后,return 后边的代码,局部变量也能正确退化为非空类型。具体比如咱们能够看官方 Collections.kt 的 Collection<T>.isNullOrEmpty()
。
4. Java 没有效果域函数
效果域函数需求类扩展和内联这两个特点,才能最大化表现其价值。没有类扩展,this
的切换需求经过继承或者匿名类来完成,做不到通用;
像 let
这种不需求切换 this
的效果域函数,由于没有类扩展能力而为了寻求通用性,也只能经过静态工具类来完成,效果是打折扣的。
Java是没有内联的,虽有 JVM 内联优化支撑,但内联优化只对 final 且调用次数数量级较大的办法有效。假如像 Kotlin 这样规模化的运用效果域函数,对功能是有不行忽视的影响的。
在(JUEJIN)一同共享知识,Keep Learning!