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