稀有猿诉

十年磨一剑,历炼出锋芒,说话千百句,不如码二行。

Understanding Kotlin Scope Functions

Kotlin是基于JVM衍生出来的新一代通用编程语言,它的目标是简洁,可读和高效,这里的高效并不是代码的运行效率高,而是说项目的开发效率高。Kotlin有太多的小巧的新特性(在Java眼中就是语法糖),比如在Kotlin中有几个作用和用法都非常接近的函数apply/with/run/let/also,它们的正统名字是作用域函数(Scope functions),今天就来学习一下这些函数的使用方法和具体区别。

Java是面向对象的王牌语言,它的特点是严谨和教条,Java写出来的代码学过Java的人大多都看得懂,所以规模以上的项目现在基本上都用Java,这对维护是有好处的。但Kotlin不一样,它有非常多的特性,融合了众多编程语言的特点,同样一件事情,可能有无数种写法,虽然号称是用标准Kotlin语言实现的,但是即使学过Kotin的人也看不懂。比如虽然你学会了Function,Object和lambda,以及像inline function和extension,但是如果用apply和with写几段方法,你就看不懂了,这就导致了Kotlin虽然易于上手,但是要想学透和提高曲线 就会陡峭许多。

到底是个啥

先来看一下Scope function到底是什么,它们的作用是在一个对象上执行一段代码,我们来看一个简单的例子:有一个类是Person,它有一些属性和方法,我们想对它的一个对象进行操作,通常会这样做:

1
2
3
4
5
val alice = Person(name="Alice", age=20,addr="Amsterdam")
println(alice)
alice.moveTo("London")
alice.incrementAge(2)
println("Two years later ${alice.name} is at ${alice.addr}")

但使用scope function,我们可以这样做:

1
2
3
4
5
6
7
8
9
val alice = Person("Alice", 20, "Amsterdam").apply {
  println(this)
  moveTo("London")
  incrementAge(2)
}
println("Two years later ${alice.name} is at ${alice.addr}")

//Person(name='Alice', age=20, addr='Amsterdam')
//Two years later Alice is at London

这两段代码的输出是完全一样的,但是第二段明显要简洁很多这就是scope function的作用,仔细看apply后面的lambda块,它是一个scope,犹如在对象的类定义之中,在这个代码块中可以直接引用对象的方法,而不是像常规的那样使用对象的引用。

注意:如果不是很尾部lambda的同学可以先行参考另外一篇文章,以加强理解。

理解Scope

作用域也可以理解为一个代码块的上下文,也就是说在一个代码中,可以直接使用的东西,环境变量之于进程,系统框架为应用准备的基础对象,都可以视为一种scope。最为明显的就是类的定义,在类中,我们可以引用this指针来代表当前对象super指针来代表基类,这也是一种scope。lambda捕获的闭包也是一种scope。

Kotlin的scope functions就是把某一个对象当作代码块的scope,代码块中的代码可以方便的使用这个对象。

Scope funtions的作用

如同开头讨论的,能用scope function写出来的东西,用常规方式也一样可以做到,那到底图个啥呢?用scope function的方式代码变得更加的简洁和紧凑,我们把针对某一对象的密集操作集中在一起放入一个代码块中,会更加的内聚和紧凑,易于扩展和维护。但也要注意不能滥用,代码块中只应该写与对象相关的操作,与scope对象不相干的事情是绝对不应该放入其中的。

Scope functions

主要有6个,它们的应用主体都是一个对象,也就是要在一个对象上面调用这些函数,然后提供一个代码块(lambda):

Scope Function Object reference Return value Description
let it lambda result Extension function
run this lambda result Extension function
run _ lambda result No object in the scope
with this lambda result Take the object as an argument
apply this context object Extension function
also it context object Extension function

它们的区别

with不是一个extension函数

其他几个都是extension函数,所以with一定要把scope object作为参数传入。

scope对象的引用方式

对于scope function来说scope对象都会作为一个context object,可以在lambda块中使用,有些是作为this指针,有些是作为lambda的默认参数名字也即it指针,但它们都指向context object,本质上是没有区别的只是指针的名字一个是this一个是it。但是,跟类的定义scope是一样的,this指针是可以省略的,但如果it作为参数,则是不能省略的,具体来说,比如说,用apply时,代码块中是this指针,那么可以直接这样写:

1
2
3
4
5
val alice = Person("Alice", 20, "Amsterdam").apply {
  println(this)
  moveTo("London")
  incrementAge(2)
}

当然 你也可以显式的把this写出来,this.moveTo(“London”),但这就麻烦多了,何必呢。所以apply最合适的场景是对对象本身的操作,如赋值和修改属性。

但如果是用also,就必须用it了,这个不能省,因为它是对scope对象的引用:

1
2
3
alice.also {
  println("Two years later ${it.name} is at ${it.addr}")
}

所以,also最适合的不是对对象本身的操作,而是一些与对象相关的副作用,如打印日志等。

返回值不同

这坨Scope functions是一个函数,它是有返回值的,这个返回是不一样的,apply/also返回的是context object,其他几个则是返回lambda中的返回值也就是lambda的最后一个表达 式或者lambda中显式的return语句。

所以,如果是想继续使用scope object,那么就要用apply/also,如果想得到某个其他值就要用let/run/with,即使说不在乎函数的返回值时,这时也推荐使用also,因为假如后续想继续添加其他操作时,可以直接在后面链接上其他的scope function。其他返回值的let/run/with一般用在一组操作的确定性的终点上面,比如统计均值,那最后的均值计算可以用run,比如文件操作,读写都可以用with。

如何选择合适的scope函数

结合它们各自的特点,可以得到如下使用建议:

如果是更改scope对象本身,用apply()

比如说要设置某个对象的一坨属性状态,这时就把目标对象作为scope,然后在其上调用apply(),在函数块内把操作都做完:

1
2
3
4
5
val alice = Person("Alice", 20, "Amsterdam").apply {
  println(this)
  moveTo("London")
  incrementAge(2)
}

如果是对象弱相关的副作用操作,就用also()

最为典型的例子就是比如说打印一些日志,这时最好的就是用also。

判断nullity,不是null时执行一些强相关操作时用let

基于当前对象,执行一些强相关的操作,这时可以用let,并且可以顺便做nullable检查。

对象作为一个参数,执行一些转化时用run/with

把当前对象作为一个参数,或者一个输入,做一些操作,执行一些转化,最终输出为其他对象时,这种时候最好用run/with,比如在不同的架构层级之间转换类型对象时,就可以用run/with。或者在网络返回和本地数据库实体之间转换时,也可以用run/with,区别不大,但用with可读性略强一些,相当于是把对象视为一个上下文,比如:

1
2
3
4
val res = nowWeather.getWeather(city)
with (res) {
   WeatherEnity(weather, city)
}

with函数体内的参数是this,可以直接引用对象的成员,可以使代码非常的简洁,对象成了上下文,又不失可读性。这就让scope函数发挥了最大的价值。

注意事项

任何技术和工具要深刻理解它们的应用范围和使用场景以避免滥用,要用到恰到好处才能发挥最大的价值。对于一些非必须的东西,更是如此。

Scope functions是应用于对象上面的,所以前提是当你需要对一个对象进行一些操作时,才可以使用scope functions,具体选择哪一个参考 上面一节的讨论。另外,就是放入代码块中的操作必须全部是scope对象相关的才可以。一个scope function中只能是一组相关的操作,不同组的操作要启用不同的scope functions。比如说网络请求response的处理,可以分为服务器状态码和返回实体的检测,转成具体数据,打印日志这么三个scope functions,而不是全放进一个里面。

总而言之,要视具体的需求和场景,并基于场景选择合适的scope function,切忌过度使用。

参考资料

Comments