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類中實現所可能的方法,對吧?這就是擴展幫助我們的地方。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容