前言
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,它有一些属性和方法,我们想对它的一个对象进行操作,通常会这样做:
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,我们可以这样做:
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,犹如在对象的类定义之中,在这个代码块中可以直接引用对象的方法,而不是像常规的那样使用对象的引用。
理解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指针,那么可以直接这样写:
val alice = Person("Alice", 20, "Amsterdam").apply {
println(this)
moveTo("London")
incrementAge(2)
}
当然 你也可以显式的把this写出来,this.moveTo("London"),但这就麻烦多了,何必呢。所以apply最合适的场景是对对象本身的操作,如赋值和修改属性。
但如果是用also,就必须用it了,这个不能省,因为它是对scope对象的引用:
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(),在函数块内把操作都做完:
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可读性略强一些,相当于是把对象视为一个上下文,比如:
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,切忌过度使用。