前言
Kotlin给我们提供了很多Java没有的便利,作用域函数(Scope Function)就是Kotlin标准库里面的提供的一些让我们减少重复代码和提高可读性的一系列函数。
下面结合我的使用经验来介绍一下Kotlin的作用域函数:
- 是什么
- 作用是什么
- 怎么使用
- 怎么选择
- 对我们开发的启发
介绍
如官网介绍所说,作用域函数(Scope Function)是能让我们创建一个临时的作用域,在这个作用域里可以有一个上下文对象给我们用,最后它还有返回值的一些函数,可以用包括:let、run、apply、with、also等关键字来使用。
简单总结,它的作用就是:可以减少冗余代码,从而让代码更简洁;可以让代码形成链式调用,从而使代码逻辑更清晰。
共有特点
- 都是在一个代码块里面执行一些代码,在这个代码块里,有返回值
- 上下文对象的引用方式,都是只读val的,不要想着在代码块改变它的引用
本质
根据我个人的理解,我认为Kotlin作用域函数的本质其实是:通过编译器把一些常用的编码范式,封装成更简洁,更容易让开发者使用的上层接口。
使用格式
扩展函数
[返回值] = [对象].[作用域函数关键字]{ [上下文对象] ->
// 代码块
}
非扩展函数
[返回值] = [作用域函数关键字]([对象]).{[上下文对象] ->
// 代码块
}
使用例子
下面总结下我在开发时候遇到的一些使用例子:
简单判空
intent?.let {
Log.i(TAG, "onCreate: ${it.data}")
}
Intent设置
Intent().apply {
putExtra("name", "totond")
putExtra("age", 18)
putExtra("time", 111)
startActivity(this)
}
Paint设置
var paint = Paint().apply {
textSize = 14.0f
color = Color.WHITE
isAntiAlias = false
}
分类
是否扩展函数
区别
- 最大的区别就是,如果需要用
?.
判空,则肯定需要扩展函数 - 用扩展函数的必定存在一个上下文对象
上下文对象——是this还是it
区别
- this适合在代码块里面,对象作为函数提供方的代码比较多的时候使用,因为可以省略12
- it适合在代码块里面,对象作为函数入参的代码比较多的时候使用,相比this可以写少两个字母
返回值——是上下文对象还是Lambda的结果
区别
- 返回上下文对象时,一般是用于对这个对象设置的操作,所以很多时候配合this做上下文对象
- 返回Lambda结果时,一般是要返回一个代码块里面计算、处理后的结果
怎么选
从上面我们了解到这些作用域函数的用法,但是有好几种作用域函数,我们想要用它们的时候,要怎么选呢?我个人是根据下面的方式来选的,大家可以参考下:
函数 | 上下文对象的引用方式 | 返回值 | 是否扩展函数 |
---|---|---|---|
let | it | Lambda 表达式的结果值 | 是 |
run | this | Lambda 表达式的结果值 | 是 |
run | - | Lambda 表达式的结果值 | 不是: 不使用上下文对象来调用 |
with | this | Lambda 表达式的结果值 | 不是: 上下文对象作为参数传递. |
apply | this | 上下文对象本身 | 是 |
also | it | 上下文对象本身 | 是 |
对着表看:
-
上下文对象的引用方式、返回值、是否扩展函数,这3个要素,有没有可以随便的。例如很多情况下,返回值就是可以随便的
-
根据步骤1中不可随便的要素,来进行排除,排除优先级:返回值 > 是否扩展函数 > 上下文对象的引用方式
- 返回值:最优先排除,因为这个确定性最高,一般要不要返回值,在写代码块之前就想好了
- 是否扩展函数:用不用扩展函数,使用差异性很大,而且要判空就要有扩展函数的
- 上下文对象的引用方式:最容易模糊的选择的要素,因为选哪个都能实现想要的效果,用错了代码会不够简洁:
- this适合在代码块里面,对象作为函数提供方的代码比较多的时候使用,因为可以省略
- it适合在代码块里面,对象作为函数入参的代码比较多的时候使用,相比this可以写少两个字母
其实this也就比it多两个字母
优缺点分析
使用场景分析
按照我的理解,作用域函数其实是一个锦上添花的功能,不用它用普通的ifelse也可以实现,例如:
// 作用域函数写法
val result = input?.let {
"strLen = ${it.length + 100}"
}
Log.i(TAG, "testLet: $result")
// 普通写法
val result1 = if (input != null) {
"strLen = ${input.length + 100}"
} else {
null
}
Log.i(TAG, "testLet: $result1")
但是,从上面的例子可以看出,作用域函数写法看起来是更加简单明了,虽然不熟悉的人看了可能会有点理解成本,但是熟悉了之后就很容易理解了。然后,我们一般处理一些流程的时候,通常会这样写代码:
// 一般写法 input -> A -> B -> C
val A = toA(input)
if (A != null) {
val B = AToB(A)
if (B != null) {
val C = BToC(B)
Log.i(TAG, "C is $C")
}
}
这样看起来太过层嵌套if语句了,我们可以用作用域函数把它优化为:
// 作用域写法 input -> A -> B -> C
toA(input)?.let {
AToB(it)
}?.let {
BToC(it)
}?.let {
Log.i(TAG, "C is $it")
}
这样写,实际就是利用作用域函数把判空的逻辑隐藏,利用链式调用的方式把逻辑以平铺的方式展示出来,看上去是不是比一般写法更清晰一点?但是,如果我们要利用if的多个分支或者需要利用流程的中间值的时候,用这种链式调用就可能会有点不方便了,例如:
// if多了为空的分支,c要引用中间值a
val a = toA(input)
if (a != null) {
val B = AToB(A)
if (B != null) {
val C = BToC(B) + a
Log.i(TAG, "C is $C")
}
} else {
Log.i(TAG, "testCompare: a is null")
}
当然这些也可以通过封装值到data类等方法解决,但是这样子就有种为了写链式调用代码来写代码的意味了,所以选择作用域函数的时候,我会特别留意这些点,后续这个代码块会不会扩展为,多参数输入、多分支走向、中间值引用等
优点:
- 省去冗余代码,让代码更简洁。如使用了this,基本可以用来取代Builder模式
- 让代码能够保持链式调用,逻辑更清晰(看熟悉了之后)
缺点:
- 有上手难度,一开始记不住这么多类型,可能每次用的时候要查表
- 多个作用域函数组成的链式调用,可扩展性没有普通使用那么好,使用的时候需要看好场景,不是所有的地方都能使用
启发
了解了kotlin的这些作用域函数之后,我发现了这些也算是官方给我们写的例子,教我们怎么使用Lambda和扩展函数。
我们在实际开发中,完全可以模仿官方这种设计思想,封装一些实用的函数,如:
类型的转换
当我们某些数据类型要进行转换成另外一个类型的时候,用java写一般都是会封装成啥xxUtil,现在我们可以直接用扩展函数,在给这个对象“加”方法:
fun String.LetterToNum(): Int{
return when (this) {
"A" -> 1
"B" -> 2
"C" -> 3
else -> 0
}
}
Log.i(TAG, "A to num = ${"A".LetterToNum()}")
//输出:A to num = 1
简化一些方法的调用
如果有些方法,要调用的话需要另外一个语句,不能保持链式调用的话,可以通过转换为扩展函数的方式去使用,如:
项目里用的把Disposable加到CompositeDisposable:
Observable.just(data)
.observeOn(Schedulers.io())
.subscribeOn(AndroidSchedulers.mainThread())
.subscribe()
.addComposite(compositeDisposable)
fun Disposable.addComposite(compositeDisposable: CompositeDisposable) {
compositeDisposable.add(this)
}
打印分割线:
定义:
// 打印分割线
private fun Unit.divider() {
Log.i(TAG, "--------------------------------------------------------------")
}
使用:
testRun().divider()
testLet().divider()
testApply().divider()
testAlso().divider()
testWith().divider()
输出:
I: testRun: strLen = 103
I: --------------------------------------------------------------
I: testLet: strLen = 103
I: --------------------------------------------------------------
I: testApply: yan
I: --------------------------------------------------------------
I: testAlso: yan
I: --------------------------------------------------------------
I: testWith: strLen = 103
I: --------------------------------------------------------------
后话
以上是我开发中使用kotlin作用域函数所积累到的一些经验,如有错漏,敬请指正。