Kotlin扩展
扩展函数
Kotlin中要扩展一个类的功能,除了使用继承(直接继承或继承一个接口使用委托)外,更便捷的方式是为该类定义扩展函数或扩展属性。此时称该类为接收者(Receiver),通常我们会把扩展(Extension)定义为顶层的,以便于在各处使用。
fun Fragment?.isAlive(): Boolean {
return this?.activity != null
&& this.activity?.isDestroyed == false
&& this.activity?.isFinishing == false
&& this.isAdded
&& !this.isDetached
}
扩展函数两种类型声明与转换
当使用一个变量引用该扩展函数,通过IDE的自动补全,可以发现变量的类型为(Fragment) -> Boolean
,可实际上,将类型声明为扩展更符合原义,即同一个函数可以声明为两种类型。
val a: (Fragment) -> Boolean = Fragment::isAlive
val b: Fragment.() -> Boolean = Fragment::isAlive
不过这也说明,这两种类型其实在Kotlin认为是一致的,可以通过反编译得知两个变量的类型都为Function1
。编译器所理解的扩展函数,实际上是将接收者作为其第一个参数的普通函数罢了。
public interface Function1<in P1, out R> : Function<R> {
public operator fun invoke(p1: P1): R
}
因此,通过将扩展函数引用(Receiver::method)赋予一个函数变量并指定类型,并用该变量调用函数,将扩展函数转化为普通函数的形式,反之亦然。
而扩展函数引用,实际与类方法引用(Class::method)在语法上是完全一致的,类方法也支持通过这种方式进行调用形式的转换。
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
Handler().postDelayed({
print(isAlive()) //扩展函数
print(a(this)) //通过扩展函数引用调用
print(b()) //通过扩展函数引用调用
val c: (Fragment) -> Boolean = Fragment::isAdded //类方法
val d: Fragment.() -> Boolean = Fragment::isAdded
print(c(this)) //通过类方法引用调用
print(d()) //通过类方法引用调用
}, 5000)
...
}
方法与函数
从前,在过程式和函数式编程语言(C,JavaScript)中,我们似乎更愿意使用名称“函数”,面向对象的语言(Java)中我们更愿意使用名称“方法”,来区分将函数操纵的对象放在括号内还是括号前的不同书写方式,虽然现在通常并不区分两种名称的使用了。Java中,我们经常在各种Util类中定义静态方法,以“函数”的形式来扩充类的能力,而Kotlin的扩展函数,则提供了一种更面向对象的表达方式。
扩展属性
Kotlin中通过var
和val
声明的属性(Property),要求必须进行初始化,这个初始化实际上是为其字段(backing field)赋值。属性除了字段外,还包括val的getter方法,var的getter和setter方法。
而扩展属性并没有实际位于类中,它没有backing field,也不支持初始化。扩展属性虽然称为属性,原理上不过是定义了getter或setter扩展方法,然后通过属性名调用扩展方法而已。
var File.content: String
get() {
return readText()
}
set(value) {
writeText(value)
}
val Float.dp: Float
get() {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, this, appContext.resources.displayMetrics)
}
适用于所有类型的扩展函数
Kotlin标准库中定义4个适用于所有类型的扩展函数,声明如下:
public inline fun <T, R> T.run(block: T.() -> R): R
public inline fun <T, R> T.let(block: (T) -> R): R
public inline fun <T> T.apply(block: T.() -> Unit): T
public inline fun <T> T.also(block: (T) -> Unit): T
这4个扩展函数内部都是直接调用block,而未进行任何特殊操作,区别仅在于接收的函数类型参数与返回值不同。可是要如何记忆这4个函数声明而不用每次都查阅其定义呢?或许写的多了自然能够熟练区分,但这里提供一种方法辅助记忆。
首先思考一下,我们在什么时候使用这4个函数,两种常见的场景是:
- 判空
- 链式与嵌套调用
现有一个可空类型的属性s
和接收不可空类型的字符串处理方法op
,当我们需要用op
处理s
时。使用progress1()
会报错,Smart cast to 'String' is impossible, because 's' is a mutable property that could have been changed by this time;使用progress2()
虽然消除了报错,但实际上依然不是安全的,且随意使用!!
并不是一个好的编码习惯;这时我们需要用progress3()
来正确判空。
var s: String? = ""
fun op(s: String) = ""
fun progress1() {
if (s != null) { op(s) }
}
fun progress2() {
if (s != null) { op(s!!) }
}
fun progress3() {
s?.let { op(it) }
s?.run { op(this) }
s?.also { op(it) }
s?.apply { op(this) }
}
而这4个扩展函数,其实都可以正确判空,虽然在Kotlin编码习惯中,普遍更多的使用let
函数。
再来看链式与嵌套调用,通常这种使用方式主要是为了减少临时变量的引入。从链式与嵌套调用的整个流程来看,只有返回值的不同才能对这种链式操作造成影响,而这4个扩展函数只有2种返回类型。
从链式与嵌套调用的单个操作的使用来看,我们可以使用函数引用和lambda表达式来向扩展函数传递参数,而这4个扩展函数只有两种函数参数。
下面进行尝试,对于使用函数引用作为扩展函数参数的情况,我们前面说过,Kotlin会把相同功能是“方法”与“函数”看做同一种类型,因此4个扩展函数使用上并无差异。
fun test(fragment: Fragment?) {
fragment?.run(Fragment::isAlive)
fragment?.let(Fragment::isAlive)
fragment?.apply(Fragment::isAlive)
fragment?.also(Fragment::isAlive)
}
对于使用lambda表达式作为扩展函数参数的情况,不同的lambda表达式类型的内部完全能实现相同的功能而仅有写法上的差异而已。
fun test(fragment: Fragment?) {
fragment?.run {
activity != null
&& activity?.isDestroyed == false
&& activity?.isFinishing == false
&& isAdded
&& isDetached
}
fragment?.let {
it.activity != null
&& it.activity?.isDestroyed == false
&& it.activity?.isFinishing == false
&& it.isAdded
&& it.isDetached
}
}
以上我们证明了,对于所有情景,这4个扩展函数我们只需保留2个返回值类型不同的函数即可实现所有功能。2个3字母的扩展函数返回值为同一类型,2个非3字母的扩展函数返回值为同一类型。
可以在3字母的函数中选择1个,在非3字母的函数中选择1个,作为开发中使用”主力“。例如,使用run
和apply
函数,用于判空和链式与嵌套调用时使用,它们在lambda表达式内部用this
表示接收者,可以省略this
更方便的使用类方法。当然也可以使用let
和also
,lambda表达式内部用it
表示参数,或者起一个更有意义的名字让程序更可读 。