Kotlin面向对象之扩展(Extensions)

Kotlin与C#和Gosu类似,可以为一个类扩展一个新功能,但是不必继承该类或使用设计模式,如装饰器模式。Kotlin是通过被称为扩展的特殊声明完成这项工作的。Kotlin支持函数扩展和属性扩展。

函数扩展(Extension Functions)

为了声明一个扩展函数,我们需要以接收者类型作为该函数的前缀,也就是说:接收者类型就是被扩展的类型。如下示例了为MutableList<Int>增加了一个swap方法:

fun MutableList<Int>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this' corresponds to the list
    this[index1] = this[index2]
    this[index2] = tmp
}

上述扩展函数的函数体内部,this关键字代表着被扩展类型的对象(.操作符前的对象)。现在,我们可以使用MutableList<Int>类型的任意对象调用已经扩展的函数:

val l = mutableListOf(1, 2, 3)
l.swap(0, 2) // 'this' inside 'swap()' will hold the value of 'l'

当然,如果我们为其加上泛型,则该函数对于任何MutableList <T>是有意义的:

fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this' corresponds to the list
    this[index1] = this[index2]
    this[index2] = tmp
}

我们在扩展函数名称之前声明泛型参数,使其在目标接收类型表达式中可用。

扩展被静态解析(Extensions are resolved statically)

扩展的实现并没有真实的修改被扩展的类。扩展的定义,并没有在目标类中添加一个新的成员,仅是可以使目标类型的对象通过.操作符调用扩展函数而已。

要强调的是扩展函数是被静态调用的,也就是说它们并非目标类型的真实方法。意思就是说被调用的扩展函数被函数体调用的表达式类型决定(形参类型),而非运行时看到的实际传入类型。例如:

open class C
class D: C()
fun C.foo() = "c"
fun D.foo() = "d"
fun printFoo(c: C) {
    println(c.foo())
}

printFoo(D())

该示例的结果将打印出c,因为被调用的扩展函数仅取决于函数声明时候的参数c的类型,是类C的实例。

如果一个类有成员函数,且具有相同函数签名的扩展函数,则在调用的时候将调用成员函数,如:

class C {
    fun foo() { println("member") }
}

fun C.foo() { println("extension") }

如果我们以C类的任意实例c来调用c.foo()方法,结果将是打印member,而非extension。

但是,扩展函数可以重载具有相同名称但不同签名的成员函数,这是完全可行的:

class C {
    fun foo() { println("member") }
}

fun C.foo(i: Int) { println("extension") }

调用C().foo(1)方法将打印extension

被扩展的类可以为null(Nullable Receiver)

请注意,可以使用可null的目标类型定义扩展。即使其值为null,也可以在目标类型对象变量上调用此类扩展,只需要在扩展函数内部进行this == null判断。如此这般,在Kotlin中就可以调用toString()方法但无需进行null检查:因为该检查在扩展函数内部进行了。

fun Any?.toString(): String {
    if (this == null) return "null"
    // after the null check, 'this' is autocast to a non-null type, so the toString() below
    // resolves to the member function of the Any class
    return toString()
}

属性扩展(Extension Properties)

与函数扩展类似,Kotlin支持属性扩展:

val <T> List<T>.lastIndex: Int
    get() = size - 1

由于扩展的实现并没有在目标类中插入真实的成员,因此不能够让一个扩展属性拥有一个备份字段(backing field)。这就是为什么初始化块不允许初始化扩展属性。它们只能通过显式提供getter/setter方法来操作。例如:

val Foo.bar = 1 // error: initializers are not allowed for extension properties

伴生对象扩展(Companion Object Extensions)

如果一个类定义了伴生对象,则可以为该伴生对象扩展函数或属性:

class MyClass {
    companion object { }  // will be called "Companion"
}

fun MyClass.Companion.foo() {
    // ...
}

与伴生对象的常规成员类似,只能使用类名作为限定词来调用:

MyClass.foo()

范围扩展(Scope of Extensions)

大多数情况下,我们定义扩展都是直接在包下的最高层级进行:

package foo.bar
 
fun Baz.goo() { ... } 

要在其声明包之外使用这样的扩展,我们需要在调用处导入它:

package com.example.usage

import foo.bar.goo // importing all extensions by name "goo"
                   // or
import foo.bar.*   // importing everything from "foo.bar"

fun usage(baz: Baz) {
    baz.goo()
)

声明扩展作为成员(Declaring Extensions as Members)

在一个类中,您可以为另一类声明扩展。在这样的扩展中,将存在多个隐式接收器 - 可以在没有限定符的情况下就访问到的对象成员。 声明扩展的类的实例称为调用接收者,扩展方法的接收类型称为扩展接收者。

class D {
    fun bar() { ... }
}

class C {
    fun baz() { ... }

    fun D.foo() {
        bar()   // calls D.bar
        baz()   // calls C.baz
    }

    fun caller(d: D) {
        d.foo()   // call the extension function
    }
}

在调用接收者的成员与扩展接收者成员之间发生命名冲突的情况下,扩展接收者优先。要引用调用接收者的成员,可以使用如下语法:

class C {
    fun D.foo() {
        toString()         // calls D.toString()
        this@C.toString()  // calls C.toString()
    }
}

作为成员的扩展声明可以被open修饰符修饰,以便在子类中重写。这意味着这个扩展函数对于调用接收者是真实存在的,但是对于扩展接收者而言是静态的。

open class D {
}

class D1 : D() {
}

open class C {
    open fun D.foo() {
        println("D.foo in C")
    }

    open fun D1.foo() {
        println("D1.foo in C")
    }

    fun caller(d: D) {
        d.foo()   // call the extension function
    }
}

class C1 : C() {
    override fun D.foo() {
        println("D.foo in C1")
    }

    override fun D1.foo() {
        println("D1.foo in C1")
    }
}

C().caller(D())   // prints "D.foo in C"
C1().caller(D())  // prints "D.foo in C1" - dispatch receiver is resolved virtually
C().caller(D1())  // prints "D.foo in C" - extension receiver is resolved statically

扩展的设计目的(Motivation)

在Java中,我们习惯使用名为“* Utils”的类:FileUtilsStringUtils等。著名的java.util.Collections属于同一范畴。对于这些Utils类的使用,通常会令人不快的:

// Java
Collections.swap(list, Collections.binarySearch(list, Collections.max(otherList)), Collections.max(list))

工具类的类名总是出现。我们可以使用静态导入,如这样写:

// Java
swap(list, binarySearch(list, max(otherList)), max(list))

这稍微好了一点,但我们没有从强大的IDE处获取丝毫帮助。如果我们可以如下写,就会好很多:

// Java
list.swap(list.binarySearch(otherList.max()), list.max())

但是我们在List类中实现所可能的方法,对吧?这就是扩展帮助我们的地方。

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

推荐阅读更多精彩内容