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中通过varval声明的属性(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个,作为开发中使用”主力“。例如,使用runapply函数,用于判空和链式与嵌套调用时使用,它们在lambda表达式内部用this表示接收者,可以省略this更方便的使用类方法。当然也可以使用letalso,lambda表达式内部用it表示参数,或者起一个更有意义的名字让程序更可读 。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335