前言
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 这样规模化的使用作用域函数,对性能是有不可忽视的影响的。
5. 常见作用域函数模板化结构
run
,let
,apply
,also
,4 个作用域函数,其实是 2 个特性的组合结果:
- 调用作用域函数的对象,是作为
this
传入,还是作为唯一参数(默认为it
)传入; - 作用域函数的返回值,是调用的对象,还是 lambda 的返回值。
用伪代码来解释一下:
val result = object.xxx { param ->
val lambdaResult = param.toString()
lambdaResult
}
xxx
可以是run
,let
,apply
,also
其中一个。
如果xxx = run/let
,那么 result == lambdaResult
; 而如果xxx = apply/also
,那么 result == object
。且lambdaResult
这一行会报错,因为apply
,also
的 lambda 返回值必须是 Unit。
lambda 最后一行的值会作为 lambda 的返回值。它等价于
return@xxx lambdaResult
。@xxx
表示返回的是这个lambda,而不是退出整个上层方法。
如果xxx = let/also
,那么param == object
; 而如果xxx = run/apply
的话,那么 lambda 是个无参数 lambda,不存在param
, param ->
会报错;
如果 lambda 为单参数 lambda,此时
param ->
可以省略,Kotlin 提供默认的单参数名it
总结成表就是:
特性 | 返回值为this | 返回值为lambda结果 |
---|---|---|
调用对象转换为this | apply | run |
调用对象转换为it | also | let |
快速记忆:4 个常用作用域函数分别是 2 个特性的两两组合,不需要专门记忆。
6. run/let/apply/also 使用场景举例
已经知道这 4 个作用域函数的特点,那么怎么用好它们呢?下面一起来看下这几个作用域函数的使用场景。
6.1 run函数
这是项目中的一段代码:
mRecordViewHelper?.run {
mEffectsView.visibility = View.INVISIBLE
mLyricViewer.visibility = View.INVISIBLE
mStatusView = AudioRecordStatusView(CommonContext.getApplicationContext(), null)
enableSoloCountBackView()
}
run
方法很适合用在对某个对象做一系列操作。可空对象mRecordViewHelper
使用run
方法转换成非空对象this
传入到 lambda 中,并在 lambda 内部进行一系列的赋值和方法调用。
run
的返回值是 lambda 的最后一行结果,在这个示例是Unit
。使用 lambda 结果的用例较少,主要就是这种转换为this
的用法。
6.2 let函数
使用run
还是使用let
更像是编码习惯问题,因为有些人不习惯把调用对象转换成this
,而更喜欢用入参it
。
使用run
还是使用let
的确没有太大区别。不过这几种情况,更建议使用let
:
- 调用对象主要作为参数,而不是用于初始化或方法调用时。
// 赋值和调用的方法都是类的成员和方法,不是 mRecordViewHelper 的成员和方法
mRecordViewHelper?.run {
myFirstView = firstView
mySecondView = secondView
setTargetView(targetView)
}
// 使用 let,不会存在两个 this 混用,代码可读性更高。
mRecordViewHelper?.let {
myFirstView = it.firstView
mySecondView = it.secondView
setTargetView(it.targetView)
}
- 当 lambda 中需要用到类的
this
时;
// 因this 的变化,导致引用类的 this 需要加@MainActivity 这和 Java匿名类 MainActivity.this 效果一样
mRecordViewHelper?.run {
setOnClickListener(this@MainActivity)
}
// let 不会改变 this,在需要引用类的 this 的时候比较方便。
mRecordViewHelper?.let {
it.setOnClickListener(this)
}
总结一下:
- 当 lambda 主要执行的是调用对象的方法和赋值时,建议使用
run
; - 而当调用对象主要用作参数时,建议使用
let
; - 当 lambda 会用到类的
this
时,建议使用let
。
6.3 apply函数
apply
和run
的区别主要在于,apply
返回的是调用对象。这个特性使得apply
很适合用来做初始化的工作。如:
class VideoConfig {
val profile = VideoEncodeProfile().apply {// 初始化数值
audioBitRate = 44100
videoWidth = 480
videoHeight = 480
videoFrameRate = 30
setDefaultQuality()
}
}
apply
很适合用来做 property 的初始化,这样 property 的初始化代码就不用写在 init 块里,做到了代码的高内聚。
6.4 also函数
also 的适用场景,和run
与let
差不多,是与apply
来做对比的。具体建议也和run
与let
类似:
- 当 lambda 主要执行的是调用对象的方法和赋值时,建议使用
apply
; - 而当调用对象主要用作参数时,建议使用
also
; - 当 lambda 会用到类的
this
时,建议使用also
。
7. 更多的作用域函数
在Kotlin中,除了run
,let
,apply
,also
之外,还有其他高效的作用域函数。但另一方面掌握了这 4 个作用域函数,已经覆盖大部分使用场景。剩下的几个使用需求没有那么的迫切,但能掌握是更好咯,可以帮助写出更有 Kotlin 样的代码。
作者:子不语Any
链接:https://juejin.cn/post/7134709872179806215
网友评论